feat(export/zip): add option to export with share theme

This commit is contained in:
Elian Doran 2025-06-23 18:13:47 +03:00
parent 55bb2fdb9b
commit a9f68f5487
No known key found for this signature in database
6 changed files with 119 additions and 14 deletions

View File

@ -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">

View File

@ -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'.`);
}

View File

@ -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") {

View File

@ -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();
}

View File

@ -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;
}

View 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);
}