Change how attribute indexing and completion works (#1061)
* Creation of separate aspring-page objects for pages linked to, but not created * Show "No results" instead of broken markdown table for no query results * Show schema validation errors * Deno upgrade * Adds config support to plugs (see examples) * Moves all builtin schemas to plug config * Adds core plug just for builtin schemas * Changes how attributes are indexed and completed, now attempts to derive a JSON schema for ad hoc attributespull/1062/head
parent
6f91b65457
commit
80f9c14b96
|
@ -33,7 +33,7 @@ jobs:
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.45
|
deno-version: v1.46
|
||||||
|
|
||||||
- name: Run bundle build
|
- name: Run bundle build
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.45
|
deno-version: v1.46
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: deno task build
|
run: deno task build
|
||||||
- name: Bundle
|
- name: Bundle
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.45
|
deno-version: v1.46
|
||||||
|
|
||||||
- name: Build bundles
|
- name: Build bundles
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.45
|
deno-version: v1.46
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: deno task build
|
run: deno task build
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
FROM gitpod/workspace-full:latest
|
FROM gitpod/workspace-full:latest
|
||||||
|
|
||||||
RUN curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.45.3
|
RUN curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.46.1
|
||||||
RUN /home/gitpod/.deno/bin/deno completions bash > /home/gitpod/.bashrc.d/90-deno && \
|
RUN /home/gitpod/.deno/bin/deno completions bash > /home/gitpod/.bashrc.d/90-deno && \
|
||||||
echo 'export DENO_INSTALL="/home/gitpod/.deno"' >> /home/gitpod/.bashrc.d/90-deno && \
|
echo 'export DENO_INSTALL="/home/gitpod/.deno"' >> /home/gitpod/.bashrc.d/90-deno && \
|
||||||
echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> /home/gitpod/.bashrc.d/90-deno
|
echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> /home/gitpod/.bashrc.d/90-deno
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM denoland/deno:debian-1.45.4
|
FROM denoland/deno:debian-1.46.1
|
||||||
|
|
||||||
# The volume that will keep the space data
|
# The volume that will keep the space data
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,11 @@ export async function compileManifest(
|
||||||
);
|
);
|
||||||
manifest.assets = assetsBundle.toJSON();
|
manifest.assets = assetsBundle.toJSON();
|
||||||
|
|
||||||
|
// Normalize the edge case of a plug with no functions
|
||||||
|
if (!manifest.functions) {
|
||||||
|
manifest.functions = {};
|
||||||
|
}
|
||||||
|
|
||||||
const jsFile = `
|
const jsFile = `
|
||||||
import { setupMessageListener } from "${
|
import { setupMessageListener } from "${
|
||||||
options.runtimeUrl || workerRuntimeUrl
|
options.runtimeUrl || workerRuntimeUrl
|
||||||
|
|
|
@ -54,12 +54,20 @@ async function loadConfigsFromSystem(
|
||||||
console.warn("Index plug not loaded yet, falling back to default config");
|
console.warn("Index plug not loaded yet, falling back to default config");
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
|
// We'll start witht the default config
|
||||||
|
let fullConfig: any = { ...defaultConfig };
|
||||||
|
// Then merge in all plug-based configs
|
||||||
|
for (const plugDef of system.loadedPlugs.values()) {
|
||||||
|
if (plugDef.manifest?.config) {
|
||||||
|
const plugConfig = cleanupJSON(plugDef.manifest.config);
|
||||||
|
fullConfig = deepObjectMerge(fullConfig, plugConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Query all space-configs
|
// Query all space-configs
|
||||||
const allConfigs: ConfigObject[] = await system.invokeFunction(
|
const allConfigs: ConfigObject[] = await system.invokeFunction(
|
||||||
"index.queryObjects",
|
"index.queryObjects",
|
||||||
["space-config", {}],
|
["space-config", {}],
|
||||||
);
|
);
|
||||||
let fullConfig: any = { ...defaultConfig };
|
|
||||||
// Now let's intelligently merge them
|
// Now let's intelligently merge them
|
||||||
for (const config of allConfigs) {
|
for (const config of allConfigs) {
|
||||||
let configObject = { [config.key]: config.value };
|
let configObject = { [config.key]: config.value };
|
||||||
|
|
|
@ -25,6 +25,7 @@ export function jsonschemaSyscalls(): SysCallMapping {
|
||||||
schema: any,
|
schema: any,
|
||||||
object: any,
|
object: any,
|
||||||
): undefined | string => {
|
): undefined | string => {
|
||||||
|
try {
|
||||||
const validate = ajv.compile(schema);
|
const validate = ajv.compile(schema);
|
||||||
if (validate(object)) {
|
if (validate(object)) {
|
||||||
return;
|
return;
|
||||||
|
@ -34,6 +35,9 @@ export function jsonschemaSyscalls(): SysCallMapping {
|
||||||
text = text.replace(/^data[\.\s]/, "");
|
text = text.replace(/^data[\.\s]/, "");
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return e.message;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jsonschema.validateSchema": (
|
"jsonschema.validateSchema": (
|
||||||
_ctx,
|
_ctx,
|
||||||
|
|
|
@ -29,6 +29,11 @@ export interface Manifest<HookT> {
|
||||||
* see: common/manifest.ts#SilverBulletHooks
|
* see: common/manifest.ts#SilverBulletHooks
|
||||||
*/
|
*/
|
||||||
functions: Record<string, FunctionDef<HookT>>;
|
functions: Record<string, FunctionDef<HookT>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of configuration options for the plug (to be merged with the system configuration).
|
||||||
|
*/
|
||||||
|
config?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Associates hooks with a function. This is the generic base structure, that identifies the function. Hooks are defined by the type parameter. */
|
/** Associates hooks with a function. This is the generic base structure, that identifies the function. Hooks are defined by the type parameter. */
|
||||||
|
|
|
@ -108,8 +108,8 @@ export async function extractFrontmatter(
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch {
|
||||||
console.warn("Could not parse frontmatter", e.message);
|
// console.warn("Could not parse frontmatter", e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,3 +158,7 @@ export function writeFile(
|
||||||
export function deleteFile(name: string): Promise<void> {
|
export function deleteFile(name: string): Promise<void> {
|
||||||
return syscall("space.deleteFile", name);
|
return syscall("space.deleteFile", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fileExists(name: string): Promise<boolean> {
|
||||||
|
return syscall("space.fileExists", name);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// TODO: Figure out how to keep this up-to-date automatically
|
// TODO: Figure out how to keep this up-to-date automatically
|
||||||
export const builtinPlugNames = [
|
export const builtinPlugNames = [
|
||||||
|
"core",
|
||||||
"editor",
|
"editor",
|
||||||
"index",
|
"index",
|
||||||
"sync",
|
"sync",
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
name: core
|
||||||
|
config:
|
||||||
|
# Built-in schemas
|
||||||
|
schema.tag:
|
||||||
|
syscall:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
argCount:
|
||||||
|
type: number
|
||||||
|
requiredPermissions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
command:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
priority:
|
||||||
|
type: number
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
mac:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
hide:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
requireMode:
|
||||||
|
type: string
|
||||||
|
enum: ["ro", "rw"]
|
||||||
|
nullable: true
|
||||||
|
space-config:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- string
|
||||||
|
- boolean
|
||||||
|
- object
|
||||||
|
- array
|
||||||
|
- null
|
||||||
|
space-style:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
style:
|
||||||
|
type: string
|
||||||
|
origin:
|
||||||
|
type: string
|
||||||
|
space-script:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
script:
|
||||||
|
type: string
|
||||||
|
# Built-in configuration schemas
|
||||||
|
schema.config.properties:
|
||||||
|
indexPage:
|
||||||
|
type: string
|
||||||
|
format: page-ref
|
||||||
|
maximumAttachmentSize:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
objectDecorators:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- where
|
||||||
|
- attributes
|
||||||
|
nullable: true
|
||||||
|
spaceIgnore:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
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 "@silverbulletmd/silverbullet/lib/resolve";
|
import { folderName } from "@silverbulletmd/silverbullet/lib/resolve";
|
||||||
import type { LinkObject } from "../index/page_links.ts";
|
import type { AspiringPageObject } from "../index/page_links.ts";
|
||||||
import { localDateString } from "$lib/dates.ts";
|
import { localDateString } from "$lib/dates.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
|
||||||
|
@ -79,19 +79,16 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
filter: ["!=~", ["attr", "name"], ["regexp", "^_", ""]],
|
filter: ["!=~", ["attr", "name"], ["regexp", "^_", ""]],
|
||||||
}, 5),
|
}, 5),
|
||||||
// And all links to non-existing pages (to augment the existing ones)
|
// And all links to non-existing pages (to augment the existing ones)
|
||||||
queryObjects<LinkObject>("link", {
|
queryObjects<AspiringPageObject>("aspiring-page", {
|
||||||
filter: ["and", ["attr", "toPage"], ["not", ["call", "pageExists", [[
|
distinct: true,
|
||||||
"attr",
|
select: [{ name: "name" }],
|
||||||
"toPage",
|
}, 5).then((aspiringPages) =>
|
||||||
]]]]],
|
|
||||||
select: [{ name: "toPage" }],
|
|
||||||
}, 5).then((brokenLinks) =>
|
|
||||||
// Rewrite them to PageMeta shaped objects
|
// Rewrite them to PageMeta shaped objects
|
||||||
brokenLinks.map((link): PageMeta => ({
|
aspiringPages.map((aspiringPage): PageMeta => ({
|
||||||
ref: link.toPage!,
|
ref: aspiringPage.name,
|
||||||
tag: "page",
|
tag: "page",
|
||||||
tags: ["non-existing"], // Picked up later in completion
|
tags: ["non-existing"], // Picked up later in completion
|
||||||
name: link.toPage!,
|
name: aspiringPage.name,
|
||||||
created: "",
|
created: "",
|
||||||
lastModified: "",
|
lastModified: "",
|
||||||
perm: "rw",
|
perm: "rw",
|
||||||
|
|
|
@ -1,6 +1,68 @@
|
||||||
name: editor
|
name: editor
|
||||||
requiredPermissions:
|
requiredPermissions:
|
||||||
- fetch
|
- fetch
|
||||||
|
config:
|
||||||
|
schema.config.properties:
|
||||||
|
shortcuts:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
command:
|
||||||
|
type: string
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
mac:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
slashCommand:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
priority:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
required:
|
||||||
|
- command
|
||||||
|
nullable: true
|
||||||
|
useSmartQuotes:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
pwaOpenLastPage:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
hideEditButton:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
hideSyncButton:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
actionButtons:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
icon:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
command:
|
||||||
|
type: string
|
||||||
|
args:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
mobile:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
required:
|
||||||
|
- icon
|
||||||
|
- command
|
||||||
|
defaultLinkStyle:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
functions:
|
functions:
|
||||||
setEditorMode:
|
setEditorMode:
|
||||||
path: "./editor.ts:setEditorMode"
|
path: "./editor.ts:setEditorMode"
|
||||||
|
|
|
@ -1,4 +1,16 @@
|
||||||
name: emoji
|
name: emoji
|
||||||
|
config:
|
||||||
|
schema.config.properties:
|
||||||
|
emoji:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
aliases:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- aliases
|
||||||
|
nullable: true
|
||||||
functions:
|
functions:
|
||||||
emojiCompleter:
|
emojiCompleter:
|
||||||
path: "./emoji.ts:emojiCompleter"
|
path: "./emoji.ts:emojiCompleter"
|
||||||
|
|
|
@ -1,6 +1,25 @@
|
||||||
name: federation
|
name: federation
|
||||||
requiredPermissions:
|
requiredPermissions:
|
||||||
- fetch
|
- fetch
|
||||||
|
config:
|
||||||
|
schema.config.properties:
|
||||||
|
libraries:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
import:
|
||||||
|
type: string
|
||||||
|
format: page-ref
|
||||||
|
exclude:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: page-ref
|
||||||
|
nullable: true
|
||||||
|
required:
|
||||||
|
- import
|
||||||
|
nullable: true
|
||||||
functions:
|
functions:
|
||||||
readFile:
|
readFile:
|
||||||
path: ./federation.ts:readFile
|
path: ./federation.ts:readFile
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { datastore } from "@silverbulletmd/silverbullet/syscalls";
|
import { datastore, system } from "@silverbulletmd/silverbullet/syscalls";
|
||||||
import type {
|
import type {
|
||||||
KV,
|
KV,
|
||||||
KvKey,
|
KvKey,
|
||||||
|
@ -7,8 +7,7 @@ import type {
|
||||||
ObjectValue,
|
ObjectValue,
|
||||||
} from "../../plug-api/types.ts";
|
} from "../../plug-api/types.ts";
|
||||||
import type { QueryProviderEvent } from "../../plug-api/types.ts";
|
import type { QueryProviderEvent } from "../../plug-api/types.ts";
|
||||||
import { builtins } from "./builtins.ts";
|
import { determineType, type SimpleJSONType } from "./attributes.ts";
|
||||||
import { determineType } from "./attributes.ts";
|
|
||||||
import { ttlCache } from "$lib/memory_cache.ts";
|
import { ttlCache } from "$lib/memory_cache.ts";
|
||||||
|
|
||||||
const indexKey = "idx";
|
const indexKey = "idx";
|
||||||
|
@ -79,12 +78,13 @@ export async function clearIndex(): Promise<void> {
|
||||||
/**
|
/**
|
||||||
* Indexes entities in the data store
|
* Indexes entities in the data store
|
||||||
*/
|
*/
|
||||||
export function indexObjects<T>(
|
export async function indexObjects<T>(
|
||||||
page: string,
|
page: string,
|
||||||
objects: ObjectValue<T>[],
|
objects: ObjectValue<T>[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const kvs: KV<T>[] = [];
|
const kvs: KV<T>[] = [];
|
||||||
const allAttributes = new Map<string, string>(); // tag:name -> attributeType
|
const schema = await system.getSpaceConfig("schema");
|
||||||
|
const allAttributes = new Map<string, SimpleJSONType>();
|
||||||
for (const obj of objects) {
|
for (const obj of objects) {
|
||||||
if (!obj.tag) {
|
if (!obj.tag) {
|
||||||
console.error("Object has no tag", obj, "this shouldn't happen");
|
console.error("Object has no tag", obj, "this shouldn't happen");
|
||||||
|
@ -92,6 +92,8 @@ export function indexObjects<T>(
|
||||||
}
|
}
|
||||||
// Index as all the tag + any additional tags specified
|
// Index as all the tag + any additional tags specified
|
||||||
const allTags = [obj.tag, ...obj.tags || []];
|
const allTags = [obj.tag, ...obj.tags || []];
|
||||||
|
const tagSchemaProperties =
|
||||||
|
schema.tag[obj.tag] && schema.tag[obj.tag].properties || {};
|
||||||
for (const tag of allTags) {
|
for (const tag of allTags) {
|
||||||
// The object itself
|
// The object itself
|
||||||
kvs.push({
|
kvs.push({
|
||||||
|
@ -99,41 +101,32 @@ export function indexObjects<T>(
|
||||||
value: obj,
|
value: obj,
|
||||||
});
|
});
|
||||||
// Index attributes
|
// Index attributes
|
||||||
const builtinAttributes = builtins[tag];
|
const schemaAttributes = schema.tag[tag] && schema.tag[tag].properties;
|
||||||
if (!builtinAttributes) {
|
if (!schemaAttributes) {
|
||||||
// This is not a builtin tag, so we index all attributes (almost, see below)
|
// There is no schema definition for this tag, so we index all attributes
|
||||||
attributeLabel: for (
|
|
||||||
const [attrName, attrValue] of Object.entries(
|
|
||||||
obj as Record<string, any>,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (attrName.startsWith("$")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Check for all tags attached to this object if they're builtins
|
|
||||||
// If so: if `attrName` is defined in the builtin, use the attributeType from there (mostly to preserve readOnly aspects)
|
|
||||||
for (const otherTag of allTags) {
|
|
||||||
const builtinAttributes = builtins[otherTag];
|
|
||||||
if (builtinAttributes && builtinAttributes[attrName]) {
|
|
||||||
allAttributes.set(
|
|
||||||
`${tag}:${attrName}`,
|
|
||||||
builtinAttributes[attrName],
|
|
||||||
);
|
|
||||||
continue attributeLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allAttributes.set(`${tag}:${attrName}`, determineType(attrValue));
|
|
||||||
}
|
|
||||||
} else if (tag !== "attribute") {
|
|
||||||
// For builtin tags, only index custom ones
|
|
||||||
for (
|
for (
|
||||||
const [attrName, attrValue] of Object.entries(
|
const [attrName, attrValue] of Object.entries(
|
||||||
obj as Record<string, any>,
|
obj as Record<string, any>,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// console.log("Indexing", tag, attrName, attrValue);
|
if (attrName.startsWith("$") || tagSchemaProperties[attrName]) {
|
||||||
// Skip builtins and internal attributes
|
continue;
|
||||||
if (builtinAttributes[attrName] || attrName.startsWith("$")) {
|
}
|
||||||
|
|
||||||
|
allAttributes.set(`${tag}:${attrName}`, determineType(attrValue));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For tags with schemas, only index attributes that are not in the schema
|
||||||
|
for (
|
||||||
|
const [attrName, attrValue] of Object.entries(
|
||||||
|
obj as Record<string, any>,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Skip schema-defined and internal attributes
|
||||||
|
if (
|
||||||
|
schemaAttributes[attrName] || tagSchemaProperties[attrName] ||
|
||||||
|
attrName.startsWith("$")
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
allAttributes.set(`${tag}:${attrName}`, determineType(attrValue));
|
allAttributes.set(`${tag}:${attrName}`, determineType(attrValue));
|
||||||
|
@ -144,17 +137,15 @@ export function indexObjects<T>(
|
||||||
if (allAttributes.size > 0) {
|
if (allAttributes.size > 0) {
|
||||||
[...allAttributes].forEach(([key, value]) => {
|
[...allAttributes].forEach(([key, value]) => {
|
||||||
const [tagName, name] = key.split(":");
|
const [tagName, name] = key.split(":");
|
||||||
const attributeType = value.startsWith("!") ? value.substring(1) : value;
|
|
||||||
kvs.push({
|
kvs.push({
|
||||||
key: ["attribute", cleanKey(key, page)],
|
key: ["ah-attr", cleanKey(key, page)],
|
||||||
value: {
|
value: {
|
||||||
ref: key,
|
ref: key,
|
||||||
tag: "attribute",
|
tag: "ah-attr",
|
||||||
tagName,
|
tagName,
|
||||||
name,
|
name,
|
||||||
attributeType,
|
|
||||||
readOnly: value.startsWith("!"),
|
|
||||||
page,
|
page,
|
||||||
|
schema: value,
|
||||||
} as T,
|
} as T,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -188,6 +179,16 @@ export function queryObjects<T>(
|
||||||
}, ttlSecs);
|
}, ttlSecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function queryDeleteObjects<T>(
|
||||||
|
tag: string,
|
||||||
|
query: ObjectQuery,
|
||||||
|
): Promise<void> {
|
||||||
|
return datastore.queryDelete({
|
||||||
|
...query,
|
||||||
|
prefix: [indexKey, tag],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function query(
|
export async function query(
|
||||||
query: KvQuery,
|
query: KvQuery,
|
||||||
variables?: Record<string, any>,
|
variables?: Record<string, any>,
|
||||||
|
@ -220,11 +221,15 @@ export async function objectSourceProvider({
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function discoverSources() {
|
export async function discoverSources() {
|
||||||
|
const schema = await system.getSpaceConfig("schema");
|
||||||
|
// Query all tags we indexed
|
||||||
return (await datastore.query({
|
return (await datastore.query({
|
||||||
prefix: [indexKey, "tag"],
|
prefix: [indexKey, "tag"],
|
||||||
select: [{ name: "name" }],
|
select: [{ name: "name" }],
|
||||||
distinct: true,
|
distinct: true,
|
||||||
})).map((
|
})).map((
|
||||||
{ value },
|
{ value },
|
||||||
) => value.name);
|
) => value.name)
|
||||||
|
// And concatenate all the tags from the schema
|
||||||
|
.concat(Object.keys(schema.tag));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { assertEquals } from "@std/assert/equals";
|
||||||
|
import { determineType, jsonTypeToString } from "./attributes.ts";
|
||||||
|
|
||||||
|
Deno.test("JSON Determine type", () => {
|
||||||
|
// Determine type tests
|
||||||
|
assertEquals(determineType(null), { type: "null" });
|
||||||
|
assertEquals(determineType(undefined), { type: "null" });
|
||||||
|
assertEquals(determineType("hello"), { type: "string" });
|
||||||
|
assertEquals(determineType(10), { type: "number" });
|
||||||
|
assertEquals(determineType(true), { type: "boolean" });
|
||||||
|
assertEquals(determineType({}), { type: "object", properties: {} });
|
||||||
|
assertEquals(determineType([]), { type: "array" });
|
||||||
|
assertEquals(determineType([1]), {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "number" },
|
||||||
|
});
|
||||||
|
assertEquals(
|
||||||
|
determineType({ name: "Pete", age: 10, siblings: ["Sarah"] }),
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
age: { type: "number" },
|
||||||
|
siblings: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Serialize JSON Type to string", () => {
|
||||||
|
assertEquals(jsonTypeToString({ type: "string" }), "string");
|
||||||
|
assertEquals(jsonTypeToString({ type: "null" }), "null");
|
||||||
|
assertEquals(jsonTypeToString({ type: "number" }), "number");
|
||||||
|
assertEquals(jsonTypeToString({ type: "boolean" }), "boolean");
|
||||||
|
assertEquals(jsonTypeToString({ type: "array" }), "any[]");
|
||||||
|
assertEquals(
|
||||||
|
jsonTypeToString({ type: "array", items: { type: "number" } }),
|
||||||
|
"number[]",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
jsonTypeToString({ type: "object", properties: {} }),
|
||||||
|
"{}",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
jsonTypeToString({
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
age: { type: "number" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"{name: string; age: number;}",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
jsonTypeToString({
|
||||||
|
anyOf: [
|
||||||
|
{ type: "string" },
|
||||||
|
{ type: "number" },
|
||||||
|
{ type: "boolean" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
"string | number | boolean",
|
||||||
|
);
|
||||||
|
});
|
|
@ -3,16 +3,23 @@ import type {
|
||||||
ObjectValue,
|
ObjectValue,
|
||||||
QueryExpression,
|
QueryExpression,
|
||||||
} from "../../plug-api/types.ts";
|
} from "../../plug-api/types.ts";
|
||||||
import { events } from "@silverbulletmd/silverbullet/syscalls";
|
import { events, system } from "@silverbulletmd/silverbullet/syscalls";
|
||||||
import { queryObjects } from "./api.ts";
|
import { queryObjects } from "./api.ts";
|
||||||
import { determineTags } from "$lib/cheap_yaml.ts";
|
import { determineTags } from "$lib/cheap_yaml.ts";
|
||||||
|
import type { TagObject } from "./tags.ts";
|
||||||
|
|
||||||
export type AttributeObject = ObjectValue<{
|
export type SimpleJSONType = {
|
||||||
|
type?: "string" | "number" | "boolean" | "any" | "array" | "object" | "null";
|
||||||
|
items?: SimpleJSONType;
|
||||||
|
properties?: Record<string, SimpleJSONType>;
|
||||||
|
anyOf?: SimpleJSONType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdhocAttributeObject = ObjectValue<{
|
||||||
name: string;
|
name: string;
|
||||||
attributeType: string;
|
schema: SimpleJSONType;
|
||||||
tagName: string;
|
tagName: string;
|
||||||
page: string;
|
page: string;
|
||||||
readOnly: boolean;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type AttributeCompleteEvent = {
|
export type AttributeCompleteEvent = {
|
||||||
|
@ -23,20 +30,11 @@ export type AttributeCompleteEvent = {
|
||||||
export type AttributeCompletion = {
|
export type AttributeCompletion = {
|
||||||
name: string;
|
name: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
// String version of JSON schema
|
||||||
attributeType: string;
|
attributeType: string;
|
||||||
readOnly: boolean;
|
readOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function determineType(v: any): string {
|
|
||||||
const t = typeof v;
|
|
||||||
if (t === "object") {
|
|
||||||
if (Array.isArray(v)) {
|
|
||||||
return "array";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered by the `attribute:complete:*` event (that is: gimme all attribute completions)
|
* Triggered by the `attribute:complete:*` event (that is: gimme all attribute completions)
|
||||||
* @param attributeCompleteEvent
|
* @param attributeCompleteEvent
|
||||||
|
@ -49,21 +47,59 @@ export async function objectAttributeCompleter(
|
||||||
attributeCompleteEvent.source === ""
|
attributeCompleteEvent.source === ""
|
||||||
? undefined
|
? undefined
|
||||||
: ["=", ["attr", "tagName"], ["string", attributeCompleteEvent.source]];
|
: ["=", ["attr", "tagName"], ["string", attributeCompleteEvent.source]];
|
||||||
const allAttributes = await queryObjects<AttributeObject>("attribute", {
|
const schema = await system.getSpaceConfig("schema");
|
||||||
|
const allAttributes = (await queryObjects<AdhocAttributeObject>("ah-attr", {
|
||||||
filter: attributeFilter,
|
filter: attributeFilter,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, {
|
select: [{ name: "name" }, { name: "schema" }, { name: "tag" }, {
|
||||||
name: "readOnly",
|
name: "tagName",
|
||||||
}, { name: "tagName" }],
|
}],
|
||||||
}, 5);
|
}, 5)).map((value) => {
|
||||||
return allAttributes.map((value) => {
|
|
||||||
return {
|
return {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
source: value.tagName,
|
source: value.tagName,
|
||||||
attributeType: value.attributeType,
|
attributeType: jsonTypeToString(value.schema),
|
||||||
readOnly: value.readOnly,
|
|
||||||
} as AttributeCompletion;
|
} as AttributeCompletion;
|
||||||
});
|
});
|
||||||
|
// Add attributes from the direct schema
|
||||||
|
addAttributeCompletionsForTag(
|
||||||
|
schema,
|
||||||
|
attributeCompleteEvent.source,
|
||||||
|
allAttributes,
|
||||||
|
);
|
||||||
|
// Look up the tag so we can check the parent as well
|
||||||
|
const sourceTags = await queryObjects<TagObject>("tag", {
|
||||||
|
filter: ["=", ["attr", "name"], ["string", attributeCompleteEvent.source]],
|
||||||
|
});
|
||||||
|
if (sourceTags.length > 0) {
|
||||||
|
addAttributeCompletionsForTag(schema, sourceTags[0].parent, allAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAttributeCompletionsForTag(
|
||||||
|
schema: any,
|
||||||
|
tag: string,
|
||||||
|
allAttributes: AttributeCompletion[],
|
||||||
|
) {
|
||||||
|
if (schema.tag[tag]) {
|
||||||
|
for (
|
||||||
|
const [name, value] of Object.entries(
|
||||||
|
schema.tag[tag].properties as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
allAttributes.push({
|
||||||
|
name,
|
||||||
|
source: tag,
|
||||||
|
attributeType: jsonTypeToString(value),
|
||||||
|
readOnly: value.readOnly,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -151,3 +187,66 @@ export function attributeCompletionsToCMCompletion(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt some reasonable stringification of a JSON schema
|
||||||
|
* @param schema
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function jsonTypeToString(schema: SimpleJSONType): string {
|
||||||
|
if (schema.anyOf) {
|
||||||
|
return schema.anyOf.map(jsonTypeToString).join(" | ");
|
||||||
|
} else if (schema.type === "array") {
|
||||||
|
if (schema.items) {
|
||||||
|
return `${jsonTypeToString(schema.items)}[]`;
|
||||||
|
} else {
|
||||||
|
return "any[]";
|
||||||
|
}
|
||||||
|
} else if (schema.type === "object") {
|
||||||
|
if (schema.properties) {
|
||||||
|
return `{${
|
||||||
|
Object.entries(schema.properties).map(([k, v]) =>
|
||||||
|
`${k}: ${jsonTypeToString(v)};`
|
||||||
|
).join(" ")
|
||||||
|
}}`;
|
||||||
|
} else {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schema.type!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function determineType(v: any): SimpleJSONType {
|
||||||
|
const t = typeof v;
|
||||||
|
if (t === "undefined" || v === null) {
|
||||||
|
return { type: "null" };
|
||||||
|
} else if (t === "object") {
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
if (v.length === 0) {
|
||||||
|
return {
|
||||||
|
type: "array",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: "array",
|
||||||
|
items: determineType(v[0]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: "object",
|
||||||
|
properties: Object.fromEntries(
|
||||||
|
Object.entries(v).map(([k, v]) => [k, determineType(v)]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (t === "number") {
|
||||||
|
return { type: "number" };
|
||||||
|
} else if (t === "boolean") {
|
||||||
|
return { type: "boolean" };
|
||||||
|
} else if (t === "string") {
|
||||||
|
return { type: "string" };
|
||||||
|
} else {
|
||||||
|
return { type: "any" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,172 +1,8 @@
|
||||||
import { system } from "@silverbulletmd/silverbullet/syscalls";
|
import { system } from "@silverbulletmd/silverbullet/syscalls";
|
||||||
import { indexObjects } from "./api.ts";
|
import type { QueryProviderEvent } from "@silverbulletmd/silverbullet/types";
|
||||||
import type {
|
|
||||||
ObjectValue,
|
|
||||||
QueryProviderEvent,
|
|
||||||
} from "@silverbulletmd/silverbullet/types";
|
|
||||||
import { applyQuery } from "@silverbulletmd/silverbullet/lib/query";
|
import { applyQuery } from "@silverbulletmd/silverbullet/lib/query";
|
||||||
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
||||||
|
|
||||||
export const builtinPseudoPage = ":builtin:";
|
|
||||||
|
|
||||||
// Types marked with a ! are read-only, they cannot be set by the user
|
|
||||||
export const builtins: Record<string, Record<string, string>> = {
|
|
||||||
page: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
displayName: "string",
|
|
||||||
aliases: "string[]",
|
|
||||||
created: "!date",
|
|
||||||
lastModified: "!date",
|
|
||||||
perm: "!rw|ro",
|
|
||||||
contentType: "!string",
|
|
||||||
size: "!number",
|
|
||||||
tags: "string[]",
|
|
||||||
},
|
|
||||||
attachment: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
created: "!date",
|
|
||||||
lastModified: "!date",
|
|
||||||
perm: "!rw|ro",
|
|
||||||
contentType: "!string",
|
|
||||||
size: "!number",
|
|
||||||
},
|
|
||||||
task: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
done: "!boolean",
|
|
||||||
page: "!string",
|
|
||||||
state: "!string",
|
|
||||||
deadline: "string",
|
|
||||||
pos: "!number",
|
|
||||||
tags: "string[]",
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
page: "!string",
|
|
||||||
tags: "string[]",
|
|
||||||
},
|
|
||||||
taskstate: {
|
|
||||||
ref: "!string",
|
|
||||||
tags: "!string[]",
|
|
||||||
state: "!string",
|
|
||||||
count: "!number",
|
|
||||||
page: "!string",
|
|
||||||
},
|
|
||||||
tag: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
page: "!string",
|
|
||||||
context: "!string",
|
|
||||||
},
|
|
||||||
attribute: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
attributeType: "!string",
|
|
||||||
tagName: "!string",
|
|
||||||
page: "!string",
|
|
||||||
readOnly: "!boolean",
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
page: "!string",
|
|
||||||
pos: "!number",
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
page: "!string",
|
|
||||||
pos: "!number",
|
|
||||||
alias: "!string",
|
|
||||||
asTemplate: "!boolean",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
ref: "!string",
|
|
||||||
name: "!string",
|
|
||||||
page: "!string",
|
|
||||||
level: "!number",
|
|
||||||
pos: "!number",
|
|
||||||
},
|
|
||||||
paragraph: {
|
|
||||||
text: "!string",
|
|
||||||
page: "!string",
|
|
||||||
pos: "!number",
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
ref: "!string",
|
|
||||||
page: "!string",
|
|
||||||
pageName: "string",
|
|
||||||
pos: "!number",
|
|
||||||
hooks: "hooksSpec",
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
ref: "!string",
|
|
||||||
page: "!string",
|
|
||||||
pos: "!number",
|
|
||||||
},
|
|
||||||
|
|
||||||
// System builtins
|
|
||||||
syscall: {
|
|
||||||
name: "!string",
|
|
||||||
requiredPermissions: "!string[]",
|
|
||||||
argCount: "!number",
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
name: "!string",
|
|
||||||
priority: "!number",
|
|
||||||
key: "!string",
|
|
||||||
mac: "!string",
|
|
||||||
hide: "!boolean",
|
|
||||||
requireMode: "!rw|ro",
|
|
||||||
},
|
|
||||||
"space-config": {
|
|
||||||
key: "!string",
|
|
||||||
value: "!any",
|
|
||||||
},
|
|
||||||
"space-style": {
|
|
||||||
style: "!string",
|
|
||||||
origin: "!string",
|
|
||||||
},
|
|
||||||
"space-script": {
|
|
||||||
script: "!string",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function loadBuiltinsIntoIndex() {
|
|
||||||
if (await system.getMode() === "ro") {
|
|
||||||
console.log("Running in read-only mode, not loading builtins");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("Loading builtins attributes into index");
|
|
||||||
const allObjects: ObjectValue<any>[] = [];
|
|
||||||
for (const [tagName, attributes] of Object.entries(builtins)) {
|
|
||||||
allObjects.push({
|
|
||||||
ref: tagName,
|
|
||||||
tag: "tag",
|
|
||||||
name: tagName,
|
|
||||||
page: builtinPseudoPage,
|
|
||||||
parent: "builtin",
|
|
||||||
});
|
|
||||||
allObjects.push(
|
|
||||||
...Object.entries(attributes).map(([name, attributeType]) => ({
|
|
||||||
ref: `${tagName}:${name}`,
|
|
||||||
tag: "attribute",
|
|
||||||
tagName,
|
|
||||||
name,
|
|
||||||
attributeType: attributeType.startsWith("!")
|
|
||||||
? attributeType.substring(1)
|
|
||||||
: attributeType,
|
|
||||||
readOnly: attributeType.startsWith("!"),
|
|
||||||
page: builtinPseudoPage,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await indexObjects(builtinPseudoPage, allObjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syscallSourceProvider({
|
export async function syscallSourceProvider({
|
||||||
query,
|
query,
|
||||||
variables,
|
variables,
|
||||||
|
|
|
@ -28,8 +28,6 @@ export async function reindexSpace(noClear = false) {
|
||||||
// Executed this way to not have to embed the search plug code here
|
// Executed this way to not have to embed the search plug code here
|
||||||
await system.invokeFunction("index.clearIndex");
|
await system.invokeFunction("index.clearIndex");
|
||||||
}
|
}
|
||||||
// Load builtins
|
|
||||||
await system.invokeFunction("index.loadBuiltinsIntoIndex");
|
|
||||||
// Pre-index SETTINGS page to get useful settings
|
// Pre-index SETTINGS page to get useful settings
|
||||||
console.log("Indexing SETTINGS page");
|
console.log("Indexing SETTINGS page");
|
||||||
await indexPage("SETTINGS");
|
await indexPage("SETTINGS");
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
name: index
|
name: index
|
||||||
functions:
|
functions:
|
||||||
loadBuiltinsIntoIndex:
|
|
||||||
path: builtins.ts:loadBuiltinsIntoIndex
|
|
||||||
env: server
|
|
||||||
events:
|
|
||||||
- system:ready
|
|
||||||
|
|
||||||
# Public API
|
# Public API
|
||||||
batchSet:
|
batchSet:
|
||||||
path: api.ts:batchSet
|
path: api.ts:batchSet
|
||||||
|
@ -21,7 +15,6 @@ functions:
|
||||||
getObjectByRef:
|
getObjectByRef:
|
||||||
path: api.ts:getObjectByRef
|
path: api.ts:getObjectByRef
|
||||||
env: server
|
env: server
|
||||||
|
|
||||||
objectSourceProvider:
|
objectSourceProvider:
|
||||||
path: api.ts:objectSourceProvider
|
path: api.ts:objectSourceProvider
|
||||||
events:
|
events:
|
||||||
|
@ -239,3 +232,397 @@ functions:
|
||||||
path: builtins.ts:commandSourceProvider
|
path: builtins.ts:commandSourceProvider
|
||||||
events:
|
events:
|
||||||
- query:command
|
- query:command
|
||||||
|
config:
|
||||||
|
# Schema for the built-in tags indexed by this plug
|
||||||
|
schema.tag:
|
||||||
|
page:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
enum:
|
||||||
|
- page
|
||||||
|
tags:
|
||||||
|
anyOf:
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
- type: string
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
pageDecoration:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
prefix:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
cssClasses:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
hide:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
renderWidgets:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
nullable: true
|
||||||
|
displayName:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
aliases:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
contentType:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
size:
|
||||||
|
type: number
|
||||||
|
readOnly: true
|
||||||
|
lastModified:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
perm:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
enum:
|
||||||
|
- ro
|
||||||
|
- rw
|
||||||
|
lastOpened:
|
||||||
|
type: number
|
||||||
|
readOnly: true
|
||||||
|
nullable: true
|
||||||
|
aspiring-page:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
enum:
|
||||||
|
- aspiring-page
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
readOnly: true
|
||||||
|
attachment:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
enum:
|
||||||
|
- attachment
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
created:
|
||||||
|
readOnly: true
|
||||||
|
type: string
|
||||||
|
contentType:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
size:
|
||||||
|
type: number
|
||||||
|
readOnly: true
|
||||||
|
lastModified:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
perm:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
enum:
|
||||||
|
- ro
|
||||||
|
- rw
|
||||||
|
item:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
enum:
|
||||||
|
- item
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
parent:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
readOnly: true
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
enum:
|
||||||
|
- tag
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
parent:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
context:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
anchor:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- anchor
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
link:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- link
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
toFile:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
toPage:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
snippet:
|
||||||
|
type: string
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
alias:
|
||||||
|
type: string
|
||||||
|
asTemplate:
|
||||||
|
type: boolean
|
||||||
|
header:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- header
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
level:
|
||||||
|
type: string
|
||||||
|
paragraph:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- paragraph
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
template:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- template
|
||||||
|
tags:
|
||||||
|
anyOf:
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
- type: string
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
pageName:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
hooks:
|
||||||
|
type: object
|
||||||
|
frontmatter:
|
||||||
|
anyOf:
|
||||||
|
- type: object
|
||||||
|
- type: string
|
||||||
|
table:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- table
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "@silverbulletmd/silverbullet/lib/tree";
|
} from "@silverbulletmd/silverbullet/lib/tree";
|
||||||
import type { LintEvent } from "../../plug-api/types.ts";
|
import type { LintEvent } from "../../plug-api/types.ts";
|
||||||
import { queryObjects } from "./api.ts";
|
import { queryObjects } from "./api.ts";
|
||||||
import type { AttributeObject } from "./attributes.ts";
|
import type { AdhocAttributeObject } from "./attributes.ts";
|
||||||
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
|
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
|
||||||
import {
|
import {
|
||||||
cleanupJSON,
|
cleanupJSON,
|
||||||
|
@ -26,8 +26,8 @@ export async function lintYAML({ tree }: LintEvent): Promise<LintDiagnostic[]> {
|
||||||
await traverseTreeAsync(tree, async (node) => {
|
await traverseTreeAsync(tree, async (node) => {
|
||||||
if (node.type === "FrontMatterCode") {
|
if (node.type === "FrontMatterCode") {
|
||||||
// Query all readOnly attributes for pages with this tag set
|
// Query all readOnly attributes for pages with this tag set
|
||||||
const readOnlyAttributes = await queryObjects<AttributeObject>(
|
const readOnlyAttributes = await queryObjects<AdhocAttributeObject>(
|
||||||
"attribute",
|
"ah-attr",
|
||||||
{
|
{
|
||||||
filter: ["and", ["=", ["attr", "tagName"], [
|
filter: ["and", ["=", ["attr", "tagName"], [
|
||||||
"array",
|
"array",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { IndexTreeEvent } from "../../plug-api/types.ts";
|
import type { IndexTreeEvent } from "@silverbulletmd/silverbullet/types";
|
||||||
import {
|
import {
|
||||||
editor,
|
editor,
|
||||||
markdown,
|
markdown,
|
||||||
|
@ -9,13 +9,14 @@ import {
|
||||||
import type { LintDiagnostic, PageMeta } from "../../plug-api/types.ts";
|
import type { LintDiagnostic, PageMeta } from "../../plug-api/types.ts";
|
||||||
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
|
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
|
||||||
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute";
|
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute";
|
||||||
import { indexObjects } from "./api.ts";
|
import { indexObjects, queryDeleteObjects } from "./api.ts";
|
||||||
import {
|
import {
|
||||||
findNodeOfType,
|
findNodeOfType,
|
||||||
renderToText,
|
renderToText,
|
||||||
traverseTreeAsync,
|
traverseTreeAsync,
|
||||||
} from "../../plug-api/lib/tree.ts";
|
} from "@silverbulletmd/silverbullet/lib/tree";
|
||||||
import { updateITags } from "@silverbulletmd/silverbullet/lib/tags";
|
import { updateITags } from "@silverbulletmd/silverbullet/lib/tags";
|
||||||
|
import type { AspiringPageObject } from "./page_links.ts";
|
||||||
|
|
||||||
export async function indexPage({ name, tree }: IndexTreeEvent) {
|
export async function indexPage({ name, tree }: IndexTreeEvent) {
|
||||||
if (name.startsWith("_")) {
|
if (name.startsWith("_")) {
|
||||||
|
@ -62,6 +63,11 @@ export async function indexPage({ name, tree }: IndexTreeEvent) {
|
||||||
|
|
||||||
// console.log("Page object", combinedPageMeta);
|
// console.log("Page object", combinedPageMeta);
|
||||||
await indexObjects<PageMeta>(name, [combinedPageMeta]);
|
await indexObjects<PageMeta>(name, [combinedPageMeta]);
|
||||||
|
|
||||||
|
// Make sure this page is no (longer) in the aspiring pages list
|
||||||
|
await queryDeleteObjects<AspiringPageObject>("aspiring-page", {
|
||||||
|
filter: ["=", ["attr", "name"], ["string", name]],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lintFrontmatter(): Promise<LintDiagnostic[]> {
|
export async function lintFrontmatter(): Promise<LintDiagnostic[]> {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
mdLinkRegex,
|
mdLinkRegex,
|
||||||
wikiLinkRegex,
|
wikiLinkRegex,
|
||||||
} from "$common/markdown_parser/constants.ts";
|
} from "$common/markdown_parser/constants.ts";
|
||||||
|
import { space } from "@silverbulletmd/silverbullet/syscalls";
|
||||||
|
|
||||||
export type LinkObject = ObjectValue<
|
export type LinkObject = ObjectValue<
|
||||||
{
|
{
|
||||||
|
@ -50,6 +51,19 @@ export type LinkObject = ObjectValue<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a page that does not yet exist, but is being linked to. A page "aspiring" to be created.
|
||||||
|
*/
|
||||||
|
export type AspiringPageObject = ObjectValue<{
|
||||||
|
// ref: page@pos
|
||||||
|
// The page the link appears on
|
||||||
|
page: string;
|
||||||
|
// And the position
|
||||||
|
pos: number;
|
||||||
|
// The page the link points to
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
||||||
const links: ObjectValue<LinkObject>[] = [];
|
const links: ObjectValue<LinkObject>[] = [];
|
||||||
const frontmatter = await extractFrontmatter(tree);
|
const frontmatter = await extractFrontmatter(tree);
|
||||||
|
@ -194,8 +208,35 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log("Found", links, "page link(s)");
|
// console.log("Found", links, "page link(s)");
|
||||||
|
if (links.length > 0) {
|
||||||
await indexObjects(name, links);
|
await indexObjects(name, links);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let's check which are aspiring pages
|
||||||
|
const aspiringPages: ObjectValue<AspiringPageObject>[] = [];
|
||||||
|
for (const link of links) {
|
||||||
|
if (link.toPage) {
|
||||||
|
// No federated links, nothing with template directives
|
||||||
|
if (link.toPage.startsWith("!") || link.toPage.includes("{{")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!await space.fileExists(`${link.toPage}.md`)) {
|
||||||
|
aspiringPages.push({
|
||||||
|
ref: `${name}@${link.pos}`,
|
||||||
|
tag: "aspiring-page",
|
||||||
|
page: name,
|
||||||
|
pos: link.pos,
|
||||||
|
name: link.toPage,
|
||||||
|
} as AspiringPageObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aspiringPages.length > 0) {
|
||||||
|
await indexObjects(name, aspiringPages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBackLinks(
|
export async function getBackLinks(
|
||||||
|
|
|
@ -44,7 +44,11 @@ export async function widget(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (Array.isArray(results)) {
|
if (Array.isArray(results)) {
|
||||||
|
if (results.length === 0) {
|
||||||
|
resultMarkdown = "No results";
|
||||||
|
} else {
|
||||||
resultMarkdown = jsonToMDTable(results);
|
resultMarkdown = jsonToMDTable(results);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resultMarkdown = results;
|
resultMarkdown = results;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,3 +41,68 @@ functions:
|
||||||
command:
|
command:
|
||||||
name: "Task: Remove Completed"
|
name: "Task: Remove Completed"
|
||||||
requireMode: rw
|
requireMode: rw
|
||||||
|
|
||||||
|
config:
|
||||||
|
schema.tag:
|
||||||
|
task:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- task
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
page:
|
||||||
|
type: string
|
||||||
|
parent:
|
||||||
|
type: string
|
||||||
|
pos:
|
||||||
|
type: number
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
done:
|
||||||
|
type: boolean
|
||||||
|
deadline:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
taskstate:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ref:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- taskstate
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
itags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
count:
|
||||||
|
type: number
|
|
@ -120,7 +120,7 @@ export class ServerSystem extends CommonSystem {
|
||||||
this.system.registerSyscalls(
|
this.system.registerSyscalls(
|
||||||
[],
|
[],
|
||||||
eventSyscalls(this.eventHook),
|
eventSyscalls(this.eventHook),
|
||||||
spaceReadSyscalls(space),
|
spaceReadSyscalls(space, this.allKnownFiles),
|
||||||
assetSyscalls(this.system),
|
assetSyscalls(this.system),
|
||||||
yamlSyscalls(),
|
yamlSyscalls(),
|
||||||
systemSyscalls(
|
systemSyscalls(
|
||||||
|
@ -169,6 +169,7 @@ export class ServerSystem extends CommonSystem {
|
||||||
this.eventHook.addLocalListener(
|
this.eventHook.addLocalListener(
|
||||||
"file:changed",
|
"file:changed",
|
||||||
async (path, localChange) => {
|
async (path, localChange) => {
|
||||||
|
this.allKnownFiles.add(path);
|
||||||
if (!localChange) {
|
if (!localChange) {
|
||||||
console.log("Outside file change: reindexing", path);
|
console.log("Outside file change: reindexing", path);
|
||||||
// Change made outside of editor, trigger reindex
|
// Change made outside of editor, trigger reindex
|
||||||
|
@ -190,6 +191,7 @@ export class ServerSystem extends CommonSystem {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Keep allKnownFiles up to date
|
||||||
this.eventHook.addLocalListener(
|
this.eventHook.addLocalListener(
|
||||||
"file:listed",
|
"file:listed",
|
||||||
(allFiles: FileMeta[]) => {
|
(allFiles: FileMeta[]) => {
|
||||||
|
@ -202,6 +204,13 @@ export class ServerSystem extends CommonSystem {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
this.eventHook.addLocalListener(
|
||||||
|
"file:deleted",
|
||||||
|
(path: string) => {
|
||||||
|
// Update list of known pages and attachments
|
||||||
|
this.allKnownFiles.delete(path);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// All setup, enable eventing
|
// All setup, enable eventing
|
||||||
eventedSpacePrimitives.enabled = true;
|
eventedSpacePrimitives.enabled = true;
|
||||||
|
|
|
@ -9,7 +9,10 @@ import type { Space } from "../../common/space.ts";
|
||||||
/**
|
/**
|
||||||
* Almost the same as web/syscalls/space.ts except leaving out client-specific stuff
|
* Almost the same as web/syscalls/space.ts except leaving out client-specific stuff
|
||||||
*/
|
*/
|
||||||
export function spaceReadSyscalls(space: Space): SysCallMapping {
|
export function spaceReadSyscalls(
|
||||||
|
space: Space,
|
||||||
|
allKnownFiles: Set<string>,
|
||||||
|
): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
"space.listPages": (): Promise<PageMeta[]> => {
|
"space.listPages": (): Promise<PageMeta[]> => {
|
||||||
return space.fetchPageList();
|
return space.fetchPageList();
|
||||||
|
@ -46,6 +49,9 @@ export function spaceReadSyscalls(space: Space): SysCallMapping {
|
||||||
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
||||||
return (await space.spacePrimitives.readFile(name)).data;
|
return (await space.spacePrimitives.readFile(name)).data;
|
||||||
},
|
},
|
||||||
|
"space.fileExists": (_ctx, name: string): boolean => {
|
||||||
|
return allKnownFiles.has(name);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
102
type/config.ts
102
type/config.ts
|
@ -58,102 +58,6 @@ type SchemaConfig = {
|
||||||
config: Record<string, any>; // any = JSONSchema
|
config: Record<string, any>; // any = JSONSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
const configSchema = {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
indexPage: { type: "string", format: "page-ref" },
|
|
||||||
shortcuts: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
command: { type: "string" },
|
|
||||||
key: { type: "string", nullable: true },
|
|
||||||
mac: { type: "string", nullable: true },
|
|
||||||
slashCommand: { type: "string", nullable: true },
|
|
||||||
priority: { type: "number", nullable: true },
|
|
||||||
},
|
|
||||||
required: ["command"],
|
|
||||||
},
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
useSmartQuotes: { type: "boolean", nullable: true },
|
|
||||||
maximumAttachmentSize: { type: "number", nullable: true },
|
|
||||||
pwaOpenLastPage: { type: "boolean", nullable: true },
|
|
||||||
hideEditButton: { type: "boolean", nullable: true },
|
|
||||||
hideSyncButton: { type: "boolean", nullable: true },
|
|
||||||
libraries: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
import: { type: "string", format: "page-ref" },
|
|
||||||
exclude: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string", format: "page-ref" },
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["import"],
|
|
||||||
},
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
actionButtons: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
icon: { type: "string" },
|
|
||||||
description: { type: "string", nullable: true },
|
|
||||||
command: { type: "string" },
|
|
||||||
args: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "object" },
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
mobile: { type: "boolean", nullable: true },
|
|
||||||
},
|
|
||||||
required: ["icon", "command"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
objectDecorators: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
required: ["where", "attributes"],
|
|
||||||
},
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
spaceIgnore: { type: "string", nullable: true },
|
|
||||||
emoji: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
aliases: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["aliases"],
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
customStyles: {
|
|
||||||
anyOf: [
|
|
||||||
{ type: "string" },
|
|
||||||
{
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
},
|
|
||||||
{ type: "null" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
defaultLinkStyle: { type: "string", nullable: true },
|
|
||||||
},
|
|
||||||
additionalProperties: true,
|
|
||||||
required: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultConfig: Config = {
|
export const defaultConfig: Config = {
|
||||||
indexPage: "index",
|
indexPage: "index",
|
||||||
hideSyncButton: false,
|
hideSyncButton: false,
|
||||||
|
@ -162,7 +66,11 @@ export const defaultConfig: Config = {
|
||||||
actionButtons: [], // Actually defaults to defaultActionButtons
|
actionButtons: [], // Actually defaults to defaultActionButtons
|
||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
config: configSchema,
|
config: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
tag: {},
|
tag: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -73,7 +73,7 @@ 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 "@silverbulletmd/silverbullet/lib/tree";
|
import { findNodeMatching } from "@silverbulletmd/silverbullet/lib/tree";
|
||||||
import type { LinkObject } from "../plugs/index/page_links.ts";
|
import type { AspiringPageObject } from "../plugs/index/page_links.ts";
|
||||||
import type { Config, ConfigContainer } from "../type/config.ts";
|
import type { Config, ConfigContainer } from "../type/config.ts";
|
||||||
import { editor } from "@silverbulletmd/silverbullet/syscalls";
|
import { editor } from "@silverbulletmd/silverbullet/syscalls";
|
||||||
|
|
||||||
|
@ -754,19 +754,15 @@ export class Client implements ConfigContainer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const allPages = await this.clientSystem.queryObjects<PageMeta>("page", {});
|
const allPages = await this.clientSystem.queryObjects<PageMeta>("page", {});
|
||||||
const allBrokenLinkPages = (await this.clientSystem.queryObjects<
|
const allAspiringPages = (await this.clientSystem.queryObjects<
|
||||||
LinkObject
|
AspiringPageObject
|
||||||
>("link", {
|
>("aspiring-page", {
|
||||||
filter: ["and", ["attr", "toPage"], ["not", ["call", "pageExists", [[
|
select: [{ name: "name" }],
|
||||||
"attr",
|
})).map((aspiringPage): PageMeta => ({
|
||||||
"toPage",
|
ref: aspiringPage.name,
|
||||||
]]]]],
|
|
||||||
select: [{ name: "toPage" }],
|
|
||||||
})).map((link): PageMeta => ({
|
|
||||||
ref: link.toPage!,
|
|
||||||
tag: "page",
|
tag: "page",
|
||||||
_isBrokenLink: true,
|
_isAspiring: true,
|
||||||
name: link.toPage!,
|
name: aspiringPage.name,
|
||||||
created: "",
|
created: "",
|
||||||
lastModified: "",
|
lastModified: "",
|
||||||
perm: "rw",
|
perm: "rw",
|
||||||
|
@ -774,7 +770,7 @@ export class Client implements ConfigContainer {
|
||||||
|
|
||||||
this.ui.viewDispatch({
|
this.ui.viewDispatch({
|
||||||
type: "update-page-list",
|
type: "update-page-list",
|
||||||
allPages: allPages.concat(allBrokenLinkPages),
|
allPages: allPages.concat(allAspiringPages),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,12 +70,12 @@ export function PageNavigator({
|
||||||
name: (pageMeta.pageDecoration?.prefix ?? "") + pageMeta.name,
|
name: (pageMeta.pageDecoration?.prefix ?? "") + pageMeta.name,
|
||||||
description,
|
description,
|
||||||
orderId: orderId,
|
orderId: orderId,
|
||||||
hint: pageMeta._isBrokenLink ? "Create page" : undefined,
|
hint: pageMeta._isAspiring ? "Create page" : undefined,
|
||||||
cssClass,
|
cssClass,
|
||||||
});
|
});
|
||||||
} else if (mode === "meta") {
|
} else if (mode === "meta") {
|
||||||
// Special behavior for #template and #meta pages
|
// Special behavior for #template and #meta pages
|
||||||
if (pageMeta._isBrokenLink) {
|
if (pageMeta._isAspiring) {
|
||||||
// Skip over broken links
|
// Skip over broken links
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,9 @@ export function spaceReadSyscalls(editor: Client): SysCallMapping {
|
||||||
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
||||||
return (await editor.space.spacePrimitives.readFile(name)).data;
|
return (await editor.space.spacePrimitives.readFile(name)).data;
|
||||||
},
|
},
|
||||||
|
"space.fileExists": (_ctx, name: string): boolean => {
|
||||||
|
return editor.clientSystem.allKnownFiles.has(name);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
An aspiring page is a [[Pages|page]] that does not yet exist, but is already linked to.
|
||||||
|
|
||||||
|
Aspiring pages appear in the [[Page Picker]] (with a `Create page` hint) as well as in auto complete when creating [[Links]].
|
|
@ -208,7 +208,7 @@ So, what’s the fuss all about?
|
||||||
* Ah yes, you can also still take notes with SilverBullet. That still works. I think.
|
* Ah yes, you can also still take notes with SilverBullet. That still works. I think.
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
* Tag pages: when you click on a #tag you will now be directed to a page that shows all pages, tasks, items and paragraphs tagged with that tag.
|
* Tag pages: when you click on a `#tag` you will now be directed to a page that shows all pages, tasks, items and paragraphs tagged with that tag.
|
||||||
* Action buttons (top right buttons) can now be configured; see [[SETTINGS]] for how to do this.
|
* Action buttons (top right buttons) can now be configured; see [[SETTINGS]] for how to do this.
|
||||||
* Headers are now indexed, meaning you can query them [[Objects#header]] and also reference them by name via page links using `#` that I just demonstrated 👈. See [[Links]] for more information on all the types of link formats that SilverBullet now supports.
|
* Headers are now indexed, meaning you can query them [[Objects#header]] and also reference them by name via page links using `#` that I just demonstrated 👈. See [[Links]] for more information on all the types of link formats that SilverBullet now supports.
|
||||||
* New {[Task: Remove Completed]} command to remove all completed tasks from a page
|
* New {[Task: Remove Completed]} command to remove all completed tasks from a page
|
||||||
|
|
|
@ -2,19 +2,17 @@
|
||||||
|
|
||||||
We would like to keep our space clean, these are some tools that help you do that.
|
We would like to keep our space clean, these are some tools that help you do that.
|
||||||
|
|
||||||
# Broken links
|
# Aspiring pages
|
||||||
This shows all internal links that are broken.
|
This shows all page links that link to a page that does not (yet) exist. These could be broken links or just pages _aspiring_ to be created.
|
||||||
|
|
||||||
```template
|
```template
|
||||||
{{#let @brokenLinks = {
|
{{#let @brokenLinks = {aspiring-page}}}
|
||||||
link where toPage and not pageExists(toPage)
|
|
||||||
}}}
|
|
||||||
{{#if @brokenLinks}}
|
{{#if @brokenLinks}}
|
||||||
{{#each @brokenLinks}}
|
{{#each @brokenLinks}}
|
||||||
* [[{{ref}}]]: broken link to [[{{toPage}}]]
|
* [[{{ref}}]]: broken link to [[{{name}}]]
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
No broken links, all good!
|
No aspiring pages, all good!
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
```
|
```
|
||||||
|
|
|
@ -39,6 +39,14 @@ Note that you can also query this page using the `level/intermediate` directly:
|
||||||
level/intermediate
|
level/intermediate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## aspiring-page
|
||||||
|
[[Aspiring Pages]] are pages that are linked to, but not yet created.
|
||||||
|
|
||||||
|
```query
|
||||||
|
aspiring-page
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## table
|
## table
|
||||||
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 [[Tags]] in any of its cells.
|
||||||
|
|
||||||
|
@ -48,7 +56,6 @@ Markdown table rows are indexed using the `table` tag, any additional tags can b
|
||||||
| Some Row | This is an example row in between two others |
|
| Some Row | This is an example row in between two others |
|
||||||
| Another key | This time without a tag |
|
| Another key | This time without a tag |
|
||||||
|
|
||||||
|
|
||||||
```query
|
```query
|
||||||
table
|
table
|
||||||
```
|
```
|
||||||
|
@ -153,10 +160,10 @@ In addition, the `snippet` attribute attempts to capture a little bit of context
|
||||||
|
|
||||||
_Note_: this is the data source used for the {[Mentions: Toggle]} feature as well page {[Page: Rename]}.
|
_Note_: this is the data source used for the {[Mentions: Toggle]} feature as well page {[Page: Rename]}.
|
||||||
|
|
||||||
Here is a query that shows all links that appear in this particular page:
|
Here is a query that shows some links that appear in this particular page:
|
||||||
|
|
||||||
```query
|
```query
|
||||||
link where page = @page.name
|
link where page = @page.name limit 5
|
||||||
```
|
```
|
||||||
|
|
||||||
## anchor
|
## anchor
|
||||||
|
@ -185,13 +192,6 @@ Here are the tags used/defined in this page:
|
||||||
tag where page = @page.name select name, parent
|
tag where page = @page.name select name, parent
|
||||||
```
|
```
|
||||||
|
|
||||||
## attribute
|
|
||||||
This is another meta tag, which is used to index all [[Attributes]] used in your space. This is used by e.g. attribute completion in various contexts. You likely don’t need to use this tag directly, but it’s there.
|
|
||||||
|
|
||||||
```query
|
|
||||||
attribute where page = @page.name limit 1
|
|
||||||
```
|
|
||||||
|
|
||||||
## space-config
|
## space-config
|
||||||
This stores all configuration picked up as part of [[Space Config]]
|
This stores all configuration picked up as part of [[Space Config]]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue