Object decorators (#995)
Implemented objectDecorators to replace pageDecorations in SETTINGSpull/1003/head
parent
69578ae09f
commit
75471fa86b
|
@ -32,7 +32,7 @@ export abstract class CommonSystem {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected mq: DataStoreMQ,
|
protected mq: DataStoreMQ,
|
||||||
protected ds: DataStore,
|
public ds: DataStore,
|
||||||
public eventHook: EventHook,
|
public eventHook: EventHook,
|
||||||
public readOnlyMode: boolean,
|
public readOnlyMode: boolean,
|
||||||
protected enableSpaceScript: boolean,
|
protected enableSpaceScript: boolean,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { assertEquals } from "$std/testing/asserts.ts";
|
||||||
|
import { parseExpression } from "$common/expression_parser.ts";
|
||||||
|
|
||||||
|
Deno.test("Test expression parser", () => {
|
||||||
|
// Just a sanity check here
|
||||||
|
assertEquals(parseExpression("1 + 2"), ["+", ["number", 1], ["number", 2]]);
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { QueryExpression } from "$sb/types.ts";
|
||||||
|
import { parseTreeToAST } from "$sb/lib/tree.ts";
|
||||||
|
import { expressionLanguage } from "$common/template/template_parser.ts";
|
||||||
|
import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts";
|
||||||
|
import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts";
|
||||||
|
|
||||||
|
export function parseExpression(s: string): QueryExpression {
|
||||||
|
const ast = parseTreeToAST(
|
||||||
|
lezerToParseTree(s, expressionLanguage.parser.parse(s).topNode),
|
||||||
|
);
|
||||||
|
return expressionToKvQueryExpression(ast[1]);
|
||||||
|
}
|
|
@ -2,7 +2,10 @@ import YAML from "js-yaml";
|
||||||
import { INDEX_TEMPLATE, SETTINGS_TEMPLATE } from "./PAGE_TEMPLATES.ts";
|
import { INDEX_TEMPLATE, SETTINGS_TEMPLATE } from "./PAGE_TEMPLATES.ts";
|
||||||
import { SpacePrimitives } from "./spaces/space_primitives.ts";
|
import { SpacePrimitives } from "./spaces/space_primitives.ts";
|
||||||
import { cleanupJSON } from "../plug-api/lib/json.ts";
|
import { cleanupJSON } from "../plug-api/lib/json.ts";
|
||||||
import type { BuiltinSettings } from "../type/web.ts";
|
import { BuiltinSettings } from "$type/settings.ts";
|
||||||
|
import { DataStore, ObjectEnricher } from "$lib/data/datastore.ts";
|
||||||
|
import { parseExpression } from "$common/expression_parser.ts";
|
||||||
|
import { QueryExpression } from "$sb/types.ts";
|
||||||
|
|
||||||
const yamlSettingsRegex = /^(```+|~~~+)ya?ml\r?\n([\S\s]+?)\1/m;
|
const yamlSettingsRegex = /^(```+|~~~+)ya?ml\r?\n([\S\s]+?)\1/m;
|
||||||
|
|
||||||
|
@ -100,3 +103,37 @@ export async function ensureAndLoadSettingsAndIndex(
|
||||||
cleanupJSON(settings);
|
cleanupJSON(settings);
|
||||||
return { ...defaultSettings, ...settings };
|
return { ...defaultSettings, ...settings };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateObjectDecorators(
|
||||||
|
settings: BuiltinSettings,
|
||||||
|
ds: DataStore,
|
||||||
|
) {
|
||||||
|
if (settings.objectDecorators) {
|
||||||
|
// Reload object decorators
|
||||||
|
const newDecorators: ObjectEnricher[] = [];
|
||||||
|
for (
|
||||||
|
const decorator of settings.objectDecorators
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const parsedWhere = parseExpression(decorator.where);
|
||||||
|
const parsedDynamicAttributes: Record<string, QueryExpression> = {};
|
||||||
|
for (const [key, value] of Object.entries(decorator.attributes)) {
|
||||||
|
parsedDynamicAttributes[key] = parseExpression(value);
|
||||||
|
}
|
||||||
|
newDecorators.push({
|
||||||
|
where: parsedWhere,
|
||||||
|
attributes: parsedDynamicAttributes,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(
|
||||||
|
"Error parsing object decorator",
|
||||||
|
decorator,
|
||||||
|
"got error",
|
||||||
|
e.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info(`Loaded ${newDecorators.length} object decorators`);
|
||||||
|
ds.objectEnrichers = newDecorators;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { DataStore } from "$lib/data/datastore.ts";
|
||||||
|
import { MemoryKvPrimitives } from "$lib/data/memory_kv_primitives.ts";
|
||||||
|
|
||||||
|
Deno.bench("DataStore enrichment benchmark with match", (b) => {
|
||||||
|
// Dummy datastore with a single object enricher
|
||||||
|
const datastore = new DataStore(new MemoryKvPrimitives(), {});
|
||||||
|
|
||||||
|
datastore.objectEnrichers = [
|
||||||
|
{
|
||||||
|
where: ["=", ["attr", "tags"], ["string", "person"]],
|
||||||
|
attributes: {
|
||||||
|
fullName: ["+", ["+", ["attr", "firstName"], ["string", " "]], [
|
||||||
|
"attr",
|
||||||
|
"lastName",
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
b.start();
|
||||||
|
// Let's try with half a million entries
|
||||||
|
for (let i = 0; i < 500000; i++) {
|
||||||
|
const obj = {
|
||||||
|
firstName: "Pete",
|
||||||
|
lastName: "Smith",
|
||||||
|
tags: ["person"],
|
||||||
|
};
|
||||||
|
datastore.enrichObject(obj);
|
||||||
|
}
|
||||||
|
b.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.bench("DataStore enrichment benchmark without match", (b) => {
|
||||||
|
// Dummy datastore with a single object enricher
|
||||||
|
const datastore = new DataStore(new MemoryKvPrimitives(), {});
|
||||||
|
|
||||||
|
datastore.objectEnrichers = [
|
||||||
|
{
|
||||||
|
where: ["=", ["attr", "tags"], ["string", "person"]],
|
||||||
|
attributes: {
|
||||||
|
fullName: ["+", ["+", ["attr", "firstName"], ["string", " "]], [
|
||||||
|
"attr",
|
||||||
|
"lastName",
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
b.start();
|
||||||
|
// Let's try with half a million entries
|
||||||
|
for (let i = 0; i < 500000; i++) {
|
||||||
|
const obj = {
|
||||||
|
firstName: "Pete",
|
||||||
|
lastName: "Smith",
|
||||||
|
tags: ["peson"],
|
||||||
|
};
|
||||||
|
datastore.enrichObject(obj);
|
||||||
|
}
|
||||||
|
b.end();
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import { IndexedDBKvPrimitives } from "../data/indexeddb_kv_primitives.ts";
|
import { IndexedDBKvPrimitives } from "../data/indexeddb_kv_primitives.ts";
|
||||||
import { DataStore } from "../data/datastore.ts";
|
import { cleanupEmptyObjects, DataStore } from "../data/datastore.ts";
|
||||||
import { DenoKvPrimitives } from "../data/deno_kv_primitives.ts";
|
import { DenoKvPrimitives } from "../data/deno_kv_primitives.ts";
|
||||||
import { KvPrimitives } from "../data/kv_primitives.ts";
|
import { KvPrimitives } from "../data/kv_primitives.ts";
|
||||||
import { assertEquals } from "$std/testing/asserts.ts";
|
import { assertEquals } from "$std/testing/asserts.ts";
|
||||||
|
@ -103,6 +103,65 @@ async function test(db: KvPrimitives) {
|
||||||
}),
|
}),
|
||||||
[{ key: ["kv", "complicated"], value: { random: [] } }],
|
[{ key: ["kv", "complicated"], value: { random: [] } }],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Test object enrichment
|
||||||
|
datastore.objectEnrichers = [
|
||||||
|
{ // fullName
|
||||||
|
where: ["=", ["attr", "tags"], ["string", "person"]],
|
||||||
|
attributes: {
|
||||||
|
fullName: ["+", ["+", ["attr", "firstName"], ["string", " "]], [
|
||||||
|
"attr",
|
||||||
|
"lastName",
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: ["=", ["attr", "tags"], ["string", "person"]],
|
||||||
|
attributes: {
|
||||||
|
"pageDecoration.prefix.bla.doh": ["+", ["string", "🧑 "], [
|
||||||
|
"attr",
|
||||||
|
"fullName",
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: ["=", ["attr", "tags"], ["string", "person"]],
|
||||||
|
attributes: {
|
||||||
|
"existingObjAttribute.another": ["string", "value"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const obj: Record<string, any> = {
|
||||||
|
firstName: "Pete",
|
||||||
|
lastName: "Smith",
|
||||||
|
existingObjAttribute: {
|
||||||
|
something: true,
|
||||||
|
},
|
||||||
|
tags: ["person"],
|
||||||
|
};
|
||||||
|
const pristineCopy = JSON.parse(JSON.stringify(obj));
|
||||||
|
|
||||||
|
datastore.enrichObject(obj);
|
||||||
|
assertEquals(obj.fullName, "Pete Smith");
|
||||||
|
assertEquals(obj.pageDecoration, {
|
||||||
|
prefix: { bla: { doh: "🧑 Pete Smith" } },
|
||||||
|
});
|
||||||
|
assertEquals(obj.existingObjAttribute.something, true);
|
||||||
|
assertEquals(obj.existingObjAttribute.another, "value");
|
||||||
|
|
||||||
|
// And now let's clean it again
|
||||||
|
datastore.cleanEnrichedObject(obj);
|
||||||
|
|
||||||
|
assertEquals(obj, pristineCopy);
|
||||||
|
|
||||||
|
// Validate no async functions are called in the object enrichment
|
||||||
|
datastore.objectEnrichers = [
|
||||||
|
{
|
||||||
|
where: ["call", "$query", []],
|
||||||
|
attributes: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Deno.test("Test Deno KV DataStore", async () => {
|
Deno.test("Test Deno KV DataStore", async () => {
|
||||||
|
@ -122,3 +181,22 @@ Deno.test("Test IndexDB DataStore", {
|
||||||
await test(db);
|
await test(db);
|
||||||
db.close();
|
db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("Test cleanupEmptyObjects", () => {
|
||||||
|
const testObject: any = {
|
||||||
|
attribute1: 10,
|
||||||
|
another: { nested: 20, removeMe: {} },
|
||||||
|
list: [],
|
||||||
|
removeMe: {
|
||||||
|
deeply: {},
|
||||||
|
},
|
||||||
|
another2: [1, 2, 3],
|
||||||
|
};
|
||||||
|
cleanupEmptyObjects(testObject);
|
||||||
|
assertEquals(testObject, {
|
||||||
|
attribute1: 10,
|
||||||
|
another: { nested: 20 },
|
||||||
|
list: [],
|
||||||
|
another2: [1, 2, 3],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { applyQueryNoFilterKV } from "../../plug-api/lib/query.ts";
|
import { applyQueryNoFilterKV } from "../../plug-api/lib/query.ts";
|
||||||
import { FunctionMap, KV, KvKey, KvQuery } from "../../plug-api/types.ts";
|
import {
|
||||||
|
FunctionMap,
|
||||||
|
KV,
|
||||||
|
KvKey,
|
||||||
|
KvQuery,
|
||||||
|
QueryExpression,
|
||||||
|
} from "../../plug-api/types.ts";
|
||||||
import { builtinFunctions } from "../builtin_query_functions.ts";
|
import { builtinFunctions } from "../builtin_query_functions.ts";
|
||||||
import { KvPrimitives } from "./kv_primitives.ts";
|
import { KvPrimitives } from "./kv_primitives.ts";
|
||||||
import { evalQueryExpression } from "../../plug-api/lib/query_expression.ts";
|
import { evalQueryExpression } from "../../plug-api/lib/query_expression.ts";
|
||||||
|
@ -11,6 +17,7 @@ export class DataStore {
|
||||||
constructor(
|
constructor(
|
||||||
readonly kv: KvPrimitives,
|
readonly kv: KvPrimitives,
|
||||||
public functionMap: FunctionMap = builtinFunctions,
|
public functionMap: FunctionMap = builtinFunctions,
|
||||||
|
public objectEnrichers: ObjectEnricher[] = [],
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,11 +25,17 @@ export class DataStore {
|
||||||
return (await this.batchGet([key]))[0];
|
return (await this.batchGet([key]))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
batchGet<T = any>(keys: KvKey[]): Promise<(T | null)[]> {
|
async batchGet<T = any>(keys: KvKey[]): Promise<(T | null)[]> {
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return Promise.resolve([]);
|
return [];
|
||||||
}
|
}
|
||||||
return this.kv.batchGet(keys);
|
const results = await this.kv.batchGet(keys);
|
||||||
|
|
||||||
|
// Enrich the objects based on object enrichers
|
||||||
|
for (const entry of results) {
|
||||||
|
this.enrichObject(entry);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: KvKey, value: any): Promise<void> {
|
set(key: KvKey, value: any): Promise<void> {
|
||||||
|
@ -41,6 +54,7 @@ export class DataStore {
|
||||||
console.warn(`Duplicate key ${keyString} in batchSet, skipping`);
|
console.warn(`Duplicate key ${keyString} in batchSet, skipping`);
|
||||||
} else {
|
} else {
|
||||||
allKeyStrings.add(keyString);
|
allKeyStrings.add(keyString);
|
||||||
|
this.cleanEnrichedObject(value);
|
||||||
uniqueEntries.push({ key, value });
|
uniqueEntries.push({ key, value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +91,8 @@ export class DataStore {
|
||||||
for await (
|
for await (
|
||||||
const entry of this.kv.query(query)
|
const entry of this.kv.query(query)
|
||||||
) {
|
) {
|
||||||
|
// Enrich
|
||||||
|
this.enrichObject(entry.value);
|
||||||
// Filter
|
// Filter
|
||||||
if (
|
if (
|
||||||
query.filter &&
|
query.filter &&
|
||||||
|
@ -118,4 +134,139 @@ export class DataStore {
|
||||||
}
|
}
|
||||||
return this.batchDelete(keys);
|
return this.batchDelete(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches the object with the attributes defined in the object enrichers on the fly
|
||||||
|
* @param object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
enrichObject(object: any) {
|
||||||
|
// Check if this object looks like an object value
|
||||||
|
if (!object || typeof object !== "object") {
|
||||||
|
// Skip
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const enricher of this.objectEnrichers) {
|
||||||
|
const whereEvalResult = evalQueryExpression(
|
||||||
|
enricher.where,
|
||||||
|
object,
|
||||||
|
{}, // We will not support variables in enrichers for now
|
||||||
|
this.functionMap,
|
||||||
|
);
|
||||||
|
if (whereEvalResult instanceof Promise) {
|
||||||
|
// For performance reasons we can only allow synchronous where clauses
|
||||||
|
throw new Error(
|
||||||
|
`Enricher where clause cannot be an async function: ${enricher.where}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
whereEvalResult
|
||||||
|
) {
|
||||||
|
// The `where` matches so we should enrich this object
|
||||||
|
for (
|
||||||
|
const [attributeSelector, expression] of Object.entries(
|
||||||
|
enricher.attributes,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Recursively travel to the attribute based on the selector, which may contain .'s to go deeper
|
||||||
|
let objectValue = object;
|
||||||
|
const selectorParts = attributeSelector.split(".");
|
||||||
|
for (const part of selectorParts.slice(0, -1)) {
|
||||||
|
if (typeof objectValue[part] !== "object") {
|
||||||
|
// Pre-create the object if it doesn't exist
|
||||||
|
objectValue[part] = {};
|
||||||
|
}
|
||||||
|
objectValue = objectValue[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = evalQueryExpression(
|
||||||
|
expression,
|
||||||
|
object,
|
||||||
|
{},
|
||||||
|
this.functionMap,
|
||||||
|
);
|
||||||
|
if (value instanceof Promise) {
|
||||||
|
// For performance reasons we can only allow synchronous expressions
|
||||||
|
throw new Error(
|
||||||
|
`Enricher dynamic attribute expression cannot be an async function: ${expression}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
objectValue[selectorParts[selectorParts.length - 1]] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverses the enriching of the object with the attributes defined in objectEnrichers
|
||||||
|
* @param object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
cleanEnrichedObject(object: any) {
|
||||||
|
// Check if this object looks like an object value
|
||||||
|
if (!object || typeof object !== "object") {
|
||||||
|
// Skip
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const enricher of this.objectEnrichers) {
|
||||||
|
if (
|
||||||
|
evalQueryExpression(
|
||||||
|
enricher.where,
|
||||||
|
object,
|
||||||
|
{}, // We will not support variables in enrichers for now
|
||||||
|
this.functionMap,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// The `where` matches so we should clean this object from the dynamic attributes
|
||||||
|
for (
|
||||||
|
const [attributeSelector, _expression] of Object.entries(
|
||||||
|
enricher.attributes,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Recursively travel to the attribute based on the selector, which may contain .'s to go deeper
|
||||||
|
let objectValue = object;
|
||||||
|
const selectorParts = attributeSelector.split(".");
|
||||||
|
for (const part of selectorParts.slice(0, -1)) {
|
||||||
|
if (typeof objectValue[part] !== "object") {
|
||||||
|
// This shouldn't happen, but let's back out
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
objectValue = objectValue[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete objectValue[selectorParts[selectorParts.length - 1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clean up empty objects, this is somewhat questionable, because it also means that if the user intentionally kept empty objects in there, these will be wiped
|
||||||
|
cleanupEmptyObjects(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ObjectEnricher = {
|
||||||
|
// If this expression evaluates to true for the given object
|
||||||
|
where: QueryExpression;
|
||||||
|
// Dynamically add these attributes to the object, can use "." syntax for deeper attribute definition
|
||||||
|
attributes: Record<string, QueryExpression>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively removes empty objects from the object
|
||||||
|
* @param object
|
||||||
|
*/
|
||||||
|
export function cleanupEmptyObjects(object: any) {
|
||||||
|
for (const key in object) {
|
||||||
|
// Skip arrays
|
||||||
|
if (Array.isArray(object[key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof object[key] === "object") {
|
||||||
|
cleanupEmptyObjects(object[key]);
|
||||||
|
if (Object.keys(object[key]).length === 0) {
|
||||||
|
delete object[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,4 +16,3 @@ export * as YAML from "./syscalls/yaml.ts";
|
||||||
export * as mq from "./syscalls/mq.ts";
|
export * as mq from "./syscalls/mq.ts";
|
||||||
export * from "./syscall.ts";
|
export * from "./syscall.ts";
|
||||||
export * as datastore from "./syscalls/datastore.ts";
|
export * as datastore from "./syscalls/datastore.ts";
|
||||||
export * as decoration from "./syscalls/decoration.ts";
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { PageMeta } from "$sb/types.ts";
|
|
||||||
|
|
||||||
export function applyDecorationsToPages(
|
|
||||||
pages: PageMeta[],
|
|
||||||
): Promise<PageMeta[]> {
|
|
||||||
return syscall("decoration.applyDecorationsToPages", pages);
|
|
||||||
}
|
|
|
@ -26,8 +26,6 @@ export type PageMeta = ObjectValue<
|
||||||
* Decorates a page when it matches certain criteria
|
* Decorates a page when it matches certain criteria
|
||||||
*/
|
*/
|
||||||
export type PageDecoration = {
|
export type PageDecoration = {
|
||||||
where?: string;
|
|
||||||
whereParsed?: QueryExpression;
|
|
||||||
prefix: string;
|
prefix: string;
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,6 @@ 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 { decoration } from "$sb/syscalls.ts";
|
|
||||||
import type { LinkObject } from "../index/page_links.ts";
|
import type { LinkObject } from "../index/page_links.ts";
|
||||||
|
|
||||||
// A meta page is a page tagged with either #template or #meta
|
// A meta page is a page tagged with either #template or #meta
|
||||||
|
@ -121,9 +120,6 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
|
|
||||||
const folder = folderName(completeEvent.pageName);
|
const folder = folderName(completeEvent.pageName);
|
||||||
|
|
||||||
// Decorate the pages
|
|
||||||
allPages = await decoration.applyDecorationsToPages(allPages as PageMeta[]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: completeEvent.pos - prefix.length,
|
from: completeEvent.pos - prefix.length,
|
||||||
options: allPages.map((pageMeta) => {
|
options: allPages.map((pageMeta) => {
|
||||||
|
|
|
@ -53,6 +53,7 @@ export async function renderTemplateWidgets(side: "top" | "bottom"): Promise<
|
||||||
await language.parseLanguage("expression", blockDef.where!),
|
await language.parseLanguage("expression", blockDef.where!),
|
||||||
);
|
);
|
||||||
const parsedExpression = expressionToKvQueryExpression(exprAST[1]);
|
const parsedExpression = expressionToKvQueryExpression(exprAST[1]);
|
||||||
|
|
||||||
if (await evalQueryExpression(parsedExpression, pageMeta, {}, {})) {
|
if (await evalQueryExpression(parsedExpression, pageMeta, {}, {})) {
|
||||||
// Match! We're happy
|
// Match! We're happy
|
||||||
const templateText = await space.readPage(template.ref);
|
const templateText = await space.readPage(template.ref);
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { SilverBulletHooks } from "../lib/manifest.ts";
|
import { SilverBulletHooks } from "../lib/manifest.ts";
|
||||||
import { ensureAndLoadSettingsAndIndex } from "$common/settings.ts";
|
import {
|
||||||
|
ensureAndLoadSettingsAndIndex,
|
||||||
|
updateObjectDecorators,
|
||||||
|
} from "$common/settings.ts";
|
||||||
import { AssetBundlePlugSpacePrimitives } from "$common/spaces/asset_bundle_space_primitives.ts";
|
import { AssetBundlePlugSpacePrimitives } from "$common/spaces/asset_bundle_space_primitives.ts";
|
||||||
import { FilteredSpacePrimitives } from "$common/spaces/filtered_space_primitives.ts";
|
import { FilteredSpacePrimitives } from "$common/spaces/filtered_space_primitives.ts";
|
||||||
import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts";
|
import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts";
|
||||||
|
@ -10,13 +13,13 @@ import { DataStore } from "$lib/data/datastore.ts";
|
||||||
import { KvPrimitives } from "$lib/data/kv_primitives.ts";
|
import { KvPrimitives } from "$lib/data/kv_primitives.ts";
|
||||||
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
||||||
import { System } from "$lib/plugos/system.ts";
|
import { System } from "$lib/plugos/system.ts";
|
||||||
import { BuiltinSettings } from "../type/web.ts";
|
|
||||||
import { JWTIssuer } from "./crypto.ts";
|
import { JWTIssuer } from "./crypto.ts";
|
||||||
import { compile as gitIgnoreCompiler } from "gitignore-parser";
|
import { compile as gitIgnoreCompiler } from "gitignore-parser";
|
||||||
import { ServerSystem } from "./server_system.ts";
|
import { ServerSystem } from "./server_system.ts";
|
||||||
import { determineShellBackend, NotSupportedShell } from "./shell_backend.ts";
|
import { determineShellBackend, NotSupportedShell } from "./shell_backend.ts";
|
||||||
import { ShellBackend } from "./shell_backend.ts";
|
import { ShellBackend } from "./shell_backend.ts";
|
||||||
import { determineStorageBackend } from "./storage_backend.ts";
|
import { determineStorageBackend } from "./storage_backend.ts";
|
||||||
|
import { BuiltinSettings } from "$type/settings.ts";
|
||||||
|
|
||||||
export type SpaceServerConfig = {
|
export type SpaceServerConfig = {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
@ -136,5 +139,9 @@ export class SpaceServer {
|
||||||
|
|
||||||
async reloadSettings() {
|
async reloadSettings() {
|
||||||
this.settings = await ensureAndLoadSettingsAndIndex(this.spacePrimitives);
|
this.settings = await ensureAndLoadSettingsAndIndex(this.spacePrimitives);
|
||||||
|
|
||||||
|
if (this.serverSystem) {
|
||||||
|
updateObjectDecorators(this.settings, this.serverSystem.ds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { ActionButton, EmojiConfig, Shortcut } from "$lib/web.ts";
|
||||||
|
import { Manifest } from "$lib/manifest.ts";
|
||||||
|
|
||||||
|
export type ObjectDecorator = {
|
||||||
|
// The expression to match against the object
|
||||||
|
where: string;
|
||||||
|
// The dynamic attributes to add to the object
|
||||||
|
attributes: Record<string, string>; // attributePath -> expression
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BuiltinSettings = {
|
||||||
|
indexPage: string;
|
||||||
|
shortcuts?: Shortcut[];
|
||||||
|
useSmartQuotes?: boolean;
|
||||||
|
maximumAttachmentSize?: number;
|
||||||
|
// Open the last page that was open when the app was closed
|
||||||
|
pwaOpenLastPage?: boolean;
|
||||||
|
// UI visuals
|
||||||
|
hideEditButton?: boolean;
|
||||||
|
hideSyncButton?: boolean;
|
||||||
|
actionButtons: ActionButton[];
|
||||||
|
objectDecorators?: ObjectDecorator[];
|
||||||
|
// Format: compatible with docker ignore
|
||||||
|
spaceIgnore?: string;
|
||||||
|
emoji?: EmojiConfig;
|
||||||
|
// DEPRECATED: Use space styles instead
|
||||||
|
customStyles?: string | string[];
|
||||||
|
// DEPRECATED: Use shortcuts instead
|
||||||
|
plugOverrides?: Record<string, Partial<Manifest>>;
|
||||||
|
|
||||||
|
// NOTE: Bit niche, maybe delete at some point?
|
||||||
|
defaultLinkStyle?: string;
|
||||||
|
};
|
36
type/web.ts
36
type/web.ts
|
@ -1,38 +1,8 @@
|
||||||
import { Manifest } from "../lib/manifest.ts";
|
|
||||||
import { PageDecoration, PageMeta } from "../plug-api/types.ts";
|
|
||||||
import { AppCommand } from "../lib/command.ts";
|
import { AppCommand } from "../lib/command.ts";
|
||||||
import { defaultSettings } from "$common/settings.ts";
|
import { defaultSettings } from "$common/settings.ts";
|
||||||
import {
|
import { FilterOption, Notification, PanelMode } from "$lib/web.ts";
|
||||||
ActionButton,
|
import { BuiltinSettings } from "$type/settings.ts";
|
||||||
EmojiConfig,
|
import { PageMeta } from "$sb/types.ts";
|
||||||
FilterOption,
|
|
||||||
Notification,
|
|
||||||
PanelMode,
|
|
||||||
Shortcut,
|
|
||||||
} from "$lib/web.ts";
|
|
||||||
|
|
||||||
export type BuiltinSettings = {
|
|
||||||
indexPage: string;
|
|
||||||
shortcuts?: Shortcut[];
|
|
||||||
useSmartQuotes?: boolean;
|
|
||||||
maximumAttachmentSize?: number;
|
|
||||||
// Open the last page that was open when the app was closed
|
|
||||||
pwaOpenLastPage?: boolean;
|
|
||||||
// UI visuals
|
|
||||||
hideEditButton?: boolean;
|
|
||||||
hideSyncButton?: boolean;
|
|
||||||
actionButtons: ActionButton[];
|
|
||||||
pageDecorations?: PageDecoration[];
|
|
||||||
// Format: compatible with docker ignore
|
|
||||||
spaceIgnore?: string;
|
|
||||||
emoji?: EmojiConfig;
|
|
||||||
// DEPRECATED: Use space styles instead
|
|
||||||
customStyles?: string | string[];
|
|
||||||
// DEPRECATED: Use shortcuts instead
|
|
||||||
plugOverrides?: Record<string, Partial<Manifest>>;
|
|
||||||
// NOTE: Bit niche, maybe delete at some point?
|
|
||||||
defaultLinkStyle?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PanelConfig = {
|
export type PanelConfig = {
|
||||||
mode?: PanelMode;
|
mode?: PanelMode;
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
PathPageNavigator,
|
PathPageNavigator,
|
||||||
} from "./navigator.ts";
|
} from "./navigator.ts";
|
||||||
|
|
||||||
import { AppViewState, BuiltinSettings } from "../type/web.ts";
|
import { AppViewState } from "../type/web.ts";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppEvent,
|
AppEvent,
|
||||||
|
@ -54,12 +54,16 @@ import { PageRef } from "../plug-api/lib/page_ref.ts";
|
||||||
import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts";
|
import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts";
|
||||||
import { KvPrimitives } from "$lib/data/kv_primitives.ts";
|
import { KvPrimitives } from "$lib/data/kv_primitives.ts";
|
||||||
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
||||||
import { ensureAndLoadSettingsAndIndex } from "$common/settings.ts";
|
import {
|
||||||
|
ensureAndLoadSettingsAndIndex,
|
||||||
|
updateObjectDecorators,
|
||||||
|
} from "$common/settings.ts";
|
||||||
import { LimitedMap } from "$lib/limited_map.ts";
|
import { LimitedMap } from "$lib/limited_map.ts";
|
||||||
import { plugPrefix } from "$common/spaces/constants.ts";
|
import { plugPrefix } from "$common/spaces/constants.ts";
|
||||||
import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts";
|
import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts";
|
||||||
import { findNodeMatching } from "$sb/lib/tree.ts";
|
import { findNodeMatching } from "$sb/lib/tree.ts";
|
||||||
import type { LinkObject } from "../plugs/index/page_links.ts";
|
import type { LinkObject } from "../plugs/index/page_links.ts";
|
||||||
|
import { BuiltinSettings } from "$type/settings.ts";
|
||||||
|
|
||||||
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
||||||
|
|
||||||
|
@ -248,6 +252,7 @@ export class Client {
|
||||||
this.settings = await ensureAndLoadSettingsAndIndex(
|
this.settings = await ensureAndLoadSettingsAndIndex(
|
||||||
this.space.spacePrimitives,
|
this.space.spacePrimitives,
|
||||||
);
|
);
|
||||||
|
updateObjectDecorators(this.settings, this.stateDataStore);
|
||||||
this.ui.viewDispatch({
|
this.ui.viewDispatch({
|
||||||
type: "settings-loaded",
|
type: "settings-loaded",
|
||||||
settings: this.settings,
|
settings: this.settings,
|
||||||
|
|
|
@ -43,7 +43,6 @@ import { createKeyBindings } from "./editor_state.ts";
|
||||||
import { CommonSystem } from "$common/common_system.ts";
|
import { CommonSystem } from "$common/common_system.ts";
|
||||||
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
||||||
import { plugPrefix } from "$common/spaces/constants.ts";
|
import { plugPrefix } from "$common/spaces/constants.ts";
|
||||||
import { decorationSyscalls } from "./syscalls/decoration.ts";
|
|
||||||
|
|
||||||
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
||||||
|
|
||||||
|
@ -169,7 +168,6 @@ export class ClientSystem extends CommonSystem {
|
||||||
spaceReadSyscalls(this.client),
|
spaceReadSyscalls(this.client),
|
||||||
systemSyscalls(this.system, false, this, this.client),
|
systemSyscalls(this.system, false, this, this.client),
|
||||||
markdownSyscalls(),
|
markdownSyscalls(),
|
||||||
decorationSyscalls(this.client),
|
|
||||||
assetSyscalls(this.system),
|
assetSyscalls(this.system),
|
||||||
yamlSyscalls(),
|
yamlSyscalls(),
|
||||||
templateSyscalls(this.ds),
|
templateSyscalls(this.ds),
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||||
import { Terminal } from "preact-feather";
|
import { Terminal } from "preact-feather";
|
||||||
import { AppCommand } from "../../lib/command.ts";
|
import { AppCommand } from "../../lib/command.ts";
|
||||||
import { FilterOption } from "$lib/web.ts";
|
import { FilterOption } from "$lib/web.ts";
|
||||||
import { BuiltinSettings } from "../../type/web.ts";
|
|
||||||
import { parseCommand } from "$common/command.ts";
|
import { parseCommand } from "$common/command.ts";
|
||||||
|
import { BuiltinSettings } from "$type/settings.ts";
|
||||||
|
|
||||||
export function CommandPalette({
|
export function CommandPalette({
|
||||||
commands,
|
commands,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { PageMeta } from "../plug-api/types.ts";
|
import { PageMeta } from "../plug-api/types.ts";
|
||||||
import { Action, AppViewState } from "../type/web.ts";
|
import { Action, AppViewState } from "../type/web.ts";
|
||||||
import { decoratePageMeta } from "./syscalls/decoration.ts";
|
|
||||||
|
|
||||||
export default function reducer(
|
export default function reducer(
|
||||||
state: AppViewState,
|
state: AppViewState,
|
||||||
|
@ -47,16 +46,10 @@ export default function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "update-current-page-meta": {
|
case "update-current-page-meta": {
|
||||||
if (state.settings.pageDecorations) {
|
|
||||||
decoratePageMeta(
|
|
||||||
action.meta,
|
|
||||||
state.settings.pageDecorations,
|
|
||||||
);
|
|
||||||
// Update in the allPages list as well
|
// Update in the allPages list as well
|
||||||
state.allPages = state.allPages.map((pageMeta) =>
|
state.allPages = state.allPages.map((pageMeta) =>
|
||||||
pageMeta.name === action.meta.name ? action.meta : pageMeta
|
pageMeta.name === action.meta.name ? action.meta : pageMeta
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentPageMeta: action.meta,
|
currentPageMeta: action.meta,
|
||||||
|
@ -83,12 +76,6 @@ export default function reducer(
|
||||||
if (oldPageMetaItem && oldPageMetaItem.lastOpened) {
|
if (oldPageMetaItem && oldPageMetaItem.lastOpened) {
|
||||||
pageMeta.lastOpened = oldPageMetaItem.lastOpened;
|
pageMeta.lastOpened = oldPageMetaItem.lastOpened;
|
||||||
}
|
}
|
||||||
if (state.settings.pageDecorations) {
|
|
||||||
decoratePageMeta(
|
|
||||||
pageMeta,
|
|
||||||
state.settings.pageDecorations,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (pageMeta.name === state.currentPage) {
|
if (pageMeta.name === state.currentPage) {
|
||||||
currPageMeta = pageMeta;
|
currPageMeta = pageMeta;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
import { PageDecoration, PageMeta } from "$sb/types.ts";
|
|
||||||
import { SysCallMapping } from "$lib/plugos/system.ts";
|
|
||||||
import { Client } from "../client.ts";
|
|
||||||
import { parseTreeToAST } from "$sb/lib/tree.ts";
|
|
||||||
import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts";
|
|
||||||
import { expressionLanguage } from "$common/template/template_parser.ts";
|
|
||||||
import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts";
|
|
||||||
import { evalQueryExpression } from "$sb/lib/query_expression.ts";
|
|
||||||
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
|
||||||
|
|
||||||
export function decorationSyscalls(
|
|
||||||
client: Client,
|
|
||||||
): SysCallMapping {
|
|
||||||
return {
|
|
||||||
"decoration.applyDecorationsToPages": (
|
|
||||||
_ctx,
|
|
||||||
pages: PageMeta[],
|
|
||||||
): PageMeta[] => {
|
|
||||||
if (client.settings.pageDecorations) {
|
|
||||||
for (const pageMeta of pages) {
|
|
||||||
decoratePageMeta(pageMeta, client.settings.pageDecorations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pages;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decorates (= attaches a pageDecoration field) to the pageMeta object when a matching decorator is found
|
|
||||||
*/
|
|
||||||
export function decoratePageMeta(
|
|
||||||
pageMeta: PageMeta,
|
|
||||||
decorations: PageDecoration[],
|
|
||||||
) {
|
|
||||||
if (!pageMeta) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const decoration of decorations) {
|
|
||||||
if (!decoration.where) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// whereParsed is effectively a cached version of the parsed where expression
|
|
||||||
// Let's check if it's populated
|
|
||||||
if (!decoration.whereParsed) {
|
|
||||||
// If not, populate it
|
|
||||||
try {
|
|
||||||
const ast = parseTreeToAST(lezerToParseTree(
|
|
||||||
decoration.where,
|
|
||||||
expressionLanguage.parser.parse(decoration.where).topNode,
|
|
||||||
));
|
|
||||||
decoration.whereParsed = expressionToKvQueryExpression(
|
|
||||||
ast[1],
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(
|
|
||||||
"Failed to parse 'where' expression in decoration:",
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
evalQueryExpression(
|
|
||||||
decoration.whereParsed,
|
|
||||||
pageMeta,
|
|
||||||
{},
|
|
||||||
builtinFunctions,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
pageMeta.pageDecoration = decoration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,8 @@ An attempt at documenting the changes/new features introduced in each release.
|
||||||
## Edge
|
## Edge
|
||||||
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
|
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
|
||||||
|
|
||||||
* Nothing yet since 0.8.4. Stay tuned!
|
* New power-user feature: [[Object Decorators]]. Its primary use case is to apply [[Page Decorations]], but you may find other uses as well. Speaking of which...
|
||||||
|
* **Breaking change:**The way [[Page Decorations]] are specified has changed. It has now been replaced with [[Object Decorators]](as just mentioned), which are a more generic mechanism. Your existing `pageDecorations` will stop working. You’ll have to rewrite based on the new format. See [[Page Decorations]] for examples.
|
||||||
|
|
||||||
## 0.8.4
|
## 0.8.4
|
||||||
* [[Page Picker#Keyboard shortcuts]]: allow folder completion using Shift-Space (by [Marek S. Łukasiewicz](https://github.com/silverbulletmd/silverbullet/pull/961))
|
* [[Page Picker#Keyboard shortcuts]]: allow folder completion using Shift-Space (by [Marek S. Łukasiewicz](https://github.com/silverbulletmd/silverbullet/pull/961))
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
description: Adds a Table of Contents to pages
|
description: Adds a Table of Contents to pages
|
||||||
tags: template
|
tags: template
|
||||||
hooks.top:
|
hooks.top:
|
||||||
where: 'true'
|
where: 'not pageDecoration.disableTOC'
|
||||||
# Show all the way at the top
|
# Show all the way at the top
|
||||||
order: 0
|
order: 0
|
||||||
---
|
---
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
Object decorators are an **advanced technique** that can be used to add attributes to [[Objects]] dynamically whose values are _calculated dynamically_ (on-the-fly) based on an [[Expression Language|expression]].
|
||||||
|
|
||||||
|
> **warning** Warning
|
||||||
|
> This feature is still experimental and may change in the (near) future.
|
||||||
|
|
||||||
|
The primary use case is [[Page Decorations]], but it is a powerful mechanism that probably has wider applications. As always, with great power comes great responsibility.
|
||||||
|
|
||||||
|
# Syntax
|
||||||
|
Object decorations are specified in [[^SETTINGS]] using the following syntax:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
objectDecorators:
|
||||||
|
- where: '<<filter expression>>'
|
||||||
|
attributes:
|
||||||
|
<<attributePath>>: '<<value expression>>'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** To make changes take effect you may have to reload your client (just refresh the page).
|
||||||
|
|
||||||
|
A few things of note:
|
||||||
|
|
||||||
|
* `<<filter expression>>` is a [[YAML]] string-encoded expression using SilverBullet’s [[Expression Language]]. Some examples:
|
||||||
|
* `where: 'tags = "book"'` to make this apply to all objects that has `book` as one of its tags.
|
||||||
|
* `<<attributePath>>` can either be a simple attribute name, or a nested one using the `attribute.subAttribute` syntax. Some examples:
|
||||||
|
* `fullName`
|
||||||
|
* `pageDecoration.prefix`
|
||||||
|
* `<<value expression>>` like `<<expression>>` must be a YAML string-encoded expression using the [[Expression Language]], some examples together with the attribute path:
|
||||||
|
* `alwaysTen: '10'` (attaches an attribute named `alwaysTen` with the numeric value `10` to all objects matching the `where` clause)
|
||||||
|
* `fullName: 'firstName + " " + lastName'` (attaches a `fullName` attribute that concatenates the `firstName` and `lastName` attributes with a space in between)
|
||||||
|
* `nameLength: 'count(name)'` (attaches an attribute `nameLength` with the string length of `name` — not particularly useful, but to demonstrate you can call [[Functions]] here too)
|
||||||
|
* For performance reasons, all expressions (both filter and value expressions) need to be _synchronously evaluatable_.
|
||||||
|
* Generally, this means they need to be “simple expressions” that require no expensive calls.
|
||||||
|
* Simple expressions include simple things like literals, arithmetic, calling some of the cheap [[Functions]] such as `today()` or string manipulation functions.
|
||||||
|
* Expensive calls include any additional database queries, or any function call (custom or otherwise) that are _asynchronous_. These are _not supported_.
|
||||||
|
* This requirement **will be checked at runtime**. Watch your server logs and browser’s JavaScript JavaScript console to see these errors.
|
||||||
|
|
||||||
|
# Example
|
||||||
|
Let’s say that you use the `human` tag to track various humans in your space, as follows:
|
||||||
|
|
||||||
|
```#human
|
||||||
|
firstName: Steve
|
||||||
|
lastName: Bee
|
||||||
|
---
|
||||||
|
firstName: Stephanie
|
||||||
|
lastName: Bee
|
||||||
|
```
|
||||||
|
|
||||||
|
This would get you the follow data set:
|
||||||
|
|
||||||
|
```query
|
||||||
|
human select firstName, lastName
|
||||||
|
```
|
||||||
|
|
||||||
|
However, you would like to dynamically compute an additional attribute for all humans, namely `fullName`. This can be done as follows in your [[^SETTINGS]]:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
objectDecorators:
|
||||||
|
- where: 'tag = "human"'
|
||||||
|
attributes:
|
||||||
|
fullName: 'firstName + " " + lastName'
|
||||||
|
```
|
||||||
|
|
||||||
|
Which will give you the following:
|
||||||
|
|
||||||
|
```query
|
||||||
|
human select fullName, firstName, lastName
|
||||||
|
```
|
||||||
|
|
||||||
|
Tadaa!
|
|
@ -13,6 +13,8 @@ In addition, many objects will also contain:
|
||||||
* `tags`: an optional set of additional, explicitly assigned tags.
|
* `tags`: an optional set of additional, explicitly assigned tags.
|
||||||
* `itags`: a set of _implicit_ or _inherited_ tags: including the object’s `tag`, `tags` as well as any tags _assigned to its containing page_. This is useful to answer queries like, “give me all tasks on pages where that page is tagged with `person`“, which would be expressed as `task where itags = "person"` (although technically that would also match any tags that have the `#person` explicitly assigned).
|
* `itags`: a set of _implicit_ or _inherited_ tags: including the object’s `tag`, `tags` as well as any tags _assigned to its containing page_. This is useful to answer queries like, “give me all tasks on pages where that page is tagged with `person`“, which would be expressed as `task where itags = "person"` (although technically that would also match any tags that have the `#person` explicitly assigned).
|
||||||
|
|
||||||
|
In addition, an object’s attribute set can be dynamically extended using [[Object Decorators]].
|
||||||
|
|
||||||
Beside these, any number of additional tag-specific and custom [[Attributes]] can be defined (see below).
|
Beside these, any number of additional tag-specific and custom [[Attributes]] can be defined (see below).
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
|
|
|
@ -1,36 +1,49 @@
|
||||||
---
|
---
|
||||||
pageDecoration.prefix: "🎄 "
|
pageDecoration.prefix: "🎄 "
|
||||||
|
pageDecoration.disableTOC: true
|
||||||
---
|
---
|
||||||
Page decorations allow you to “decorate” pages in various ways.
|
Page decorations allow you to “decorate” pages in various ways.
|
||||||
|
|
||||||
For now “various ways” means just one way (adding a visual prefix), but in the future, more such decorations will likely be added.
|
> **warning** Warning
|
||||||
|
> This feature is still experimental and may change in the (near) future.
|
||||||
There are two ways to decorate a page.
|
|
||||||
|
|
||||||
# Frontmatter
|
|
||||||
The first is demonstrated in the [[Frontmatter]] of this page, by using the special `pageDecoration` attribute.
|
|
||||||
|
|
||||||
# Settings
|
|
||||||
The more useful way is to apply decorations to pages _dynamically_, you can use the `pageDecorations` attribute in [[SETTINGS]].
|
|
||||||
|
|
||||||
Every page decoration has two parts:
|
|
||||||
* `where`: the [[Expression Language]] expression that has to evaluate to `true` for a given page for that decoration to be applied.
|
|
||||||
* A set of decorations to apply, see [[#Supported decorations]]
|
|
||||||
|
|
||||||
For example:
|
|
||||||
```yaml
|
|
||||||
pageDecorations:
|
|
||||||
- where: "tags = 'person'"
|
|
||||||
prefix: "🧑 "
|
|
||||||
```
|
|
||||||
|
|
||||||
This will prefix all pages tagged with `#person` with a 🧑 emoji.
|
|
||||||
|
|
||||||
Here on silverbullet.md, we have a decoration like this for pages tagged with #plug: [[Plugs/Emoji]] and [[Plugs/Git]] for instance.
|
|
||||||
|
|
||||||
# Supported decorations
|
# Supported decorations
|
||||||
|
|
||||||
* `prefix`: A (visual) string prefix (often an emoji) to add to all page names. This prefix will appear in the top bar as well as in (live preview) links to this page. For example, the name of this page is actually “Page Decorations”, but when you link to it, you’ll see it’s prefixed with a 🎄: [[Page Decorations]]
|
* `prefix`: A (visual) string prefix (often an emoji) to add to all page names. This prefix will appear in the top bar as well as in (live preview) links to this page. For example, the name of this page is actually “Page Decorations”, but when you link to it, you’ll see it’s prefixed with a 🎄: [[Page Decorations]]
|
||||||
* `hide` When this is set to `true`, the page will not be shown in [[Page Picker]], [[Meta Picker]], or suggested for completion of [[Links]]. It will otherwise behave as normal - will be [[Plugs/Index|indexed]] and found in [[Live Queries]]. The page can be opened through [[All Pages Picker]], or linked normally when the full name is typed out without completion.
|
* `hide` When this is set to `true`, the page will not be shown in [[Page Picker]], [[Meta Picker]], or suggested for completion of [[Links]]. It will otherwise behave as normal - will be [[Plugs/Index|indexed]] and found in [[Live Queries]]. The page can be opened through [[All Pages Picker]], or linked normally when the full name is typed out without completion.
|
||||||
|
* `disableTOC` (not technically built-in, but a feature of the [[^Library/Core/Widget/Table of Contents]] widget): disable the [[Table of Contents]] for this particular page.
|
||||||
|
|
||||||
|
There are two ways to apply decorations to pages:
|
||||||
|
|
||||||
|
# With [[Frontmatter]] directly
|
||||||
|
This is demonstrated in the [[Frontmatter]] at the top of this page, by using the special `pageDecoration` attribute. This is how we get the fancy tree in front of the page name. Sweet.
|
||||||
|
|
||||||
|
# With [[Object Decorators]]
|
||||||
|
The more useful way is to apply decorations to pages _dynamically_, for this we will leverage the more powerful [[Object Decorators]] feature. Read the [[Object Decorators]] page for a more in-depth explanation of how this feature works if you’re interested (as you should be, because it’s pretty cool on its own).
|
||||||
|
|
||||||
|
For the purposes of [[Page Decorations]], let us limit simply to some useful examples.
|
||||||
|
|
||||||
|
## Example: page prefix
|
||||||
|
Let’s say we want to put a 🧑 prefix on every page tagged with `#person`. We can achieve this as follows in our [[^SETTINGS]]:
|
||||||
|
```yaml
|
||||||
|
objectDecorations:
|
||||||
|
- where: "tags = 'person'"
|
||||||
|
pageDecoration.prefix: '"🧑 "'
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the (perhaps) strange double quoting there, both the `where` and the value for the attributes are [[Expression Language|expressions]] encoded inside of YAML. It’s a bit weird, but it works.
|
||||||
|
|
||||||
|
## Example: disabling [[Table of Contents]]
|
||||||
|
Let’s say that adding this `pageDecoration.disableTOC` to the front matter is too much effort to disable the TOC on some pages. Therefore, you would like to simplify this by simply adding a `#notoc` tag to your pages.
|
||||||
|
|
||||||
|
You can do this as follows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
objectDecorations:
|
||||||
|
- where: 'tags = "notoc"'
|
||||||
|
attributes:
|
||||||
|
pageDecoration.disableTOC: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Plug prefix
|
||||||
|
Here on silverbullet.md, we have a decoration like this for pages tagged with `#plug`: [[Plugs/Emoji]] and [[Plugs/Git]] for instance.
|
||||||
|
|
||||||
Later, more such decorations may be added.
|
|
|
@ -58,10 +58,17 @@ shortcuts:
|
||||||
- command: "{[Upload: File]}"
|
- command: "{[Upload: File]}"
|
||||||
priority: 1
|
priority: 1
|
||||||
|
|
||||||
# Page decorations
|
# Object decorators, see the "Page Decorators" page for more info
|
||||||
pageDecorations:
|
objectDecorators:
|
||||||
- where: 'tags = "plug"'
|
- where: 'tags = "plug"'
|
||||||
prefix: "🔌 "
|
attributes:
|
||||||
|
pageDecoration.prefix: "'🔌 '"
|
||||||
|
- where: 'tag = "human"'
|
||||||
|
attributes:
|
||||||
|
fullName: 'firstName + " " + lastName'
|
||||||
|
- where: 'tags = "notoc"'
|
||||||
|
attributes:
|
||||||
|
pageDecoration.disableTOC: "true"
|
||||||
|
|
||||||
# Toggles between “smart” ‘quotes’ (left and right) and "simple" 'quotes' (good ol' ASCII)
|
# Toggles between “smart” ‘quotes’ (left and right) and "simple" 'quotes' (good ol' ASCII)
|
||||||
useSmartQuotes: true
|
useSmartQuotes: true
|
||||||
|
|
Loading…
Reference in New Issue