diff --git a/lib/web.ts b/lib/web.ts index 13a79800..8b85b272 100644 --- a/lib/web.ts +++ b/lib/web.ts @@ -33,3 +33,9 @@ export type ActionButton = { export type EmojiConfig = { aliases: string[][]; }; + +export type Decoration = { + tag: string; + prefix: string; +}; + diff --git a/plugs/editor/complete.ts b/plugs/editor/complete.ts index 2e63fadb..f2fa760d 100644 --- a/plugs/editor/complete.ts +++ b/plugs/editor/complete.ts @@ -7,9 +7,20 @@ import { import { listFilesCached } from "../federation/federation.ts"; import { queryObjects } from "../index/plug_api.ts"; import { folderName } from "$sb/lib/resolve.ts"; +import { readSetting } from "$sb/lib/settings_page.ts"; +import { editor } from "$sb/syscalls.ts" +import type { Decoration } from "$lib/web.ts"; + +let decorations: Decoration[] = []; // Completion export async function pageComplete(completeEvent: CompleteEvent) { + try { + await updateDecoratorConfig(); + } catch (err: any) { + await editor.flashNotification(err.message, "error"); + } + // Try to match [[wikilink]] let isWikilink = true; let match = /\[\[([^\]@$#:\{}]*)$/.exec(completeEvent.linePrefix); @@ -82,10 +93,17 @@ export async function pageComplete(completeEvent: CompleteEvent) { from: completeEvent.pos - match[1].length, options: allPages.map((pageMeta) => { const completions: any[] = []; + let namePrefix = ""; + const decor = decorations.find(d => pageMeta.tags?.some((t: any) => d.tag === t)); + if (decor) { + namePrefix = decor.prefix; + } if (isWikilink) { if (pageMeta.displayName) { + const decoratedName = namePrefix + pageMeta.displayName; completions.push({ label: `${pageMeta.displayName}`, + displayLabel: decoratedName, boost: new Date(pageMeta.lastModified).getTime(), apply: pageMeta.tag === "template" ? pageMeta.name @@ -96,8 +114,10 @@ export async function pageComplete(completeEvent: CompleteEvent) { } if (Array.isArray(pageMeta.aliases)) { for (const alias of pageMeta.aliases) { + const decoratedName = namePrefix + alias; completions.push({ label: `${alias}`, + displayLabel: decoratedName, boost: new Date(pageMeta.lastModified).getTime(), apply: pageMeta.tag === "template" ? pageMeta.name @@ -107,8 +127,10 @@ export async function pageComplete(completeEvent: CompleteEvent) { }); } } + const decoratedName = namePrefix + pageMeta.name; completions.push({ label: `${pageMeta.name}`, + displayLabel: decoratedName, boost: new Date(pageMeta.lastModified).getTime(), type: "page", }); @@ -135,6 +157,7 @@ export async function pageComplete(completeEvent: CompleteEvent) { }; } + function fileMetaToPageMeta(fileMeta: FileMeta): PageMeta { const name = fileMeta.name.substring(0, fileMeta.name.length - 3); return { @@ -146,3 +169,17 @@ function fileMetaToPageMeta(fileMeta: FileMeta): PageMeta { lastModified: new Date(fileMeta.lastModified).toISOString(), } as PageMeta; } + +let lastConfigUpdate = 0; + +async function updateDecoratorConfig() { + // Update at most every 5 seconds + if (Date.now() < lastConfigUpdate + 5000) return; + lastConfigUpdate = Date.now(); + const decoratorConfig = await readSetting("decorations"); + if (!decoratorConfig) { + return; + } + + decorations = decoratorConfig; +} diff --git a/plugs/markdown/markdown_render.ts b/plugs/markdown/markdown_render.ts index 467beace..f3c1cde3 100644 --- a/plugs/markdown/markdown_render.ts +++ b/plugs/markdown/markdown_render.ts @@ -10,6 +10,7 @@ import { import { encodePageRef, parsePageRef } from "$sb/lib/page_ref.ts"; import { Fragment, renderHtml, Tag } from "./html_render.ts"; import { isLocalPath } from "$sb/lib/resolve.ts"; +import { PageMeta } from "$sb/types.ts"; export type MarkdownRenderOptions = { failOnUnknown?: true; @@ -554,20 +555,29 @@ function traverseTag( export function renderMarkdownToHtml( t: ParseTree, options: MarkdownRenderOptions = {}, + allPages: PageMeta[] = [], ) { preprocess(t); const htmlTree = posPreservingRender(t, options); - if (htmlTree && options.translateUrls) { + if (htmlTree) { traverseTag(htmlTree, (t) => { if (typeof t === "string") { return; } - if (t.name === "img") { + if (t.name === "img" && options.translateUrls) { t.attrs!.src = options.translateUrls!(t.attrs!.src!, "image"); } if (t.name === "a" && t.attrs!.href) { - t.attrs!.href = options.translateUrls!(t.attrs!.href, "link"); + if (options.translateUrls) { + t.attrs!.href = options.translateUrls!(t.attrs!.href, "link"); + } + if (t.attrs!["data-ref"]?.length) { + const pageMeta = allPages.find(p => t.attrs!["data-ref"]!.startsWith(p.name)); + if (pageMeta) { + t.body = [(pageMeta.pageDecorations?.prefix ?? "") + t.body] + } + } if (t.body.length === 0) { t.body = [t.attrs!.href]; } diff --git a/type/web.ts b/type/web.ts index 3b373330..616b8063 100644 --- a/type/web.ts +++ b/type/web.ts @@ -5,6 +5,7 @@ import { defaultSettings } from "$common/settings.ts"; import { ActionButton, EmojiConfig, + Decoration, FilterOption, Notification, PanelMode, @@ -24,6 +25,7 @@ export type BuiltinSettings = { // Format: compatible with docker ignore spaceIgnore?: string; emoji?: EmojiConfig; + decorations?: Decoration[]; }; export type PanelConfig = { diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts index 6a92d4e2..d7f23d73 100644 --- a/web/cm_plugins/markdown_widget.ts +++ b/web/cm_plugins/markdown_widget.ts @@ -93,7 +93,7 @@ export class MarkdownWidget extends WidgetType { extendedMarkdownLanguage, trimmedMarkdown, ); - + const html = renderMarkdownToHtml(mdTree, { // Annotate every element with its position so we can use it to put // the cursor there when the user clicks on the table. @@ -109,7 +109,7 @@ export class MarkdownWidget extends WidgetType { return url; }, preserveAttributes: true, - }); + }, this.client.ui.viewState.allPages); if (cachedHtml === html) { // HTML still same as in cache, no need to re-render diff --git a/web/cm_plugins/wiki_link.ts b/web/cm_plugins/wiki_link.ts index 797e633c..a0e4d733 100644 --- a/web/cm_plugins/wiki_link.ts +++ b/web/cm_plugins/wiki_link.ts @@ -66,9 +66,9 @@ export function cleanWikiLinkPlugin(client: Client) { } return; } - + const pageMeta = client.ui.viewState.allPages.find(p => p.name == url); const linkText = alias || - (url.includes("/") ? url.split("/").pop()! : url); + (pageMeta?.pageDecorations.prefix ?? "") + (url.includes("/") ? url.split("/").pop()! : url); // And replace it with a widget widgets.push( diff --git a/web/components/page_navigator.tsx b/web/components/page_navigator.tsx index b289dd63..e9adca83 100644 --- a/web/components/page_navigator.tsx +++ b/web/components/page_navigator.tsx @@ -1,5 +1,5 @@ import { FilterList } from "./filter.tsx"; -import { FilterOption } from "$lib/web.ts"; +import { FilterOption, Decoration } from "$lib/web.ts"; import { CompletionContext, CompletionResult } from "@codemirror/autocomplete"; import { PageMeta } from "../../plug-api/types.ts"; import { isFederationPath } from "$sb/lib/resolve.ts"; @@ -65,6 +65,7 @@ export function PageNavigator({ } options.push({ ...pageMeta, + name: (pageMeta.pageDecorations?.prefix ?? "") + pageMeta.name, description, orderId: orderId, }); diff --git a/web/components/top_bar.tsx b/web/components/top_bar.tsx index 28c64c6c..e827c03b 100644 --- a/web/components/top_bar.tsx +++ b/web/components/top_bar.tsx @@ -30,6 +30,7 @@ export function TopBar({ lhs, onClick, rhs, + pageNamePrefix, }: { pageName?: string; unsavedChanges: boolean; @@ -46,6 +47,7 @@ export function TopBar({ actionButtons: ActionButton[]; lhs?: ComponentChildren; rhs?: ComponentChildren; + pageNamePrefix?: string; }) { return (
+
{pageNamePrefix}
)} + pageNamePrefix={viewState.currentPageMeta?.pageDecorations?.prefix ?? ""} />
{!!viewState.panels.lhs.mode && ( diff --git a/web/reducer.ts b/web/reducer.ts index 9b36f3d9..229d7fd0 100644 --- a/web/reducer.ts +++ b/web/reducer.ts @@ -1,4 +1,6 @@ +import { PageMeta } from "../plug-api/types.ts"; import { Action, AppViewState } from "../type/web.ts"; +import { PageState } from "./navigator.ts"; export default function reducer( state: AppViewState, @@ -20,6 +22,18 @@ export default function reducer( }; case "page-loaded": { const mouseDetected = window.matchMedia("(pointer:fine)").matches; + const pageMeta = state.allPages.find(p => p.name == action.meta.name); + const decor = state.settings.decorations?.filter(d => pageMeta?.tags?.some(t => d.tag === t)); + if (decor && decor.length > 0) { + const mergedDecorations = decor.reduceRight((accumulator, el) => { + accumulator = {...accumulator, ...el}; + return accumulator; + }); + if (mergedDecorations) { + const { tag, ...currPageDecorations } = mergedDecorations; + action.meta.pageDecorations = currPageDecorations; + } + } return { ...state, isLoading: false, @@ -58,16 +72,37 @@ export default function reducer( const oldPageMeta = new Map( [...state.allPages].map((pm) => [pm.name, pm]), ); + let currPageMeta = oldPageMeta.get(state.currentPage!); + if (currPageMeta === undefined) { + currPageMeta = {} as PageMeta; + } for (const pageMeta of action.allPages) { const oldPageMetaItem = oldPageMeta.get(pageMeta.name); if (oldPageMetaItem && oldPageMetaItem.lastOpened) { pageMeta.lastOpened = oldPageMetaItem.lastOpened; } + const decor = state.settings.decorations?.filter(d => pageMeta.tags?.some((t: any) => d.tag === t)); + // Page can have multiple decorations applied via different tags, accumulate them. + // The decorations higher in the decorations list defined in SETTINGS gets + // higher precedence. + if (decor && decor.length > 0) { + const mergedDecorations = decor.reduceRight((accumulator, el) => { + accumulator = {...accumulator, ...el}; + return accumulator; + }); + if (mergedDecorations) { + const { tag, ...currPageDecorations} = mergedDecorations; + pageMeta.pageDecorations = currPageDecorations; + if (pageMeta.name === state.currentPage) { + currPageMeta!.pageDecorations = currPageDecorations; + } + } + } } - return { ...state, allPages: action.allPages, + currentPageMeta: currPageMeta, }; } case "start-navigate": { diff --git a/web/styles/main.scss b/web/styles/main.scss index 3afcc8de..70997251 100644 --- a/web/styles/main.scss +++ b/web/styles/main.scss @@ -207,6 +207,16 @@ body { } } +.sb-page-prefix { + display: flex; + align-items: baseline; + flex: 0 0 auto; + text-align: left; + padding: 1px; + margin-right: 3px; + font-family: var(--ui-font); +} + .sb-panel { iframe { background-color: var(--root-background-color);