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 = {
aliases: string[][];
};
export type Decoration = {
tag: string;
prefix: string;
};

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

@ -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 (
<div
@ -57,6 +59,7 @@ export function TopBar({
<div className="main">
<div className="inner">
<div className="wrapper">
<div className="sb-page-prefix">{pageNamePrefix}</div>
<span
id="sb-current-page"
className={isLoading

View File

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

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