mirror of
https://github.com/zadam/trilium.git
synced 2025-11-08 06:15:48 +01:00
feat(export/zip): add option to export with share theme
This commit is contained in:
@@ -85,6 +85,13 @@ const TPL = /*html*/`
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="export-subtree-format" value="share">
|
||||
Share format
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
|
||||
@@ -147,7 +147,7 @@ function register(router: Router) {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
const format = req.query.format || "html";
|
||||
|
||||
if (typeof format !== "string" || !["html", "markdown"].includes(format)) {
|
||||
if (typeof format !== "string" || !["html", "markdown", "share"].includes(format)) {
|
||||
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function exportBranch(req: Request, res: Response) {
|
||||
const taskContext = new TaskContext(taskId, "export");
|
||||
|
||||
try {
|
||||
if (type === "subtree" && (format === "html" || format === "markdown")) {
|
||||
if (type === "subtree" && (format === "html" || format === "markdown" || format === "share")) {
|
||||
zipExportService.exportToZip(taskContext, branch, format, res);
|
||||
} else if (type === "single") {
|
||||
if (format !== "html" && format !== "markdown") {
|
||||
|
||||
@@ -4,7 +4,7 @@ import dateUtils from "../date_utils.js";
|
||||
import path from "path";
|
||||
import mimeTypes from "mime-types";
|
||||
import packageInfo from "../../../package.json" with { type: "json" };
|
||||
import { getContentDisposition, escapeHtml } from "../utils.js";
|
||||
import { getContentDisposition } from "../utils.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import sanitize from "sanitize-filename";
|
||||
import fs from "fs";
|
||||
@@ -22,9 +22,11 @@ import type { NoteMetaFile } from "../meta/note_meta.js";
|
||||
import HtmlExportProvider from "./zip/html.js";
|
||||
import { AdvancedExportOptions, ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js";
|
||||
import MarkdownExportProvider from "./zip/markdown.js";
|
||||
import ShareThemeExportProvider from "./zip/share_theme.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
|
||||
async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
|
||||
if (!["html", "markdown"].includes(format)) {
|
||||
async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown" | "share", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
|
||||
if (!["html", "markdown", "share"].includes(format)) {
|
||||
throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`);
|
||||
}
|
||||
|
||||
@@ -135,7 +137,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
prefix: branch.prefix,
|
||||
dataFileName: fileName,
|
||||
type: "text", // export will have text description
|
||||
format: format
|
||||
format: (format === "markdown" ? "markdown" : "html")
|
||||
};
|
||||
return meta;
|
||||
}
|
||||
@@ -165,7 +167,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
if (note.type === "text") {
|
||||
meta.format = format;
|
||||
meta.format = (format === "markdown" ? "markdown" : "html");
|
||||
}
|
||||
|
||||
noteIdToMeta[note.noteId] = meta as NoteMeta;
|
||||
@@ -296,13 +298,18 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
}
|
||||
}
|
||||
|
||||
function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
|
||||
if (["html", "markdown"].includes(noteMeta?.format || "")) {
|
||||
function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note?: BNote): string | Buffer {
|
||||
const isText = ["html", "markdown"].includes(noteMeta?.format || "");
|
||||
if (isText) {
|
||||
content = content.toString();
|
||||
content = rewriteFn(content, noteMeta);
|
||||
}
|
||||
|
||||
return provider.prepareContent(title, content, noteMeta);
|
||||
content = provider.prepareContent(title, content, noteMeta, note, branch);
|
||||
if (isText) {
|
||||
content = rewriteFn(content as string, noteMeta);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function saveNote(noteMeta: NoteMeta, filePathPrefix: string) {
|
||||
@@ -317,7 +324,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
|
||||
let content: string | Buffer = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
|
||||
|
||||
content = prepareContent(noteMeta.title, content, noteMeta);
|
||||
content = prepareContent(noteMeta.title, content, noteMeta, undefined);
|
||||
|
||||
archive.append(content, { name: filePathPrefix + noteMeta.dataFileName });
|
||||
|
||||
@@ -333,7 +340,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
}
|
||||
|
||||
if (noteMeta.dataFileName) {
|
||||
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
|
||||
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta, note);
|
||||
|
||||
archive.append(content, {
|
||||
name: filePathPrefix + noteMeta.dataFileName,
|
||||
@@ -395,6 +402,9 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
case "markdown":
|
||||
provider = new MarkdownExportProvider(providerData);
|
||||
break;
|
||||
case "share":
|
||||
provider = new ShareThemeExportProvider(providerData);
|
||||
break;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Archiver } from "archiver";
|
||||
import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js";
|
||||
import type BNote from "../../../becca/entities/bnote.js";
|
||||
import type BBranch from "../../../becca/entities/bbranch.js";
|
||||
|
||||
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
|
||||
|
||||
@@ -44,6 +46,6 @@ export abstract class ZipExportProvider {
|
||||
}
|
||||
|
||||
abstract prepareMeta(): void;
|
||||
abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer;
|
||||
abstract prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote | undefined, branch: BBranch): string | Buffer;
|
||||
abstract afterDone(): void;
|
||||
}
|
||||
|
||||
86
apps/server/src/services/export/zip/share_theme.ts
Normal file
86
apps/server/src/services/export/zip/share_theme.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { join } from "path";
|
||||
import NoteMeta from "../../meta/note_meta";
|
||||
import { ZipExportProvider } from "./abstract_provider";
|
||||
import { RESOURCE_DIR } from "../../resource_dir";
|
||||
import { getResourceDir, isDev } from "../../utils";
|
||||
import fs from "fs";
|
||||
import { renderNoteForExport } from "../../../share/content_renderer";
|
||||
import type BNote from "../../../becca/entities/bnote.js";
|
||||
import type BBranch from "../../../becca/entities/bbranch.js";
|
||||
|
||||
export default class ShareThemeExportProvider extends ZipExportProvider {
|
||||
|
||||
private assetsMeta: NoteMeta[] = [];
|
||||
|
||||
prepareMeta(): void {
|
||||
const assets = [
|
||||
"style.css",
|
||||
"script.js",
|
||||
"boxicons.css",
|
||||
"boxicons.eot",
|
||||
"boxicons.woff2",
|
||||
"boxicons.woff",
|
||||
"boxicons.ttf",
|
||||
"boxicons.svg",
|
||||
"icon-color.svg"
|
||||
];
|
||||
|
||||
for (const asset of assets) {
|
||||
const assetMeta = {
|
||||
noImport: true,
|
||||
dataFileName: asset
|
||||
};
|
||||
this.assetsMeta.push(assetMeta);
|
||||
this.metaFile.files.push(assetMeta);
|
||||
}
|
||||
}
|
||||
|
||||
prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note: BNote, branch: BBranch): string | Buffer {
|
||||
if (!noteMeta?.notePath?.length) {
|
||||
throw new Error("Missing note path.");
|
||||
}
|
||||
const basePath = "../".repeat(noteMeta.notePath.length - 1);
|
||||
|
||||
content = renderNoteForExport(note, branch, basePath);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
afterDone(): void {
|
||||
this.#saveAssets(this.rootMeta, this.assetsMeta);
|
||||
}
|
||||
|
||||
#saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) {
|
||||
for (const assetMeta of assetsMeta) {
|
||||
if (!assetMeta.dataFileName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cssContent = getShareThemeAssets(assetMeta.dataFileName);
|
||||
this.archive.append(cssContent, { name: assetMeta.dataFileName });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getShareThemeAssets(nameWithExtension: string) {
|
||||
// Rename share.css to style.css.
|
||||
if (nameWithExtension === "style.css") {
|
||||
nameWithExtension = "share.css";
|
||||
} else if (nameWithExtension === "script.js") {
|
||||
nameWithExtension = "share.js";
|
||||
}
|
||||
|
||||
let path: string | undefined;
|
||||
if (nameWithExtension === "icon-color.svg") {
|
||||
path = join(RESOURCE_DIR, "images", nameWithExtension);
|
||||
} else if (isDev) {
|
||||
path = join(getResourceDir(), "..", "..", "client", "dist", "src", nameWithExtension);
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
throw new Error("Not yet defined.");
|
||||
}
|
||||
|
||||
return fs.readFileSync(path);
|
||||
}
|
||||
Reference in New Issue
Block a user