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
parent
f3a84e35c0
commit
850c06fd70
|
@ -33,3 +33,9 @@ export type ActionButton = {
|
||||||
export type EmojiConfig = {
|
export type EmojiConfig = {
|
||||||
aliases: string[][];
|
aliases: string[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Decoration = {
|
||||||
|
tag: string;
|
||||||
|
prefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue