From 9b05beba16d7fa358e32f451acb188dc996dd182 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Thu, 22 Jan 2026 15:32:01 +0100 Subject: [PATCH] First pass of custom tag behavior (schema and postProcess) support See API/tag.md for explanations and examples. --- client/codemirror/wiki_link.ts | 2 + client/data/object_index.ts | 106 ++++++++++++++++++++------- client/plugos/syscalls/index.ts | 2 +- client/plugos/syscalls/jsonschema.ts | 55 +++++++++----- libraries/Library/Std/APIs/Schema.md | 6 ++ libraries/Library/Std/APIs/Tag.md | 2 +- libraries/Library/Std/Config.md | 10 ++- plug-api/syscalls/config.ts | 2 +- plugs/index/lint.ts | 36 +++++++-- website/API/tag.md | 69 +++++++++++++++++ website/CHANGELOG.md | 3 + website/Folder.md | 2 +- website/Frontmatter.md | 2 +- website/Library/Website.md | 1 - website/Markdown/Hashtags.md | 2 +- website/Metadata.md | 2 +- website/Object.md | 11 +-- website/Object/item.md | 2 +- website/Object/page.md | 2 +- website/Object/paragraph.md | 2 +- website/Object/table.md | 2 +- website/Object/task.md | 2 +- website/Person/John.md | 1 - website/Schema.md | 17 +++++ website/SilverBullet.md | 2 +- website/Tag Picker.md | 2 +- website/Tag.md | 9 +++ website/Tags.md | 3 - 28 files changed, 274 insertions(+), 83 deletions(-) create mode 100644 website/API/tag.md create mode 100644 website/Schema.md create mode 100644 website/Tag.md delete mode 100644 website/Tags.md diff --git a/client/codemirror/wiki_link.ts b/client/codemirror/wiki_link.ts index f26debdb..755dbcd8 100644 --- a/client/codemirror/wiki_link.ts +++ b/client/codemirror/wiki_link.ts @@ -12,6 +12,7 @@ import { processWikiLink, type WikiLinkMatch } from "./wiki_link_processor.ts"; export function cleanWikiLinkPlugin(client: Client) { return decoratorStateField((state) => { const widgets: any[] = []; + const shortWikiLinks = client.config.get("shortWikiLinks", true); syntaxTree(state).iterate({ enter: ({ type, from, to }) => { @@ -41,6 +42,7 @@ export function cleanWikiLinkPlugin(client: Client) { matchFrom: from, matchTo: to, client, + shortWikiLinks, state, callback: (e) => { if (e.altKey) { diff --git a/client/data/object_index.ts b/client/data/object_index.ts index d66fac36..6809158b 100644 --- a/client/data/object_index.ts +++ b/client/data/object_index.ts @@ -4,17 +4,13 @@ import type { LuaCollectionQuery, LuaQueryCollection, } from "../space_lua/query_collection.ts"; -import { - jsToLuaValue, - LuaEnv, - LuaStackFrame, - type LuaTable, -} from "../space_lua/runtime.ts"; +import { jsToLuaValue, LuaEnv, LuaStackFrame } from "../space_lua/runtime.ts"; import type { DataStore } from "./datastore.ts"; import type { KV, KvKey } from "@silverbulletmd/silverbullet/type/datastore"; import type { EventHook } from "../plugos/hooks/event.ts"; import type { DataStoreMQ } from "./mq.datastore.ts"; import type { Space } from "../space.ts"; +import { validateObject } from "../plugos/syscalls/jsonschema.ts"; const indexKey = "idx"; const pageKey = "ridx"; @@ -24,6 +20,15 @@ const indexQueuedKey = ["$indexQueued"]; // Bump this one every time a full reindex is needed const desiredIndexVersion = 9; +type TagDefinition = { + metatable?: any; + mustValidate?: boolean; + schema?: any; + postProcess?: ( + o: ObjectValue, + ) => Promise | ObjectValue[] | ObjectValue; +}; + export class ObjectIndex { constructor( private ds: DataStore, @@ -82,21 +87,21 @@ export class ObjectIndex { query, env, sf, - (key, value: any) => { - const tag = key[1]; - const tagDef = this.config.get( - ["tagDefinitions", tag], - undefined, - ); - if (!tagDef || !tagDef.has("metatable")) { - // Return as is - return value; - } - // Convert to LuaTable - value = jsToLuaValue(value); - value.metatable = tagDef.get("metatable"); - return value; - }, + // (key, value: any) => { + // const tag = key[1]; + // const tagDef = this.config.get( + // ["tags", tag], + // undefined, + // ); + // if (!tagDef || !tagDef.metatable) { + // // Return as is + // return value; + // } + // // Convert to LuaTable + // value = jsToLuaValue(value); + // value.metatable = tagDef.get("metatable"); + // return value; + // }, ); }, }; @@ -267,12 +272,18 @@ export class ObjectIndex { /** * Indexes entities in the data store */ - public indexObjects( + public async indexObjects( page: string, objects: ObjectValue[], ): Promise { const kvs: KV[] = []; - for (const obj of objects) { + const tagDefinitions: Record = this.config.get( + "tags", + {}, + ); + // Taking this iteration approach as new objects may be pushed into this array on the fly + while (objects.length > 0) { + const obj = objects.shift()!; if (!obj.tag) { console.error("Object has no tag", obj, "this shouldn't happen"); continue; @@ -280,11 +291,50 @@ export class ObjectIndex { // Index as all the tag + any additional tags specified const allTags = [obj.tag, ...obj.tags || []]; for (const tag of allTags) { - // The object itself - kvs.push({ - key: [tag, this.cleanKey(obj.ref, page)], - value: obj, - }); + const tagDefinition = tagDefinitions[tag]; + // Validate object if required + if (tagDefinition?.mustValidate && tagDefinition?.schema) { + const validationError = validateObject(tagDefinition?.schema, obj); + if (validationError) { + console.error( + `Object failed ${tag} validation so won't be indexed:`, + obj, + "Error:", + validationError, + ); + continue; + } + } + if (tagDefinition?.postProcess) { + let newObjects = await tagDefinition.postProcess(obj); + + if (!Array.isArray(newObjects)) { + // Probably returned single object, let's normalize + newObjects = [newObjects]; + } + for (const newObj of newObjects) { + if (!newObj.ref) { + console.error("postProcess object did not contain ref", newObj); + continue; + } + if (newObj.ref === obj.ref) { + // Got the same object back here, let's just index it without further processing + kvs.push({ + key: [tag, this.cleanKey(newObj.ref, page)], + value: newObj, + }); + } else { + // Some other object + objects.push(newObj); + } + } + } else { + // Just insert it directly + kvs.push({ + key: [tag, this.cleanKey(obj.ref, page)], + value: obj, + }); + } } } if (kvs.length > 0) { diff --git a/client/plugos/syscalls/index.ts b/client/plugos/syscalls/index.ts index 92ce8c5f..62fa5652 100644 --- a/client/plugos/syscalls/index.ts +++ b/client/plugos/syscalls/index.ts @@ -65,7 +65,7 @@ export function indexSyscalls( if (!tagDef.has("name")) { throw new Error("A tag name is required"); } - client.config.set(["tagDefinitions", tagDef.get("name")], tagDef); + client.config.set(["tags", tagDef.get("name")], tagDef); }, }; } diff --git a/client/plugos/syscalls/jsonschema.ts b/client/plugos/syscalls/jsonschema.ts index 590e38f6..4e05957f 100644 --- a/client/plugos/syscalls/jsonschema.ts +++ b/client/plugos/syscalls/jsonschema.ts @@ -1,5 +1,5 @@ import type { SysCallMapping } from "../system.ts"; -import { Ajv } from "ajv"; +import { Ajv, type ValidateFunction } from "ajv"; const ajv = new Ajv(); @@ -18,6 +18,38 @@ ajv.addFormat("page-ref", { async: false, }); +const schemaCache = new Map(); + +export function validateObject(schema: any, object: any): undefined | string { + try { + const schemaKey = JSON.stringify(schema); + if (!schemaCache.has(schemaKey)) { + const validate = ajv.compile(schema); + schemaCache.set(schemaKey, validate); + } + const validate = schemaCache.get(schemaKey)!; + if (validate(object)) { + return; + } else { + let text = ajv.errorsText(validate.errors); + text = text.replaceAll("/", "."); + text = text.replace(/^data[\.\s]/, ""); + return text; + } + } catch (e: any) { + return e.message; + } +} + +export function validateSchema(schema: any): undefined | string { + const valid = ajv.validateSchema(schema); + if (valid) { + return; + } else { + return ajv.errorsText(ajv.errors); + } +} + export function jsonschemaSyscalls(): SysCallMapping { return { "jsonschema.validateObject": ( @@ -25,30 +57,13 @@ export function jsonschemaSyscalls(): SysCallMapping { schema: any, object: any, ): undefined | string => { - try { - const validate = ajv.compile(schema); - if (validate(object)) { - return; - } else { - let text = ajv.errorsText(validate.errors); - text = text.replaceAll("/", "."); - text = text.replace(/^data[\.\s]/, ""); - return text; - } - } catch (e: any) { - return e.message; - } + return validateObject(schema, object); }, "jsonschema.validateSchema": ( _ctx, schema: any, ): undefined | string => { - const valid = ajv.validateSchema(schema); - if (valid) { - return; - } else { - return ajv.errorsText(ajv.errors); - } + return validateSchema(schema); }, }; } diff --git a/libraries/Library/Std/APIs/Schema.md b/libraries/Library/Std/APIs/Schema.md index 3921059b..19568322 100644 --- a/libraries/Library/Std/APIs/Schema.md +++ b/libraries/Library/Std/APIs/Schema.md @@ -70,10 +70,16 @@ function schema.nullableArray(typ) end end +-- Used to specify we're expecting a function, but doesn't deeply validate function schema.func() return {} end +-- Used to specify we're expecting a schema, but doesn't deeply validate +function schema.schema() + return { type = "object" } +end + function schema.null() return { type = "null" } end diff --git a/libraries/Library/Std/APIs/Tag.md b/libraries/Library/Std/APIs/Tag.md index 99f42912..15bf0798 100644 --- a/libraries/Library/Std/APIs/Tag.md +++ b/libraries/Library/Std/APIs/Tag.md @@ -18,7 +18,7 @@ tag = tag or {} -- For future use function tag.define(spec) - config.set("tagDefinitions", spec.name, spec) + config.set({"tags", spec.name}, spec) end -- Set up tags.* short cut via meta tables diff --git a/libraries/Library/Std/Config.md b/libraries/Library/Std/Config.md index 48e28f22..4f403038 100644 --- a/libraries/Library/Std/Config.md +++ b/libraries/Library/Std/Config.md @@ -324,13 +324,17 @@ config.define("taskStates", { }) -- Don't use directly, WIP -config.define("tagDefinitions", { +config.define("tags", { type = "object", additionalProperties = { type = "object", properties = { - schema = { type = "object" }, - metatable = { }, + name = schema.string(), + schema = schema.schema(), + -- Whether or not an object HAS to validate to be indexed (defaults to false), has a performance penalty + mustValidate = schema.boolean(), + -- Invoked by the object indexer, takes a proposed object as input, returns an array of objects (can be empty to skip indexing altogether) + postProcess = schema.func(), }, }, }) diff --git a/plug-api/syscalls/config.ts b/plug-api/syscalls/config.ts index 01ba662f..4803f90f 100644 --- a/plug-api/syscalls/config.ts +++ b/plug-api/syscalls/config.ts @@ -6,7 +6,7 @@ import { syscall } from "../syscall.ts"; * @param defaultValue The default value to return if the path doesn't exist * @returns The value at the path, or the default value */ -export function get(path: string, defaultValue: T): Promise { +export function get(path: string | string[], defaultValue: T): Promise { return syscall("config.get", path, defaultValue); } diff --git a/plugs/index/lint.ts b/plugs/index/lint.ts index f5a43a43..5e45332a 100644 --- a/plugs/index/lint.ts +++ b/plugs/index/lint.ts @@ -1,8 +1,7 @@ -import { lua } from "@silverbulletmd/silverbullet/syscalls"; +import { config, jsonschema, lua } from "@silverbulletmd/silverbullet/syscalls"; import { findNodeOfType, renderToText, - traverseTree, traverseTreeAsync, } from "@silverbulletmd/silverbullet/lib/tree"; import type { @@ -11,20 +10,45 @@ import type { } from "@silverbulletmd/silverbullet/type/client"; import YAML from "js-yaml"; +import { extractFrontMatter } from "./frontmatter.ts"; -export function lintYAML( +export async function lintYAML( { tree, name }: LintEvent, -): LintDiagnostic[] { +): Promise { const diagnostics: LintDiagnostic[] = []; - traverseTree(tree, (node) => { + const frontmatter = extractFrontMatter(tree); + + await traverseTreeAsync(tree, async (node) => { if (node.type === "FrontMatterCode") { + const yamlText = renderToText(node); const lintResult = lintYaml( - renderToText(node), + yamlText, node.from!, name, ); if (lintResult) { diagnostics.push(lintResult); + } else { + const parsed = YAML.load(yamlText); + // Parses as valid YAML, now let's see if we need to do schema validation + for (const tag of frontmatter.tags || []) { + const schema = await config.get(["tags", tag, "schema"], undefined); + + if (schema) { + const validationError = await jsonschema.validateObject( + schema, + parsed, + ); + if (validationError) { + diagnostics.push({ + message: `${tag} validation failed: ${validationError}`, + severity: "error", + from: node.from!, + to: node.to!, + }); + } + } + } } return true; } diff --git a/website/API/tag.md b/website/API/tag.md new file mode 100644 index 00000000..93cbff7a --- /dev/null +++ b/website/API/tag.md @@ -0,0 +1,69 @@ +#api/space-lua #maturity/experimental + +Provides APIs to define and configure custom [[Tag|Tags]]. + +Enables you to customize tags in a few ways: + +* Define a [[Schema]] for your tag, which is used to validate objects with your custom tag (and to offer auto complete in the future) and show validation errors in the editor ([[Frontmatter]] only) +* Define how objects part of your tags are indexed. +* Tweak styling of tags in the editor using [[Space Style]] + +# API +## tag.define(spec) +Defines a custom tag. `spec` is a table that can contain: +* `name` (required) the name of the tag +* `mustValidate` a boolean defining whether or not schema validation must pass for the object to be indexed +* `schema` [[Schema]] to validate agains +* `postProcess` callback function invoked when an object with tag `name` has been indexed, allows you to make changes to it, skip indexing altogether or generate additional objects + +# Use cases +## Schema validation +To define a JSON schema for validating an object (e.g. a page) tagged with `#person` ensuring that the `age` attribute is always a number, you can do the following: +```lua +tag.define { + name = "person", + -- mustValidate = true, + schema = { + type = "object", + properties = { + age = schema.number() + } + } +} +``` +The result of this is that when editing a page tagged with `#person` where this schema does not validate, you will see this error being highlighted. + +If you set `mustValidate` to `true`, schema validation will happen during the index phase (in addition to linting in the editor) and non-validating objects will not be indexed (with errors being reported in the JavaScript console). Use this to ensure all your objects confirm to your schema (it does come at a slight performance penalty at the indexing phase). + +## Post processing +Based on your page’s markdown, an indexer produces a list of objects to be indexed. If you define a `postProcess` callback function for a custom tag, this function will be invoked with objects with that tag when the indexer encounters them. `postProcess` can inspect the object and do a few things: + +* Make changes to the object and return it: this is the most common scenario, it allows you to attach additional attributes to the object before it’s persisted to the database. +* Return an empty table (`{}`): in this case the object will not be indexed at all. +* Return a list of objects that should be indexed instead: this may include the original object or a modification of it. This allows you to generate a set of custom objects, e.g. based on further parsing of the data in the object. A use case here could be to extract additional attributes from an existing attribute. + +> **note** Note +> `postProcess` will only be invoked when a page is indexed. This generally happens after making a change. To apply newly defined `postProcess` functionality to all pages in your space, you have to reindex the entire space using `Space: Reindex`. + +### Example: adding [[Page Decorations]] +The following dynamically adds a 🧑 prefix [[Page Decorations|page decoration]] to all pages tagged with `#person`, such as [[Person/John]] and [[Person/Zef]]. +```space-lua +tag.define { + name = "person", + postProcess = function(o) + o.pageDecoration = { prefix = "🧑 " } + return o + end +} +``` + +## Styling +Tags get assigned a `data-tag-name` attribute in the DOM, which you can use to do custom styling with [[Space Style]]. + +Example: #my-red-tag + +```space-style +a[data-tag-name="my-red-tag"] { + background-color: red; +} +``` diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index f6c44327..9e553de4 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -4,6 +4,9 @@ An attempt at documenting the changes/new features introduced in each release. Whenever a commit is pushed to the `main` branch, within ~10 minutes, it will be released as a docker image with the `:v2` tag, and a binary in the [edge release](https://github.com/silverbulletmd/silverbullet/releases/tag/edge). If you want to live on the bleeding edge of SilverBullet goodness (or regression) this is where to do it. * New `shortWikiLinks` config (defaulting to `true`) that decides whether a wiki link should be rendered in its short form (rendering just the last segment, e.g. `Person/John` would show as `John`). To always render the full name, put `config.set("shortWikiLinks", false)` in your [[CONFIG]]. +* Experimental support for custom tag behavior — see [[API/tag]] for more information and examples, currently supports: + * Schema validation + * Post processing during index phase * Upgraded dependencies, specifically CodeMirror. CodeMirror now no longer allows `Alt-` and `Alt-` [[Keyboard Shortcuts]], meaning I had to remap a few existing ones. It’s basically a mission impossible to pick great ones, but I tried: * `Quick note` is now bound to both `Ctrl-q q` (type `Ctrl-q` first, then hit `q` again) and `Ctrl-q Ctrl-q` (hit `Ctrl-q` twice) * `Navigate: Home` is now bound to `Ctrl-g h` diff --git a/website/Folder.md b/website/Folder.md index 20e73e23..7b911d60 100644 --- a/website/Folder.md +++ b/website/Folder.md @@ -2,4 +2,4 @@ While folders technically exist in SilverBullet, they’re not necessarily the k There’s no explicit way to create a folder, but you can do so by simply putting slashes `/` in your page name (also on Windows). SilverBullet will automatically create the folder hierarchy on disk, if necessary. -What are some alternatives to using folders for organization? [[Tags]], primarily. \ No newline at end of file +What are some alternatives to using folders for organization? [[Tag]], primarily. \ No newline at end of file diff --git a/website/Frontmatter.md b/website/Frontmatter.md index 59f74a0b..0072cc90 100644 --- a/website/Frontmatter.md +++ b/website/Frontmatter.md @@ -38,7 +38,7 @@ While SilverBullet allows arbitrary metadata to be added to pages, there are a f * `name` (==DISALLOWED==): is an attribute used for page names, _you should not set it_. * `displayName` (`string`): very similar in effect as `aliases` but will use this name for the page in certain contexts. * `aliases` (`array of strings`): allow you to specify a list of alternative names for this page, which can be used to navigate or link to this page -* `tags` (`array of strings` or `string`): an alternative (and perhaps preferred) way to assign [[Tags]] to a page. There are various ways to define these, take your pick: +* `tags` (`array of strings` or `string`): an alternative (and perhaps preferred) way to assign [[Tag]] to a page. There are various ways to define these, take your pick: ```yaml tags: tag1, tag2 # with commas tags: tag1 tag2 # with spaces diff --git a/website/Library/Website.md b/website/Library/Website.md index a07f615a..1ded19dd 100644 --- a/website/Library/Website.md +++ b/website/Library/Website.md @@ -36,4 +36,3 @@ event.listen { margin: 0px !important; } ``` - diff --git a/website/Markdown/Hashtags.md b/website/Markdown/Hashtags.md index 4f55e7b3..919ba710 100644 --- a/website/Markdown/Hashtags.md +++ b/website/Markdown/Hashtags.md @@ -1,4 +1,4 @@ -These can be used in text to assign an [[Object#tag]]. If hashtags are the only content of first paragraph, they are applied to the entire page. +These can be used in text to assign an [[Tag]]. If hashtags are the only content of first paragraph, they are applied to the entire page. Hashtags can contain letters, dashes, underscores and other characters, but not: - Whitespace (space, newline etc.) diff --git a/website/Metadata.md b/website/Metadata.md index 48c82cba..4412b3c5 100644 --- a/website/Metadata.md +++ b/website/Metadata.md @@ -1,5 +1,5 @@ Metadata is data about data. Most [[Object]] have a set of default attributes that can be augmented in a few additional ways: -* [[Tags]]: to tag the object (and add to the `tags` attribute directly) +* [[Tag]]: to tag the object (and add to the `tags` attribute directly) * [[Frontmatter]]: at the top of [[Page]], a [[YAML]] encoded block can be used to define additional attributes to a page * [[Attribute]] syntax diff --git a/website/Object.md b/website/Object.md index e8604929..ddff8c30 100644 --- a/website/Object.md +++ b/website/Object.md @@ -1,6 +1,6 @@ SilverBullet automatically builds and maintains an index of _objects_ extracted from all [[Markdown]] [[Page]] in your [[Space|Space]]. It subsequently allows you to use [[Space Lua/Lua Integrated Query]] to query this database in (potentially) useful ways. -By design, the truth remains in the markdown: all data indexed as objects will have a representation in markdown text as well. This index can be flushed at any time and be rebuilt from its source markdown files kept in your space (and you can do so on demand if you like using the `Space: Reindex` command). +By design, the truth remains in the markdown: all data indexed as objects will have a representation in markdown text as well. This index can be flushed at any time and be rebuilt from its source markdown files kept in your space (and you can do so on demand using the `Space: Reindex` command). # Object representation Every object has a set of [[Attribute|Attributes]], some predefined, but you can add any additional custom attributes that you like. @@ -13,10 +13,7 @@ In addition, many objects will also contain: * `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 `query[[from index.tag "task" where table.includes(_.itags, "person")]]` (although technically that would also match any tags that have the `#person` explicitly assigned). -Beside these, any number of additional tag-specific and custom [[Attribute]] can be defined (see below). +Beside these, any number of additional tag-specific and custom [[Attribute]] can be defined. + + -# Tags -Every object has a main `tag`, which signifies the type of object being described. If you’re familiar with SQL databases, you can think of these as _tables_, or in object-oriented parlance you can think of them as _classes_. In addition, any number of additional tags can be assigned as well via the `tags` attribute. You can use either the main `tag` or any of the `tags` as query sources in [[Space Lua/Lua Integrated Query]] — examples below. -${index.} -# Built-in tags -${widgets.subPages()} \ No newline at end of file diff --git a/website/Object/item.md b/website/Object/item.md index cd22db41..e5507f7a 100644 --- a/website/Object/item.md +++ b/website/Object/item.md @@ -1,4 +1,4 @@ -List items (both bullet point and numbered items) are indexed with the `item` tag, additional tags can be added using [[Tags]]. +List items (both bullet point and numbered items) are indexed with the `item` tag, additional tags can be added using [[Tag]]. Here is an example of a #quote item using a custom [[Attribute|attribute]]: diff --git a/website/Object/page.md b/website/Object/page.md index ff4e9168..081123e3 100644 --- a/website/Object/page.md +++ b/website/Object/page.md @@ -1,4 +1,4 @@ -Every page in your space is available via the `page` tag. You can attach _additional_ tags to a page, by either specifying them in the `tags` attribute [[Frontmatter]], or by putting additional [[Tags]] in a stand alone paragraph with no other (textual) content in them. +Every page in your space is available via the `page` tag. You can attach _additional_ tags to a page, by either specifying them in the `tags` attribute [[Frontmatter]], or by putting additional [[Tag]] in a stand alone paragraph with no other (textual) content in them. In addition to `ref` and `tags`, the `page` tag defines a bunch of additional attributes as can be seen in this example query: diff --git a/website/Object/paragraph.md b/website/Object/paragraph.md index 899ef188..1d82c547 100644 --- a/website/Object/paragraph.md +++ b/website/Object/paragraph.md @@ -1,4 +1,4 @@ -Top-level paragraphs (that is: paragraphs not embedded in a list) are indexed using the `paragraph` tag, any additional tags can be added using [[Tags]]. +Top-level paragraphs (that is: paragraphs not embedded in a list) are indexed using the `paragraph` tag, any additional tags can be added using [[Tag]]. By default, paragraphs are only indexed when they contain a tag. However, you can enable indexing _all_ paragraphs by adding the following to your [[CONFIG]]: diff --git a/website/Object/table.md b/website/Object/table.md index 71a7d990..30fbfbde 100644 --- a/website/Object/table.md +++ b/website/Object/table.md @@ -1,4 +1,4 @@ -Markdown table rows are indexed using the `table` tag, any additional tags can be added using [[Tags]] in any of its cells. +Markdown table rows are indexed using the `table` tag, any additional tags can be added using [[Tag]] in any of its cells. | Title | Description Text | | --- | ----- | diff --git a/website/Object/task.md b/website/Object/task.md index 90d4e690..245993af 100644 --- a/website/Object/task.md +++ b/website/Object/task.md @@ -1,4 +1,4 @@ -Every task in your space is tagged with the `task` tag by default. You tag it with additional tags by using [[Tags]] in the task name, e.g. +Every task in your space is tagged with the `task` tag by default. You tag it with additional tags by using [[Tag]] in the task name, e.g. * [ ] My task #upnext diff --git a/website/Person/John.md b/website/Person/John.md index cbc427f7..88992b2b 100644 --- a/website/Person/John.md +++ b/website/Person/John.md @@ -4,7 +4,6 @@ lastName: Doe location: America tags: person --- - John Doe is a engineer living in the US. # Notable achievements diff --git a/website/Schema.md b/website/Schema.md new file mode 100644 index 00000000..4ba57dca --- /dev/null +++ b/website/Schema.md @@ -0,0 +1,17 @@ +SilverBullet relies on [JSON Schema](https://json-schema.org) for various types of validation, specifically: + +* [[Tag#Custom tags]] +* [[^Library/Std/Config]] options + +Often these schemas are encoded using [[Space Lua]], so take the shape of: + +```lua +local schema = { + type = "object", + properties = { + -- ... + } +} +``` + +There are is the [[^Library/Std/APIs/Schema]] API for convenience. \ No newline at end of file diff --git a/website/SilverBullet.md b/website/SilverBullet.md index 24c1927e..21b0bdf6 100644 --- a/website/SilverBullet.md +++ b/website/SilverBullet.md @@ -14,7 +14,7 @@ And if you are comfortable **programming** a little bit — now we’re really t # Programmable notes Dynamically generating content, _programmable notes_... why would you want that, and how does it work? -Let’s say you have documented a set of product features in individual pages that you’ve [[Tags|tagged]] with a #feature tag, and annotated with a few custom [[Frontmatter]] [[Attribute|Attributes]]. +Let’s say you have documented a set of product features in individual pages that you’ve [[Tag|tagged]] with a #feature tag, and annotated with a few custom [[Frontmatter]] [[Attribute|Attributes]]. With a simple [[Space Lua/Lua Integrated Query|Query]] and [[Template]], you can now dynamically build a product feature list, ordered by _awesomeness_ (`Alt-click` or hover and click the edit button to see the underlying code): diff --git a/website/Tag Picker.md b/website/Tag Picker.md index 8b2b4079..57318319 100644 --- a/website/Tag Picker.md +++ b/website/Tag Picker.md @@ -1 +1 @@ -Triggered via ${widgets.commandButton("Navigate: Tag Picker")}, allows you to quickly navigate to [[Tags|tag pages]]. \ No newline at end of file +Triggered via ${widgets.commandButton("Navigate: Tag Picker")}, allows you to quickly navigate to [[Tag|tag pages]]. \ No newline at end of file diff --git a/website/Tag.md b/website/Tag.md new file mode 100644 index 00000000..66bd2a5f --- /dev/null +++ b/website/Tag.md @@ -0,0 +1,9 @@ +Tags in SilverBullet are used to encode types of [[Object|Objects]], they’re the SilverBullet analogy to tables in SQL databases. + +Every [[Object]] has a main `tag`, which signifies the type of object being described. If you’re familiar with SQL databases, you can think of these as _tables_, or in object-oriented parlance you can think of them as _classes_. In addition, any number of additional tags can be assigned as well via the `tags` attribute. You can use either the main `tag` or any of the `tags` as query sources in [[Space Lua/Lua Integrated Query]]. + +# Built-in tags +${widgets.subPages("Object")} + +# Custom tags +To create a tag, you can simply use it with the [[Markdown/Hashtags]] syntax. The moment you use a hash tag somewhere, you will auto complete it. If you’d like to tweak or otherwise enhance your custom tags, have a look at the [[API/tag]] API. diff --git a/website/Tags.md b/website/Tags.md deleted file mode 100644 index 7e2de528..00000000 --- a/website/Tags.md +++ /dev/null @@ -1,3 +0,0 @@ -Tags in SilverBullet are used to encode types of [[Object]]. - -See [[Object#Tags]] for more information. \ No newline at end of file