mirror of
https://github.com/zadam/trilium.git
synced 2026-03-28 06:40:17 +01:00
Compare commits
1 Commits
feature/st
...
standalone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ec2160eff |
@@ -1,18 +0,0 @@
|
||||
import { type ExportFormat, type ZipExportProviderData, ZipExportProvider } from "@triliumnext/core";
|
||||
|
||||
import contentCss from "@triliumnext/ckeditor5/src/theme/ck-content.css?raw";
|
||||
|
||||
export async function standaloneZipExportProviderFactory(format: ExportFormat, data: ZipExportProviderData): Promise<ZipExportProvider> {
|
||||
switch (format) {
|
||||
case "html": {
|
||||
const { default: HtmlExportProvider } = await import("@triliumnext/core/src/services/export/zip/html.js");
|
||||
return new HtmlExportProvider(data, { contentCss });
|
||||
}
|
||||
case "markdown": {
|
||||
const { default: MarkdownExportProvider } = await import("@triliumnext/core/src/services/export/zip/markdown.js");
|
||||
return new MarkdownExportProvider(data);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported export format: '${format}'`);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,7 @@
|
||||
import type { FileStream, ZipArchive, ZipEntry, ZipProvider } from "@triliumnext/core/src/services/zip_provider.js";
|
||||
import { strToU8, unzip, zipSync } from "fflate";
|
||||
|
||||
type ZipOutput = {
|
||||
send?: (body: unknown) => unknown;
|
||||
write?: (chunk: Uint8Array | string) => unknown;
|
||||
end?: (chunk?: Uint8Array | string) => unknown;
|
||||
};
|
||||
|
||||
class BrowserZipArchive implements ZipArchive {
|
||||
readonly #entries: Record<string, Uint8Array> = {};
|
||||
#destination: ZipOutput | null = null;
|
||||
|
||||
append(content: string | Uint8Array, options: { name: string }) {
|
||||
this.#entries[options.name] = typeof content === "string" ? strToU8(content) : content;
|
||||
}
|
||||
|
||||
pipe(destination: unknown) {
|
||||
this.#destination = destination as ZipOutput;
|
||||
}
|
||||
|
||||
async finalize(): Promise<void> {
|
||||
if (!this.#destination) {
|
||||
throw new Error("ZIP output destination not set.");
|
||||
}
|
||||
|
||||
const content = zipSync(this.#entries, { level: 9 });
|
||||
|
||||
if (typeof this.#destination.send === "function") {
|
||||
this.#destination.send(content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.#destination.end === "function") {
|
||||
if (typeof this.#destination.write === "function") {
|
||||
this.#destination.write(content);
|
||||
this.#destination.end();
|
||||
} else {
|
||||
this.#destination.end(content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Unsupported ZIP output destination.");
|
||||
}
|
||||
}
|
||||
import type { ZipEntry, ZipProvider } from "@triliumnext/core/src/services/import/zip_provider.js";
|
||||
import { unzip } from "fflate";
|
||||
|
||||
export default class BrowserZipProvider implements ZipProvider {
|
||||
createZipArchive(): ZipArchive {
|
||||
return new BrowserZipArchive();
|
||||
}
|
||||
|
||||
createFileStream(_filePath: string): FileStream {
|
||||
throw new Error("File stream creation is not supported in the browser.");
|
||||
}
|
||||
|
||||
readZipFile(
|
||||
buffer: Uint8Array,
|
||||
processEntry: (entry: ZipEntry, readContent: () => Promise<Uint8Array>) => Promise<void>
|
||||
|
||||
@@ -157,7 +157,6 @@ async function initialize(): Promise<void> {
|
||||
executionContext: new BrowserExecutionContext(),
|
||||
crypto: new BrowserCryptoProvider(),
|
||||
zip: new BrowserZipProvider(),
|
||||
zipExportProviderFactory: (await import("./lightweight/zip_export_provider_factory.js")).standaloneZipExportProviderFactory,
|
||||
messaging: messagingProvider!,
|
||||
request: new FetchRequestProvider(),
|
||||
platform: new StandalonePlatformProvider(queryString),
|
||||
|
||||
@@ -162,13 +162,6 @@ self.addEventListener("fetch", (event) => {
|
||||
// Only handle same-origin
|
||||
if (url.origin !== self.location.origin) return;
|
||||
|
||||
// API-ish: local-first via bridge (must be checked before navigate handling,
|
||||
// because export triggers a navigation to an /api/ URL)
|
||||
if (isLocalFirst(url)) {
|
||||
event.respondWith(forwardToClientLocalServer(event.request, event.clientId));
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML files: network-first to ensure updates are reflected immediately
|
||||
if (event.request.mode === "navigate" || url.pathname.endsWith(".html")) {
|
||||
event.respondWith(networkFirst(event.request));
|
||||
@@ -176,11 +169,17 @@ self.addEventListener("fetch", (event) => {
|
||||
}
|
||||
|
||||
// Static assets: cache-first for performance
|
||||
if (event.request.method === "GET") {
|
||||
if (event.request.method === "GET" && !isLocalFirst(url)) {
|
||||
event.respondWith(cacheFirst(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// API-ish: local-first via bridge
|
||||
if (isLocalFirst(url)) {
|
||||
event.respondWith(forwardToClientLocalServer(event.request, event.clientId));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default
|
||||
event.respondWith(fetch(event.request));
|
||||
});
|
||||
|
||||
@@ -135,7 +135,6 @@ async function main() {
|
||||
},
|
||||
crypto: new NodejsCryptoProvider(),
|
||||
zip: new NodejsZipProvider(),
|
||||
zipExportProviderFactory: (await import("@triliumnext/server/src/services/export/zip/factory.js")).serverZipExportProviderFactory,
|
||||
request: new NodeRequestProvider(),
|
||||
executionContext: new ClsHookedExecutionContext(),
|
||||
messaging: new WebSocketMessagingProvider(),
|
||||
|
||||
@@ -54,8 +54,8 @@ async function registerHandlers() {
|
||||
}
|
||||
|
||||
async function exportData() {
|
||||
const { zipExportService } = (await import("@triliumnext/core"));
|
||||
await zipExportService.exportToZipFile("root", "html", DEMO_ZIP_PATH);
|
||||
const { exportToZipFile } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
|
||||
await exportToZipFile("root", "html", DEMO_ZIP_PATH);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import debounce from "@triliumnext/client/src/services/debounce.js";
|
||||
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/core";
|
||||
import cls from "@triliumnext/server/src/services/cls.js";
|
||||
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/server/src/services/export/zip/abstract_provider.js";
|
||||
import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js";
|
||||
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
|
||||
import type { NoteMetaFile } from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
@@ -10,7 +11,7 @@ import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { extractZip, importData, startElectron } from "./utils.js";
|
||||
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
|
||||
|
||||
interface NoteMapping {
|
||||
rootNoteId: string;
|
||||
@@ -152,7 +153,7 @@ async function exportData(noteId: string, format: ExportFormat, outputPath: stri
|
||||
await fsExtra.mkdir(outputPath);
|
||||
|
||||
// First export as zip.
|
||||
const { zipExportService } = (await import("@triliumnext/core"));
|
||||
const { exportToZipFile } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
|
||||
|
||||
const exportOpts: AdvancedExportOptions = {};
|
||||
if (format === "html") {
|
||||
@@ -204,7 +205,7 @@ async function exportData(noteId: string, format: ExportFormat, outputPath: stri
|
||||
};
|
||||
}
|
||||
|
||||
await zipExportService.exportToZipFile(noteId, format, zipFilePath, exportOpts);
|
||||
await exportToZipFile(noteId, format, zipFilePath, exportOpts);
|
||||
await extractZip(zipFilePath, outputPath, ignoredFiles);
|
||||
} finally {
|
||||
if (await fsExtra.exists(zipFilePath)) {
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@triliumnext/core": "workspace:*",
|
||||
"@triliumnext/express-partial-content": "workspace:*",
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/turndown-plugin-gfm": "workspace:*",
|
||||
"@types/archiver": "7.0.0",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cls-hooked": "4.3.9",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { beforeAll } from "vitest";
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { initializeCore } from "@triliumnext/core";
|
||||
import { serverZipExportProviderFactory } from "../src/services/export/zip/factory.js";
|
||||
import ClsHookedExecutionContext from "../src/cls_provider.js";
|
||||
import NodejsCryptoProvider from "../src/crypto_provider.js";
|
||||
import NodejsZipProvider from "../src/zip_provider.js";
|
||||
@@ -30,7 +29,6 @@ beforeAll(async () => {
|
||||
},
|
||||
crypto: new NodejsCryptoProvider(),
|
||||
zip: new NodejsZipProvider(),
|
||||
zipExportProviderFactory: serverZipExportProviderFactory,
|
||||
executionContext: new ClsHookedExecutionContext(),
|
||||
schema: readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
|
||||
platform: new ServerPlatformProvider(),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { type ExportFormat, NoteParams, SearchParams, zipExportService, zipImportService } from "@triliumnext/core";
|
||||
import { NoteParams, SearchParams, zipImportService } from "@triliumnext/core";
|
||||
import type { Request, Router } from "express";
|
||||
import type { ParsedQs } from "qs";
|
||||
|
||||
import becca from "../becca/becca.js";
|
||||
import zipExportService from "../services/export/zip.js";
|
||||
import type { ExportFormat } from "../services/export/zip/abstract_provider.js";
|
||||
import noteService from "../services/notes.js";
|
||||
import SearchContext from "../services/search/search_context.js";
|
||||
import searchService from "../services/search/services/search.js";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* are loaded later and will result in an empty string.
|
||||
*/
|
||||
|
||||
import { getLog, initializeCore, sql_init } from "@triliumnext/core";
|
||||
import { getLog,initializeCore, sql_init } from "@triliumnext/core";
|
||||
import fs from "fs";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
@@ -53,7 +53,6 @@ async function startApplication() {
|
||||
},
|
||||
crypto: new NodejsCryptoProvider(),
|
||||
zip: new NodejsZipProvider(),
|
||||
zipExportProviderFactory: (await import("./services/export/zip/factory.js")).serverZipExportProviderFactory,
|
||||
request: new NodeRequestProvider(),
|
||||
executionContext: new ClsHookedExecutionContext(),
|
||||
messaging: new WebSocketMessagingProvider(),
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { NotFoundError, ValidationError } from "@triliumnext/core";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import opmlExportService from "../../services/export/opml.js";
|
||||
import singleExportService from "../../services/export/single.js";
|
||||
import zipExportService from "../../services/export/zip.js";
|
||||
import { getLog } from "../../services/log.js";
|
||||
import log from "../../services/log.js";
|
||||
import TaskContext from "../../services/task_context.js";
|
||||
import { safeExtractMessageAndStackFromError } from "../../services/utils/index.js";
|
||||
import { NotFoundError, ValidationError } from "../../errors.js";
|
||||
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
|
||||
|
||||
async function exportBranch(req: Request<{ branchId: string; type: string; format: string; version: string; taskId: string }>, res: Response) {
|
||||
function exportBranch(req: Request<{ branchId: string; type: string; format: string; version: string; taskId: string }>, res: Response) {
|
||||
const { branchId, type, format, version, taskId } = req.params;
|
||||
const branch = becca.getBranch(branchId);
|
||||
|
||||
if (!branch) {
|
||||
const message = `Cannot export branch '${branchId}' since it does not exist.`;
|
||||
getLog().error(message);
|
||||
log.error(message);
|
||||
|
||||
res.setHeader("Content-Type", "text/plain").status(500).send(message);
|
||||
return;
|
||||
@@ -25,7 +25,7 @@ async function exportBranch(req: Request<{ branchId: string; type: string; forma
|
||||
|
||||
try {
|
||||
if (type === "subtree" && (format === "html" || format === "markdown" || format === "share")) {
|
||||
await zipExportService.exportToZip(taskContext, branch, format, res);
|
||||
zipExportService.exportToZip(taskContext, branch, format, res);
|
||||
} else if (type === "single") {
|
||||
if (format !== "html" && format !== "markdown") {
|
||||
throw new ValidationError("Invalid export type.");
|
||||
@@ -41,7 +41,7 @@ async function exportBranch(req: Request<{ branchId: string; type: string; forma
|
||||
const message = `Export failed with following error: '${errMessage}'. More details might be in the logs.`;
|
||||
taskContext.reportError(message);
|
||||
|
||||
getLog().error(errMessage + errStack);
|
||||
log.error(errMessage + errStack);
|
||||
|
||||
res.setHeader("Content-Type", "text/plain").status(500).send(message);
|
||||
}
|
||||
30
apps/server/src/routes/api/other.ts
Normal file
30
apps/server/src/routes/api/other.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { RenderMarkdownResponse, ToMarkdownResponse } from "@triliumnext/commons";
|
||||
import { markdownImportService } from "@triliumnext/core";
|
||||
import type { Request } from "express";
|
||||
|
||||
import markdown from "../../services/export/markdown.js";
|
||||
|
||||
function renderMarkdown(req: Request) {
|
||||
const { markdownContent } = req.body;
|
||||
if (!markdownContent || typeof markdownContent !== 'string') {
|
||||
throw new Error('markdownContent parameter is required and must be a string');
|
||||
}
|
||||
return {
|
||||
htmlContent: markdownImportService.renderToHtml(markdownContent, "")
|
||||
} satisfies RenderMarkdownResponse;
|
||||
}
|
||||
|
||||
function toMarkdown(req: Request) {
|
||||
const { htmlContent } = req.body;
|
||||
if (!htmlContent || typeof htmlContent !== 'string') {
|
||||
throw new Error('htmlContent parameter is required and must be a string');
|
||||
}
|
||||
return {
|
||||
markdownContent: markdown.toMarkdown(htmlContent)
|
||||
} satisfies ToMarkdownResponse;
|
||||
}
|
||||
|
||||
export default {
|
||||
renderMarkdown,
|
||||
toMarkdown
|
||||
};
|
||||
@@ -22,10 +22,12 @@ import backendLogRoute from "./api/backend_log.js";
|
||||
import clipperRoute from "./api/clipper.js";
|
||||
import databaseRoute from "./api/database.js";
|
||||
import etapiTokensApiRoutes from "./api/etapi_tokens.js";
|
||||
import exportRoute from "./api/export.js";
|
||||
import filesRoute from "./api/files.js";
|
||||
import fontsRoute from "./api/fonts.js";
|
||||
import loginApiRoute from "./api/login.js";
|
||||
import metricsRoute from "./api/metrics.js";
|
||||
import otherRoute from "./api/other.js";
|
||||
import passwordApiRoute from "./api/password.js";
|
||||
import recoveryCodes from './api/recovery_codes.js';
|
||||
import scriptRoute from "./api/script.js";
|
||||
@@ -128,6 +130,10 @@ function register(app: express.Application) {
|
||||
// TODO: Re-enable once we support route()
|
||||
// route(GET, "/api/revisions/:revisionId/download", [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision);
|
||||
|
||||
route(GET, "/api/branches/:branchId/export/:type/:format/:version/:taskId", [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
|
||||
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
|
||||
|
||||
apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword);
|
||||
apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword);
|
||||
|
||||
@@ -192,6 +198,8 @@ function register(app: express.Application) {
|
||||
|
||||
asyncApiRoute(GET, "/api/backend-log", backendLogRoute.getBackendLog);
|
||||
route(GET, "/api/fonts", [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
apiRoute(PST, "/api/other/render-markdown", otherRoute.renderMarkdown);
|
||||
apiRoute(PST, "/api/other/to-markdown", otherRoute.toMarkdown);
|
||||
|
||||
shareRoutes.register(router);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type AttributeRow, dayjs, formatLogMessage } from "@triliumnext/commons";
|
||||
import { type AbstractBeccaEntity, Becca, branches as branchService, NoteParams, SearchContext, sync_mutex as syncMutex, zipExportService } from "@triliumnext/core";
|
||||
import { type AbstractBeccaEntity, Becca, branches as branchService, NoteParams, SearchContext, sync_mutex as syncMutex } from "@triliumnext/core";
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import xml2js from "xml2js";
|
||||
@@ -19,6 +19,7 @@ import backupService from "./backup.js";
|
||||
import cloningService from "./cloning.js";
|
||||
import config from "./config.js";
|
||||
import dateNoteService from "./date_notes.js";
|
||||
import exportService from "./export/zip.js";
|
||||
import log from "./log.js";
|
||||
import noteService from "./notes.js";
|
||||
import optionsService from "./options.js";
|
||||
@@ -661,7 +662,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
return { note: launcherNote };
|
||||
};
|
||||
|
||||
this.exportSubtreeToZipFile = async (noteId, format, zipFilePath) => await zipExportService.exportToZipFile(noteId, format, zipFilePath);
|
||||
this.exportSubtreeToZipFile = async (noteId, format, zipFilePath) => await exportService.exportToZipFile(noteId, format, zipFilePath);
|
||||
|
||||
this.runOnFrontend = async (_script, params = []) => {
|
||||
let script: string;
|
||||
|
||||
284
apps/server/src/services/export/markdown.ts
Normal file
284
apps/server/src/services/export/markdown.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { gfm } from "@triliumnext/turndown-plugin-gfm";
|
||||
import Turnish, { type Rule } from "turnish";
|
||||
|
||||
let instance: Turnish | null = null;
|
||||
|
||||
// TODO: Move this to a dedicated file someday.
|
||||
export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
note: "NOTE",
|
||||
tip: "TIP",
|
||||
important: "IMPORTANT",
|
||||
caution: "CAUTION",
|
||||
warning: "WARNING"
|
||||
};
|
||||
|
||||
export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note;
|
||||
|
||||
const fencedCodeBlockFilter: Rule = {
|
||||
filter (node, options) {
|
||||
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
|
||||
},
|
||||
|
||||
replacement (content, node, options) {
|
||||
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
|
||||
return content;
|
||||
}
|
||||
|
||||
const className = node.firstChild.getAttribute("class") || "";
|
||||
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
|
||||
|
||||
return `\n\n${options.fence}${language}\n${node.firstChild.textContent}\n${options.fence}\n\n`;
|
||||
}
|
||||
};
|
||||
|
||||
function toMarkdown(content: string) {
|
||||
if (instance === null) {
|
||||
instance = new Turnish({
|
||||
headingStyle: "atx",
|
||||
bulletListMarker: "*",
|
||||
emDelimiter: "_",
|
||||
codeBlockStyle: "fenced",
|
||||
blankReplacement(_content, node) {
|
||||
if (node.nodeName === "SECTION" && node.classList.contains("include-note")) {
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
// Original implementation as per https://github.com/mixmark-io/turndown/blob/master/src/turndown.js.
|
||||
return ("isBlock" in node && node.isBlock) ? '\n\n' : '';
|
||||
},
|
||||
});
|
||||
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
|
||||
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
|
||||
instance.addRule("img", buildImageFilter());
|
||||
instance.addRule("admonition", buildAdmonitionFilter());
|
||||
instance.addRule("inlineLink", buildInlineLinkFilter());
|
||||
instance.addRule("figure", buildFigureFilter());
|
||||
instance.addRule("math", buildMathFilter());
|
||||
instance.addRule("li", buildListItemFilter());
|
||||
instance.use(gfm);
|
||||
instance.keep([ "kbd", "sup", "sub" ]);
|
||||
}
|
||||
|
||||
return instance.render(content);
|
||||
}
|
||||
|
||||
function rewriteLanguageTag(source: string) {
|
||||
if (!source) {
|
||||
return source;
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
case "text-x-trilium-auto":
|
||||
return "";
|
||||
case "application-javascript-env-frontend":
|
||||
case "application-javascript-env-backend":
|
||||
return "javascript";
|
||||
case "text-x-nginx-conf":
|
||||
return "nginx";
|
||||
default:
|
||||
return source.split("-").at(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove once upstream delivers a fix for https://github.com/mixmark-io/turndown/issues/467.
|
||||
function buildImageFilter() {
|
||||
const ESCAPE_PATTERNS = {
|
||||
before: /([\\*`[\]_]|(?:^[-+>])|(?:^~~~)|(?:^#{1-6}))/g,
|
||||
after: /((?:^\d+(?=\.)))/
|
||||
};
|
||||
|
||||
const escapePattern = new RegExp(`(?:${ESCAPE_PATTERNS.before.source}|${ESCAPE_PATTERNS.after.source})`, 'g');
|
||||
|
||||
function escapeMarkdown (content: string) {
|
||||
return content.replace(escapePattern, (match, before, after) => {
|
||||
return before ? `\\${before}` : `${after}\\`;
|
||||
});
|
||||
}
|
||||
|
||||
function escapeLinkDestination(destination: string) {
|
||||
return destination
|
||||
.replace(/([()])/g, '\\$1')
|
||||
.replace(/ /g, "%20");
|
||||
}
|
||||
|
||||
function escapeLinkTitle (title: string) {
|
||||
return title.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
const imageFilter: Rule = {
|
||||
filter: "img",
|
||||
replacement(content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
|
||||
// Preserve image verbatim if it has a width or height attribute.
|
||||
if (node.hasAttribute("width") || node.hasAttribute("height")) {
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with upstream.
|
||||
const untypedNode = (node as any);
|
||||
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')));
|
||||
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '');
|
||||
const title = cleanAttribute(untypedNode.getAttribute('title'));
|
||||
const titlePart = title ? ` "${escapeLinkTitle(title)}"` : '';
|
||||
|
||||
return src ? `` : '';
|
||||
}
|
||||
};
|
||||
return imageFilter;
|
||||
}
|
||||
|
||||
function buildAdmonitionFilter() {
|
||||
function parseAdmonitionType(_node: Node) {
|
||||
if (!("getAttribute" in _node)) {
|
||||
return DEFAULT_ADMONITION_TYPE;
|
||||
}
|
||||
|
||||
const node = _node as Element;
|
||||
const classList = node.getAttribute("class")?.split(" ") ?? [];
|
||||
|
||||
for (const className of classList) {
|
||||
if (className === "admonition") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mappedType = ADMONITION_TYPE_MAPPINGS[className];
|
||||
if (mappedType) {
|
||||
return mappedType;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_ADMONITION_TYPE;
|
||||
}
|
||||
|
||||
const admonitionFilter: Rule = {
|
||||
filter(node, options) {
|
||||
return node.nodeName === "ASIDE" && node.classList.contains("admonition");
|
||||
},
|
||||
replacement(content, node) {
|
||||
// Parse the admonition type.
|
||||
const admonitionType = parseAdmonitionType(node);
|
||||
|
||||
content = content.replace(/^\n+|\n+$/g, '');
|
||||
content = content.replace(/^/gm, '> ');
|
||||
content = `> [!${admonitionType}]\n${content}`;
|
||||
|
||||
return `\n\n${content}\n\n`;
|
||||
}
|
||||
};
|
||||
return admonitionFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variation of the original ruleset: https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
|
||||
*
|
||||
* Detects if the URL is a Trilium reference link and returns it verbatim if that's the case.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function buildInlineLinkFilter(): Rule {
|
||||
return {
|
||||
filter (node, options) {
|
||||
return (
|
||||
options.linkStyle === 'inlined' &&
|
||||
node.nodeName === 'A' &&
|
||||
!!node.getAttribute('href')
|
||||
);
|
||||
},
|
||||
|
||||
replacement (content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
|
||||
// Return reference links verbatim.
|
||||
if (node.classList.contains("reference-link")) {
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
// Otherwise treat as normal.
|
||||
// TODO: Call super() somehow instead of duplicating the implementation.
|
||||
let href = node.getAttribute('href');
|
||||
if (href) href = href.replace(/([()])/g, '\\$1');
|
||||
let title = cleanAttribute(node.getAttribute('title'));
|
||||
if (title) title = ` "${title.replace(/"/g, '\\"')}"`;
|
||||
return `[${content}](${href}${title})`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildFigureFilter(): Rule {
|
||||
return {
|
||||
filter(node, options) {
|
||||
return node.nodeName === 'FIGURE'
|
||||
&& node.classList.contains("image");
|
||||
},
|
||||
replacement(content, node) {
|
||||
return (node as HTMLElement).outerHTML;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Keep in line with https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
|
||||
function buildListItemFilter(): Rule {
|
||||
return {
|
||||
filter: "li",
|
||||
replacement(content, node, options) {
|
||||
content = content
|
||||
.trim()
|
||||
.replace(/\n/gm, '\n '); // indent
|
||||
let prefix = `${options.bulletListMarker} `;
|
||||
const parent = node.parentNode as HTMLElement;
|
||||
if (parent.nodeName === 'OL') {
|
||||
const start = parent.getAttribute('start');
|
||||
const index = Array.prototype.indexOf.call(parent.children, node);
|
||||
prefix = `${start ? Number(start) + index : index + 1}. `;
|
||||
} else if (parent.classList.contains("todo-list")) {
|
||||
const isChecked = node.querySelector("input[type=checkbox]:checked");
|
||||
prefix = (isChecked ? "- [x] " : "- [ ] ");
|
||||
}
|
||||
|
||||
const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildMathFilter(): Rule {
|
||||
const MATH_INLINE_PREFIX = "\\(";
|
||||
const MATH_INLINE_SUFFIX = "\\)";
|
||||
|
||||
const MATH_DISPLAY_PREFIX = "\\[";
|
||||
const MATH_DISPLAY_SUFFIX = "\\]";
|
||||
|
||||
return {
|
||||
filter(node) {
|
||||
return node.nodeName === "SPAN" && node.classList.contains("math-tex");
|
||||
},
|
||||
replacement(_, node) {
|
||||
// We have to use the raw HTML text, otherwise the content is escaped too much.
|
||||
const content = (node as HTMLElement).innerText;
|
||||
|
||||
// Inline math
|
||||
if (content.startsWith(MATH_INLINE_PREFIX) && content.endsWith(MATH_INLINE_SUFFIX)) {
|
||||
return `$${content.substring(MATH_INLINE_PREFIX.length, content.length - MATH_INLINE_SUFFIX.length)}$`;
|
||||
}
|
||||
|
||||
// Display math
|
||||
if (content.startsWith(MATH_DISPLAY_PREFIX) && content.endsWith(MATH_DISPLAY_SUFFIX)) {
|
||||
return `$$${content.substring(MATH_DISPLAY_PREFIX.length, content.length - MATH_DISPLAY_SUFFIX.length)}$$`;
|
||||
}
|
||||
|
||||
// Unknown.
|
||||
return content;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Taken from upstream since it's not exposed.
|
||||
// https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js
|
||||
function cleanAttribute(attribute: string | null | undefined) {
|
||||
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
|
||||
}
|
||||
|
||||
export default {
|
||||
toMarkdown
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { utils } from "@triliumnext/core";
|
||||
import type { Response } from "express";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import type BBranch from "../../becca/entities/bbranch.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import { getContentDisposition, stripTags } from "../utils/index.js";
|
||||
|
||||
function exportToOpml(taskContext: TaskContext<"export">, branch: BBranch, version: string, res: Response) {
|
||||
if (!["1.0", "2.0"].includes(version)) {
|
||||
@@ -58,7 +58,7 @@ function exportToOpml(taskContext: TaskContext<"export">, branch: BBranch, versi
|
||||
|
||||
const filename = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.title}.opml`;
|
||||
|
||||
res.setHeader("Content-Disposition", getContentDisposition(filename));
|
||||
res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
|
||||
res.setHeader("Content-Type", "text/x-opml");
|
||||
|
||||
res.write(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -82,7 +82,7 @@ function exportToOpml(taskContext: TaskContext<"export">, branch: BBranch, versi
|
||||
function prepareText(text: string) {
|
||||
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, "\n").replace(/ /g, " "); // nbsp isn't in XML standard (only HTML)
|
||||
|
||||
const stripped = stripTags(newLines);
|
||||
const stripped = utils.stripTags(newLines);
|
||||
|
||||
const escaped = escapeXmlAttribute(stripped);
|
||||
|
||||
@@ -8,10 +8,9 @@ import becca from "../../becca/becca.js";
|
||||
import type BBranch from "../../becca/entities/bbranch.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import { escapeHtml,getContentDisposition } from "../utils/index.js";
|
||||
import { escapeHtml,getContentDisposition } from "../utils.js";
|
||||
import mdService from "./markdown.js";
|
||||
import { ExportFormat } from "../../meta.js";
|
||||
import { encodeBase64 } from "../utils/binary.js";
|
||||
import type { ExportFormat } from "./zip/abstract_provider.js";
|
||||
|
||||
function exportSingleNote(taskContext: TaskContext<"export">, branch: BBranch, format: ExportFormat, res: Response) {
|
||||
const note = branch.getNote();
|
||||
@@ -89,11 +88,11 @@ function inlineAttachments(content: string) {
|
||||
}
|
||||
|
||||
const imageContent = note.getContent();
|
||||
if (typeof imageContent === "string") {
|
||||
if (!Buffer.isBuffer(imageContent)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const base64Content = encodeBase64(imageContent);
|
||||
const base64Content = imageContent.toString("base64");
|
||||
const srcValue = `data:${note.mime};base64,${base64Content}`;
|
||||
|
||||
return `src="${srcValue}"`;
|
||||
@@ -106,11 +105,11 @@ function inlineAttachments(content: string) {
|
||||
}
|
||||
|
||||
const attachmentContent = attachment.getContent();
|
||||
if (typeof attachmentContent === "string") {
|
||||
if (!Buffer.isBuffer(attachmentContent)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const base64Content = encodeBase64(attachmentContent);
|
||||
const base64Content = attachmentContent.toString("base64");
|
||||
const srcValue = `data:${attachment.mime};base64,${base64Content}`;
|
||||
|
||||
return `src="${srcValue}"`;
|
||||
@@ -123,11 +122,11 @@ function inlineAttachments(content: string) {
|
||||
}
|
||||
|
||||
const attachmentContent = attachment.getContent();
|
||||
if (typeof attachmentContent === "string") {
|
||||
if (!Buffer.isBuffer(attachmentContent)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const base64Content = encodeBase64(attachmentContent);
|
||||
const base64Content = attachmentContent.toString("base64");
|
||||
const hrefValue = `data:${attachment.mime};base64,${base64Content}`;
|
||||
|
||||
return `href="${hrefValue}" download="${escapeHtml(attachment.title)}"`;
|
||||
@@ -1,32 +1,43 @@
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { ValidationError } from "@triliumnext/core";
|
||||
import archiver from "archiver";
|
||||
import type { Response } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
import packageInfo from "../../../package.json" with { type: "json" };
|
||||
import becca from "../../becca/becca.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import dateUtils from "../utils/date.js";
|
||||
import { getLog } from "../log.js";
|
||||
import dateUtils from "../date_utils.js";
|
||||
import log from "../log.js";
|
||||
import type AttachmentMeta from "../meta/attachment_meta.js";
|
||||
import type AttributeMeta from "../meta/attribute_meta.js";
|
||||
import type NoteMeta from "../meta/note_meta.js";
|
||||
import type { NoteMetaFile } from "../meta/note_meta.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import TaskContext from "../task_context.js";
|
||||
import { getZipProvider } from "../zip_provider.js";
|
||||
import { getContentDisposition } from "../utils/index"
|
||||
import { AdvancedExportOptions, ZipExportProviderData } from "./zip/abstract_provider.js";
|
||||
import { getZipExportProviderFactory } from "./zip_export_provider_factory.js";
|
||||
import { AttachmentMeta, AttributeMeta, ExportFormat, NoteMeta, NoteMetaFile } from "../../meta";
|
||||
import { ValidationError } from "../../errors";
|
||||
import { extname } from "../utils/path";
|
||||
import { getContentDisposition, waitForStreamToFinish } from "../utils.js";
|
||||
import { AdvancedExportOptions, type ExportFormat, ZipExportProviderData } from "./zip/abstract_provider.js";
|
||||
import HtmlExportProvider from "./zip/html.js";
|
||||
import MarkdownExportProvider from "./zip/markdown.js";
|
||||
import ShareThemeExportProvider from "./zip/share_theme.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, format: ExportFormat, res: Record<string, any>, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
|
||||
const archive = getZipProvider().createZipArchive();
|
||||
async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, format: ExportFormat, res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
|
||||
if (!["html", "markdown", "share"].includes(format)) {
|
||||
throw new ValidationError(`Only 'html', 'markdown' and 'share' allowed as export format, '${format}' given`);
|
||||
}
|
||||
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 9 } // Sets the compression level.
|
||||
});
|
||||
const rewriteFn = (zipExportOptions?.customRewriteLinks ? zipExportOptions?.customRewriteLinks(rewriteLinks, getNoteTargetUrl) : rewriteLinks);
|
||||
const provider = await buildProvider();
|
||||
const log = getLog();
|
||||
const provider = buildProvider();
|
||||
|
||||
const noteIdToMeta: Record<string, NoteMeta> = {};
|
||||
|
||||
async function buildProvider() {
|
||||
function buildProvider() {
|
||||
const providerData: ZipExportProviderData = {
|
||||
getNoteTargetUrl,
|
||||
archive,
|
||||
@@ -35,7 +46,16 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
|
||||
zipExportOptions
|
||||
};
|
||||
|
||||
return getZipExportProviderFactory()(format, providerData);
|
||||
switch (format) {
|
||||
case "html":
|
||||
return new HtmlExportProvider(providerData);
|
||||
case "markdown":
|
||||
return new MarkdownExportProvider(providerData);
|
||||
case "share":
|
||||
return new ShareThemeExportProvider(providerData);
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
function getUniqueFilename(existingFileNames: Record<string, number>, fileName: string) {
|
||||
@@ -76,7 +96,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
|
||||
fileName = fileName.slice(0, 30 - croppedExt.length) + croppedExt;
|
||||
}
|
||||
|
||||
const existingExtension = extname(fileName).toLowerCase();
|
||||
const existingExtension = path.extname(fileName).toLowerCase();
|
||||
const newExtension = provider.mapExtension(type, mime, existingExtension, format);
|
||||
|
||||
// if the note is already named with the extension (e.g. "image.jpg"), then it's silly to append the exact same extension again
|
||||
@@ -323,7 +343,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
|
||||
|
||||
content = prepareContent(noteMeta.title, content, noteMeta, undefined);
|
||||
|
||||
archive.append(typeof content === "string" ? content : new Uint8Array(content), {
|
||||
archive.append(typeof content === "string" ? content : Buffer.from(content), {
|
||||
name: filePathPrefix + noteMeta.dataFileName
|
||||
});
|
||||
|
||||
@@ -341,7 +361,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
|
||||
if (noteMeta.dataFileName) {
|
||||
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta, note);
|
||||
|
||||
archive.append(content as string | Uint8Array, {
|
||||
archive.append(content as string | Buffer, {
|
||||
name: filePathPrefix + noteMeta.dataFileName,
|
||||
date: dateUtils.parseDateTime(note.utcDateModified)
|
||||
});
|
||||
@@ -357,7 +377,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
|
||||
const attachment = note.getAttachmentById(attachmentMeta.attachmentId);
|
||||
const content = attachment.getContent();
|
||||
|
||||
archive.append(typeof content === "string" ? content : new Uint8Array(content), {
|
||||
archive.append(typeof content === "string" ? content : Buffer.from(content), {
|
||||
name: filePathPrefix + attachmentMeta.dataFileName,
|
||||
date: dateUtils.parseDateTime(note.utcDateModified)
|
||||
});
|
||||
@@ -451,7 +471,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
|
||||
|
||||
|
||||
async function exportToZipFile(noteId: string, format: ExportFormat, zipFilePath: string, zipExportOptions?: AdvancedExportOptions) {
|
||||
const { destination, waitForFinish } = getZipProvider().createFileStream(zipFilePath);
|
||||
const fileOutputStream = fs.createWriteStream(zipFilePath);
|
||||
const taskContext = new TaskContext("no-progress-reporting", "export", null);
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
@@ -460,10 +480,10 @@ async function exportToZipFile(noteId: string, format: ExportFormat, zipFilePath
|
||||
throw new ValidationError(`Note ${noteId} not found.`);
|
||||
}
|
||||
|
||||
await exportToZip(taskContext, note.getParentBranches()[0], format, destination as Record<string, any>, false, zipExportOptions);
|
||||
await waitForFinish();
|
||||
await exportToZip(taskContext, note.getParentBranches()[0], format, fileOutputStream, false, zipExportOptions);
|
||||
await waitForStreamToFinish(fileOutputStream);
|
||||
|
||||
getLog().info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
|
||||
log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -1,10 +1,13 @@
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { ExportFormat } from "@triliumnext/core";
|
||||
import { Archiver } from "archiver";
|
||||
import mimeTypes from "mime-types";
|
||||
|
||||
import type BBranch from "../../../becca/entities/bbranch.js";
|
||||
import type BNote from "../../../becca/entities/bnote.js";
|
||||
import { ExportFormat, NoteMeta, NoteMetaFile } from "../../../meta.js";
|
||||
import type { ZipArchive } from "../../zip_provider.js";
|
||||
import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js";
|
||||
|
||||
export type { ExportFormat, NoteMeta } from "@triliumnext/core";
|
||||
|
||||
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
|
||||
|
||||
@@ -29,7 +32,7 @@ export interface AdvancedExportOptions {
|
||||
export interface ZipExportProviderData {
|
||||
branch: BBranch;
|
||||
getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null;
|
||||
archive: ZipArchive;
|
||||
archive: Archiver;
|
||||
zipExportOptions: AdvancedExportOptions | undefined;
|
||||
rewriteFn: RewriteLinksFn;
|
||||
}
|
||||
@@ -37,7 +40,7 @@ export interface ZipExportProviderData {
|
||||
export abstract class ZipExportProvider {
|
||||
branch: BBranch;
|
||||
getNoteTargetUrl: (targetNoteId: string, sourceMeta: NoteMeta) => string | null;
|
||||
archive: ZipArchive;
|
||||
archive: Archiver;
|
||||
zipExportOptions?: AdvancedExportOptions;
|
||||
rewriteFn: RewriteLinksFn;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { type ExportFormat, ZipExportProvider, type ZipExportProviderData } from "@triliumnext/core";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { getResourceDir, isDev } from "../../utils.js";
|
||||
|
||||
function readContentCss(): string {
|
||||
const cssFile = isDev
|
||||
? path.join(__dirname, "../../../../../../node_modules/ckeditor5/dist/ckeditor5-content.css")
|
||||
: path.join(getResourceDir(), "ckeditor5-content.css");
|
||||
return fs.readFileSync(cssFile, "utf-8");
|
||||
}
|
||||
|
||||
export async function serverZipExportProviderFactory(format: ExportFormat, data: ZipExportProviderData): Promise<ZipExportProvider> {
|
||||
switch (format) {
|
||||
case "html": {
|
||||
const { default: HtmlExportProvider } = await import("@triliumnext/core/src/services/export/zip/html.js");
|
||||
return new HtmlExportProvider(data, { contentCss: readContentCss() });
|
||||
}
|
||||
case "markdown": {
|
||||
const { default: MarkdownExportProvider } = await import("@triliumnext/core/src/services/export/zip/markdown.js");
|
||||
return new MarkdownExportProvider(data);
|
||||
}
|
||||
case "share": {
|
||||
const { default: ShareThemeExportProvider } = await import("./share_theme.js");
|
||||
return new ShareThemeExportProvider(data);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported export format: '${format}'`);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,16 @@
|
||||
import fs from "fs";
|
||||
import html from "html";
|
||||
import path from "path";
|
||||
|
||||
import { escapeHtml } from "../../utils/index";
|
||||
import { ZipExportProvider, ZipExportProviderData } from "./abstract_provider.js";
|
||||
import { NoteMeta } from "../../../meta";
|
||||
|
||||
export interface HtmlExportProviderOptions {
|
||||
contentCss?: string;
|
||||
}
|
||||
import type NoteMeta from "../../meta/note_meta.js";
|
||||
import { escapeHtml, getResourceDir, isDev } from "../../utils";
|
||||
import { ZipExportProvider } from "./abstract_provider.js";
|
||||
|
||||
export default class HtmlExportProvider extends ZipExportProvider {
|
||||
|
||||
private navigationMeta: NoteMeta | null = null;
|
||||
private indexMeta: NoteMeta | null = null;
|
||||
private cssMeta: NoteMeta | null = null;
|
||||
private options: HtmlExportProviderOptions;
|
||||
|
||||
constructor(data: ZipExportProviderData, options?: HtmlExportProviderOptions) {
|
||||
super(data);
|
||||
this.options = options ?? {};
|
||||
}
|
||||
|
||||
prepareMeta(metaFile) {
|
||||
if (this.zipExportOptions?.skipExtraFiles) return;
|
||||
@@ -178,9 +170,11 @@ export default class HtmlExportProvider extends ZipExportProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.contentCss) {
|
||||
this.archive.append(this.options.contentCss, { name: cssMeta.dataFileName });
|
||||
}
|
||||
const cssFile = isDev
|
||||
? path.join(__dirname, "../../../../../../node_modules/ckeditor5/dist/ckeditor5-content.css")
|
||||
: path.join(getResourceDir(), "ckeditor5-content.css");
|
||||
const cssContent = fs.readFileSync(cssFile, "utf-8");
|
||||
this.archive.append(cssContent, { name: cssMeta.dataFileName });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NoteMeta } from "../../../meta.js";
|
||||
import NoteMeta from "../../meta/note_meta";
|
||||
import mdService from "../markdown.js";
|
||||
import { ZipExportProvider } from "./abstract_provider.js";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ExportFormat, icon_packs as iconPackService, ZipExportProvider } from "@triliumnext/core";
|
||||
import ejs from "ejs";
|
||||
import fs, { readdirSync, readFileSync } from "fs";
|
||||
import { convert as convertToText } from "html-to-text";
|
||||
@@ -10,10 +9,12 @@ import type BBranch from "../../../becca/entities/bbranch.js";
|
||||
import type BNote from "../../../becca/entities/bnote.js";
|
||||
import { getClientDir, getShareThemeAssetDir } from "../../../routes/assets";
|
||||
import { getDefaultTemplatePath, readTemplate, renderNoteForExport } from "../../../share/content_renderer";
|
||||
import { icon_packs as iconPackService } from "@triliumnext/core";
|
||||
import log from "../../log";
|
||||
import NoteMeta, { NoteMetaFile } from "../../meta/note_meta";
|
||||
import { RESOURCE_DIR } from "../../resource_dir";
|
||||
import { getResourceDir, isDev } from "../../utils";
|
||||
import { ExportFormat, ZipExportProvider } from "./abstract_provider.js";
|
||||
|
||||
const shareThemeAssetDir = getShareThemeAssetDir();
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ export const isWindows11 = isWindows && osVersion[0] === 10 && osVersion[2] >= 2
|
||||
|
||||
export const isElectron = !!process.versions["electron"];
|
||||
|
||||
/** @deprecated Use `isDev()` from `@triliumnext/core` instead. */
|
||||
export const isDev = !!(process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === "dev");
|
||||
|
||||
/** @deprecated */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { renderSpreadsheetToHtml } from "@triliumnext/commons";
|
||||
import { icon_packs as iconPackService, sanitize, utils } from "@triliumnext/core";
|
||||
import { sanitize } from "@triliumnext/core";
|
||||
import { icon_packs as iconPackService } from "@triliumnext/core";
|
||||
import { highlightAuto } from "@triliumnext/highlightjs";
|
||||
import ejs from "ejs";
|
||||
import escapeHtml from "escape-html";
|
||||
@@ -15,7 +16,7 @@ import BNote from "../becca/entities/bnote.js";
|
||||
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
|
||||
import log from "../services/log.js";
|
||||
import options from "../services/options.js";
|
||||
import { getResourceDir, isDev } from "../services/utils.js";
|
||||
import utils, { getResourceDir, isDev, safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
import SAttachment from "./shaca/entities/sattachment.js";
|
||||
import SBranch from "./shaca/entities/sbranch.js";
|
||||
import type SNote from "./shaca/entities/snote.js";
|
||||
@@ -223,7 +224,7 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs)
|
||||
return ejs.render(content, opts, { includer });
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const [errMessage, errStack] = utils.safeExtractMessageAndStackFromError(e);
|
||||
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||
log.error(`Rendering user provided share template (${templateId}) threw exception ${errMessage} with stacktrace: ${errStack}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,6 @@
|
||||
import type { FileStream, ZipArchive, ZipEntry, ZipProvider } from "@triliumnext/core/src/services/zip_provider.js";
|
||||
import archiver, { type Archiver } from "archiver";
|
||||
import fs from "fs";
|
||||
import type { ZipEntry, ZipProvider } from "@triliumnext/core/src/services/import/zip_provider.js";
|
||||
import type { Stream } from "stream";
|
||||
import * as yauzl from "yauzl";
|
||||
|
||||
class NodejsZipArchive implements ZipArchive {
|
||||
readonly #archive: Archiver;
|
||||
|
||||
constructor() {
|
||||
this.#archive = archiver("zip", {
|
||||
zlib: { level: 9 }
|
||||
});
|
||||
}
|
||||
|
||||
append(content: string | Uint8Array, options: { name: string; date?: Date }) {
|
||||
this.#archive.append(typeof content === "string" ? content : Buffer.from(content), options);
|
||||
}
|
||||
|
||||
pipe(destination: unknown) {
|
||||
this.#archive.pipe(destination as NodeJS.WritableStream);
|
||||
}
|
||||
|
||||
finalize(): Promise<void> {
|
||||
return this.#archive.finalize();
|
||||
}
|
||||
}
|
||||
import yauzl from "yauzl";
|
||||
|
||||
function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||
const chunks: Uint8Array[] = [];
|
||||
@@ -36,21 +12,6 @@ function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||
}
|
||||
|
||||
export default class NodejsZipProvider implements ZipProvider {
|
||||
createZipArchive(): ZipArchive {
|
||||
return new NodejsZipArchive();
|
||||
}
|
||||
|
||||
createFileStream(filePath: string): FileStream {
|
||||
const stream = fs.createWriteStream(filePath);
|
||||
return {
|
||||
destination: stream,
|
||||
waitForFinish: () => new Promise((resolve, reject) => {
|
||||
stream.on("finish", resolve);
|
||||
stream.on("error", reject);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
readZipFile(
|
||||
buffer: Uint8Array,
|
||||
processEntry: (entry: ZipEntry, readContent: () => Promise<Uint8Array>) => Promise<void>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/turndown-plugin-gfm": "workspace:*",
|
||||
"async-mutex": "0.5.0",
|
||||
"chardet": "2.1.1",
|
||||
"escape-html": "1.0.3",
|
||||
|
||||
@@ -9,8 +9,7 @@ import { initTranslations, TranslationProvider } from "./services/i18n";
|
||||
import { initSchema, initDemoArchive } from "./services/sql_init";
|
||||
import appInfo from "./services/app_info";
|
||||
import { type PlatformProvider, initPlatform } from "./services/platform";
|
||||
import { type ZipProvider, initZipProvider } from "./services/zip_provider";
|
||||
import { type ZipExportProviderFactory, initZipExportProviderFactory } from "./services/export/zip_export_provider_factory";
|
||||
import { type ZipProvider, initZipProvider } from "./services/import/zip_provider";
|
||||
import markdown from "./services/import/markdown";
|
||||
|
||||
export { getLog } from "./services/log";
|
||||
@@ -102,20 +101,15 @@ export type { RequestProvider, ExecOpts, CookieJar } from "./services/request";
|
||||
export type * from "./meta";
|
||||
export * as routeHelpers from "./routes/helpers";
|
||||
|
||||
export { getZipProvider, type ZipArchive, type ZipProvider } from "./services/zip_provider";
|
||||
export { getZipProvider, type ZipProvider } from "./services/import/zip_provider";
|
||||
export { default as zipImportService } from "./services/import/zip";
|
||||
export { default as zipExportService } from "./services/export/zip";
|
||||
export { type AdvancedExportOptions, type ZipExportProviderData } from "./services/export/zip/abstract_provider";
|
||||
export { ZipExportProvider } from "./services/export/zip/abstract_provider";
|
||||
export { type ZipExportProviderFactory } from "./services/export/zip_export_provider_factory";
|
||||
export { type ExportFormat } from "./meta";
|
||||
|
||||
export * as becca_easy_mocking from "./test/becca_easy_mocking";
|
||||
export * as becca_mocking from "./test/becca_mocking";
|
||||
|
||||
export { default as markdownImportService } from "./services/import/markdown";
|
||||
|
||||
export async function initializeCore({ dbConfig, executionContext, crypto, zip, zipExportProviderFactory, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive }: {
|
||||
export async function initializeCore({ dbConfig, executionContext, crypto, zip, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive }: {
|
||||
dbConfig: SqlServiceParams,
|
||||
executionContext: ExecutionContext,
|
||||
crypto: CryptoProvider,
|
||||
@@ -123,7 +117,6 @@ export async function initializeCore({ dbConfig, executionContext, crypto, zip,
|
||||
translations: TranslationProvider,
|
||||
platform: PlatformProvider,
|
||||
schema: string,
|
||||
zipExportProviderFactory: ZipExportProviderFactory,
|
||||
messaging?: MessagingProvider,
|
||||
request?: RequestProvider,
|
||||
getDemoArchive?: () => Promise<Uint8Array | null>,
|
||||
@@ -137,7 +130,6 @@ export async function initializeCore({ dbConfig, executionContext, crypto, zip,
|
||||
await initTranslations(translations);
|
||||
initCrypto(crypto);
|
||||
initZipProvider(zip);
|
||||
initZipExportProviderFactory(zipExportProviderFactory);
|
||||
initContext(executionContext);
|
||||
initSql(new SqlService(dbConfig, getLog()));
|
||||
initSchema(schema);
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
import becca from "../../becca/becca";
|
||||
|
||||
import { RenderMarkdownResponse, ToMarkdownResponse } from "@triliumnext/commons";
|
||||
import type { Request } from "express";
|
||||
|
||||
import markdown from "../../services/export/markdown.js";
|
||||
import { markdownImportService } from "../..";
|
||||
|
||||
function renderMarkdown(req: Request) {
|
||||
const { markdownContent } = req.body;
|
||||
if (!markdownContent || typeof markdownContent !== 'string') {
|
||||
throw new Error('markdownContent parameter is required and must be a string');
|
||||
}
|
||||
return {
|
||||
htmlContent: markdownImportService.renderToHtml(markdownContent, "")
|
||||
} satisfies RenderMarkdownResponse;
|
||||
}
|
||||
|
||||
function toMarkdown(req: Request) {
|
||||
const { htmlContent } = req.body;
|
||||
if (!htmlContent || typeof htmlContent !== 'string') {
|
||||
throw new Error('htmlContent parameter is required and must be a string');
|
||||
}
|
||||
return {
|
||||
markdownContent: markdown.toMarkdown(htmlContent)
|
||||
} satisfies ToMarkdownResponse;
|
||||
}
|
||||
|
||||
function getIconUsage() {
|
||||
const iconClassToCountMap: Record<string, number> = {};
|
||||
|
||||
@@ -51,7 +25,5 @@ function getIconUsage() {
|
||||
}
|
||||
|
||||
export default {
|
||||
getIconUsage,
|
||||
renderMarkdown,
|
||||
toMarkdown
|
||||
getIconUsage
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import imageRoute from "./api/image";
|
||||
import setupApiRoute from "./api/setup";
|
||||
import filesRoute from "./api/files";
|
||||
import importRoute from "./api/import";
|
||||
import exportRoute from "./api/export";
|
||||
|
||||
// TODO: Deduplicate with routes.ts
|
||||
const GET = "get",
|
||||
@@ -117,7 +116,6 @@ export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRout
|
||||
apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix);
|
||||
apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch);
|
||||
|
||||
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
|
||||
route(GET, "/api/revisions/:revisionId/image/:filename", [checkApiAuthOrElectron], imageRoute.returnImageFromRevision);
|
||||
route(GET, "/api/attachments/:attachmentId/image/:filename", [checkApiAuthOrElectron], imageRoute.returnAttachedImage);
|
||||
route(GET, "/api/images/:noteId/:filename", [checkApiAuthOrElectron], imageRoute.returnImageFromNote);
|
||||
@@ -141,11 +139,8 @@ export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRout
|
||||
route(PST, "/api/sync/queue-sector/:entityName/:sector", [checkApiAuth], syncApiRoute.queueSector, apiResultHandler);
|
||||
route(GET, "/api/sync/stats", [], syncApiRoute.getStats, apiResultHandler);
|
||||
|
||||
//#region Import/export
|
||||
asyncRoute(PST, "/api/notes/:parentNoteId/notes-import", [checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importNotesToBranch, apiResultHandler);
|
||||
route(PST, "/api/notes/:parentNoteId/attachments-import", [checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importAttachmentsToNote, apiResultHandler);
|
||||
asyncRoute(GET, "/api/branches/:branchId/export/:type/:format/:version/:taskId", [checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
//#endregion
|
||||
|
||||
apiRoute(GET, "/api/quick-search/:searchString", searchRoute.quickSearch);
|
||||
apiRoute(GET, "/api/search-note/:noteId", searchRoute.searchFromNote);
|
||||
@@ -198,11 +193,7 @@ export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRout
|
||||
apiRoute(PST, "/api/bulk-action/affected-notes", bulkActionRoute.getAffectedNoteCount);
|
||||
|
||||
apiRoute(GET, "/api/app-info", appInfoRoute.getAppInfo);
|
||||
|
||||
apiRoute(GET, "/api/other/icon-usage", otherRoute.getIconUsage);
|
||||
apiRoute(PST, "/api/other/render-markdown", otherRoute.renderMarkdown);
|
||||
apiRoute(PST, "/api/other/to-markdown", otherRoute.toMarkdown);
|
||||
|
||||
asyncApiRoute(GET, "/api/similar-notes/:noteId", similarNotesRoute.getSimilarNotes);
|
||||
apiRoute(PST, "/api/relation-map", relationMapApiRoute.getRelationMap);
|
||||
apiRoute(GET, "/api/recent-changes/:ancestorNoteId", recentChangesApiRoute.getRecentChanges);
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import { gfm } from "@triliumnext/turndown-plugin-gfm";
|
||||
import Turnish, { type Rule } from "turnish";
|
||||
|
||||
let instance: Turnish | null = null;
|
||||
|
||||
// TODO: Move this to a dedicated file someday.
|
||||
export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
note: "NOTE",
|
||||
@@ -13,272 +8,3 @@ export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note;
|
||||
|
||||
const fencedCodeBlockFilter: Rule = {
|
||||
filter (node, options) {
|
||||
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
|
||||
},
|
||||
|
||||
replacement (content, node, options) {
|
||||
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
|
||||
return content;
|
||||
}
|
||||
|
||||
const className = node.firstChild.getAttribute("class") || "";
|
||||
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
|
||||
|
||||
return `\n\n${options.fence}${language}\n${node.firstChild.textContent}\n${options.fence}\n\n`;
|
||||
}
|
||||
};
|
||||
|
||||
function toMarkdown(content: string) {
|
||||
if (instance === null) {
|
||||
instance = new Turnish({
|
||||
headingStyle: "atx",
|
||||
bulletListMarker: "*",
|
||||
emDelimiter: "_",
|
||||
codeBlockStyle: "fenced",
|
||||
blankReplacement(_content, node) {
|
||||
if (node.nodeName === "SECTION" && node.classList.contains("include-note")) {
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
// Original implementation as per https://github.com/mixmark-io/turndown/blob/master/src/turndown.js.
|
||||
return ("isBlock" in node && node.isBlock) ? '\n\n' : '';
|
||||
},
|
||||
});
|
||||
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
|
||||
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
|
||||
instance.addRule("img", buildImageFilter());
|
||||
instance.addRule("admonition", buildAdmonitionFilter());
|
||||
instance.addRule("inlineLink", buildInlineLinkFilter());
|
||||
instance.addRule("figure", buildFigureFilter());
|
||||
instance.addRule("math", buildMathFilter());
|
||||
instance.addRule("li", buildListItemFilter());
|
||||
instance.use(gfm);
|
||||
instance.keep([ "kbd", "sup", "sub" ]);
|
||||
}
|
||||
|
||||
return instance.render(content);
|
||||
}
|
||||
|
||||
function rewriteLanguageTag(source: string) {
|
||||
if (!source) {
|
||||
return source;
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
case "text-x-trilium-auto":
|
||||
return "";
|
||||
case "application-javascript-env-frontend":
|
||||
case "application-javascript-env-backend":
|
||||
return "javascript";
|
||||
case "text-x-nginx-conf":
|
||||
return "nginx";
|
||||
default:
|
||||
return source.split("-").at(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove once upstream delivers a fix for https://github.com/mixmark-io/turndown/issues/467.
|
||||
function buildImageFilter() {
|
||||
const ESCAPE_PATTERNS = {
|
||||
before: /([\\*`[\]_]|(?:^[-+>])|(?:^~~~)|(?:^#{1-6}))/g,
|
||||
after: /((?:^\d+(?=\.)))/
|
||||
};
|
||||
|
||||
const escapePattern = new RegExp(`(?:${ESCAPE_PATTERNS.before.source}|${ESCAPE_PATTERNS.after.source})`, 'g');
|
||||
|
||||
function escapeMarkdown (content: string) {
|
||||
return content.replace(escapePattern, (match, before, after) => {
|
||||
return before ? `\\${before}` : `${after}\\`;
|
||||
});
|
||||
}
|
||||
|
||||
function escapeLinkDestination(destination: string) {
|
||||
return destination
|
||||
.replace(/([()])/g, '\\$1')
|
||||
.replace(/ /g, "%20");
|
||||
}
|
||||
|
||||
function escapeLinkTitle (title: string) {
|
||||
return title.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
const imageFilter: Rule = {
|
||||
filter: "img",
|
||||
replacement(content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
|
||||
// Preserve image verbatim if it has a width or height attribute.
|
||||
if (node.hasAttribute("width") || node.hasAttribute("height")) {
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with upstream.
|
||||
const untypedNode = (node as any);
|
||||
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')));
|
||||
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '');
|
||||
const title = cleanAttribute(untypedNode.getAttribute('title'));
|
||||
const titlePart = title ? ` "${escapeLinkTitle(title)}"` : '';
|
||||
|
||||
return src ? `` : '';
|
||||
}
|
||||
};
|
||||
return imageFilter;
|
||||
}
|
||||
|
||||
function buildAdmonitionFilter() {
|
||||
function parseAdmonitionType(_node: Node) {
|
||||
if (!("getAttribute" in _node)) {
|
||||
return DEFAULT_ADMONITION_TYPE;
|
||||
}
|
||||
|
||||
const node = _node as Element;
|
||||
const classList = node.getAttribute("class")?.split(" ") ?? [];
|
||||
|
||||
for (const className of classList) {
|
||||
if (className === "admonition") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mappedType = ADMONITION_TYPE_MAPPINGS[className];
|
||||
if (mappedType) {
|
||||
return mappedType;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_ADMONITION_TYPE;
|
||||
}
|
||||
|
||||
const admonitionFilter: Rule = {
|
||||
filter(node, options) {
|
||||
return node.nodeName === "ASIDE" && node.classList.contains("admonition");
|
||||
},
|
||||
replacement(content, node) {
|
||||
// Parse the admonition type.
|
||||
const admonitionType = parseAdmonitionType(node);
|
||||
|
||||
content = content.replace(/^\n+|\n+$/g, '');
|
||||
content = content.replace(/^/gm, '> ');
|
||||
content = `> [!${admonitionType}]\n${content}`;
|
||||
|
||||
return `\n\n${content}\n\n`;
|
||||
}
|
||||
};
|
||||
return admonitionFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variation of the original ruleset: https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
|
||||
*
|
||||
* Detects if the URL is a Trilium reference link and returns it verbatim if that's the case.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function buildInlineLinkFilter(): Rule {
|
||||
return {
|
||||
filter (node, options) {
|
||||
return (
|
||||
options.linkStyle === 'inlined' &&
|
||||
node.nodeName === 'A' &&
|
||||
!!node.getAttribute('href')
|
||||
);
|
||||
},
|
||||
|
||||
replacement (content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
|
||||
// Return reference links verbatim.
|
||||
if (node.classList.contains("reference-link")) {
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
// Otherwise treat as normal.
|
||||
// TODO: Call super() somehow instead of duplicating the implementation.
|
||||
let href = node.getAttribute('href');
|
||||
if (href) href = href.replace(/([()])/g, '\\$1');
|
||||
let title = cleanAttribute(node.getAttribute('title'));
|
||||
if (title) title = ` "${title.replace(/"/g, '\\"')}"`;
|
||||
return `[${content}](${href}${title})`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildFigureFilter(): Rule {
|
||||
return {
|
||||
filter(node, options) {
|
||||
return node.nodeName === 'FIGURE'
|
||||
&& node.classList.contains("image");
|
||||
},
|
||||
replacement(content, node) {
|
||||
return (node as HTMLElement).outerHTML;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Keep in line with https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
|
||||
function buildListItemFilter(): Rule {
|
||||
return {
|
||||
filter: "li",
|
||||
replacement(content, node, options) {
|
||||
content = content
|
||||
.trim()
|
||||
.replace(/\n/gm, '\n '); // indent
|
||||
let prefix = `${options.bulletListMarker} `;
|
||||
const parent = node.parentNode as HTMLElement;
|
||||
if (parent.nodeName === 'OL') {
|
||||
const start = parent.getAttribute('start');
|
||||
const index = Array.prototype.indexOf.call(parent.children, node);
|
||||
prefix = `${start ? Number(start) + index : index + 1}. `;
|
||||
} else if (parent.classList.contains("todo-list")) {
|
||||
const isChecked = node.querySelector("input[type=checkbox]:checked");
|
||||
prefix = (isChecked ? "- [x] " : "- [ ] ");
|
||||
}
|
||||
|
||||
const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildMathFilter(): Rule {
|
||||
const MATH_INLINE_PREFIX = "\\(";
|
||||
const MATH_INLINE_SUFFIX = "\\)";
|
||||
|
||||
const MATH_DISPLAY_PREFIX = "\\[";
|
||||
const MATH_DISPLAY_SUFFIX = "\\]";
|
||||
|
||||
return {
|
||||
filter(node) {
|
||||
return node.nodeName === "SPAN" && node.classList.contains("math-tex");
|
||||
},
|
||||
replacement(_, node) {
|
||||
// We have to use the raw HTML text, otherwise the content is escaped too much.
|
||||
const content = (node as HTMLElement).innerText;
|
||||
|
||||
// Inline math
|
||||
if (content.startsWith(MATH_INLINE_PREFIX) && content.endsWith(MATH_INLINE_SUFFIX)) {
|
||||
return `$${content.substring(MATH_INLINE_PREFIX.length, content.length - MATH_INLINE_SUFFIX.length)}$`;
|
||||
}
|
||||
|
||||
// Display math
|
||||
if (content.startsWith(MATH_DISPLAY_PREFIX) && content.endsWith(MATH_DISPLAY_SUFFIX)) {
|
||||
return `$$${content.substring(MATH_DISPLAY_PREFIX.length, content.length - MATH_DISPLAY_SUFFIX.length)}$$`;
|
||||
}
|
||||
|
||||
// Unknown.
|
||||
return content;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Taken from upstream since it's not exposed.
|
||||
// https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js
|
||||
function cleanAttribute(attribute: string | null | undefined) {
|
||||
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
|
||||
}
|
||||
|
||||
export default {
|
||||
toMarkdown
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ExportFormat } from "../../meta.js";
|
||||
import type { ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js";
|
||||
|
||||
export type ZipExportProviderFactory = (format: ExportFormat, data: ZipExportProviderData) => Promise<ZipExportProvider>;
|
||||
|
||||
let factory: ZipExportProviderFactory | null = null;
|
||||
|
||||
export function initZipExportProviderFactory(f: ZipExportProviderFactory) {
|
||||
factory = f;
|
||||
}
|
||||
|
||||
export function getZipExportProviderFactory(): ZipExportProviderFactory {
|
||||
if (!factory) throw new Error("ZipExportProviderFactory not initialized.");
|
||||
return factory;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
import becca from "../../becca/becca.js";
|
||||
@@ -12,7 +13,7 @@ import { getContext } from "../context.js";
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function testImport(fileName: string, mimetype: string) {
|
||||
const buffer = fs.readFileSync(`${scriptDir}/samples/${fileName}`);
|
||||
const buffer = fs.readFileSync(path.join(scriptDir, "samples", fileName));
|
||||
const taskContext = TaskContext.getInstance("import-mdx", "importNotes", {
|
||||
textImportedAsText: true,
|
||||
codeImportedAsCode: true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
import zip, { removeTriliumTags } from "./zip.js";
|
||||
@@ -12,7 +13,7 @@ import { getContext } from "../context.js";
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function testImport(fileName: string) {
|
||||
const mdxSample = fs.readFileSync(`${scriptDir}/samples/${fileName}`);
|
||||
const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", fileName));
|
||||
const taskContext = TaskContext.getInstance("import-mdx", "importNotes", {
|
||||
textImportedAsText: true
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
|
||||
import { basename, dirname } from "../utils/path.js";
|
||||
import { getZipProvider } from "../zip_provider.js";
|
||||
import { getZipProvider } from "./zip_provider.js";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import BAttachment from "../../becca/entities/battachment.js";
|
||||
|
||||
@@ -2,24 +2,6 @@ export interface ZipEntry {
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface ZipArchiveEntryOptions {
|
||||
name: string;
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
export interface ZipArchive {
|
||||
append(content: string | Uint8Array, options: ZipArchiveEntryOptions): void;
|
||||
pipe(destination: unknown): void;
|
||||
finalize(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface FileStream {
|
||||
/** An opaque writable destination that can be passed to {@link ZipArchive.pipe}. */
|
||||
destination: unknown;
|
||||
/** Resolves when the stream has finished writing (or rejects on error). */
|
||||
waitForFinish(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ZipProvider {
|
||||
/**
|
||||
* Iterates over every entry in a ZIP buffer, calling `processEntry` for each one.
|
||||
@@ -29,11 +11,6 @@ export interface ZipProvider {
|
||||
buffer: Uint8Array,
|
||||
processEntry: (entry: ZipEntry, readContent: () => Promise<Uint8Array>) => Promise<void>
|
||||
): Promise<void>;
|
||||
|
||||
createZipArchive(): ZipArchive;
|
||||
|
||||
/** Creates a writable file stream for the given path. */
|
||||
createFileStream(filePath: string): FileStream;
|
||||
}
|
||||
|
||||
let zipProvider: ZipProvider | null = null;
|
||||
@@ -2,6 +2,7 @@ import { type AttachmentRow, type AttributeRow, type BranchRow, dayjs, type Note
|
||||
import fs from "fs";
|
||||
import html2plaintext from "html2plaintext";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
import url from "url";
|
||||
|
||||
import becca from "../becca/becca.js";
|
||||
@@ -27,7 +28,6 @@ import { getSql } from "./sql/index.js";
|
||||
import { sanitizeHtml } from "./sanitizer.js";
|
||||
import { ValidationError } from "../errors.js";
|
||||
import * as cls from "./context.js";
|
||||
import { basename } from "./utils/path.js";
|
||||
|
||||
interface FoundLink {
|
||||
name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink";
|
||||
@@ -552,7 +552,7 @@ async function downloadImage(noteId: string, imageUrl: string) {
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(unescapedUrl);
|
||||
const title = basename(parsedUrl.pathname || "");
|
||||
const title = path.basename(parsedUrl.pathname || "");
|
||||
|
||||
const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import unescape from "unescape";
|
||||
import { basename, extname } from "./path";
|
||||
import { NoteMeta } from "../../meta";
|
||||
|
||||
export function isDev() { return getPlatform().getEnv("TRILIUM_ENV") === "dev"; }
|
||||
export function isElectron() { return getPlatform().isElectron; }
|
||||
export function isMac() { return getPlatform().isMac; }
|
||||
export function isWindows() { return getPlatform().isWindows; }
|
||||
|
||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -803,6 +803,9 @@ importers:
|
||||
'@triliumnext/highlightjs':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/highlightjs
|
||||
'@triliumnext/turndown-plugin-gfm':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/turndown-plugin-gfm
|
||||
'@types/archiver':
|
||||
specifier: 7.0.0
|
||||
version: 7.0.0
|
||||
@@ -1701,9 +1704,6 @@ importers:
|
||||
'@triliumnext/commons':
|
||||
specifier: workspace:*
|
||||
version: link:../commons
|
||||
'@triliumnext/turndown-plugin-gfm':
|
||||
specifier: workspace:*
|
||||
version: link:../turndown-plugin-gfm
|
||||
async-mutex:
|
||||
specifier: 0.5.0
|
||||
version: 0.5.0
|
||||
@@ -17587,6 +17587,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.6.1
|
||||
'@ckeditor/ckeditor5-widget': 47.6.1
|
||||
ckeditor5: 47.6.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-html-embed@47.6.1':
|
||||
dependencies:
|
||||
@@ -17596,6 +17598,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.6.1
|
||||
'@ckeditor/ckeditor5-widget': 47.6.1
|
||||
ckeditor5: 47.6.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-html-support@47.6.1':
|
||||
dependencies:
|
||||
@@ -17611,6 +17615,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.6.1
|
||||
ckeditor5: 47.6.1
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-icons@47.6.1': {}
|
||||
|
||||
@@ -17628,6 +17634,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.6.1
|
||||
ckeditor5: 47.6.1
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-import-word@47.6.1':
|
||||
dependencies:
|
||||
@@ -17651,6 +17659,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.6.1
|
||||
'@ckeditor/ckeditor5-utils': 47.6.1
|
||||
ckeditor5: 47.6.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-inspector@5.0.0': {}
|
||||
|
||||
@@ -17661,6 +17671,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.6.1
|
||||
'@ckeditor/ckeditor5-utils': 47.6.1
|
||||
ckeditor5: 47.6.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-line-height@47.6.1':
|
||||
dependencies:
|
||||
@@ -17685,6 +17697,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.6.1
|
||||
ckeditor5: 47.6.1
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-list-multi-level@47.6.1':
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user