Make attribute extensible
parent
b7b666ee1d
commit
2f1f266482
|
@ -1,5 +1,6 @@
|
|||
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import type { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
|
||||
export type AttributeContext = "page" | "item" | "task";
|
||||
|
||||
|
@ -7,6 +8,47 @@ type AttributeEntry = {
|
|||
type: string;
|
||||
};
|
||||
|
||||
export type AttributeCompleteEvent = {
|
||||
source: string;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
export type AttributeCompletion = {
|
||||
name: string;
|
||||
source: string;
|
||||
type: string;
|
||||
builtin?: boolean;
|
||||
};
|
||||
|
||||
const builtinAttributes: Record<string, Record<string, string>> = {
|
||||
page: {
|
||||
name: "string",
|
||||
lastModified: "number",
|
||||
perm: "rw|ro",
|
||||
contentType: "string",
|
||||
size: "number",
|
||||
tags: "array",
|
||||
},
|
||||
task: {
|
||||
name: "string",
|
||||
done: "boolean",
|
||||
page: "string",
|
||||
deadline: "string",
|
||||
pos: "number",
|
||||
tags: "array",
|
||||
},
|
||||
item: {
|
||||
name: "string",
|
||||
page: "string",
|
||||
pos: "number",
|
||||
tags: "array",
|
||||
},
|
||||
tag: {
|
||||
name: "string",
|
||||
freq: "number",
|
||||
},
|
||||
};
|
||||
|
||||
function determineType(v: any): string {
|
||||
const t = typeof v;
|
||||
if (t === "object") {
|
||||
|
@ -37,6 +79,52 @@ export async function indexAttributes(
|
|||
);
|
||||
}
|
||||
|
||||
export async function customAttributeCompleter(
|
||||
attributeCompleteEvent: AttributeCompleteEvent,
|
||||
): Promise<AttributeCompletion[]> {
|
||||
const sourcePrefix = attributeCompleteEvent.source === "*"
|
||||
? ""
|
||||
: `${attributeCompleteEvent.source}:`;
|
||||
const allAttributes = await index.queryPrefix(
|
||||
`${attributeKeyPrefix}${sourcePrefix}`,
|
||||
);
|
||||
return allAttributes.map((attr) => {
|
||||
const [_prefix, context, name] = attr.key.split(":");
|
||||
return {
|
||||
name,
|
||||
source: context,
|
||||
type: attr.value.type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function builtinAttributeCompleter(
|
||||
attributeCompleteEvent: AttributeCompleteEvent,
|
||||
): AttributeCompletion[] {
|
||||
let allAttributes = builtinAttributes[attributeCompleteEvent.source];
|
||||
if (attributeCompleteEvent.source === "*") {
|
||||
allAttributes = {};
|
||||
for (const [source, attributes] of Object.entries(builtinAttributes)) {
|
||||
for (const [name, type] of Object.entries(attributes)) {
|
||||
allAttributes[name] = `${type}|${source}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!allAttributes) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(allAttributes).map(([name, type]) => {
|
||||
return {
|
||||
name,
|
||||
source: attributeCompleteEvent.source === "*"
|
||||
? type.split("|")[1]
|
||||
: attributeCompleteEvent.source,
|
||||
type: attributeCompleteEvent.source === "*" ? type.split("|")[0] : type,
|
||||
builtin: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function attributeComplete(completeEvent: CompleteEvent) {
|
||||
const inlineAttributeMatch = /([^\[\{}]|^)\[(\w+)$/.exec(
|
||||
completeEvent.linePrefix,
|
||||
|
@ -49,41 +137,50 @@ export async function attributeComplete(completeEvent: CompleteEvent) {
|
|||
} else if (completeEvent.parentNodes.includes("ListItem")) {
|
||||
type = "item";
|
||||
}
|
||||
const allAttributes = await index.queryPrefix(
|
||||
`${attributeKeyPrefix}${type}:`,
|
||||
);
|
||||
const completions = (await events.dispatchEvent(
|
||||
`attribute:complete:${type}`,
|
||||
{
|
||||
source: type,
|
||||
prefix: inlineAttributeMatch[2],
|
||||
} as AttributeCompleteEvent,
|
||||
)).flat() as AttributeCompletion[];
|
||||
return {
|
||||
from: completeEvent.pos - inlineAttributeMatch[2].length,
|
||||
options: allAttributes.map((attr) => {
|
||||
const [_prefix, _context, name] = attr.key.split(":");
|
||||
return {
|
||||
label: name,
|
||||
apply: `${name}: `,
|
||||
detail: attr.value.type,
|
||||
type: "attribute",
|
||||
};
|
||||
}),
|
||||
options: attributeCompletionsToCMCompletion(
|
||||
completions.filter((completion) => !completion.builtin),
|
||||
),
|
||||
};
|
||||
}
|
||||
const attributeMatch = /^(\w+)$/.exec(completeEvent.linePrefix);
|
||||
if (attributeMatch) {
|
||||
if (completeEvent.parentNodes.includes("FrontMatterCode")) {
|
||||
const allAttributes = await index.queryPrefix(
|
||||
`${attributeKeyPrefix}page:`,
|
||||
);
|
||||
const completions = (await events.dispatchEvent(
|
||||
`attribute:complete:page`,
|
||||
{
|
||||
source: "page",
|
||||
prefix: attributeMatch[1],
|
||||
} as AttributeCompleteEvent,
|
||||
)).flat() as AttributeCompletion[];
|
||||
return {
|
||||
from: completeEvent.pos - attributeMatch[1].length,
|
||||
options: allAttributes.map((attr) => {
|
||||
const [_prefix, _context, name] = attr.key.split(":");
|
||||
return {
|
||||
label: name,
|
||||
apply: `${name}: `,
|
||||
detail: attr.value.type,
|
||||
type: "attribute",
|
||||
};
|
||||
}),
|
||||
options: attributeCompletionsToCMCompletion(
|
||||
completions.filter((completion) => !completion.builtin),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function attributeCompletionsToCMCompletion(
|
||||
completions: AttributeCompletion[],
|
||||
) {
|
||||
return completions.map(
|
||||
(completion) => ({
|
||||
label: completion.name,
|
||||
apply: `${completion.name}: `,
|
||||
detail: `${completion.type} (${completion.source})`,
|
||||
type: "attribute",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -96,6 +96,22 @@ functions:
|
|||
events:
|
||||
- editor:complete
|
||||
|
||||
customAttributeCompleter:
|
||||
path: ./attributes.ts:customAttributeCompleter
|
||||
events:
|
||||
- attribute:complete:page
|
||||
- attribute:complete:task
|
||||
- attribute:complete:item
|
||||
- attribute:complete:*
|
||||
|
||||
builtinAttributeCompleter:
|
||||
path: ./attributes.ts:builtinAttributeCompleter
|
||||
events:
|
||||
- attribute:complete:page
|
||||
- attribute:complete:task
|
||||
- attribute:complete:item
|
||||
- attribute:complete:*
|
||||
|
||||
# Commands
|
||||
commandComplete:
|
||||
path: "./command.ts:commandComplete"
|
||||
|
|
|
@ -2,36 +2,10 @@ import { events } from "$sb/plugos-syscall/mod.ts";
|
|||
import { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { buildHandebarOptions } from "./util.ts";
|
||||
import type { PageMeta } from "../../web/types.ts";
|
||||
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
||||
|
||||
const builtinAttributes: Record<string, Record<string, string>> = {
|
||||
page: {
|
||||
name: "string",
|
||||
lastModified: "number",
|
||||
perm: "rw|ro",
|
||||
contentType: "string",
|
||||
size: "number",
|
||||
tags: "array",
|
||||
},
|
||||
task: {
|
||||
name: "string",
|
||||
done: "boolean",
|
||||
page: "string",
|
||||
deadline: "string",
|
||||
pos: "number",
|
||||
tags: "array",
|
||||
},
|
||||
item: {
|
||||
name: "string",
|
||||
page: "string",
|
||||
pos: "number",
|
||||
tags: "array",
|
||||
},
|
||||
tag: {
|
||||
name: "string",
|
||||
freq: "number",
|
||||
},
|
||||
};
|
||||
import {
|
||||
AttributeCompleteEvent,
|
||||
AttributeCompletion,
|
||||
} from "../core/attributes.ts";
|
||||
|
||||
export async function queryComplete(completeEvent: CompleteEvent) {
|
||||
const querySourceMatch = /#query\s+([\w\-_]*)$/.exec(
|
||||
|
@ -61,51 +35,22 @@ export async function queryComplete(completeEvent: CompleteEvent) {
|
|||
if (querySourceMatch && whereMatch) {
|
||||
const type = querySourceMatch[1];
|
||||
const attributePrefix = whereMatch[3];
|
||||
// console.log("Type", type);
|
||||
// console.log("Where", attributePrefix);
|
||||
const allAttributes = await index.queryPrefix(
|
||||
`attr:${type}:`,
|
||||
);
|
||||
|
||||
const completions = (await events.dispatchEvent(
|
||||
`attribute:complete:${type}`,
|
||||
{
|
||||
source: type,
|
||||
prefix: attributePrefix,
|
||||
} as AttributeCompleteEvent,
|
||||
)).flat() as AttributeCompletion[];
|
||||
return {
|
||||
from: completeEvent.pos - attributePrefix.length,
|
||||
options: compileAttributeCompletions(allAttributes, type),
|
||||
options: attributeCompletionsToCMCompletion(completions),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function compileAttributeCompletions(
|
||||
allAttributes: { key: string; value: any }[],
|
||||
type?: string,
|
||||
) {
|
||||
let allCompletions: any[] = allAttributes.map((attr) => {
|
||||
const [_prefix, context, name] = attr.key.split(":");
|
||||
return {
|
||||
label: name,
|
||||
detail: `${attr.value.type} (${context})`,
|
||||
type: "attribute",
|
||||
};
|
||||
});
|
||||
const allContexts = type ? [type] : Object.keys(builtinAttributes);
|
||||
|
||||
for (const context of allContexts) {
|
||||
allCompletions = allCompletions.concat(
|
||||
builtinAttributes[context]
|
||||
? Object.entries(
|
||||
builtinAttributes[context],
|
||||
).map(([name, type]) => ({
|
||||
label: name,
|
||||
detail: `${type} (${context}: builtin)`,
|
||||
type: "attribute",
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
}
|
||||
return allCompletions;
|
||||
}
|
||||
|
||||
export async function templateVariableComplete(completeEvent: CompleteEvent) {
|
||||
const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix);
|
||||
if (!match) {
|
||||
|
@ -123,9 +68,16 @@ export async function templateVariableComplete(completeEvent: CompleteEvent) {
|
|||
})),
|
||||
);
|
||||
|
||||
const allAttributes = await index.queryPrefix(`attr:`);
|
||||
const completions = (await events.dispatchEvent(
|
||||
`attribute:complete:*`,
|
||||
{
|
||||
source: "*",
|
||||
prefix: match[1],
|
||||
} as AttributeCompleteEvent,
|
||||
)).flat() as AttributeCompletion[];
|
||||
|
||||
allCompletions = allCompletions.concat(
|
||||
compileAttributeCompletions(allAttributes),
|
||||
attributeCompletionsToCMCompletion(completions),
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -133,3 +85,15 @@ export async function templateVariableComplete(completeEvent: CompleteEvent) {
|
|||
options: allCompletions,
|
||||
};
|
||||
}
|
||||
|
||||
export function attributeCompletionsToCMCompletion(
|
||||
completions: AttributeCompletion[],
|
||||
) {
|
||||
return completions.map(
|
||||
(completion) => ({
|
||||
label: completion.name,
|
||||
detail: `${completion.type} (${completion.source})`,
|
||||
type: "attribute",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -334,12 +334,11 @@ export class HttpServer {
|
|||
fsRouter
|
||||
.get(
|
||||
filePathRegex,
|
||||
// corsMiddleware,
|
||||
async ({ params, response, request }) => {
|
||||
const name = params[0];
|
||||
console.log("Requested file", name);
|
||||
if (!request.headers.has("X-Sync-Mode") && name.endsWith(".md")) {
|
||||
// It can happen that during a sync, authentication expires
|
||||
// It can happen that during a sync, authentication expires, this may result in a redirect to the login page and then back to this particular file. This particular file may be an .md file, which isn't great to show so we're redirecting to the associated SB UI page.
|
||||
console.log("Request was without X-Sync-Mode, redirecting to page");
|
||||
response.redirect(`/${name.slice(0, -3)}`);
|
||||
return;
|
||||
|
@ -401,7 +400,7 @@ export class HttpServer {
|
|||
|
||||
response.body = fileData.data;
|
||||
} catch (e: any) {
|
||||
console.error("Error GETting of file", name, e);
|
||||
console.error("Error GETting file", name, e);
|
||||
response.status = 404;
|
||||
response.body = "Not found";
|
||||
}
|
||||
|
@ -409,7 +408,6 @@ export class HttpServer {
|
|||
)
|
||||
.put(
|
||||
filePathRegex,
|
||||
// corsMiddleware,
|
||||
async ({ request, response, params }) => {
|
||||
const name = params[0];
|
||||
console.log("Saving file", name);
|
||||
|
|
Loading…
Reference in New Issue