Merge remote-tracking branch 'origin/develop' into find_replace
@ -0,0 +1,2 @@
|
||||
Adam Zivner <adam.zivner@gmail.com>
|
||||
Adam Zivner <zadam.apps@gmail.com>
|
||||
@ -1,48 +1,3 @@
|
||||
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
|
||||
|
||||
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.printed-content .page-break:after,
|
||||
.printed-content .page-break > * {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ck-content li p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.admonition {
|
||||
--accent-color: var(--card-border-color);
|
||||
border: 1px solid var(--accent-color);
|
||||
box-shadow: var(--card-box-shadow);
|
||||
background: var(--card-background-color);
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
margin: 1.25em 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admonition p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admonition p, h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.admonition.note { --accent-color: #69c7ff; }
|
||||
.admonition.tip { --accent-color: #40c025; }
|
||||
.admonition.important { --accent-color: #9839f7; }
|
||||
.admonition.caution { --accent-color: #ff2e2e; }
|
||||
.admonition.warning { --accent-color: #e2aa03; }
|
||||
|
||||
/*
|
||||
* CKEditor 5 (v41.0.0) content styles.
|
||||
* Generated on Fri, 26 Jan 2024 10:23:49 GMT.
|
||||
@ -0,0 +1,30 @@
|
||||
.ck.ck-sticky-panel > .ck-progress-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
border-left: 1px solid var(--ck-color-base-border);
|
||||
border-top: 1px solid var(--ck-color-base-border);
|
||||
border-right: 1px solid var(--ck-color-base-border);
|
||||
}
|
||||
|
||||
.ck.ck-sticky-panel > .ck-progress-bar > .ck-uploading-progress {
|
||||
align-self: center;
|
||||
padding: 3px 5px;
|
||||
font-weight: bold;
|
||||
|
||||
color: var(--ck-color-base-foreground);
|
||||
background-color: var(--ck-color-base-border);
|
||||
|
||||
transition-property: width;
|
||||
transition-duration: .5s;
|
||||
transition-timing-function: linear;
|
||||
|
||||
}
|
||||
|
||||
.ck.ck-sticky-panel > .ck-progress-bar > .ck-uploading-cancel {
|
||||
align-self: flex-end;
|
||||
padding: 0 5px;
|
||||
font-weight: bold;
|
||||
color: var(--ck-color-base-error);
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import { DecoupledEditor as DecoupledEditorBase } from '@ckeditor/ckeditor5-editor-decoupled';
|
||||
import { Essentials } from '@ckeditor/ckeditor5-essentials';
|
||||
import { Alignment } from '@ckeditor/ckeditor5-alignment';
|
||||
import { FontSize, FontFamily, FontColor, FontBackgroundColor } from '@ckeditor/ckeditor5-font';
|
||||
import { CKFinderUploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder';
|
||||
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
|
||||
import { Bold, Italic, Strikethrough, Underline } from '@ckeditor/ckeditor5-basic-styles';
|
||||
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
|
||||
import { CKBox } from '@ckeditor/ckeditor5-ckbox';
|
||||
import { CKFinder } from '@ckeditor/ckeditor5-ckfinder';
|
||||
import { EasyImage } from '@ckeditor/ckeditor5-easy-image';
|
||||
import { Heading } from '@ckeditor/ckeditor5-heading';
|
||||
import { Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image';
|
||||
import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent';
|
||||
import { Link } from '@ckeditor/ckeditor5-link';
|
||||
import { List, ListProperties } from '@ckeditor/ckeditor5-list';
|
||||
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
|
||||
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
|
||||
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
|
||||
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
|
||||
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
|
||||
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
|
||||
export default class DecoupledEditor extends DecoupledEditorBase {
|
||||
static builtinPlugins: (typeof TextTransformation | typeof Essentials | typeof Alignment | typeof FontBackgroundColor | typeof FontColor | typeof FontFamily | typeof FontSize | typeof CKFinderUploadAdapter | typeof Paragraph | typeof Heading | typeof Autoformat | typeof Bold | typeof Italic | typeof Strikethrough | typeof Underline | typeof BlockQuote | typeof Image | typeof ImageCaption | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof CloudServices | typeof CKBox | typeof CKFinder | typeof EasyImage | typeof List | typeof ListProperties | typeof Indent | typeof IndentBlock | typeof Link | typeof MediaEmbed | typeof PasteFromOffice | typeof Table | typeof TableToolbar | typeof PictureEditing)[];
|
||||
static defaultConfig: {
|
||||
toolbar: {
|
||||
items: string[];
|
||||
};
|
||||
image: {
|
||||
resizeUnit: "px";
|
||||
toolbar: string[];
|
||||
};
|
||||
table: {
|
||||
contentToolbar: string[];
|
||||
};
|
||||
list: {
|
||||
properties: {
|
||||
styles: boolean;
|
||||
startIndex: boolean;
|
||||
reversed: boolean;
|
||||
};
|
||||
};
|
||||
language: string;
|
||||
};
|
||||
}
|
||||
@ -1,360 +0,0 @@
|
||||
/*
|
||||
* This code is an adaptation of https://github.com/antoniotejada/Trilium-SyntaxHighlightWidget with additional improvements, such as:
|
||||
*
|
||||
* - support for selecting the language manually;
|
||||
* - support for determining the language automatically, if a special language is selected ("Auto-detected");
|
||||
* - limit for highlighting.
|
||||
*
|
||||
* TODO: Generally this class can be done directly in the CKEditor repository.
|
||||
*/
|
||||
|
||||
import library_loader from "../../../services/library_loader.js";
|
||||
import mime_types from "../../../services/mime_types.js";
|
||||
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
|
||||
export async function initSyntaxHighlighting(editor: TextEditor) {
|
||||
if (!isSyntaxHighlightEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
|
||||
initTextEditor(editor);
|
||||
}
|
||||
|
||||
const HIGHLIGHT_MAX_BLOCK_COUNT = 500;
|
||||
|
||||
const tag = "SyntaxHighlightWidget";
|
||||
const debugLevels = ["error", "warn", "info", "log", "debug"];
|
||||
const debugLevel = debugLevels.indexOf("warn");
|
||||
|
||||
let warn = function (...args: unknown[]) {};
|
||||
if (debugLevel >= debugLevels.indexOf("warn")) {
|
||||
warn = console.warn.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let info = function (...args: unknown[]) {};
|
||||
if (debugLevel >= debugLevels.indexOf("info")) {
|
||||
info = console.info.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let log = function (...args: unknown[]) {};
|
||||
if (debugLevel >= debugLevels.indexOf("log")) {
|
||||
log = console.log.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let dbg = function (...args: unknown[]) {};
|
||||
if (debugLevel >= debugLevels.indexOf("debug")) {
|
||||
dbg = console.debug.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
function assert(e: boolean, msg?: string) {
|
||||
console.assert(e, tag + ": " + msg);
|
||||
}
|
||||
|
||||
// TODO: Should this be scoped to note?
|
||||
let markerCounter = 0;
|
||||
|
||||
function initTextEditor(textEditor: TextEditor) {
|
||||
log("initTextEditor");
|
||||
|
||||
const document = textEditor.model.document;
|
||||
|
||||
// Create a conversion from model to view that converts
|
||||
// hljs:hljsClassName:uniqueId into a span with hljsClassName
|
||||
// See the list of hljs class names at
|
||||
// https://github.com/highlightjs/highlight.js/blob/6b8c831f00c4e87ecd2189ebbd0bb3bbdde66c02/docs/css-classes-reference.rst
|
||||
|
||||
textEditor.conversion.for("editingDowncast").markerToHighlight({
|
||||
model: "hljs",
|
||||
view: ({ markerName }) => {
|
||||
dbg("markerName " + markerName);
|
||||
// markerName has the pattern addMarker:cssClassName:uniqueId
|
||||
const [, cssClassName, id] = markerName.split(":");
|
||||
|
||||
// The original code at
|
||||
// https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
|
||||
// has this comment
|
||||
// Marker removal from the view has a bug:
|
||||
// https://github.com/ckeditor/ckeditor5/issues/7499
|
||||
// A minimal option is to return a new object for each converted marker...
|
||||
return {
|
||||
name: "span",
|
||||
classes: [cssClassName],
|
||||
attributes: {
|
||||
// ...however, adding a unique attribute should be future-proof..
|
||||
"data-syntax-result": id
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// XXX This is done at BalloonEditor.create time, so it assumes this
|
||||
// document is always attached to this textEditor, empirically that
|
||||
// seems to be the case even with two splits showing the same note,
|
||||
// it's not clear if CKEditor5 has apis to attach and detach
|
||||
// documents around
|
||||
document.registerPostFixer(function (writer) {
|
||||
log("postFixer");
|
||||
// Postfixers are a simpler way of tracking changes than onchange
|
||||
// See
|
||||
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
|
||||
const changes = document.differ.getChanges();
|
||||
let dirtyCodeBlocks = new Set<CKNode>();
|
||||
|
||||
function lookForCodeBlocks(node: CKNode) {
|
||||
for (const child of node._children) {
|
||||
if (child.is("element", "paragraph")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.is("element", "codeBlock")) {
|
||||
dirtyCodeBlocks.add(child);
|
||||
} else if (child.childCount > 0) {
|
||||
lookForCodeBlocks(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
dbg("change " + JSON.stringify(change));
|
||||
|
||||
if (change.name !== "paragraph" && change.name !== "codeBlock" && change?.position?.nodeAfter && change.position.nodeAfter.childCount > 0) {
|
||||
/*
|
||||
* We need to look for code blocks recursively, as they can be placed within a <div> due to
|
||||
* general HTML support or normally underneath other elements such as tables, blockquotes, etc.
|
||||
*/
|
||||
lookForCodeBlocks(change.position.nodeAfter);
|
||||
} else if (change.type == "insert" && change.name == "codeBlock") {
|
||||
// A new code block was inserted
|
||||
const codeBlock = change.position?.nodeAfter;
|
||||
// Even if it's a new codeblock, it needs dirtying in case
|
||||
// it already has children, like when pasting one or more
|
||||
// full codeblocks, undoing a delete, changing the language,
|
||||
// etc (the postfixer won't get later changes for those).
|
||||
if (codeBlock) {
|
||||
log("dirtying inserted codeBlock " + JSON.stringify(codeBlock.toJSON()));
|
||||
dirtyCodeBlocks.add(codeBlock);
|
||||
}
|
||||
} else if (change.type == "remove" && change.name == "codeBlock" && change.position) {
|
||||
// An existing codeblock was removed, do nothing. Note the
|
||||
// node is no longer in the editor so the codeblock cannot
|
||||
// be inspected here. No need to dirty the codeblock since
|
||||
// it has been removed
|
||||
log("removing codeBlock at path " + JSON.stringify(change.position.toJSON()));
|
||||
} else if ((change.type == "remove" || change.type == "insert") && change?.position?.parent.is("element", "codeBlock")) {
|
||||
// Text was added or removed from the codeblock, force a
|
||||
// highlight
|
||||
const codeBlock = change.position.parent;
|
||||
log("dirtying codeBlock " + JSON.stringify(codeBlock.toJSON()));
|
||||
dirtyCodeBlocks.add(codeBlock);
|
||||
}
|
||||
}
|
||||
for (let codeBlock of dirtyCodeBlocks) {
|
||||
highlightCodeBlock(codeBlock, writer);
|
||||
}
|
||||
// Adding markers doesn't modify the document data so no need for
|
||||
// postfixers to run again
|
||||
return false;
|
||||
});
|
||||
|
||||
// This assumes the document is empty and a explicit call to highlight
|
||||
// is not necessary here. Empty documents have a single children of type
|
||||
// paragraph with no text
|
||||
assert(document.getRoot().childCount == 1 && document.getRoot().getChild(0).name == "paragraph" && document.getRoot().getChild(0).isEmpty);
|
||||
}
|
||||
|
||||
/**
|
||||
* This implements highlighting via ephemeral markers (not stored in the
|
||||
* document).
|
||||
*
|
||||
* XXX Another option would be to use formatting markers, which would have
|
||||
* the benefit of making it work for readonly notes. On the flip side,
|
||||
* the formatting would be stored with the note and it would need a
|
||||
* way to remove that formatting when editing back the note.
|
||||
*/
|
||||
function highlightCodeBlock(codeBlock: CKNode, writer: Writer) {
|
||||
log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON()));
|
||||
const model = codeBlock.root.document.model;
|
||||
|
||||
// Can't invoke addMarker with an already existing marker name,
|
||||
// clear all highlight markers first. Marker names follow the
|
||||
// pattern hljs:cssClassName:uniqueId, eg hljs:hljs-comment:1
|
||||
const codeBlockRange = model.createRangeIn(codeBlock);
|
||||
for (const marker of model.markers.getMarkersIntersectingRange(codeBlockRange)) {
|
||||
dbg("removing marker " + marker.name);
|
||||
writer.removeMarker(marker.name);
|
||||
}
|
||||
|
||||
// Don't highlight if plaintext (note this needs to remove the markers
|
||||
// above first, in case this was a switch from non plaintext to
|
||||
// plaintext)
|
||||
const mimeType = codeBlock.getAttribute("language");
|
||||
if (mimeType == "text-plain") {
|
||||
// XXX There's actually a plaintext language that could be used
|
||||
// if you wanted the non-highlight formatting of
|
||||
// highlight.js css applied, see
|
||||
// https://github.com/highlightjs/highlight.js/issues/700
|
||||
log("not highlighting plaintext codeblock");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the corresponding language for the given mimetype.
|
||||
const highlightJsLanguage = mime_types.getHighlightJsNameForMime(mimeType);
|
||||
|
||||
if (mimeType !== mime_types.MIME_TYPE_AUTO && !highlightJsLanguage) {
|
||||
console.warn(`Unsupported highlight.js for mime type ${mimeType}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't highlight if the code is too big, as the typing performance will be highly degraded.
|
||||
if (codeBlock.childCount >= HIGHLIGHT_MAX_BLOCK_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// highlight.js needs the full text without HTML tags, eg for the
|
||||
// text
|
||||
// #include <stdio.h>
|
||||
// the highlighted html is
|
||||
// <span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><stdio.h></span></span>
|
||||
// But CKEditor codeblocks have <br> instead of \n
|
||||
|
||||
// Do a two pass algorithm:
|
||||
// - First pass collect the codeblock children text, change <br> to
|
||||
// \n
|
||||
// - invoke highlight.js on the collected text generating html
|
||||
// - Second pass parse the highlighted html spans and match each
|
||||
// char to the CodeBlock text. Issue addMarker CKEditor calls for
|
||||
// each span
|
||||
|
||||
// XXX This is brittle and assumes how highlight.js generates html
|
||||
// (blanks, which characters escapes, etc), a better approach
|
||||
// would be to use highlight.js beta api TreeTokenizer?
|
||||
|
||||
// Collect all the text nodes to pass to the highlighter Text is
|
||||
// direct children of the codeBlock
|
||||
let text = "";
|
||||
for (let i = 0; i < codeBlock.childCount; ++i) {
|
||||
let child = codeBlock.getChild(i);
|
||||
|
||||
// We only expect text and br elements here
|
||||
if (child.is("$text")) {
|
||||
dbg("child text " + child.data);
|
||||
text += child.data;
|
||||
} else if (child.is("element") && child.name == "softBreak") {
|
||||
dbg("softBreak");
|
||||
text += "\n";
|
||||
} else {
|
||||
warn("Unkown child " + JSON.stringify(child.toJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
let highlightRes;
|
||||
if (mimeType === mime_types.MIME_TYPE_AUTO) {
|
||||
highlightRes = hljs.highlightAuto(text);
|
||||
} else {
|
||||
highlightRes = hljs.highlight(text, { language: highlightJsLanguage });
|
||||
}
|
||||
dbg("text\n" + text);
|
||||
dbg("html\n" + highlightRes.value);
|
||||
|
||||
let iHtml = 0;
|
||||
let html = highlightRes.value;
|
||||
let spanStack = [];
|
||||
let iChild = -1;
|
||||
let childText = "";
|
||||
let child = null;
|
||||
let iChildText = 0;
|
||||
|
||||
while (iHtml < html.length) {
|
||||
// Advance the text index and fetch a new child if necessary
|
||||
if (iChildText >= childText.length) {
|
||||
iChild++;
|
||||
if (iChild < codeBlock.childCount) {
|
||||
dbg("Fetching child " + iChild);
|
||||
child = codeBlock.getChild(iChild);
|
||||
if (child.is("$text")) {
|
||||
dbg("child text " + child.data);
|
||||
childText = child.data;
|
||||
iChildText = 0;
|
||||
} else if (child.is("element", "softBreak")) {
|
||||
dbg("softBreak");
|
||||
iChildText = 0;
|
||||
childText = "\n";
|
||||
} else {
|
||||
warn("child unknown!!!");
|
||||
}
|
||||
} else {
|
||||
// Don't bail if beyond the last children, since there's
|
||||
// still html text, it must be a closing span tag that
|
||||
// needs to be dealt with below
|
||||
childText = "";
|
||||
}
|
||||
}
|
||||
|
||||
// This parsing is made slightly simpler and faster by only
|
||||
// expecting <span> and </span> tags in the highlighted html
|
||||
if (html[iHtml] == "<" && html[iHtml + 1] != "/") {
|
||||
// new span, note they can be nested eg C preprocessor lines
|
||||
// are inside a hljs-meta span, hljs-title function names
|
||||
// inside a hljs-function span, etc
|
||||
let iStartQuot = html.indexOf('"', iHtml + 1);
|
||||
let iEndQuot = html.indexOf('"', iStartQuot + 1);
|
||||
let className = html.slice(iStartQuot + 1, iEndQuot);
|
||||
// XXX highlight js uses scope for Python "title function_",
|
||||
// etc for now just use the first style only
|
||||
// See https://highlightjs.readthedocs.io/en/latest/css-classes-reference.html#a-note-on-scopes-with-sub-scopes
|
||||
let iBlank = className.indexOf(" ");
|
||||
if (iBlank > 0) {
|
||||
className = className.slice(0, iBlank);
|
||||
}
|
||||
dbg("Found span start " + className);
|
||||
|
||||
iHtml = html.indexOf(">", iHtml) + 1;
|
||||
|
||||
// push the span
|
||||
let posStart = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
|
||||
spanStack.push({ className: className, posStart: posStart });
|
||||
} else if (html[iHtml] == "<" && html[iHtml + 1] == "/") {
|
||||
// Done with this span, pop the span and mark the range
|
||||
iHtml = html.indexOf(">", iHtml + 1) + 1;
|
||||
|
||||
let stackTop = spanStack.pop();
|
||||
let posStart = stackTop?.posStart;
|
||||
let className = stackTop?.className;
|
||||
let posEnd = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
|
||||
let range = writer.createRange(posStart, posEnd);
|
||||
let markerName = "hljs:" + className + ":" + markerCounter;
|
||||
// Use an incrementing number for the uniqueId, random of
|
||||
// 10000000 is known to cause collisions with a few
|
||||
// codeblocks of 10s of lines on real notes (each line is
|
||||
// one or more marker).
|
||||
// Wrap-around for good measure so all numbers are positive
|
||||
// XXX Another option is to catch the exception and retry or
|
||||
// go through the markers and get the largest + 1
|
||||
markerCounter = (markerCounter + 1) & 0xffffff;
|
||||
dbg("Found span end " + className);
|
||||
dbg("Adding marker " + markerName + ": " + JSON.stringify(range.toJSON()));
|
||||
writer.addMarker(markerName, { range: range, usingOperation: false });
|
||||
} else {
|
||||
// Text, we should also have text in the children
|
||||
assert(iChild < codeBlock.childCount && iChildText < childText.length, "Found text in html with no corresponding child text!!!!");
|
||||
if (html[iHtml] == "&") {
|
||||
// highlight.js only encodes
|
||||
// .replace(/&/g, '&')
|
||||
// .replace(/</g, '<')
|
||||
// .replace(/>/g, '>')
|
||||
// .replace(/"/g, '"')
|
||||
// .replace(/'/g, ''');
|
||||
// see https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/lib/utils.js#L5
|
||||
let iAmpEnd = html.indexOf(";", iHtml);
|
||||
dbg(html.slice(iHtml, iAmpEnd));
|
||||
iHtml = iAmpEnd + 1;
|
||||
} else {
|
||||
// regular text
|
||||
dbg(html[iHtml]);
|
||||
iHtml++;
|
||||
}
|
||||
iChildText++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,80 +1,116 @@
|
||||
|
||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { composePlugins, withNx, withWeb } = require('@nx/webpack');
|
||||
const { join } = require('path');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, 'dist'),
|
||||
},
|
||||
devServer: {
|
||||
port: 4200,
|
||||
client: {
|
||||
overlay: {
|
||||
errors: true,
|
||||
warnings: false,
|
||||
runtimeErrors: true
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
tsConfig: './tsconfig.app.json',
|
||||
compiler: 'swc',
|
||||
main: "./src/index.ts",
|
||||
additionalEntryPoints: [
|
||||
{
|
||||
entryName: "desktop",
|
||||
entryPath: "./src/desktop.ts"
|
||||
},
|
||||
{
|
||||
entryName: "mobile",
|
||||
entryPath: "./src/mobile.ts"
|
||||
},
|
||||
{
|
||||
entryName: "login",
|
||||
entryPath: "./src/login.ts"
|
||||
},
|
||||
{
|
||||
entryName: "setup",
|
||||
entryPath: "./src/setup.ts"
|
||||
},
|
||||
{
|
||||
entryName: "share",
|
||||
entryPath: "./src/share.ts"
|
||||
},
|
||||
{
|
||||
// TriliumNextTODO: integrate set_password into setup entry point/view
|
||||
entryName: "set_password",
|
||||
entryPath: "./src/set_password.ts"
|
||||
}
|
||||
],
|
||||
externalDependencies: [
|
||||
"electron"
|
||||
],
|
||||
baseHref: '/',
|
||||
assets: [
|
||||
"./src/assets",
|
||||
"./src/stylesheets",
|
||||
"./src/libraries",
|
||||
"./src/fonts",
|
||||
"./src/translations"
|
||||
],
|
||||
styles: [],
|
||||
stylePreprocessorOptions: {
|
||||
sassOptions: {
|
||||
quietDeps: true
|
||||
}
|
||||
module.exports = composePlugins(
|
||||
withNx({
|
||||
tsConfig: join(__dirname, './tsconfig.app.json'),
|
||||
compiler: "tsc",
|
||||
main: join(__dirname, "./src/index.ts"),
|
||||
additionalEntryPoints: [
|
||||
{
|
||||
entryName: "desktop",
|
||||
entryPath: join(__dirname, "./src/desktop.ts")
|
||||
},
|
||||
{
|
||||
entryName: "mobile",
|
||||
entryPath: join(__dirname, "./src/mobile.ts")
|
||||
},
|
||||
{
|
||||
entryName: "login",
|
||||
entryPath: join(__dirname, "./src/login.ts")
|
||||
},
|
||||
{
|
||||
entryName: "setup",
|
||||
entryPath: join(__dirname, "./src/setup.ts")
|
||||
},
|
||||
outputHashing: false,
|
||||
optimization: process.env['NODE_ENV'] === 'production',
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
fallback: {
|
||||
path: false,
|
||||
fs: false,
|
||||
util: false
|
||||
{
|
||||
entryName: "share",
|
||||
entryPath: join(__dirname, "./src/share.ts")
|
||||
},
|
||||
{
|
||||
// TriliumNextTODO: integrate set_password into setup entry point/view
|
||||
entryName: "set_password",
|
||||
entryPath: join(__dirname, "./src/set_password.ts")
|
||||
}
|
||||
],
|
||||
externalDependencies: [
|
||||
"electron"
|
||||
],
|
||||
baseHref: '/',
|
||||
outputHashing: false,
|
||||
optimization: process.env['NODE_ENV'] === 'production'
|
||||
}),
|
||||
withWeb({
|
||||
styles: [],
|
||||
stylePreprocessorOptions: {
|
||||
sassOptions: {
|
||||
quietDeps: true
|
||||
}
|
||||
},
|
||||
}),
|
||||
(config) => {
|
||||
config.output = {
|
||||
path: join(__dirname, 'dist')
|
||||
};
|
||||
|
||||
config.devServer = {
|
||||
port: 4200,
|
||||
client: {
|
||||
overlay: {
|
||||
errors: true,
|
||||
warnings: false,
|
||||
runtimeErrors: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.resolve.fallback = {
|
||||
path: false,
|
||||
fs: false,
|
||||
util: false
|
||||
};
|
||||
|
||||
const assets = [ "assets", "stylesheets", "libraries", "fonts", "translations" ]
|
||||
config.plugins.push(new CopyPlugin({
|
||||
patterns: assets.map((asset) => ({
|
||||
from: join(__dirname, "src", asset),
|
||||
to: asset
|
||||
}))
|
||||
}));
|
||||
|
||||
inlineSvg(config);
|
||||
externalJson(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function inlineSvg(config) {
|
||||
if (!config.module?.rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Alter Nx's asset rule to avoid inlining SVG if they have ?raw prepended.
|
||||
const existingRule = config.module.rules.find((r) => r.test.toString() === /\.svg$/.toString());
|
||||
existingRule.resourceQuery = { not: [/raw/] };
|
||||
|
||||
// Add a rule for prepending ?raw SVGs.
|
||||
config.module.rules.push({
|
||||
resourceQuery: /raw/,
|
||||
type: 'asset/source',
|
||||
});
|
||||
}
|
||||
|
||||
function externalJson(config) {
|
||||
if (!config.module?.rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a rule for prepending ?external.
|
||||
config.module.rules.push({
|
||||
resourceQuery: /external/,
|
||||
type: 'asset/resource',
|
||||
});
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
TRILIUM_PORT=37743
|
||||
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* This script is used internally by the `rebuild-deps` target of the `desktop`. Normally we could use
|
||||
* `electron-rebuild` CLI directly, but it would rebuild the monorepo-level dependencies and breaks
|
||||
* the server build (and it doesn't expose a CLI option to override this).
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import { rebuild } from "@electron/rebuild"
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(scriptDir, "..");
|
||||
|
||||
function getElectronVersion() {
|
||||
const packageJsonPath = join(rootDir, "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
return packageJson.devDependencies.electron;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const distDir = join(rootDir, "dist");
|
||||
|
||||
rebuild({
|
||||
// We force the project root path to avoid electron-rebuild from rebuilding the monorepo-level dependency and breaking the server.
|
||||
projectRootPath: distDir,
|
||||
buildPath: distDir,
|
||||
force: true,
|
||||
electronVersion: getElectronVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* This script is used internally by the `rebuild-deps` target of the `desktop`. Normally we could use
|
||||
* `electron-rebuild` CLI directly, but it would rebuild the monorepo-level dependencies and breaks
|
||||
* the server build (and it doesn't expose a CLI option to override this).
|
||||
*/
|
||||
|
||||
// TODO: Deduplicate with apps/desktop/scripts/rebuild.ts.
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import { rebuild } from "@electron/rebuild"
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(scriptDir, "..");
|
||||
|
||||
function getElectronVersion() {
|
||||
const packageJsonPath = join(rootDir, "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
return packageJson.devDependencies.electron;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const distDir = join(rootDir, "dist");
|
||||
|
||||
rebuild({
|
||||
// We force the project root path to avoid electron-rebuild from rebuilding the monorepo-level dependency and breaking the server.
|
||||
projectRootPath: distDir,
|
||||
buildPath: distDir,
|
||||
force: true,
|
||||
electronVersion: getElectronVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
After Width: | Height: | Size: 703 B |
|
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,29 @@
|
||||
<p>Bookmarks allows creating <a href="#root/_help_QEAPj01N5f7w">links</a> to
|
||||
a certain part of a note, such as referencing a particular heading.</p>
|
||||
<p>Technically, bookmarks are HTML anchors.</p>
|
||||
<p>This feature was introduced in TriliumNext 0.94.0.</p>
|
||||
<h2>Interaction</h2>
|
||||
<ul>
|
||||
<li>To create a bookmark:
|
||||
<ul>
|
||||
<li>Place the cursor at the desired position where to place the bookmark.</li>
|
||||
<li>Look for the
|
||||
<img src="Bookmarks_plus.png" width="15" height="16">button in the <a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>,
|
||||
and then press the
|
||||
<img src="1_Bookmarks_plus.png" width="12" height="15">button.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>To place a link to a bookmark:
|
||||
<ul>
|
||||
<li>Place the cursor at the desired position of the link.</li>
|
||||
<li>From the <a href="#root/_help_QEAPj01N5f7w">link</a> pane, select the <em>Bookmarks</em> section
|
||||
and select the desired bookmark.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Limitations</h2>
|
||||
<ul>
|
||||
<li>Currently it's not possible to create a link to a bookmark from a different
|
||||
note. This functionality will be added after the internal links feature
|
||||
is enhanced to support bookmarks.</li>
|
||||
</ul>
|
||||
|
After Width: | Height: | Size: 636 B |
|
After Width: | Height: | Size: 57 KiB |
@ -0,0 +1,84 @@
|
||||
# Plugin migration guide
|
||||
This guide walks through the basic steps to take to integrate a CKEditor 5 plugin for use inside the Trilium monorepo, which allows:
|
||||
|
||||
* Making modifications to the implementation without having to maintain a new repo.
|
||||
* Integrating an older plugin based on the legacy installation method so that it works well with the new one.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This guide assumes that the CKEditor plugin is written in TypeScript. If it isn't, then you will have to port it to TypeScript to match the rest of the monorepo.
|
||||
|
||||
## Step 1. Creating the project skeleton
|
||||
|
||||
First, we are going to generate a project from scratch so that it picks up the latest template for building CKEditor plugins, whereas the plugin which is being integrated might be based on the legacy method.
|
||||
|
||||
Outside the `Notes` repository, we are going to use the CKEditor generator to generate the new project structure for us. We are not doing it directly inside `Notes` repository since it's going to use a different package manager (Yarn/NPM vs `pnpm`) and it also creates its own Git repository.
|
||||
|
||||
```
|
||||
npx ckeditor5-package-generator @triliumnext/ckeditor5-foo --use-npm --lang ts --installation-methods current
|
||||
```
|
||||
|
||||
Of course, replace `foo` with the name of the plugin. Generally it's better to stick with the original name of the plugin which can be determined by looking at the prefix of file names (e.g. `mermaid` from `mermaidui` or `mermaidediting`).
|
||||
|
||||
## Step 2. Copy the new project
|
||||
|
||||
1. Go to the newly created `ckeditor5-foo` directory.
|
||||
2. Remove `node_modules` since we are going to use `pnpm` to handle it.
|
||||
3. Remove `.git` from it.
|
||||
4. Copy the folder into the `Notes` repo, as a subdirectory of `packages`.
|
||||
|
||||
## Step 3. Updating dependencies
|
||||
|
||||
In the newly copied package, go to `package.json` and edit:
|
||||
|
||||
1. In `devDependencies`, change `ckeditor5` from `latest` to the same version as the one described in `packages/ckeditor5/package.json` (fixed version, e.g. `43.2.0`).
|
||||
2. In `peerDependencies`, change `ckeditor5` to the same version as from the previous step.
|
||||
3. Similarly, update `vitest` dependencies to match the monorepo one.
|
||||
4. Remove the `prepare` entry from the `scripts` section.
|
||||
5. Change `build:dist` to simply `build` in order to integrate it with NX.
|
||||
|
||||
## Step 4. Install missing dependencies and build errors
|
||||
|
||||
Run `pnpm build-dist` on the `Notes` root, and:
|
||||
|
||||
1. If there is an error about `Invalid module name in augmentation, module '@ckeditor/ckeditor5-core' cannot be found.`, simply replace `@ckeditor/ckeditor5-core` with `ckeditor5`.
|
||||
2. Run the build command again and ensure there are no build errors.
|
||||
3. Commit the changes.
|
||||
|
||||
## Step 5. Using `git subtree` to pull in the original repo
|
||||
|
||||
Instead of copying the files from the existing plugin we are actually going to carry over the history for traceability. To do so, we will use a temporary directory inside the repo:
|
||||
|
||||
```
|
||||
git subtree add --prefix=_regroup/<name> https://[...]/repo.git <main_branch>
|
||||
```
|
||||
|
||||
This will bring in all the commits of the upstream repo from the provided branch and rewrite them to be placed under the desired directory.
|
||||
|
||||
## Step 6. Integrate the plugin
|
||||
|
||||
1. Start by copying each sub-plugin (except the main one such as `FooEditing` and `FooUI`).
|
||||
1. If they are written in JavaScript, port them to TypeScript.
|
||||
1. Remove any non-TypeScript type documentation.
|
||||
2. If they have non-standard imports to CKEditor, such as `'ckeditor5/src/core.js'`, rewrite them to simply `ckeditor`.
|
||||
2. Install any necessary dependencies used by the source code (try going based on compilation errors rather than simply copying over all dependencies from `package.json`).
|
||||
3. Keep the existing TypeScript files that were generated automatically and integrate the changes into them.
|
||||
4. In `tsconfig.json` of the plugin, set `compilerOptions.composite` to `true`.
|
||||
5. Add a workspace dependency to the new plugin in `packages/ckeditor5/package.json`.
|
||||
6. In `packages/ckeditor5` look for `plugins.ts` and import the top-level plugin in `EXTERNAL_PLUGINS`.
|
||||
|
||||
## Handling CSS
|
||||
|
||||
Some plugins have custom CSS whereas some don't.
|
||||
|
||||
1. `import` the CSS in the `index.ts` of the plugin.
|
||||
2. When building the plugin, `dist/index.css` will be updated.
|
||||
3. In `plugins.ts` from `packages/ckeditor5`, add an import to the CSS.
|
||||
|
||||
## Integrating from another monorepo
|
||||
|
||||
This is a more complicated use-case if the upstream plugin belongs to a monorepo of another project (similar to how `trilium-ckeditor5` used to be).
|
||||
|
||||
1. Create a fresh Git clone of the upstream monorepo to obtain the plugin from.
|
||||
2. Run `git filter-repo --path packages/ckeditor5-foo/` (the trailing slash is very important!).
|
||||
3. Run `git subtree add` just like in the previous steps but point to the local Git directory instead (by appending `/.git` to the absolute path of the repository).
|
||||
4. Follow same integration steps as normal.
|
||||
|
After Width: | Height: | Size: 703 B |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 636 B |
|
After Width: | Height: | Size: 57 KiB |
@ -0,0 +1,19 @@
|
||||
# Configurations to normalize the IDE behavior.
|
||||
# http://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,jsx,ts}]
|
||||
quote_type = single
|
||||
|
||||
[package.json]
|
||||
indent_style = space
|
||||
tab_width = 2
|
||||
@ -0,0 +1,46 @@
|
||||
/* eslint-env node */
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: 'ckeditor5',
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint'
|
||||
],
|
||||
root: true,
|
||||
ignorePatterns: [
|
||||
// Ignore the entire `dist/` (the NIM build).
|
||||
'dist/**',
|
||||
// Ignore compiled JavaScript files, as they are generated automatically.
|
||||
'src/**/*.js',
|
||||
// Also, do not check typing declarations, too.
|
||||
'src/**/*.d.ts'
|
||||
],
|
||||
rules: {
|
||||
// This rule disallows importing from any path other than the package main entrypoint.
|
||||
'ckeditor5-rules/allow-imports-only-from-main-package-entry-point': 'error',
|
||||
// This rule ensures that all imports from `@ckeditor/*` packages are done through the main package entry points.
|
||||
// This is required for the editor types to work properly and to ease migration to the installation methods
|
||||
// introduced in CKEditor 5 version 42.0.0.
|
||||
'ckeditor5-rules/no-legacy-imports': 'error',
|
||||
// As required by the ECMAScript (ESM) standard, all imports must include a file extension.
|
||||
// If the import does not include it, this rule will try to automatically detect the correct file extension.
|
||||
'ckeditor5-rules/require-file-extensions-in-imports': [
|
||||
'error',
|
||||
{
|
||||
extensions: [ '.ts', '.js', '.json' ]
|
||||
}
|
||||
]
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [ 'tests/**/*.[jt]s', 'sample/**/*.[jt]s' ],
|
||||
rules: {
|
||||
// To write complex tests, you may need to import files that are not exported in DLL files by default.
|
||||
// Hence, imports CKEditor 5 packages in test files are not checked.
|
||||
'ckeditor5-rules/ckeditor-imports': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
* text=auto
|
||||
|
||||
*.htaccess eol=lf
|
||||
*.cgi eol=lf
|
||||
*.sh eol=lf
|
||||
|
||||
*.css text
|
||||
*.htm text
|
||||
*.html text
|
||||
*.js text
|
||||
*.ts text
|
||||
*.json text
|
||||
*.php text
|
||||
*.txt text
|
||||
*.md text
|
||||
|
||||
*.png -text
|
||||
*.gif -text
|
||||
*.jpg -text
|
||||
@ -0,0 +1,10 @@
|
||||
build/
|
||||
coverage/
|
||||
dist/
|
||||
node_modules/
|
||||
tmp/
|
||||
sample/ckeditor.dist.js
|
||||
|
||||
# Ignore compiled TypeScript files.
|
||||
src/**/*.js
|
||||
src/**/*.d.ts
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "stylelint-config-ckeditor5"
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
Software License Agreement
|
||||
==========================
|
||||
|
||||
Copyright (c) 2025. All rights reserved.
|
||||
|
||||
Licensed under the terms of [MIT license](https://opensource.org/licenses/MIT).
|
||||
@ -0,0 +1,141 @@
|
||||
@triliumnext/ckeditor5-admonition
|
||||
=================================
|
||||
|
||||
This package was created by the [ckeditor5-package-generator](https://www.npmjs.com/package/ckeditor5-package-generator) package.
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Developing the package](#developing-the-package)
|
||||
* [Available scripts](#available-scripts)
|
||||
* [`start`](#start)
|
||||
* [`test`](#test)
|
||||
* [`lint`](#lint)
|
||||
* [`stylelint`](#stylelint)
|
||||
* [`build:dist`](#builddist)
|
||||
* [`translations:synchronize`](#translationssynchronize)
|
||||
* [`translations:validate`](#translationsvalidate)
|
||||
* [`ts:build` and `ts:clear`](#tsbuild-and-tsclear)
|
||||
* [License](#license)
|
||||
|
||||
## Developing the package
|
||||
|
||||
To read about the CKEditor 5 Framework, visit the [CKEditor 5 Framework documentation](https://ckeditor.com/docs/ckeditor5/latest/framework/index.html).
|
||||
|
||||
## Available scripts
|
||||
|
||||
NPM scripts are a convenient way to provide commands in a project. They are defined in the `package.json` file and shared with people contributing to the project. It ensures developers use the same command with the same options (flags).
|
||||
|
||||
All the scripts can be executed by running `npm run <script>`. Pre and post commands with matching names will be run for those as well.
|
||||
|
||||
The following scripts are available in the package.
|
||||
|
||||
### `start`
|
||||
|
||||
Starts an HTTP server with the live-reload mechanism that allows previewing and testing of plugins available in the package.
|
||||
|
||||
When the server starts, the default browser will open the developer sample. This can be disabled by passing the `--no-open` option to that command.
|
||||
|
||||
You can also define the language that will translate the created editor by specifying the `--language [LANG]` option. It defaults to `'en'`.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Starts the server and open the browser.
|
||||
npm run start
|
||||
|
||||
# Disable auto-opening the browser.
|
||||
npm run start -- --no-open
|
||||
|
||||
# Create the editor with the interface in German.
|
||||
npm run start -- --language=de
|
||||
```
|
||||
|
||||
### `test`
|
||||
|
||||
Allows executing unit tests for the package specified in the `tests/` directory. To check the code coverage, add the `--coverage` modifier. See other [CLI flags](https://vitest.dev/guide/cli.html) in Vitest.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Execute tests.
|
||||
npm run test
|
||||
|
||||
# Generate code coverage report after each change in the sources.
|
||||
npm run test -- --coverage
|
||||
```
|
||||
|
||||
### `lint`
|
||||
|
||||
Runs ESLint, which analyzes the code (all `*.ts` files) to quickly find problems.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Execute eslint.
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### `stylelint`
|
||||
|
||||
Similar to the `lint` task, stylelint analyzes the CSS code (`*.css` files in the `theme/` directory) in the package.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Execute stylelint.
|
||||
npm run stylelint
|
||||
```
|
||||
|
||||
### `build:dist`
|
||||
|
||||
Creates npm and browser builds of your plugin. These builds can be added to the editor by following the [Configuring CKEditor 5 features](https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/configuration.html) guide.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Builds the `npm` and browser files thats are ready to publish.
|
||||
npm run build:dist
|
||||
```
|
||||
|
||||
### `translations:synchronize`
|
||||
|
||||
Synchronizes translation messages (arguments of the `t()` function) by performing the following steps:
|
||||
|
||||
* Collect all translation messages from the package by finding `t()` calls in source files.
|
||||
* Detect if translation context is valid, i.e. whether the provided values do not interfere with the values specified in the `@ckeditor/ckeditor5-core` package.
|
||||
* If there are no validation errors, update all translation files (`*.po` files) to be in sync with the context file:
|
||||
* unused translation entries are removed,
|
||||
* missing translation entries are added with empty string as the message translation,
|
||||
* missing translation files are created for languages that do not have own `*.po` file yet.
|
||||
|
||||
The task may end with an error if one of the following conditions is met:
|
||||
|
||||
* Found the `Unused context` error – entries specified in the `lang/contexts.json` file are not used in source files. They should be removed.
|
||||
* Found the `Duplicated contex` error – some of the entries are duplicated. Consider removing them from the `lang/contexts.json` file, or rewriting them.
|
||||
* Found the `Missing context` error – entries specified in source files are not described in the `lang/contexts.json` file. They should be added.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
npm run translations:synchronize
|
||||
```
|
||||
|
||||
### `translations:validate`
|
||||
|
||||
Peforms only validation steps as described in [`translations:synchronize`](#translationssynchronize) script, but without modifying any files. It only checks the correctness of the context file against the `t()` function calls.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
npm run translations:validate
|
||||
```
|
||||
|
||||
### `ts:build` and `ts:clear`
|
||||
|
||||
These scripts compile TypeScript and remove the compiled files. They are used in the aforementioned life cycle scripts, and there is no need to call them manually.
|
||||
|
||||
## License
|
||||
|
||||
The `@triliumnext/ckeditor5-admonition` package is available under [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
However, it is the default license of packages created by the [ckeditor5-package-generator](https://www.npmjs.com/package/ckeditor5-package-generator) package and can be changed.
|
||||
@ -0,0 +1,22 @@
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"name": "Admonition",
|
||||
"className": "Admonition",
|
||||
"description": "Implements admonitions (warning, info boxes) in a similar fashion to blockquotes",
|
||||
"path": "src/admonition.ts",
|
||||
"uiComponents": [
|
||||
{
|
||||
"name": "admonition",
|
||||
"type": "Button",
|
||||
"iconPath": "theme/icons/admonition.svg"
|
||||
}
|
||||
],
|
||||
"htmlOutput": [
|
||||
{
|
||||
"elements": "aside"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"Admonition": "Toolbar button tooltip for the Admonition feature."
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
#
|
||||
# !!! IMPORTANT !!!
|
||||
#
|
||||
# Before you edit this file, please keep in mind that contributing to the project
|
||||
# translations is possible ONLY via the Transifex online service.
|
||||
#
|
||||
# To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
|
||||
#
|
||||
# To learn more, check out the official contributor's guide:
|
||||
# https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgctxt "Toolbar button tooltip for the Admoniton feature."
|
||||
msgid "Admonition"
|
||||
msgstr "Admonition"
|
||||
@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "@triliumnext/ckeditor5-admonition",
|
||||
"version": "1.0.0",
|
||||
"description": "Admonition (info box, warning box) feature for CKEditor 5.",
|
||||
"keywords": [
|
||||
"ckeditor",
|
||||
"ckeditor5",
|
||||
"ckeditor 5",
|
||||
"ckeditor5-feature",
|
||||
"ckeditor5-plugin",
|
||||
"ckeditor5-package-generator"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.ts",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./*": "./dist/*",
|
||||
"./browser/*": null,
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=5.7.1"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "43.0.1",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "^3.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "~8.32.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/browser": "^3.0.5",
|
||||
"@vitest/coverage-istanbul": "^3.0.5",
|
||||
"ckeditor5": "45.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "^14.1.0",
|
||||
"lint-staged": "^15.0.0",
|
||||
"stylelint": "^16.0.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.0.4",
|
||||
"vite-plugin-svgo": "~2.0.0",
|
||||
"vitest": "^3.0.5",
|
||||
"webdriverio": "^9.0.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "45.0.0"
|
||||
},
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"scripts": {
|
||||
"build": "node ./scripts/build-dist.mjs",
|
||||
"ts:build": "tsc -p ./tsconfig.release.json",
|
||||
"ts:clear": "npx rimraf --glob \"src/**/*.@(js|d.ts)\"",
|
||||
"lint": "eslint \"**/*.{js,ts}\" --quiet",
|
||||
"start": "ckeditor5-package-tools start",
|
||||
"stylelint": "stylelint --quiet --allow-empty-input 'theme/**/*.css'",
|
||||
"test": "vitest",
|
||||
"test:debug": "vitest --inspect-brk --no-file-parallelism --browser.headless=false",
|
||||
"prepublishOnly": "npm run ts:build && ckeditor5-package-tools export-package-as-javascript",
|
||||
"postpublish": "npm run ts:clear && ckeditor5-package-tools export-package-as-typescript",
|
||||
"translations:synchronize": "ckeditor5-package-tools translations:synchronize",
|
||||
"translations:validate": "ckeditor5-package-tools translations:synchronize --validate-only"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,ts}": [
|
||||
"eslint --quiet"
|
||||
],
|
||||
"**/*.css": [
|
||||
"stylelint --quiet --allow-empty-input"
|
||||
]
|
||||
},
|
||||
"nx": {
|
||||
"name": "ckeditor5-admonition",
|
||||
"targets": {
|
||||
"build": {
|
||||
"cache": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
editor: ClassicEditor;
|
||||
}
|
||||
}
|
||||
|
||||
import {
|
||||
ClassicEditor,
|
||||
Autoformat,
|
||||
Base64UploadAdapter,
|
||||
BlockQuote,
|
||||
Bold,
|
||||
Code,
|
||||
CodeBlock,
|
||||
Essentials,
|
||||
Heading,
|
||||
Image,
|
||||
ImageCaption,
|
||||
ImageStyle,
|
||||
ImageToolbar,
|
||||
ImageUpload,
|
||||
Indent,
|
||||
Italic,
|
||||
Link,
|
||||
List,
|
||||
MediaEmbed,
|
||||
Paragraph,
|
||||
Table,
|
||||
TableToolbar
|
||||
} from 'ckeditor5';
|
||||
|
||||
import CKEditorInspector from '@ckeditor/ckeditor5-inspector';
|
||||
|
||||
import Admonition from '../src/admonition.js';
|
||||
|
||||
import 'ckeditor5/ckeditor5.css';
|
||||
|
||||
ClassicEditor
|
||||
.create( document.getElementById( 'editor' )!, {
|
||||
licenseKey: 'GPL',
|
||||
plugins: [
|
||||
Admonition,
|
||||
Essentials,
|
||||
Autoformat,
|
||||
BlockQuote,
|
||||
Bold,
|
||||
Heading,
|
||||
Image,
|
||||
ImageCaption,
|
||||
ImageStyle,
|
||||
ImageToolbar,
|
||||
ImageUpload,
|
||||
Indent,
|
||||
Italic,
|
||||
Link,
|
||||
List,
|
||||
MediaEmbed,
|
||||
Paragraph,
|
||||
Table,
|
||||
TableToolbar,
|
||||
CodeBlock,
|
||||
Code,
|
||||
Base64UploadAdapter
|
||||
],
|
||||
toolbar: [
|
||||
'undo',
|
||||
'redo',
|
||||
'|',
|
||||
'admonition',
|
||||
'|',
|
||||
'heading',
|
||||
'|',
|
||||
'bold',
|
||||
'italic',
|
||||
'link',
|
||||
'code',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
'|',
|
||||
'outdent',
|
||||
'indent',
|
||||
'|',
|
||||
'uploadImage',
|
||||
'blockQuote',
|
||||
'insertTable',
|
||||
'mediaEmbed',
|
||||
'codeBlock'
|
||||
],
|
||||
image: {
|
||||
toolbar: [
|
||||
'imageStyle:inline',
|
||||
'imageStyle:block',
|
||||
'imageStyle:side',
|
||||
'|',
|
||||
'imageTextAlternative'
|
||||
]
|
||||
},
|
||||
table: {
|
||||
contentToolbar: [
|
||||
'tableColumn',
|
||||
'tableRow',
|
||||
'mergeTableCells'
|
||||
]
|
||||
}
|
||||
} )
|
||||
.then( editor => {
|
||||
window.editor = editor;
|
||||
CKEditorInspector.attach( editor );
|
||||
window.console.log( 'CKEditor 5 is ready.', editor );
|
||||
} )
|
||||
.catch( err => {
|
||||
window.console.error( err.stack );
|
||||
} );
|
||||
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license Copyright (c) 2020-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md.
|
||||
*/
|
||||
|
||||
/* eslint-env node */
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import upath from 'upath';
|
||||
import chalk from 'chalk';
|
||||
import { build } from '@ckeditor/ckeditor5-dev-build-tools';
|
||||
|
||||
function dist( path ) {
|
||||
return upath.join( 'dist', path );
|
||||
}
|
||||
|
||||
( async () => {
|
||||
const tsconfig = 'tsconfig.dist.ckeditor5.json';
|
||||
|
||||
/**
|
||||
* Step 1
|
||||
*/
|
||||
console.log( chalk.cyan( '1/2: Generating NPM build...' ) );
|
||||
|
||||
const require = createRequire( import.meta.url );
|
||||
const pkg = require( upath.resolve( process.cwd(), './package.json' ) );
|
||||
|
||||
await build( {
|
||||
input: 'src/index.ts',
|
||||
output: dist( './index.js' ),
|
||||
tsconfig: 'tsconfig.dist.json',
|
||||
external: [
|
||||
'ckeditor5',
|
||||
'ckeditor5-premium-features',
|
||||
...Object.keys( {
|
||||
...pkg.dependencies,
|
||||
...pkg.peerDependencies
|
||||
} )
|
||||
],
|
||||
clean: true,
|
||||
sourceMap: true,
|
||||
declarations: true,
|
||||
translations: '**/*.po'
|
||||
} );
|
||||
} )();
|
||||
@ -0,0 +1,17 @@
|
||||
import { Plugin } from 'ckeditor5';
|
||||
|
||||
import AdmonitionEditing from './admonitionediting.js';
|
||||
import AdmonitionUI from './admonitionui.js';
|
||||
import AdmonitionAutoformat from './admonitionautoformat.js';
|
||||
|
||||
export default class Admonition extends Plugin {
|
||||
|
||||
public static get requires() {
|
||||
return [ AdmonitionEditing, AdmonitionUI, AdmonitionAutoformat ] as const;
|
||||
}
|
||||
|
||||
public static get pluginName() {
|
||||
return 'Admonition' as const;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { Autoformat, blockAutoformatEditing, Plugin } from "ckeditor5";
|
||||
import { AdmonitionType, ADMONITION_TYPES } from "./admonitioncommand.js";
|
||||
|
||||
function tryParseAdmonitionType(match: RegExpMatchArray) {
|
||||
if (match.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ADMONITION_TYPES as readonly string[]).includes(match[1])) {
|
||||
return match[1] as AdmonitionType;
|
||||
}
|
||||
}
|
||||
|
||||
export default class AdmonitionAutoformat extends Plugin {
|
||||
|
||||
static get requires() {
|
||||
return [ Autoformat ];
|
||||
}
|
||||
|
||||
afterInit() {
|
||||
if (!this.editor.commands.get("admonition")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = (this as any);
|
||||
blockAutoformatEditing(this.editor, instance, /^\!\!\[*\! (.+) $/, ({ match }) => {
|
||||
const type = tryParseAdmonitionType(match);
|
||||
|
||||
if (type) {
|
||||
// User has entered the admonition type, so we insert as-is.
|
||||
this.editor.execute("admonition", { forceValue: type });
|
||||
} else {
|
||||
// User has not entered a valid type, assume it's part of the text of the admonition.
|
||||
this.editor.execute("admonition");
|
||||
if (match.length > 1) {
|
||||
this.editor.execute("insertText", { text: (match[1] ?? "") + " " });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module admonition/admonitionediting
|
||||
*/
|
||||
|
||||
import { Delete, Enter, Plugin, ViewDocumentDeleteEvent, ViewDocumentEnterEvent } from 'ckeditor5';
|
||||
import AdmonitionCommand, { AdmonitionType, ADMONITION_TYPES, DEFAULT_ADMONITION_TYPE, ADMONITION_TYPE_ATTRIBUTE } from './admonitioncommand.js';
|
||||
|
||||
/**
|
||||
* The block quote editing.
|
||||
*
|
||||
* Introduces the `'admonition'` command and the `'aside'` model element.
|
||||
*
|
||||
* @extends module:core/plugin~Plugin
|
||||
*/
|
||||
export default class AdmonitionEditing extends Plugin {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static get pluginName() {
|
||||
return 'AdmonitionEditing' as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static get requires() {
|
||||
return [ Enter, Delete ] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public init(): void {
|
||||
const editor = this.editor;
|
||||
const schema = editor.model.schema;
|
||||
|
||||
editor.commands.add( 'admonition', new AdmonitionCommand( editor ) );
|
||||
|
||||
schema.register( 'aside', {
|
||||
inheritAllFrom: '$container',
|
||||
allowAttributes: ADMONITION_TYPE_ATTRIBUTE
|
||||
} );
|
||||
|
||||
editor.conversion.for("upcast").elementToElement({
|
||||
view: {
|
||||
name: "aside",
|
||||
classes: "admonition",
|
||||
},
|
||||
model: (viewElement, { writer }) => {
|
||||
let type: AdmonitionType = DEFAULT_ADMONITION_TYPE;
|
||||
for (const className of viewElement.getClassNames()) {
|
||||
if (className !== "admonition" && (ADMONITION_TYPES as readonly string[]).includes(className)) {
|
||||
type = className as AdmonitionType;
|
||||
}
|
||||
}
|
||||
|
||||
const attributes: Record<string, unknown> = {};
|
||||
attributes[ADMONITION_TYPE_ATTRIBUTE] = type;
|
||||
return writer.createElement("aside", attributes);
|
||||
}
|
||||
});
|
||||
|
||||
editor.conversion.for("downcast")
|
||||
.elementToElement( {
|
||||
model: 'aside',
|
||||
view: "aside"
|
||||
})
|
||||
.attributeToAttribute({
|
||||
model: ADMONITION_TYPE_ATTRIBUTE,
|
||||
view: (value) => ({
|
||||
key: "class",
|
||||
value: [ "admonition", value as string ]
|
||||
})
|
||||
});
|
||||
|
||||
// Postfixer which cleans incorrect model states connected with block quotes.
|
||||
editor.model.document.registerPostFixer( writer => {
|
||||
const changes = editor.model.document.differ.getChanges();
|
||||
|
||||
for ( const entry of changes ) {
|
||||
if ( entry.type == 'insert' ) {
|
||||
const element = entry.position.nodeAfter;
|
||||
|
||||
if ( !element ) {
|
||||
// We are inside a text node.
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( element.is( 'element', 'aside' ) && element.isEmpty ) {
|
||||
// Added an empty aside - remove it.
|
||||
writer.remove( element );
|
||||
|
||||
return true;
|
||||
} else if ( element.is( 'element', 'aside' ) && !schema.checkChild( entry.position, element ) ) {
|
||||
// Added a aside in incorrect place. Unwrap it so the content inside is not lost.
|
||||
writer.unwrap( element );
|
||||
|
||||
return true;
|
||||
} else if ( element.is( 'element' ) ) {
|
||||
// Just added an element. Check that all children meet the scheme rules.
|
||||
const range = writer.createRangeIn( element );
|
||||
|
||||
for ( const child of range.getItems() ) {
|
||||
if (
|
||||
child.is( 'element', 'aside' ) &&
|
||||
!schema.checkChild( writer.createPositionBefore( child ), child )
|
||||
) {
|
||||
writer.unwrap( child );
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ( entry.type == 'remove' ) {
|
||||
const parent = entry.position.parent;
|
||||
|
||||
if ( parent.is( 'element', 'aside' ) && parent.isEmpty ) {
|
||||
// Something got removed and now aside is empty. Remove the aside as well.
|
||||
writer.remove( parent );
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} );
|
||||
|
||||
const viewDocument = this.editor.editing.view.document;
|
||||
const selection = editor.model.document.selection;
|
||||
const admonitionCommand = editor.commands.get( 'admonition' );
|
||||
if (!admonitionCommand) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Overwrite default Enter key behavior.
|
||||
// If Enter key is pressed with selection collapsed in empty block inside a quote, break the quote.
|
||||
this.listenTo<ViewDocumentEnterEvent>( viewDocument, 'enter', ( evt, data ) => {
|
||||
if ( !selection.isCollapsed || !admonitionCommand.value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positionParent = selection.getLastPosition()!.parent;
|
||||
|
||||
if ( positionParent.isEmpty ) {
|
||||
editor.execute( 'admonition' );
|
||||
editor.editing.view.scrollToTheSelection();
|
||||
|
||||
data.preventDefault();
|
||||
evt.stop();
|
||||
}
|
||||
}, { context: 'aside' } );
|
||||
|
||||
// Overwrite default Backspace key behavior.
|
||||
// If Backspace key is pressed with selection collapsed in first empty block inside a quote, break the quote.
|
||||
this.listenTo<ViewDocumentDeleteEvent>( viewDocument, 'delete', ( evt, data ) => {
|
||||
if ( data.direction != 'backward' || !selection.isCollapsed || !admonitionCommand!.value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positionParent = selection.getLastPosition()!.parent;
|
||||
|
||||
if ( positionParent.isEmpty && !positionParent.previousSibling ) {
|
||||
editor.execute( 'admonition' );
|
||||
editor.editing.view.scrollToTheSelection();
|
||||
|
||||
data.preventDefault();
|
||||
evt.stop();
|
||||
}
|
||||
}, { context: 'aside' } );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module admonition/admonitionui
|
||||
*/
|
||||
|
||||
import { Plugin, addListToDropdown, createDropdown, ListDropdownItemDefinition, SplitButtonView, ViewModel } from 'ckeditor5';
|
||||
|
||||
import '../theme/blockquote.css';
|
||||
import admonitionIcon from '../theme/icons/admonition.svg';
|
||||
import { AdmonitionType } from './admonitioncommand.js';
|
||||
import { Collection } from 'ckeditor5';
|
||||
|
||||
interface AdmonitionDefinition {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const ADMONITION_TYPES: Record<AdmonitionType, AdmonitionDefinition> = {
|
||||
note: {
|
||||
title: "Note"
|
||||
},
|
||||
tip: {
|
||||
title: "Tip"
|
||||
},
|
||||
important: {
|
||||
title: "Important"
|
||||
},
|
||||
caution: {
|
||||
title: "Caution"
|
||||
},
|
||||
warning: {
|
||||
title: "Warning"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The block quote UI plugin.
|
||||
*
|
||||
* It introduces the `'admonition'` button.
|
||||
*
|
||||
* @extends module:core/plugin~Plugin
|
||||
*/
|
||||
export default class AdmonitionUI extends Plugin {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static get pluginName() {
|
||||
return 'AdmonitionUI' as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public init(): void {
|
||||
const editor = this.editor;
|
||||
|
||||
editor.ui.componentFactory.add( 'admonition', () => {
|
||||
const buttonView = this._createButton();
|
||||
|
||||
return buttonView;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a button for admonition command to use either in toolbar or in menu bar.
|
||||
*/
|
||||
private _createButton() {
|
||||
const editor = this.editor;
|
||||
const locale = editor.locale;
|
||||
const command = editor.commands.get( 'admonition' )!;
|
||||
const dropdownView = createDropdown(locale, SplitButtonView);
|
||||
const splitButtonView = dropdownView.buttonView;
|
||||
const t = locale.t;
|
||||
|
||||
addListToDropdown(dropdownView, this._getDropdownItems())
|
||||
|
||||
// Button configuration.
|
||||
splitButtonView.set( {
|
||||
label: t( 'Admonition' ),
|
||||
icon: admonitionIcon,
|
||||
isToggleable: true,
|
||||
tooltip: true
|
||||
} );
|
||||
splitButtonView.on("execute", () => {
|
||||
editor.execute("admonition", { usePreviousChoice: true });
|
||||
editor.editing.view.focus();
|
||||
});
|
||||
splitButtonView.bind( 'isOn' ).to( command, 'value', value => (!!value) as boolean);
|
||||
|
||||
// Dropdown configuration
|
||||
dropdownView.bind( 'isEnabled' ).to( command, 'isEnabled' );
|
||||
dropdownView.on("execute", evt => {
|
||||
editor.execute("admonition", { forceValue: ( evt.source as any ).commandParam } );
|
||||
editor.editing.view.focus();
|
||||
});
|
||||
|
||||
return dropdownView;
|
||||
}
|
||||
|
||||
private _getDropdownItems() {
|
||||
const itemDefinitions = new Collection<ListDropdownItemDefinition>();
|
||||
const command = this.editor.commands.get("admonition");
|
||||
if (!command) {
|
||||
return itemDefinitions;
|
||||
}
|
||||
|
||||
for (const [ type, admonition ] of Object.entries(ADMONITION_TYPES)) {
|
||||
const definition: ListDropdownItemDefinition = {
|
||||
type: "button",
|
||||
model: new ViewModel({
|
||||
commandParam: type,
|
||||
label: admonition.title,
|
||||
role: 'menuitemradio',
|
||||
withText: true
|
||||
})
|
||||
}
|
||||
|
||||
definition.model.bind("isOn").to(command, "value", currentType => currentType === type);
|
||||
itemDefinitions.add(definition);
|
||||
}
|
||||
|
||||
return itemDefinitions;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import AdmonitionCommand from './admonitioncommand.js';
|
||||
import AdmonitionEditing from './admonitionediting.js';
|
||||
import AdmonitionUI from './admonitionui.js';
|
||||
import type { Admonition } from './index.js';
|
||||
|
||||
declare module 'ckeditor5' {
|
||||
interface PluginsMap {
|
||||
[ Admonition.pluginName ]: Admonition;
|
||||
[ AdmonitionEditing.pluginName ]: AdmonitionEditing;
|
||||
[ AdmonitionUI.pluginName ]: AdmonitionUI;
|
||||
}
|
||||
|
||||
interface CommandsMap {
|
||||
admonition: AdmonitionCommand;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import admonitionIcon from '../theme/icons/admonition.svg';
|
||||
import './augmentation.js';
|
||||
import "../theme/blockquote.css";
|
||||
|
||||
export { default as Admonition } from './admonition.js';
|
||||
export { default as AdmonitionEditing } from './admonitionediting.js';
|
||||
export { default as AdmonitionUI } from './admonitionui.js';
|
||||
export { default as AdmonitionAutoformat } from './admonitionautoformat.js';
|
||||
export type { default as AdmonitionCommand } from './admonitioncommand.js';
|
||||
|
||||
export const icons = {
|
||||
admonitionIcon
|
||||
};
|
||||