mirror of
https://github.com/zadam/trilium.git
synced 2025-11-08 14:25:51 +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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function register(router: Router) {
|
|||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
const format = req.query.format || "html";
|
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'.`);
|
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");
|
const taskContext = new TaskContext(taskId, "export");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type === "subtree" && (format === "html" || format === "markdown")) {
|
if (type === "subtree" && (format === "html" || format === "markdown" || format === "share")) {
|
||||||
zipExportService.exportToZip(taskContext, branch, format, res);
|
zipExportService.exportToZip(taskContext, branch, format, res);
|
||||||
} else if (type === "single") {
|
} else if (type === "single") {
|
||||||
if (format !== "html" && format !== "markdown") {
|
if (format !== "html" && format !== "markdown") {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import dateUtils from "../date_utils.js";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import mimeTypes from "mime-types";
|
import mimeTypes from "mime-types";
|
||||||
import packageInfo from "../../../package.json" with { type: "json" };
|
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 protectedSessionService from "../protected_session.js";
|
||||||
import sanitize from "sanitize-filename";
|
import sanitize from "sanitize-filename";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -22,9 +22,11 @@ import type { NoteMetaFile } from "../meta/note_meta.js";
|
|||||||
import HtmlExportProvider from "./zip/html.js";
|
import HtmlExportProvider from "./zip/html.js";
|
||||||
import { AdvancedExportOptions, ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js";
|
import { AdvancedExportOptions, ZipExportProvider, ZipExportProviderData } from "./zip/abstract_provider.js";
|
||||||
import MarkdownExportProvider from "./zip/markdown.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) {
|
async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown" | "share", res: Response | fs.WriteStream, setHeaders = true, zipExportOptions?: AdvancedExportOptions) {
|
||||||
if (!["html", "markdown"].includes(format)) {
|
if (!["html", "markdown", "share"].includes(format)) {
|
||||||
throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`);
|
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,
|
prefix: branch.prefix,
|
||||||
dataFileName: fileName,
|
dataFileName: fileName,
|
||||||
type: "text", // export will have text description
|
type: "text", // export will have text description
|
||||||
format: format
|
format: (format === "markdown" ? "markdown" : "html")
|
||||||
};
|
};
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
@@ -165,7 +167,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
|||||||
taskContext.increaseProgressCount();
|
taskContext.increaseProgressCount();
|
||||||
|
|
||||||
if (note.type === "text") {
|
if (note.type === "text") {
|
||||||
meta.format = format;
|
meta.format = (format === "markdown" ? "markdown" : "html");
|
||||||
}
|
}
|
||||||
|
|
||||||
noteIdToMeta[note.noteId] = meta as NoteMeta;
|
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 {
|
function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta, note?: BNote): string | Buffer {
|
||||||
if (["html", "markdown"].includes(noteMeta?.format || "")) {
|
const isText = ["html", "markdown"].includes(noteMeta?.format || "");
|
||||||
|
if (isText) {
|
||||||
content = content.toString();
|
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) {
|
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>`;
|
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 });
|
archive.append(content, { name: filePathPrefix + noteMeta.dataFileName });
|
||||||
|
|
||||||
@@ -333,7 +340,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (noteMeta.dataFileName) {
|
if (noteMeta.dataFileName) {
|
||||||
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
|
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta, note);
|
||||||
|
|
||||||
archive.append(content, {
|
archive.append(content, {
|
||||||
name: filePathPrefix + noteMeta.dataFileName,
|
name: filePathPrefix + noteMeta.dataFileName,
|
||||||
@@ -395,6 +402,9 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
|||||||
case "markdown":
|
case "markdown":
|
||||||
provider = new MarkdownExportProvider(providerData);
|
provider = new MarkdownExportProvider(providerData);
|
||||||
break;
|
break;
|
||||||
|
case "share":
|
||||||
|
provider = new ShareThemeExportProvider(providerData);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Archiver } from "archiver";
|
import { Archiver } from "archiver";
|
||||||
import type { default as NoteMeta, NoteMetaFile } from "../../meta/note_meta.js";
|
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;
|
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
|
||||||
|
|
||||||
@@ -44,6 +46,6 @@ export abstract class ZipExportProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract prepareMeta(): void;
|
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;
|
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