Whoops, forgot to commit this before
parent
e919aa82e9
commit
ce5186c7c2
|
@ -0,0 +1,21 @@
|
|||
import { assertEquals } from "$lib/test_deps.ts";
|
||||
import { deepEqual, deepObjectMerge, expandPropertyNames } from "./json.ts";
|
||||
|
||||
Deno.test("utils", () => {
|
||||
assertEquals(deepEqual({ a: 1 }, { a: 1 }), true);
|
||||
assertEquals(deepObjectMerge({ a: 1 }, { a: 2 }), { a: 2 });
|
||||
assertEquals(
|
||||
deepObjectMerge({ list: [1, 2, 3] }, { list: [4, 5, 6] }),
|
||||
{ list: [1, 2, 3, 4, 5, 6] },
|
||||
);
|
||||
assertEquals(deepObjectMerge({ a: { b: 1 } }, { a: { c: 2 } }), {
|
||||
a: { b: 1, c: 2 },
|
||||
});
|
||||
assertEquals(expandPropertyNames({ "a.b": 1 }), { a: { b: 1 } });
|
||||
assertEquals(expandPropertyNames({ a: { "a.b": 1 } }), {
|
||||
a: { a: { b: 1 } },
|
||||
});
|
||||
assertEquals(expandPropertyNames({ a: [{ "a.b": 1 }] }), {
|
||||
a: [{ a: { b: 1 } }],
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
// Compares two objects deeply
|
||||
export function deepEqual(a: any, b: any): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (typeof a !== typeof b) {
|
||||
return false;
|
||||
}
|
||||
if (typeof a === "object") {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
if (aKeys.length !== bKeys.length) {
|
||||
return false;
|
||||
}
|
||||
for (const key of aKeys) {
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expands property names in an object containing a .-separated path
|
||||
export function expandPropertyNames(a: any): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (typeof a !== "object") {
|
||||
return a;
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
return a.map(expandPropertyNames);
|
||||
}
|
||||
const expanded: any = {};
|
||||
for (const key of Object.keys(a)) {
|
||||
const parts = key.split(".");
|
||||
let target = expanded;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (!target[part]) {
|
||||
target[part] = {};
|
||||
}
|
||||
target = target[part];
|
||||
}
|
||||
target[parts[parts.length - 1]] = expandPropertyNames(a[key]);
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
|
||||
export function deepObjectMerge(a: any, b: any): any {
|
||||
if (typeof a !== typeof b) {
|
||||
return b;
|
||||
}
|
||||
if (typeof a === "object") {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return [...a, ...b];
|
||||
} else {
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
const merged = { ...a };
|
||||
for (const key of bKeys) {
|
||||
if (aKeys.includes(key)) {
|
||||
merged[key] = deepObjectMerge(a[key], b[key]);
|
||||
} else {
|
||||
merged[key] = b[key];
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
return b;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// import { parse } from "./parse_tree.ts";
|
||||
import {
|
||||
addParentPointers,
|
||||
collectNodesMatching,
|
||||
findParentMatching,
|
||||
nodeAtPos,
|
||||
parseTreeToAST,
|
||||
removeParentPointers,
|
||||
renderToText,
|
||||
replaceNodesMatching,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { assertEquals, assertNotEquals } from "$lib/test_deps.ts";
|
||||
import { parse } from "$common/markdown_parser/parse_tree.ts";
|
||||
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
|
||||
|
||||
const mdTest1 = `
|
||||
# Heading
|
||||
## Sub _heading_ cool
|
||||
|
||||
Hello, this is some **bold** text and *italic*. And [a link](http://zef.me).
|
||||
|
||||
%% My comment here
|
||||
%% And second line
|
||||
|
||||
And an @mention
|
||||
|
||||
http://zef.plus
|
||||
|
||||
- This is a list [[PageLink]]
|
||||
- With another item
|
||||
- TODOs:
|
||||
- [ ] A task that's not yet done
|
||||
- [x] Hello
|
||||
- And a _third_ one [[Wiki Page]] yo
|
||||
`;
|
||||
|
||||
const mdTest2 = `
|
||||
Hello
|
||||
|
||||
* Item 1
|
||||
*
|
||||
|
||||
Sup`;
|
||||
|
||||
const mdTest3 = `
|
||||
\`\`\`yaml
|
||||
name: something
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
Deno.test("Test parsing", () => {
|
||||
const mdTree = parse(extendedMarkdownLanguage, mdTest1);
|
||||
addParentPointers(mdTree);
|
||||
// console.log(JSON.stringify(mdTree, null, 2));
|
||||
const wikiLink = nodeAtPos(mdTree, mdTest1.indexOf("Wiki Page"))!;
|
||||
assertEquals(wikiLink.type, "WikiLinkPage");
|
||||
assertNotEquals(
|
||||
findParentMatching(wikiLink, (n) => n.type === "BulletList"),
|
||||
null,
|
||||
);
|
||||
|
||||
const allTodos = collectNodesMatching(mdTree, (n) => n.type === "Task");
|
||||
assertEquals(allTodos.length, 2);
|
||||
|
||||
// Render back into markdown should be equivalent
|
||||
assertEquals(renderToText(mdTree), mdTest1);
|
||||
|
||||
removeParentPointers(mdTree);
|
||||
replaceNodesMatching(mdTree, (n) => {
|
||||
if (n.type === "Task") {
|
||||
return {
|
||||
type: "Tosk",
|
||||
};
|
||||
}
|
||||
});
|
||||
// console.log(JSON.stringify(mdTree, null, 2));
|
||||
let mdTree3 = parse(extendedMarkdownLanguage, mdTest3);
|
||||
// console.log(JSON.stringify(mdTree3, null, 2));
|
||||
});
|
||||
|
||||
Deno.test("AST functions", () => {
|
||||
const mdTree = parse(extendedMarkdownLanguage, mdTest1);
|
||||
console.log(JSON.stringify(parseTreeToAST(mdTree), null, 2));
|
||||
});
|
|
@ -0,0 +1,237 @@
|
|||
export type ParseTree = {
|
||||
type?: string; // undefined === text node
|
||||
from?: number;
|
||||
to?: number;
|
||||
text?: string;
|
||||
children?: ParseTree[];
|
||||
// Only present after running addParentPointers
|
||||
parent?: ParseTree;
|
||||
};
|
||||
|
||||
export type AST = [string, ...AST[]] | string;
|
||||
|
||||
export function addParentPointers(tree: ParseTree) {
|
||||
if (!tree.children) {
|
||||
return;
|
||||
}
|
||||
for (const child of tree.children) {
|
||||
if (child.parent) {
|
||||
// Already added parent pointers before
|
||||
return;
|
||||
}
|
||||
child.parent = tree;
|
||||
addParentPointers(child);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeParentPointers(tree: ParseTree) {
|
||||
delete tree.parent;
|
||||
if (!tree.children) {
|
||||
return;
|
||||
}
|
||||
for (const child of tree.children) {
|
||||
removeParentPointers(child);
|
||||
}
|
||||
}
|
||||
|
||||
export function findParentMatching(
|
||||
tree: ParseTree,
|
||||
matchFn: (tree: ParseTree) => boolean,
|
||||
): ParseTree | null {
|
||||
let node = tree.parent;
|
||||
while (node) {
|
||||
if (matchFn(node)) {
|
||||
return node;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function collectNodesOfType(
|
||||
tree: ParseTree,
|
||||
nodeType: string,
|
||||
): ParseTree[] {
|
||||
return collectNodesMatching(tree, (n) => n.type === nodeType);
|
||||
}
|
||||
|
||||
export function collectNodesMatching(
|
||||
tree: ParseTree,
|
||||
matchFn: (tree: ParseTree) => boolean,
|
||||
): ParseTree[] {
|
||||
if (matchFn(tree)) {
|
||||
return [tree];
|
||||
}
|
||||
let results: ParseTree[] = [];
|
||||
if (tree.children) {
|
||||
for (const child of tree.children) {
|
||||
results = [...results, ...collectNodesMatching(child, matchFn)];
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function collectNodesMatchingAsync(
|
||||
tree: ParseTree,
|
||||
matchFn: (tree: ParseTree) => Promise<boolean>,
|
||||
): Promise<ParseTree[]> {
|
||||
if (await matchFn(tree)) {
|
||||
return [tree];
|
||||
}
|
||||
let results: ParseTree[] = [];
|
||||
if (tree.children) {
|
||||
for (const child of tree.children) {
|
||||
results = [
|
||||
...results,
|
||||
...await collectNodesMatchingAsync(child, matchFn),
|
||||
];
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// return value: returning undefined = not matched, continue, null = delete, new node = replace
|
||||
export function replaceNodesMatching(
|
||||
tree: ParseTree,
|
||||
substituteFn: (tree: ParseTree) => ParseTree | null | undefined,
|
||||
) {
|
||||
if (tree.children) {
|
||||
const children = tree.children.slice();
|
||||
for (const child of children) {
|
||||
const subst = substituteFn(child);
|
||||
if (subst !== undefined) {
|
||||
const pos = tree.children.indexOf(child);
|
||||
if (subst) {
|
||||
tree.children.splice(pos, 1, subst);
|
||||
} else {
|
||||
// null = delete
|
||||
tree.children.splice(pos, 1);
|
||||
}
|
||||
} else {
|
||||
replaceNodesMatching(child, substituteFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function replaceNodesMatchingAsync(
|
||||
tree: ParseTree,
|
||||
substituteFn: (tree: ParseTree) => Promise<ParseTree | null | undefined>,
|
||||
) {
|
||||
if (tree.children) {
|
||||
const children = tree.children.slice();
|
||||
for (const child of children) {
|
||||
const subst = await substituteFn(child);
|
||||
if (subst !== undefined) {
|
||||
const pos = tree.children.indexOf(child);
|
||||
if (subst) {
|
||||
tree.children.splice(pos, 1, subst);
|
||||
} else {
|
||||
// null = delete
|
||||
tree.children.splice(pos, 1);
|
||||
}
|
||||
} else {
|
||||
await replaceNodesMatchingAsync(child, substituteFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findNodeMatching(
|
||||
tree: ParseTree,
|
||||
matchFn: (tree: ParseTree) => boolean,
|
||||
): ParseTree | null {
|
||||
return collectNodesMatching(tree, matchFn)[0];
|
||||
}
|
||||
|
||||
export function findNodeOfType(
|
||||
tree: ParseTree,
|
||||
nodeType: string,
|
||||
): ParseTree | null {
|
||||
return collectNodesMatching(tree, (n) => n.type === nodeType)[0];
|
||||
}
|
||||
|
||||
export function traverseTree(
|
||||
tree: ParseTree,
|
||||
// Return value = should stop traversal?
|
||||
matchFn: (tree: ParseTree) => boolean,
|
||||
): void {
|
||||
// Do a collect, but ignore the result
|
||||
collectNodesMatching(tree, matchFn);
|
||||
}
|
||||
|
||||
export async function traverseTreeAsync(
|
||||
tree: ParseTree,
|
||||
// Return value = should stop traversal?
|
||||
matchFn: (tree: ParseTree) => Promise<boolean>,
|
||||
): Promise<void> {
|
||||
// Do a collect, but ignore the result
|
||||
await collectNodesMatchingAsync(tree, matchFn);
|
||||
}
|
||||
|
||||
// Finds non-text node at position
|
||||
export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null {
|
||||
if (pos < tree.from! || pos >= tree.to!) {
|
||||
return null;
|
||||
}
|
||||
if (!tree.children) {
|
||||
return tree;
|
||||
}
|
||||
for (const child of tree.children) {
|
||||
const n = nodeAtPos(child, pos);
|
||||
if (n && n.text !== undefined) {
|
||||
// Got a text node, let's return its parent
|
||||
return tree;
|
||||
} else if (n) {
|
||||
// Got it
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Turn ParseTree back into text
|
||||
export function renderToText(tree?: ParseTree): string {
|
||||
if (!tree) {
|
||||
return "";
|
||||
}
|
||||
const pieces: string[] = [];
|
||||
if (tree.text !== undefined) {
|
||||
return tree.text;
|
||||
}
|
||||
for (const child of tree.children!) {
|
||||
pieces.push(renderToText(child));
|
||||
}
|
||||
return pieces.join("");
|
||||
}
|
||||
|
||||
export function cloneTree(tree: ParseTree): ParseTree {
|
||||
const newTree = { ...tree };
|
||||
if (tree.children) {
|
||||
newTree.children = tree.children.map(cloneTree);
|
||||
}
|
||||
delete newTree.parent;
|
||||
return newTree;
|
||||
}
|
||||
|
||||
export function parseTreeToAST(tree: ParseTree, omitTrimmable = true): AST {
|
||||
const parseErrorNodes = collectNodesOfType(tree, "⚠");
|
||||
if (parseErrorNodes.length > 0) {
|
||||
throw new Error(
|
||||
`Parse error in: ${renderToText(tree)}`,
|
||||
);
|
||||
}
|
||||
if (tree.text !== undefined) {
|
||||
return tree.text;
|
||||
}
|
||||
const ast: AST = [tree.type!];
|
||||
for (const node of tree.children!) {
|
||||
if (node.type && !node.type.endsWith("Mark")) {
|
||||
ast.push(parseTreeToAST(node, omitTrimmable));
|
||||
}
|
||||
if (node.text && (omitTrimmable && node.text.trim() || !omitTrimmable)) {
|
||||
ast.push(node.text);
|
||||
}
|
||||
}
|
||||
return ast;
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
import type { ParseTree } from "./lib/tree.ts";
|
||||
import type { TextChange } from "../web/change.ts";
|
||||
|
||||
export type FileMeta = {
|
||||
name: string;
|
||||
created: number;
|
||||
lastModified: number;
|
||||
contentType: string;
|
||||
size: number;
|
||||
perm: "ro" | "rw";
|
||||
noSync?: boolean;
|
||||
};
|
||||
|
||||
export type PageMeta = ObjectValue<
|
||||
{
|
||||
name: string;
|
||||
created: string; // indexing it as a string
|
||||
lastModified: string; // indexing it as a string
|
||||
lastOpened?: number;
|
||||
perm: "ro" | "rw";
|
||||
} & Record<string, any>
|
||||
>;
|
||||
|
||||
export type AttachmentMeta = {
|
||||
name: string;
|
||||
contentType: string;
|
||||
created: number;
|
||||
lastModified: number;
|
||||
size: number;
|
||||
perm: "ro" | "rw";
|
||||
};
|
||||
|
||||
export type SyscallMeta = {
|
||||
name: string;
|
||||
requiredPermissions: string[];
|
||||
argCount: number;
|
||||
};
|
||||
|
||||
// Message Queue related types
|
||||
export type MQMessage = {
|
||||
id: string;
|
||||
queue: string;
|
||||
body: any;
|
||||
retries?: number;
|
||||
};
|
||||
|
||||
export type MQStats = {
|
||||
queued: number;
|
||||
processing: number;
|
||||
dlq: number;
|
||||
};
|
||||
|
||||
export type MQSubscribeOptions = {
|
||||
batchSize?: number;
|
||||
pollInterval?: number;
|
||||
};
|
||||
|
||||
// Key-Value Store related types
|
||||
export type KvKey = string[];
|
||||
|
||||
export type KV<T = any> = {
|
||||
key: KvKey;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type OrderBy = {
|
||||
expr: QueryExpression;
|
||||
desc: boolean;
|
||||
};
|
||||
|
||||
export type Select = {
|
||||
name: string;
|
||||
expr?: QueryExpression;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
querySource?: string;
|
||||
filter?: QueryExpression;
|
||||
orderBy?: OrderBy[];
|
||||
select?: Select[];
|
||||
limit?: QueryExpression;
|
||||
render?: string;
|
||||
renderAll?: boolean;
|
||||
distinct?: boolean;
|
||||
};
|
||||
|
||||
export type KvQuery = Omit<Query, "querySource"> & {
|
||||
prefix?: KvKey;
|
||||
};
|
||||
|
||||
export type QueryExpression =
|
||||
| ["and", QueryExpression, QueryExpression]
|
||||
| ["or", QueryExpression, QueryExpression]
|
||||
| ["=", QueryExpression, QueryExpression]
|
||||
| ["!=", QueryExpression, QueryExpression]
|
||||
| ["=~", QueryExpression, QueryExpression]
|
||||
| ["!=~", QueryExpression, QueryExpression]
|
||||
| ["<", QueryExpression, QueryExpression]
|
||||
| ["<=", QueryExpression, QueryExpression]
|
||||
| [">", QueryExpression, QueryExpression]
|
||||
| [">=", QueryExpression, QueryExpression]
|
||||
| ["in", QueryExpression, QueryExpression]
|
||||
| ["attr"] // .
|
||||
| ["attr", string] // name
|
||||
| ["attr", QueryExpression, string] // something.name
|
||||
| ["global", string] // @name
|
||||
| ["number", number]
|
||||
| ["string", string]
|
||||
| ["boolean", boolean]
|
||||
| ["null"]
|
||||
| ["not", QueryExpression]
|
||||
| ["array", QueryExpression[]]
|
||||
| ["object", [string, QueryExpression][]]
|
||||
| ["regexp", string, string] // regex, modifier
|
||||
| ["pageref", string]
|
||||
| ["-", QueryExpression]
|
||||
| ["+", QueryExpression, QueryExpression]
|
||||
| ["-", QueryExpression, QueryExpression]
|
||||
| ["*", QueryExpression, QueryExpression]
|
||||
| ["%", QueryExpression, QueryExpression]
|
||||
| ["/", QueryExpression, QueryExpression]
|
||||
| ["?", QueryExpression, QueryExpression, QueryExpression]
|
||||
| ["query", Query]
|
||||
| ["call", string, QueryExpression[]];
|
||||
|
||||
export type FunctionMap = Record<
|
||||
string,
|
||||
(...args: any[]) => any
|
||||
>;
|
||||
|
||||
/**
|
||||
* An ObjectValue that can be indexed by the `index` plug, needs to have a minimum of
|
||||
* of two fields:
|
||||
* - ref: a unique reference (id) for the object, ideally a page reference
|
||||
* - tags: a list of tags that the object belongs to
|
||||
*/
|
||||
export type ObjectValue<T> = {
|
||||
ref: string;
|
||||
tag: string; // main tag
|
||||
tags?: string[];
|
||||
itags?: string[]; // implicit or inherited tags (inherited from the page for instance)
|
||||
} & T;
|
||||
|
||||
export type ObjectQuery = Omit<Query, "prefix">;
|
||||
|
||||
// Code widget stuff
|
||||
export type CodeWidgetCallback = (
|
||||
bodyText: string,
|
||||
pageName: string,
|
||||
) => Promise<CodeWidgetContent | null>;
|
||||
|
||||
export type CodeWidgetContent = {
|
||||
html?: string;
|
||||
markdown?: string;
|
||||
script?: string;
|
||||
buttons?: CodeWidgetButton[];
|
||||
};
|
||||
|
||||
export type CodeWidgetButton = {
|
||||
widgetTarget?: boolean;
|
||||
description: string;
|
||||
svg: string;
|
||||
invokeFunction: string;
|
||||
};
|
||||
|
||||
export type LintDiagnostic = {
|
||||
from: number;
|
||||
to: number;
|
||||
severity: "error" | "warning" | "info" | "hint";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type UploadFile = {
|
||||
name: string;
|
||||
contentType: string;
|
||||
content: Uint8Array;
|
||||
};
|
||||
|
||||
export type AppEvent =
|
||||
| "page:click"
|
||||
| "editor:complete"
|
||||
| "minieditor:complete"
|
||||
| "slash:complete"
|
||||
| "editor:lint"
|
||||
| "page:load"
|
||||
| "editor:init"
|
||||
| "editor:pageLoaded" // args: pageName, previousPage, isSynced
|
||||
| "editor:pageReloaded"
|
||||
| "editor:pageSaved"
|
||||
| "editor:modeswitch"
|
||||
| "plugs:loaded"
|
||||
| "editor:pageModified";
|
||||
|
||||
export type QueryProviderEvent = {
|
||||
query: Query;
|
||||
variables?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type ClickEvent = {
|
||||
page: string;
|
||||
pos: number;
|
||||
metaKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
altKey: boolean;
|
||||
};
|
||||
|
||||
export type IndexEvent = {
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type IndexTreeEvent = {
|
||||
name: string;
|
||||
tree: ParseTree;
|
||||
};
|
||||
|
||||
export type PublishEvent = {
|
||||
uri?: string;
|
||||
// Page name
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type LintEvent = {
|
||||
name: string;
|
||||
tree: ParseTree;
|
||||
};
|
||||
|
||||
export type CompleteEvent = {
|
||||
pageName: string;
|
||||
linePrefix: string;
|
||||
pos: number;
|
||||
parentNodes: string[];
|
||||
};
|
||||
|
||||
export type SlashCompletionOption = {
|
||||
label: string;
|
||||
detail?: string;
|
||||
invoke: string;
|
||||
order?: number;
|
||||
} & Record<string, any>;
|
||||
|
||||
export type SlashCompletions = {
|
||||
// Ignore this one, only for compatibility with regular completions
|
||||
from?: number;
|
||||
// The actual completions
|
||||
options: SlashCompletionOption[];
|
||||
};
|
||||
|
||||
export type WidgetContent = {
|
||||
html?: string;
|
||||
script?: string;
|
||||
markdown?: string;
|
||||
url?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
/** PageModifiedEvent payload for "editor:pageModified". Fired when the document text changes
|
||||
*/
|
||||
export type PageModifiedEvent = {
|
||||
changes: TextChange[];
|
||||
};
|
Loading…
Reference in New Issue