Fixes #560 and provides an initial implementation for assigning (#940)

decorators to tags.

This patch enables decorations for user specified tags - starting
with handling only a single decoration - a 'prefix' to be added
to the page name. Prefix is handled in the top bar title,
page navigator, wiki-links appearing within a page as well as
page autocomplete suggestions.
pull/951/head
Deepak Narayan 2024-07-13 17:26:00 +05:30 committed by GitHub
parent f3a84e35c0
commit 850c06fd70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 114 additions and 9 deletions

View File

@ -33,3 +33,9 @@ export type ActionButton = {
export type EmojiConfig = { export type EmojiConfig = {
aliases: string[][]; aliases: string[][];
}; };
export type Decoration = {
tag: string;
prefix: string;
};

View File

@ -7,9 +7,20 @@ import {
import { listFilesCached } from "../federation/federation.ts"; import { listFilesCached } from "../federation/federation.ts";
import { queryObjects } from "../index/plug_api.ts"; import { queryObjects } from "../index/plug_api.ts";
import { folderName } from "$sb/lib/resolve.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 // Completion
export async function pageComplete(completeEvent: CompleteEvent) { export async function pageComplete(completeEvent: CompleteEvent) {
try {
await updateDecoratorConfig();
} catch (err: any) {
await editor.flashNotification(err.message, "error");
}
// Try to match [[wikilink]] // Try to match [[wikilink]]
let isWikilink = true; let isWikilink = true;
let match = /\[\[([^\]@$#:\{}]*)$/.exec(completeEvent.linePrefix); let match = /\[\[([^\]@$#:\{}]*)$/.exec(completeEvent.linePrefix);
@ -82,10 +93,17 @@ export async function pageComplete(completeEvent: CompleteEvent) {
from: completeEvent.pos - match[1].length, from: completeEvent.pos - match[1].length,
options: allPages.map((pageMeta) => { options: allPages.map((pageMeta) => {
const completions: any[] = []; 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 (isWikilink) {
if (pageMeta.displayName) { if (pageMeta.displayName) {
const decoratedName = namePrefix + pageMeta.displayName;
completions.push({ completions.push({
label: `${pageMeta.displayName}`, label: `${pageMeta.displayName}`,
displayLabel: decoratedName,
boost: new Date(pageMeta.lastModified).getTime(), boost: new Date(pageMeta.lastModified).getTime(),
apply: pageMeta.tag === "template" apply: pageMeta.tag === "template"
? pageMeta.name ? pageMeta.name
@ -96,8 +114,10 @@ export async function pageComplete(completeEvent: CompleteEvent) {
} }
if (Array.isArray(pageMeta.aliases)) { if (Array.isArray(pageMeta.aliases)) {
for (const alias of pageMeta.aliases) { for (const alias of pageMeta.aliases) {
const decoratedName = namePrefix + alias;
completions.push({ completions.push({
label: `${alias}`, label: `${alias}`,
displayLabel: decoratedName,
boost: new Date(pageMeta.lastModified).getTime(), boost: new Date(pageMeta.lastModified).getTime(),
apply: pageMeta.tag === "template" apply: pageMeta.tag === "template"
? pageMeta.name ? pageMeta.name
@ -107,8 +127,10 @@ export async function pageComplete(completeEvent: CompleteEvent) {
}); });
} }
} }
const decoratedName = namePrefix + pageMeta.name;
completions.push({ completions.push({
label: `${pageMeta.name}`, label: `${pageMeta.name}`,
displayLabel: decoratedName,
boost: new Date(pageMeta.lastModified).getTime(), boost: new Date(pageMeta.lastModified).getTime(),
type: "page", type: "page",
}); });
@ -135,6 +157,7 @@ export async function pageComplete(completeEvent: CompleteEvent) {
}; };
} }
function fileMetaToPageMeta(fileMeta: FileMeta): PageMeta { function fileMetaToPageMeta(fileMeta: FileMeta): PageMeta {
const name = fileMeta.name.substring(0, fileMeta.name.length - 3); const name = fileMeta.name.substring(0, fileMeta.name.length - 3);
return { return {
@ -146,3 +169,17 @@ function fileMetaToPageMeta(fileMeta: FileMeta): PageMeta {
lastModified: new Date(fileMeta.lastModified).toISOString(), lastModified: new Date(fileMeta.lastModified).toISOString(),
} as PageMeta; } 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;
}

View File

@ -10,6 +10,7 @@ import {
import { encodePageRef, parsePageRef } from "$sb/lib/page_ref.ts"; import { encodePageRef, parsePageRef } from "$sb/lib/page_ref.ts";
import { Fragment, renderHtml, Tag } from "./html_render.ts"; import { Fragment, renderHtml, Tag } from "./html_render.ts";
import { isLocalPath } from "$sb/lib/resolve.ts"; import { isLocalPath } from "$sb/lib/resolve.ts";
import { PageMeta } from "$sb/types.ts";
export type MarkdownRenderOptions = { export type MarkdownRenderOptions = {
failOnUnknown?: true; failOnUnknown?: true;
@ -554,20 +555,29 @@ function traverseTag(
export function renderMarkdownToHtml( export function renderMarkdownToHtml(
t: ParseTree, t: ParseTree,
options: MarkdownRenderOptions = {}, options: MarkdownRenderOptions = {},
allPages: PageMeta[] = [],
) { ) {
preprocess(t); preprocess(t);
const htmlTree = posPreservingRender(t, options); const htmlTree = posPreservingRender(t, options);
if (htmlTree && options.translateUrls) { if (htmlTree) {
traverseTag(htmlTree, (t) => { traverseTag(htmlTree, (t) => {
if (typeof t === "string") { if (typeof t === "string") {
return; return;
} }
if (t.name === "img") { if (t.name === "img" && options.translateUrls) {
t.attrs!.src = options.translateUrls!(t.attrs!.src!, "image"); t.attrs!.src = options.translateUrls!(t.attrs!.src!, "image");
} }
if (t.name === "a" && t.attrs!.href) { if (t.name === "a" && t.attrs!.href) {
if (options.translateUrls) {
t.attrs!.href = options.translateUrls!(t.attrs!.href, "link"); 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) { if (t.body.length === 0) {
t.body = [t.attrs!.href]; t.body = [t.attrs!.href];
} }

View File

@ -5,6 +5,7 @@ import { defaultSettings } from "$common/settings.ts";
import { import {
ActionButton, ActionButton,
EmojiConfig, EmojiConfig,
Decoration,
FilterOption, FilterOption,
Notification, Notification,
PanelMode, PanelMode,
@ -24,6 +25,7 @@ export type BuiltinSettings = {
// Format: compatible with docker ignore // Format: compatible with docker ignore
spaceIgnore?: string; spaceIgnore?: string;
emoji?: EmojiConfig; emoji?: EmojiConfig;
decorations?: Decoration[];
}; };
export type PanelConfig = { export type PanelConfig = {

View File

@ -109,7 +109,7 @@ export class MarkdownWidget extends WidgetType {
return url; return url;
}, },
preserveAttributes: true, preserveAttributes: true,
}); }, this.client.ui.viewState.allPages);
if (cachedHtml === html) { if (cachedHtml === html) {
// HTML still same as in cache, no need to re-render // HTML still same as in cache, no need to re-render

View File

@ -66,9 +66,9 @@ export function cleanWikiLinkPlugin(client: Client) {
} }
return; return;
} }
const pageMeta = client.ui.viewState.allPages.find(p => p.name == url);
const linkText = alias || const linkText = alias ||
(url.includes("/") ? url.split("/").pop()! : url); (pageMeta?.pageDecorations.prefix ?? "") + (url.includes("/") ? url.split("/").pop()! : url);
// And replace it with a widget // And replace it with a widget
widgets.push( widgets.push(

View File

@ -1,5 +1,5 @@
import { FilterList } from "./filter.tsx"; 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 { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
import { PageMeta } from "../../plug-api/types.ts"; import { PageMeta } from "../../plug-api/types.ts";
import { isFederationPath } from "$sb/lib/resolve.ts"; import { isFederationPath } from "$sb/lib/resolve.ts";
@ -65,6 +65,7 @@ export function PageNavigator({
} }
options.push({ options.push({
...pageMeta, ...pageMeta,
name: (pageMeta.pageDecorations?.prefix ?? "") + pageMeta.name,
description, description,
orderId: orderId, orderId: orderId,
}); });

View File

@ -30,6 +30,7 @@ export function TopBar({
lhs, lhs,
onClick, onClick,
rhs, rhs,
pageNamePrefix,
}: { }: {
pageName?: string; pageName?: string;
unsavedChanges: boolean; unsavedChanges: boolean;
@ -46,6 +47,7 @@ export function TopBar({
actionButtons: ActionButton[]; actionButtons: ActionButton[];
lhs?: ComponentChildren; lhs?: ComponentChildren;
rhs?: ComponentChildren; rhs?: ComponentChildren;
pageNamePrefix?: string;
}) { }) {
return ( return (
<div <div
@ -57,6 +59,7 @@ export function TopBar({
<div className="main"> <div className="main">
<div className="inner"> <div className="inner">
<div className="wrapper"> <div className="wrapper">
<div className="sb-page-prefix">{pageNamePrefix}</div>
<span <span
id="sb-current-page" id="sb-current-page"
className={isLoading className={isLoading

View File

@ -314,6 +314,7 @@ export class MainUI {
style={{ flex: viewState.panels.lhs.mode }} style={{ flex: viewState.panels.lhs.mode }}
/> />
)} )}
pageNamePrefix={viewState.currentPageMeta?.pageDecorations?.prefix ?? ""}
/> />
<div id="sb-main"> <div id="sb-main">
{!!viewState.panels.lhs.mode && ( {!!viewState.panels.lhs.mode && (

View File

@ -1,4 +1,6 @@
import { PageMeta } from "../plug-api/types.ts";
import { Action, AppViewState } from "../type/web.ts"; import { Action, AppViewState } from "../type/web.ts";
import { PageState } from "./navigator.ts";
export default function reducer( export default function reducer(
state: AppViewState, state: AppViewState,
@ -20,6 +22,18 @@ export default function reducer(
}; };
case "page-loaded": { case "page-loaded": {
const mouseDetected = window.matchMedia("(pointer:fine)").matches; 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 { return {
...state, ...state,
isLoading: false, isLoading: false,
@ -58,16 +72,37 @@ export default function reducer(
const oldPageMeta = new Map( const oldPageMeta = new Map(
[...state.allPages].map((pm) => [pm.name, pm]), [...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) { for (const pageMeta of action.allPages) {
const oldPageMetaItem = oldPageMeta.get(pageMeta.name); const oldPageMetaItem = oldPageMeta.get(pageMeta.name);
if (oldPageMetaItem && oldPageMetaItem.lastOpened) { if (oldPageMetaItem && oldPageMetaItem.lastOpened) {
pageMeta.lastOpened = 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 { return {
...state, ...state,
allPages: action.allPages, allPages: action.allPages,
currentPageMeta: currPageMeta,
}; };
} }
case "start-navigate": { case "start-navigate": {

View File

@ -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 { .sb-panel {
iframe { iframe {
background-color: var(--root-background-color); background-color: var(--root-background-color);