mirror of https://github.com/TriliumNext/Notes
Merge branch 'develop' into patch-1
commit
80e6276d31
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @module
|
||||||
|
*
|
||||||
|
* The nightly version works uses the version described in `package.json`, just like any release.
|
||||||
|
* The problem with this approach is that production builds have a very aggressive cache, and
|
||||||
|
* usually running the nightly with this cached version of the application will mean that the
|
||||||
|
* user might run into module not found errors or styling errors caused by an old cache.
|
||||||
|
*
|
||||||
|
* This script is supposed to be run in the CI, which will update locally the version field of
|
||||||
|
* `package.json` to contain the date. For example, `0.90.9-beta` will become `0.90.9-test-YYMMDD-HHMMSS`.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
function processVersion(version) {
|
||||||
|
// Remove the beta suffix if any.
|
||||||
|
version = version.replace("-beta", "");
|
||||||
|
|
||||||
|
// Add the nightly suffix, plus the date.
|
||||||
|
const referenceDate = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.substring(2, 19)
|
||||||
|
.replace(/[-:]*/g, "")
|
||||||
|
.replace("T", "-");
|
||||||
|
version = `${version}-test-${referenceDate}`;
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const packageJsonPath = join(scriptDir, "..", "package.json");
|
||||||
|
|
||||||
|
// Read the version from package.json and process it.
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
const adjustedVersion = processVersion(currentVersion);
|
||||||
|
console.log("Current version is", currentVersion);
|
||||||
|
console.log("Adjusted version is", adjustedVersion);
|
||||||
|
|
||||||
|
// Write the adjusted version back in.
|
||||||
|
packageJson.version = adjustedVersion;
|
||||||
|
const formattedJson = JSON.stringify(packageJson, null, 4);
|
||||||
|
fs.writeFileSync(packageJsonPath, formattedJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,79 @@
|
|||||||
|
import library_loader from "./library_loader.js";
|
||||||
|
import mime_types from "./mime_types.js";
|
||||||
|
import options from "./options.js";
|
||||||
|
|
||||||
|
export function getStylesheetUrl(theme) {
|
||||||
|
if (!theme) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPrefix = "default:";
|
||||||
|
if (theme.startsWith(defaultPrefix)) {
|
||||||
|
return `${window.glob.assetPath}/node_modules/@highlightjs/cdn-assets/styles/${theme.substr(defaultPrefix.length)}.min.css`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
|
||||||
|
*
|
||||||
|
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
|
||||||
|
*/
|
||||||
|
export async function applySyntaxHighlight($container) {
|
||||||
|
if (!isSyntaxHighlightEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
|
||||||
|
|
||||||
|
const codeBlocks = $container.find("pre code");
|
||||||
|
for (const codeBlock of codeBlocks) {
|
||||||
|
$(codeBlock).parent().toggleClass("hljs");
|
||||||
|
|
||||||
|
const text = codeBlock.innerText;
|
||||||
|
|
||||||
|
const normalizedMimeType = extractLanguageFromClassList(codeBlock);
|
||||||
|
if (!normalizedMimeType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let highlightedText = null;
|
||||||
|
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
|
||||||
|
highlightedText = hljs.highlightAuto(text);
|
||||||
|
} else if (normalizedMimeType) {
|
||||||
|
const language = mime_types.getHighlightJsNameForMime(normalizedMimeType);
|
||||||
|
highlightedText = hljs.highlight(text, { language });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highlightedText) {
|
||||||
|
codeBlock.innerHTML = highlightedText.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether syntax highlighting should be enabled for code blocks, by querying the value of the `codeblockTheme` option.
|
||||||
|
* @returns whether syntax highlighting should be enabled for code blocks.
|
||||||
|
*/
|
||||||
|
export function isSyntaxHighlightEnabled() {
|
||||||
|
const theme = options.get("codeBlockTheme");
|
||||||
|
return theme && theme !== "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a HTML element, tries to extract the `language-` class name out of it.
|
||||||
|
*
|
||||||
|
* @param {string} el the HTML element from which to extract the language tag.
|
||||||
|
* @returns the normalized MIME type (e.g. `text-css` instead of `language-text-css`).
|
||||||
|
*/
|
||||||
|
function extractLanguageFromClassList(el) {
|
||||||
|
const prefix = "language-";
|
||||||
|
for (const className of el.classList) {
|
||||||
|
if (className.startsWith(prefix)) {
|
||||||
|
return className.substr(prefix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -0,0 +1,354 @@
|
|||||||
|
/*
|
||||||
|
* 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) {
|
||||||
|
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 = "debug";
|
||||||
|
|
||||||
|
let warn = function() {};
|
||||||
|
if (debugLevel >= debugLevels.indexOf("warn")) {
|
||||||
|
warn = console.warn.bind(console, tag + ": ");
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = function() {};
|
||||||
|
if (debugLevel >= debugLevels.indexOf("info")) {
|
||||||
|
info = console.info.bind(console, tag + ": ");
|
||||||
|
}
|
||||||
|
|
||||||
|
let log = function() {};
|
||||||
|
if (debugLevel >= debugLevels.indexOf("log")) {
|
||||||
|
log = console.log.bind(console, tag + ": ");
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbg = function() {};
|
||||||
|
if (debugLevel >= debugLevels.indexOf("debug")) {
|
||||||
|
dbg = console.debug.bind(console, tag + ": ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(e, msg) {
|
||||||
|
console.assert(e, tag + ": " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Should this be scoped to note?
|
||||||
|
let markerCounter = 0;
|
||||||
|
|
||||||
|
function initTextEditor(textEditor) {
|
||||||
|
log("initTextEditor");
|
||||||
|
|
||||||
|
let widget = this;
|
||||||
|
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();
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
dbg("change " + JSON.stringify(change));
|
||||||
|
|
||||||
|
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).
|
||||||
|
log("dirtying inserted codeBlock " + JSON.stringify(codeBlock.toJSON()));
|
||||||
|
dirtyCodeBlocks.add(codeBlock);
|
||||||
|
|
||||||
|
} else if (change.type == "remove" && (change.name == "codeBlock")) {
|
||||||
|
// 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, 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 + 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 + 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
import { t } from "../../../../services/i18n.js";
|
||||||
|
import library_loader from "../../../../services/library_loader.js";
|
||||||
|
import server from "../../../../services/server.js";
|
||||||
|
import OptionsWidget from "../options_widget.js";
|
||||||
|
|
||||||
|
const SAMPLE_LANGUAGE = "javascript";
|
||||||
|
const SAMPLE_CODE = `\
|
||||||
|
const n = 10;
|
||||||
|
greet(n); // Print "Hello World" for n times
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a "Hello World!" message for a given amount of times, on the standard console. The "Hello World!" text will be displayed once per line.
|
||||||
|
*
|
||||||
|
* @param {number} times The number of times to print the \`Hello World!\` message.
|
||||||
|
*/
|
||||||
|
function greet(times) {
|
||||||
|
for (let i = 0; i++; i < times) {
|
||||||
|
console.log("Hello World!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<div class="options-section">
|
||||||
|
<h4>${t("highlighting.title")}</h4>
|
||||||
|
|
||||||
|
<p>${t("highlighting.description")}</p>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-6">
|
||||||
|
<label>${t("highlighting.color-scheme")}</label>
|
||||||
|
<select class="theme-select form-select"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 side-checkbox">
|
||||||
|
<label class="form-check">
|
||||||
|
<input type="checkbox" class="word-wrap form-check-input" />
|
||||||
|
${t("code_block.word_wrapping")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="note-detail-readonly-text-content ck-content code-sample-wrapper">
|
||||||
|
<pre class="hljs"><code class="code-sample">${SAMPLE_CODE}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code-sample-wrapper {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains appearance settings for code blocks within text notes, such as the theme for the syntax highlighter.
|
||||||
|
*/
|
||||||
|
export default class CodeBlockOptions extends OptionsWidget {
|
||||||
|
doRender() {
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
this.$themeSelect = this.$widget.find(".theme-select");
|
||||||
|
this.$themeSelect.on("change", async () => {
|
||||||
|
const newTheme = this.$themeSelect.val();
|
||||||
|
library_loader.loadHighlightingTheme(newTheme);
|
||||||
|
await server.put(`options/codeBlockTheme/${newTheme}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$wordWrap = this.$widget.find("input.word-wrap");
|
||||||
|
this.$wordWrap.on("change", () => this.updateCheckboxOption("codeBlockWordWrap", this.$wordWrap));
|
||||||
|
|
||||||
|
// Set up preview
|
||||||
|
this.$sampleEl = this.$widget.find(".code-sample");
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupPreview(shouldEnableSyntaxHighlight) {
|
||||||
|
const text = SAMPLE_CODE;
|
||||||
|
if (shouldEnableSyntaxHighlight) {
|
||||||
|
library_loader
|
||||||
|
.requireLibrary(library_loader.HIGHLIGHT_JS)
|
||||||
|
.then(() => {
|
||||||
|
const highlightedText = hljs.highlight(text, {
|
||||||
|
language: SAMPLE_LANGUAGE
|
||||||
|
});
|
||||||
|
this.$sampleEl.html(highlightedText.value);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$sampleEl.text(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async optionsLoaded(options) {
|
||||||
|
const themeGroups = await server.get("options/codeblock-themes");
|
||||||
|
this.$themeSelect.empty();
|
||||||
|
|
||||||
|
for (const [ key, themes ] of Object.entries(themeGroups)) {
|
||||||
|
const $group = (key ? $("<optgroup>").attr("label", key) : null);
|
||||||
|
|
||||||
|
for (const theme of themes) {
|
||||||
|
const option = $("<option>")
|
||||||
|
.attr("value", theme.val)
|
||||||
|
.text(theme.title);
|
||||||
|
|
||||||
|
if ($group) {
|
||||||
|
$group.append(option);
|
||||||
|
} else {
|
||||||
|
this.$themeSelect.append(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$themeSelect.append($group);
|
||||||
|
}
|
||||||
|
this.$themeSelect.val(options.codeBlockTheme);
|
||||||
|
this.setCheckboxState(this.$wordWrap, options.codeBlockWordWrap);
|
||||||
|
this.$widget.closest(".note-detail-printable").toggleClass("word-wrap", options.codeBlockWordWrap === "true");
|
||||||
|
|
||||||
|
this.#setupPreview(options.codeBlockTheme !== "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* @module
|
||||||
|
*
|
||||||
|
* Manages the server-side functionality of the code blocks feature, mostly for obtaining the available themes for syntax highlighting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import themeNames from "./code_block_theme_names.json" with { type: "json" }
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { join } from "path";
|
||||||
|
import { getResourceDir } from "./utils.js";
|
||||||
|
import env from "./env.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a color scheme for the code block syntax highlight.
|
||||||
|
*/
|
||||||
|
interface ColorTheme {
|
||||||
|
/** The ID of the color scheme which should be stored in the options. */
|
||||||
|
val: string;
|
||||||
|
/** A user-friendly name of the theme. The name is already localized. */
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the supported syntax highlighting themes for code blocks, in groups.
|
||||||
|
*
|
||||||
|
* The return value is an object where the keys represent groups in their human-readable name (e.g. "Light theme")
|
||||||
|
* and the values are an array containing the information about every theme. There is also a special group with no
|
||||||
|
* title (empty string) which should be displayed at the top of the listing pages, without a group.
|
||||||
|
*
|
||||||
|
* @returns the supported themes, grouped.
|
||||||
|
*/
|
||||||
|
export function listSyntaxHighlightingThemes() {
|
||||||
|
const stylesDir = (env.isDev() ? "node_modules/@highlightjs/cdn-assets/styles" : "styles");
|
||||||
|
const path = join(getResourceDir(), stylesDir);
|
||||||
|
const systemThemes = readThemesFromFileSystem(path);
|
||||||
|
|
||||||
|
return {
|
||||||
|
"": [
|
||||||
|
{
|
||||||
|
val: "none",
|
||||||
|
title: t("code_block.theme_none")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
...groupThemesByLightOrDark(systemThemes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads all the predefined themes by listing all minified CSSes from a given directory.
|
||||||
|
*
|
||||||
|
* The theme names are mapped against a known list in order to provide more descriptive names such as "Visual Studio 2015 (Dark)" instead of "vs2015".
|
||||||
|
*
|
||||||
|
* @param path the path to read from. Usually this is the highlight.js `styles` directory.
|
||||||
|
* @returns the list of themes.
|
||||||
|
*/
|
||||||
|
function readThemesFromFileSystem(path: string): ColorTheme[] {
|
||||||
|
return fs.readdirSync(path)
|
||||||
|
.filter((el) => el.endsWith(".min.css"))
|
||||||
|
.map((name) => {
|
||||||
|
const nameWithoutExtension = name.replace(".min.css", "");
|
||||||
|
let title = nameWithoutExtension.replace(/-/g, " ");
|
||||||
|
|
||||||
|
if (title in themeNames) {
|
||||||
|
title = (themeNames as Record<string, string>)[title];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
val: `default:${nameWithoutExtension}`,
|
||||||
|
title: title
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups a list of themes by dark or light themes. This is done simply by checking whether "Dark" is present in the given theme, otherwise it's considered a light theme.
|
||||||
|
* This generally only works if the theme has a known human-readable name (see {@link #readThemesFromFileSystem()})
|
||||||
|
*
|
||||||
|
* @param listOfThemes the list of themes to be grouped.
|
||||||
|
* @returns the grouped themes by light or dark.
|
||||||
|
*/
|
||||||
|
function groupThemesByLightOrDark(listOfThemes: ColorTheme[]) {
|
||||||
|
const darkThemes = [];
|
||||||
|
const lightThemes = [];
|
||||||
|
|
||||||
|
for (const theme of listOfThemes) {
|
||||||
|
if (theme.title.includes("Dark")) {
|
||||||
|
darkThemes.push(theme);
|
||||||
|
} else {
|
||||||
|
lightThemes.push(theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: Record<string, ColorTheme[]> = {};
|
||||||
|
output[t("code_block.theme_group_light")] = lightThemes;
|
||||||
|
output[t("code_block.theme_group_dark")] = darkThemes;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"1c light": "1C (Light)",
|
||||||
|
"a11y dark": "a11y (Dark)",
|
||||||
|
"a11y light": "a11y (Light)",
|
||||||
|
"agate": "Agate (Dark)",
|
||||||
|
"an old hope": "An Old Hope (Dark)",
|
||||||
|
"androidstudio": "Android Studio (Dark)",
|
||||||
|
"arduino light": "Arduino (Light)",
|
||||||
|
"arta": "Arta (Dark)",
|
||||||
|
"ascetic": "Ascetic (Light)",
|
||||||
|
"atom one dark reasonable": "Atom One with ReasonML support (Dark)",
|
||||||
|
"atom one dark": "Atom One (Dark)",
|
||||||
|
"atom one light": "Atom One (Light)",
|
||||||
|
"brown paper": "Brown Paper (Light)",
|
||||||
|
"codepen embed": "CodePen Embed (Dark)",
|
||||||
|
"color brewer": "Color Brewer (Light)",
|
||||||
|
"dark": "Dark",
|
||||||
|
"default": "Original highlight.js Theme (Light)",
|
||||||
|
"devibeans": "devibeans (Dark)",
|
||||||
|
"docco": "Docco (Light)",
|
||||||
|
"far": "FAR (Dark)",
|
||||||
|
"felipec": "FelipeC (Dark)",
|
||||||
|
"foundation": "Foundation 4 Docs (Light)",
|
||||||
|
"github dark dimmed": "GitHub Dimmed (Dark)",
|
||||||
|
"github dark": "GitHub (Dark)",
|
||||||
|
"github": "GitHub (Light)",
|
||||||
|
"gml": "GML (Dark)",
|
||||||
|
"googlecode": "Google Code (Light)",
|
||||||
|
"gradient dark": "Gradient (Dark)",
|
||||||
|
"gradient light": "Gradient (Light)",
|
||||||
|
"grayscale": "Grayscale (Light)",
|
||||||
|
"hybrid": "hybrid (Dark)",
|
||||||
|
"idea": "Idea (Light)",
|
||||||
|
"intellij light": "IntelliJ (Light)",
|
||||||
|
"ir black": "IR Black (Dark)",
|
||||||
|
"isbl editor dark": "ISBL Editor (Dark)",
|
||||||
|
"isbl editor light": "ISBL Editor (Light)",
|
||||||
|
"kimbie dark": "Kimbie (Dark)",
|
||||||
|
"kimbie light": "Kimbie (Light)",
|
||||||
|
"lightfair": "Lightfair (Light)",
|
||||||
|
"lioshi": "Lioshi (Dark)",
|
||||||
|
"magula": "Magula (Light)",
|
||||||
|
"mono blue": "Mono Blue (Light)",
|
||||||
|
"monokai sublime": "Monokai Sublime (Dark)",
|
||||||
|
"monokai": "Monokai (Dark)",
|
||||||
|
"night owl": "Night Owl (Dark)",
|
||||||
|
"nnfx dark": "NNFX (Dark)",
|
||||||
|
"nnfx light": "NNFX (Light)",
|
||||||
|
"nord": "Nord (Dark)",
|
||||||
|
"obsidian": "Obsidian (Dark)",
|
||||||
|
"panda syntax dark": "Panda (Dark)",
|
||||||
|
"panda syntax light": "Panda (Light)",
|
||||||
|
"paraiso dark": "Paraiso (Dark)",
|
||||||
|
"paraiso light": "Paraiso (Light)",
|
||||||
|
"pojoaque": "Pojoaque (Dark)",
|
||||||
|
"purebasic": "PureBasic (Light)",
|
||||||
|
"qtcreator dark": "Qt Creator (Dark)",
|
||||||
|
"qtcreator light": "Qt Creator (Light)",
|
||||||
|
"rainbow": "Rainbow (Dark)",
|
||||||
|
"routeros": "RouterOS Script (Light)",
|
||||||
|
"school book": "School Book (Light)",
|
||||||
|
"shades of purple": "Shades of Purple (Dark)",
|
||||||
|
"srcery": "Srcery (Dark)",
|
||||||
|
"stackoverflow dark": "Stack Overflow (Dark)",
|
||||||
|
"stackoverflow light": "Stack Overflow (Light)",
|
||||||
|
"sunburst": "Sunburst (Dark)",
|
||||||
|
"tokyo night dark": "Tokyo Night (Dark)",
|
||||||
|
"tokyo night light": "Tokyo Night (Light)",
|
||||||
|
"tomorrow night blue": "Tomorrow Night Blue (Dark)",
|
||||||
|
"tomorrow night bright": "Tomorrow Night Bright (Dark)",
|
||||||
|
"vs": "Visual Studio (Light)",
|
||||||
|
"vs2015": "Visual Studio 2015 (Dark)",
|
||||||
|
"xcode": "Xcode (Light)",
|
||||||
|
"xt256": "xt256 (Dark)"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue