From b00cb52da5ea81f6ac91ce4eaf29e7a03a5c3ed3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 27 Dec 2025 21:58:18 +0200 Subject: [PATCH] feat(share): basic support for custom icon packs --- apps/server/src/services/icon_packs.ts | 8 +++- apps/server/src/share/content_renderer.ts | 47 ++++++++++++--------- packages/share-theme/src/templates/page.ejs | 3 ++ 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/apps/server/src/services/icon_packs.ts b/apps/server/src/services/icon_packs.ts index 7e8580849..486bff146 100644 --- a/apps/server/src/services/icon_packs.ts +++ b/apps/server/src/services/icon_packs.ts @@ -29,6 +29,7 @@ export interface IconPackManifest { interface ProcessResult { manifest: IconPackManifest; + manifestNoteId: string; fontMime: string; fontAttachmentId: string; title: string; @@ -38,6 +39,7 @@ interface ProcessResult { export function getIconPacks() { const defaultIconPack: ProcessResult = { manifest: boxiconsManifest, + manifestNoteId: "builtin-boxicons-v2", fontMime: "font/woff2", fontAttachmentId: "builtin-boxicons-v2", title: "Boxicons", @@ -94,6 +96,7 @@ export function processIconPack(iconPackNote: BNote): ProcessResult | undefined fontMime: attachment.mime, fontAttachmentId: attachment.attachmentId, title: iconPackNote.title, + manifestNoteId: iconPackNote.noteId, icon: iconPackNote.getIcon() }; } @@ -114,19 +117,20 @@ export function determineBestFontAttachment(iconPackNote: BNote) { return null; } -export function generateCss({ manifest, fontAttachmentId, fontMime }: ProcessResult) { +export function generateCss({ manifest, fontAttachmentId, fontMime }: ProcessResult, isShare = false) { try { const iconDeclarations: string[] = []; for (const [ key, mapping ] of Object.entries(manifest.icons)) { iconDeclarations.push(`.${manifest.prefix}.${key}::before { content: '\\${mapping.glyph.charCodeAt(0).toString(16)}'; }`); } + const downloadBaseUrl = isShare ? '/share' : ''; return `\ @font-face { font-family: 'trilium-icon-pack-${manifest.prefix}'; font-weight: normal; font-style: normal; - src: url('/api/attachments/${fontAttachmentId}/download') format('${MIME_TO_CSS_FORMAT_MAPPINGS[fontMime]}'); + src: url('${downloadBaseUrl}/api/attachments/${fontAttachmentId}/download') format('${MIME_TO_CSS_FORMAT_MAPPINGS[fontMime]}'); } .${manifest.prefix} { diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index 70d7f2b82..1a07c0110 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -1,24 +1,26 @@ -import { parse, HTMLElement, TextNode, Options } from "node-html-parser"; -import shaca from "./shaca/shaca.js"; -import assetPath, { assetUrlFragment } from "../services/asset_path.js"; -import shareRoot from "./share_root.js"; -import escapeHtml from "escape-html"; -import type SNote from "./shaca/entities/snote.js"; -import BNote from "../becca/entities/bnote.js"; -import type BBranch from "../becca/entities/bbranch.js"; -import { t } from "i18next"; -import SBranch from "./shaca/entities/sbranch.js"; -import options from "../services/options.js"; -import utils, { getResourceDir, isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; -import ejs from "ejs"; -import log from "../services/log.js"; -import { join } from "path"; -import { readFileSync } from "fs"; +import { sanitizeUrl } from "@braintree/sanitize-url"; import { highlightAuto } from "@triliumnext/highlightjs"; +import ejs from "ejs"; +import escapeHtml from "escape-html"; +import { readFileSync } from "fs"; +import { t } from "i18next"; +import { HTMLElement, Options,parse, TextNode } from "node-html-parser"; +import { join } from "path"; + import becca from "../becca/becca.js"; import BAttachment from '../becca/entities/battachment.js'; +import type BBranch from "../becca/entities/bbranch.js"; +import BNote from "../becca/entities/bnote.js"; +import assetPath, { assetUrlFragment } from "../services/asset_path.js"; +import { generateCss, getIconPacks } from "../services/icon_packs.js"; +import log from "../services/log.js"; +import options from "../services/options.js"; +import utils, { getResourceDir, isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; import SAttachment from "./shaca/entities/sattachment.js"; -import { sanitizeUrl } from "@braintree/sanitize-url"; +import SBranch from "./shaca/entities/sbranch.js"; +import type SNote from "./shaca/entities/snote.js"; +import shaca from "./shaca/shaca.js"; +import shareRoot from "./share_root.js"; const shareAdjustedAssetPath = isDev ? assetPath : `../${assetPath}`; const templateCache: Map = new Map(); @@ -54,7 +56,7 @@ function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot { return { note, branch: parentBranch - } + }; } if (parentBranch.parentNoteId === shareRoot.SHARE_ROOT_NOTE_ID) { @@ -124,6 +126,7 @@ export function renderNoteContent(note: SNote) { const customLogoId = note.getRelation("shareLogo")?.value; const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`; + const iconPacks = getIconPacks().filter(p => !!shaca.notes[p.manifestNoteId]); return renderNoteContentInternal(note, { subRoot, @@ -133,7 +136,10 @@ export function renderNoteContent(note: SNote) { logoUrl, ancestors, isStatic: false, - faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download` : `../favicon.ico` + faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download` : `../favicon.ico`, + iconPackCss: iconPacks.map(p => generateCss(p, true)) + .filter(Boolean) + .join("\n\n"), }); } @@ -146,6 +152,7 @@ interface RenderArgs { ancestors: string[]; isStatic: boolean; faviconUrl: string; + iconPackCss: string; } function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) { @@ -281,7 +288,7 @@ function renderText(result: Result, note: SNote | BNote) { if (typeof result.content !== "string") return; const parseOpts: Partial = { blockTextElements: {} - } + }; const document = parse(result.content || "", parseOpts); // Process include notes. diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 0a7db95b4..7d387cff3 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -84,6 +84,9 @@ + <%- renderSnippets("head:end") %> <%