Redoing item and task indexing
parent
0c4838e45c
commit
8bc7dee75e
|
@ -1,11 +1,10 @@
|
|||
import { parse } from "./parse_tree.ts";
|
||||
import {
|
||||
collectNodesOfType,
|
||||
findNodeOfType,
|
||||
renderToText,
|
||||
} from "@silverbulletmd/silverbullet/lib/tree";
|
||||
import { assertEquals, assertNotEquals } from "@std/assert";
|
||||
import { extendedMarkdownLanguage } from "./parser.ts";
|
||||
import { assert, assertEquals, assertNotEquals } from "@std/assert";
|
||||
import { parseMarkdown } from "./parser.ts";
|
||||
|
||||
const sample1 = `---
|
||||
type: page
|
||||
|
@ -26,7 +25,7 @@ name: Zef
|
|||
Supper`;
|
||||
|
||||
Deno.test("Test parser", () => {
|
||||
let tree = parse(extendedMarkdownLanguage, sample1);
|
||||
let tree = parseMarkdown(sample1);
|
||||
// console.log("tree", JSON.stringify(tree, null, 2));
|
||||
// Check if rendering back to text works
|
||||
assertEquals(renderToText(tree), sample1);
|
||||
|
@ -44,7 +43,7 @@ Deno.test("Test parser", () => {
|
|||
// Find frontmatter
|
||||
let node = findNodeOfType(tree, "FrontMatter");
|
||||
assertNotEquals(node, undefined);
|
||||
tree = parse(extendedMarkdownLanguage, sampleInvalid1);
|
||||
tree = parseMarkdown(sampleInvalid1);
|
||||
node = findNodeOfType(tree, "FrontMatter");
|
||||
// console.log("Invalid node", node);
|
||||
assertEquals(node, undefined);
|
||||
|
@ -61,7 +60,7 @@ And one with nested brackets: [array: [1, 2, 3]]
|
|||
`;
|
||||
|
||||
Deno.test("Test inline attribute syntax", () => {
|
||||
const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
|
||||
const tree = parseMarkdown(inlineAttributeSample);
|
||||
// console.log("Attribute parsed", JSON.stringify(tree, null, 2));
|
||||
const attributes = collectNodesOfType(tree, "Attribute");
|
||||
let nameNode = findNodeOfType(attributes[0], "AttributeName");
|
||||
|
@ -81,11 +80,8 @@ Deno.test("Test inline attribute syntax", () => {
|
|||
});
|
||||
|
||||
Deno.test("Test template directive parsing", () => {
|
||||
const tree = parse(
|
||||
extendedMarkdownLanguage,
|
||||
"Simple {{name}} and {{count({ page })}}",
|
||||
);
|
||||
console.log("Template directive", JSON.stringify(tree, null, 2));
|
||||
const tree = parseMarkdown("Simple {{name}} and {{count({ page })}}");
|
||||
assert(findNodeOfType(tree, "TemplateDirective"));
|
||||
});
|
||||
|
||||
const multiStatusTaskExample = `
|
||||
|
@ -95,7 +91,7 @@ const multiStatusTaskExample = `
|
|||
`;
|
||||
|
||||
Deno.test("Test multi-status tasks", () => {
|
||||
const tree = parse(extendedMarkdownLanguage, multiStatusTaskExample);
|
||||
const tree = parseMarkdown(multiStatusTaskExample);
|
||||
// console.log("Tasks parsed", JSON.stringify(tree, null, 2));
|
||||
const tasks = collectNodesOfType(tree, "Task");
|
||||
assertEquals(tasks.length, 3);
|
||||
|
@ -112,7 +108,7 @@ const commandLinkSample = `
|
|||
`;
|
||||
|
||||
Deno.test("Test command links", () => {
|
||||
const tree = parse(extendedMarkdownLanguage, commandLinkSample);
|
||||
const tree = parseMarkdown(commandLinkSample);
|
||||
const commands = collectNodesOfType(tree, "CommandLink");
|
||||
// console.log("Command links parsed", JSON.stringify(commands, null, 2));
|
||||
assertEquals(commands.length, 3);
|
||||
|
@ -129,7 +125,7 @@ const commandLinkArgsSample = `
|
|||
`;
|
||||
|
||||
Deno.test("Test command link arguments", () => {
|
||||
const tree = parse(extendedMarkdownLanguage, commandLinkArgsSample);
|
||||
const tree = parseMarkdown(commandLinkArgsSample);
|
||||
const commands = collectNodesOfType(tree, "CommandLink");
|
||||
assertEquals(commands.length, 2);
|
||||
|
||||
|
@ -142,22 +138,22 @@ Deno.test("Test command link arguments", () => {
|
|||
|
||||
Deno.test("Test directive parser", () => {
|
||||
const simpleExample = `Simple {{.}}`;
|
||||
let tree = parse(extendedMarkdownLanguage, simpleExample);
|
||||
let tree = parseMarkdown(simpleExample);
|
||||
assertEquals(renderToText(tree), simpleExample);
|
||||
|
||||
const eachExample = `{{#each .}}Sup{{/each}}`;
|
||||
tree = parse(extendedMarkdownLanguage, eachExample);
|
||||
tree = parseMarkdown(eachExample);
|
||||
|
||||
const ifExample = `{{#if true}}Sup{{/if}}`;
|
||||
tree = parse(extendedMarkdownLanguage, ifExample);
|
||||
tree = parseMarkdown(ifExample);
|
||||
assertEquals(renderToText(tree), ifExample);
|
||||
|
||||
const ifElseExample = `{{#if true}}Sup{{else}}Sup2{{/if}}`;
|
||||
tree = parse(extendedMarkdownLanguage, ifElseExample);
|
||||
tree = parseMarkdown(ifElseExample);
|
||||
assertEquals(renderToText(tree), ifElseExample);
|
||||
console.log("Final tree", JSON.stringify(tree, null, 2));
|
||||
// console.log("Final tree", JSON.stringify(tree, null, 2));
|
||||
|
||||
const letExample = `{{#let @p = true}}{{/let}}`;
|
||||
tree = parse(extendedMarkdownLanguage, letExample);
|
||||
tree = parseMarkdown(letExample);
|
||||
assertEquals(renderToText(tree), letExample);
|
||||
});
|
||||
|
|
|
@ -541,6 +541,8 @@ const NamedAnchor = regexParser({
|
|||
import { Table } from "./table_parser.ts";
|
||||
import { foldNodeProp } from "@codemirror/language";
|
||||
import { pWikiLinkRegex, tagRegex } from "$common/markdown_parser/constants.ts";
|
||||
import { parse } from "$common/markdown_parser/parse_tree.ts";
|
||||
import type { ParseTree } from "@silverbulletmd/silverbullet/lib/tree";
|
||||
|
||||
// FrontMatter parser
|
||||
|
||||
|
@ -661,3 +663,7 @@ export const extendedMarkdownLanguage = markdown({
|
|||
},
|
||||
],
|
||||
}).language;
|
||||
|
||||
export function parseMarkdown(text: string): ParseTree {
|
||||
return parse(extendedMarkdownLanguage, text);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import "./syscall_mock.ts";
|
||||
import { parse } from "$common/markdown_parser/parse_tree.ts";
|
||||
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
import {
|
||||
cleanAttributes,
|
||||
extractAttributes,
|
||||
} from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
import { assertEquals } from "@std/assert";
|
||||
import { renderToText } from "./tree.ts";
|
||||
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
|
||||
|
@ -19,22 +22,22 @@ const cleanedInlineAttributeSample = `
|
|||
# My document
|
||||
Top level attributes:
|
||||
|
||||
* [ ] Attribute in a task [tag:: foo]
|
||||
* Regular item [tag:: bar]
|
||||
* [ ] Attribute in a task
|
||||
* Regular item
|
||||
|
||||
1. Itemized list [tag:: baz]
|
||||
1. Itemized list
|
||||
`;
|
||||
|
||||
Deno.test("Test attribute extraction", async () => {
|
||||
const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
|
||||
const toplevelAttributes = await extractAttributes(["test"], tree, false);
|
||||
const toplevelAttributes = await extractAttributes(["test"], tree);
|
||||
// console.log("All attributes", toplevelAttributes);
|
||||
assertEquals(toplevelAttributes.name, "sup");
|
||||
assertEquals(toplevelAttributes.age, 42);
|
||||
assertEquals(toplevelAttributes.children, ["pete", "john", "mary"]);
|
||||
// Check if the attributes are still there
|
||||
assertEquals(renderToText(tree), inlineAttributeSample);
|
||||
// Now once again with cleaning
|
||||
await extractAttributes(["test"], tree, true);
|
||||
// And now clean
|
||||
cleanAttributes(tree);
|
||||
assertEquals(renderToText(tree), cleanedInlineAttributeSample);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,8 @@ import {
|
|||
findNodeOfType,
|
||||
type ParseTree,
|
||||
renderToText,
|
||||
replaceNodesMatchingAsync,
|
||||
replaceNodesMatching,
|
||||
traverseTreeAsync,
|
||||
} from "./tree.ts";
|
||||
|
||||
import { cleanupJSON } from "@silverbulletmd/silverbullet/lib/json";
|
||||
|
@ -10,21 +11,19 @@ import { cleanupJSON } from "@silverbulletmd/silverbullet/lib/json";
|
|||
import { system, YAML } from "../syscalls.ts";
|
||||
|
||||
/**
|
||||
* Extracts attributes from a tree, optionally cleaning them out of the tree.
|
||||
* Extracts attributes from a tree
|
||||
* @param tree tree to extract attributes from
|
||||
* @param clean whether or not to clean out the attributes from the tree
|
||||
* @returns mapping from attribute name to attribute value
|
||||
*/
|
||||
export async function extractAttributes(
|
||||
tags: string[],
|
||||
tree: ParseTree,
|
||||
clean: boolean,
|
||||
): Promise<Record<string, any>> {
|
||||
let attributes: Record<string, any> = {};
|
||||
await replaceNodesMatchingAsync(tree, async (n) => {
|
||||
if (n.type === "ListItem") {
|
||||
await traverseTreeAsync(tree, async (n) => {
|
||||
if (tree !== n && n.type === "ListItem") {
|
||||
// Find top-level only, no nested lists
|
||||
return n;
|
||||
return true;
|
||||
}
|
||||
if (n.type === "Attribute") {
|
||||
const nameNode = findNodeOfType(n, "AttributeName");
|
||||
|
@ -38,15 +37,10 @@ export async function extractAttributes(
|
|||
console.error("Error parsing attribute value as YAML", val, e);
|
||||
}
|
||||
}
|
||||
// Remove from tree
|
||||
if (clean) {
|
||||
return null;
|
||||
} else {
|
||||
return n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Go on...
|
||||
return undefined;
|
||||
return false;
|
||||
});
|
||||
const text = renderToText(tree);
|
||||
const spaceScriptAttributes = await system.applyAttributeExtractors(
|
||||
|
@ -60,3 +54,16 @@ export async function extractAttributes(
|
|||
};
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans attributes from a tree (as a side effect)
|
||||
* @param tree to clean attributes from
|
||||
*/
|
||||
export function cleanAttributes(tree: ParseTree) {
|
||||
replaceNodesMatching(tree, (n) => {
|
||||
if (n.type === "Attribute") {
|
||||
return null;
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,7 +9,10 @@ import {
|
|||
* Feed parsing functionality (WIP)
|
||||
*/
|
||||
|
||||
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
import {
|
||||
cleanAttributes,
|
||||
extractAttributes,
|
||||
} from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
|
||||
export type FeedItem = {
|
||||
id: string;
|
||||
|
@ -29,7 +32,6 @@ export async function extractFeedItems(tree: ParseTree): Promise<FeedItem[]> {
|
|||
for (const node of tree.children!) {
|
||||
if (node.type === "FrontMatter") {
|
||||
// Not interested
|
||||
console.log("Ignoring", node);
|
||||
continue;
|
||||
}
|
||||
if (node.type === "HorizontalRule") {
|
||||
|
@ -51,7 +53,8 @@ async function nodesToFeedItem(nodes: ParseTree[]): Promise<FeedItem> {
|
|||
const wrapperNode: ParseTree = {
|
||||
children: nodes,
|
||||
};
|
||||
const attributes = await extractAttributes(["feed"], wrapperNode, true);
|
||||
const attributes = await extractAttributes(["feed"], wrapperNode);
|
||||
cleanAttributes(wrapperNode);
|
||||
let id = attributes.id;
|
||||
delete attributes.id;
|
||||
if (!id) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { assertEquals } from "@std/assert";
|
||||
import { cleanupJSON, deepEqual, deepObjectMerge } from "./json.ts";
|
||||
import { cleanupJSON, deepClone, deepEqual, deepObjectMerge } from "./json.ts";
|
||||
|
||||
Deno.test("JSON utils", () => {
|
||||
assertEquals(deepEqual({ a: 1 }, { a: 1 }), true);
|
||||
|
@ -25,3 +25,30 @@ Deno.test("JSON utils", () => {
|
|||
|
||||
assertEquals(cleanupJSON(new Date("2023-05-03T00:00:00Z")), "2023-05-03");
|
||||
});
|
||||
|
||||
Deno.test("JSON utils - deepObjectMerge", () => {
|
||||
// Tests for deepClone
|
||||
const obj1 = { a: 1, b: { c: 2, d: [3, 4] }, e: new Date("2023-08-21") };
|
||||
const clone1 = deepClone(obj1);
|
||||
assertEquals(clone1, obj1);
|
||||
assertEquals(clone1 === obj1, false); // Ensuring deep clone, not shallow
|
||||
assertEquals(clone1.b === obj1.b, false); // Nested object should be different reference
|
||||
assertEquals(clone1.e === obj1.e, false); // Date object should be different reference
|
||||
|
||||
const arrayTest = [1, 2, { a: 3, b: [4, 5] }];
|
||||
const cloneArray = deepClone(arrayTest);
|
||||
assertEquals(cloneArray, arrayTest);
|
||||
assertEquals(cloneArray === arrayTest, false); // Array itself should be different reference
|
||||
assertEquals(cloneArray[2] === arrayTest[2], false); // Nested object in array should be different reference
|
||||
|
||||
const nullTest = { a: null, b: undefined, c: { d: null } };
|
||||
const cloneNullTest = deepClone(nullTest);
|
||||
assertEquals(cloneNullTest, nullTest);
|
||||
assertEquals(cloneNullTest === nullTest, false); // Ensure it's a deep clone
|
||||
assertEquals(cloneNullTest.c === nullTest.c, false); // Nested object should be different reference
|
||||
|
||||
const dateTest = new Date();
|
||||
const cloneDateTest = deepClone(dateTest);
|
||||
assertEquals(cloneDateTest.getTime(), dateTest.getTime());
|
||||
assertEquals(cloneDateTest === dateTest, false); // Date instance should be different reference
|
||||
});
|
||||
|
|
|
@ -139,3 +139,39 @@ export function deepObjectMerge(a: any, b: any, reverseArrays = false): any {
|
|||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
export function deepClone<T>(obj: T, ignoreKeys: string[] = []): T {
|
||||
// Handle null, undefined, or primitive types (string, number, boolean, symbol, bigint)
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Handle Date
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as any;
|
||||
}
|
||||
|
||||
// Handle Array
|
||||
if (Array.isArray(obj)) {
|
||||
const arrClone: any[] = [];
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
arrClone[i] = deepClone(obj[i], ignoreKeys);
|
||||
}
|
||||
return arrClone as any;
|
||||
}
|
||||
|
||||
// Handle Object
|
||||
if (obj instanceof Object) {
|
||||
const objClone: { [key: string]: any } = {};
|
||||
for (const key in obj) {
|
||||
if (ignoreKeys.includes(key)) {
|
||||
objClone[key] = obj[key];
|
||||
} else if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
objClone[key] = deepClone(obj[key], ignoreKeys);
|
||||
}
|
||||
}
|
||||
return objClone as T;
|
||||
}
|
||||
|
||||
throw new Error("Unsupported data type.");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import type { FrontMatter } from "./frontmatter.ts";
|
||||
import type { ObjectValue } from "../types.ts";
|
||||
import {
|
||||
type ParseTree,
|
||||
traverseTree,
|
||||
} from "@silverbulletmd/silverbullet/lib/tree";
|
||||
|
||||
export function updateITags<T>(obj: ObjectValue<T>, frontmatter: FrontMatter) {
|
||||
const itags = [obj.tag, ...frontmatter.tags || []];
|
||||
|
@ -12,3 +16,37 @@ export function updateITags<T>(obj: ObjectValue<T>, frontmatter: FrontMatter) {
|
|||
}
|
||||
obj.itags = itags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a set of hashtags from a tree
|
||||
* @param n the tree to extract from
|
||||
* @returns
|
||||
*/
|
||||
export function extractHashTags(n: ParseTree): string[] {
|
||||
const tags = new Set<string>();
|
||||
traverseTree(n, (n) => {
|
||||
if (n.type === "Hashtag") {
|
||||
tags.add(n.children![0].text!.substring(1));
|
||||
return true;
|
||||
} else if (n.type === "OrderedList" || n.type === "BulletList") {
|
||||
// Don't traverse into sub-lists
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return [...tags];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans hashtags from a tree as a side effect
|
||||
* @param n
|
||||
*/
|
||||
export function cleanHashTags(n: ParseTree) {
|
||||
traverseTree(n, (n) => {
|
||||
if (n.type === "Hashtag") {
|
||||
n.children = [];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ export async function indexHeaders({ name: pageName, tree }: IndexTreeEvent) {
|
|||
const extractedAttributes = await extractAttributes(
|
||||
["header", ...tags],
|
||||
n,
|
||||
true,
|
||||
);
|
||||
const name = n.children!.slice(1).map(renderToText).join("").trim();
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import "../../plug-api/lib/syscall_mock.ts";
|
||||
import { parseMarkdown } from "$common/markdown_parser/parser.ts";
|
||||
import { extractItems } from "./item.ts";
|
||||
import { assertEquals } from "@std/assert";
|
||||
|
||||
const itemsMd = `
|
||||
* Item 1 #tag1 #tag2 [age: 100]
|
||||
* Item 1.1 #tag3 #tag1
|
||||
* Item 1.1.1
|
||||
`;
|
||||
|
||||
Deno.test("Test item extraction", async () => {
|
||||
const t = parseMarkdown(itemsMd);
|
||||
const items = await extractItems("test", t);
|
||||
|
||||
assertEquals(items[0].name, "Item 1");
|
||||
assertEquals(items[0].age, 100);
|
||||
assertEquals(items[0].page, "test");
|
||||
assertEquals(items[0].parent, undefined);
|
||||
assertEquals(items[0].text, "Item 1 #tag1 #tag2 [age: 100]");
|
||||
assertEquals(new Set(items[0].tags), new Set(["tag1", "tag2"]));
|
||||
assertEquals(new Set(items[0].itags), new Set(["item", "tag1", "tag2"]));
|
||||
|
||||
assertEquals(items[1].name, "Item 1.1");
|
||||
assertEquals(new Set(items[1].tags), new Set(["tag3", "tag1"]));
|
||||
assertEquals(
|
||||
new Set(items[1].itags),
|
||||
new Set(["tag3", "tag2", "tag1", "item"]),
|
||||
);
|
||||
assertEquals(items[1].parent, items[0].ref);
|
||||
|
||||
assertEquals(items[2].parent, items[1].ref);
|
||||
});
|
|
@ -1,16 +1,27 @@
|
|||
import type { IndexTreeEvent } from "../../plug-api/types.ts";
|
||||
import type { IndexTreeEvent, ObjectValue } from "../../plug-api/types.ts";
|
||||
|
||||
import {
|
||||
collectNodesOfType,
|
||||
findParentMatching,
|
||||
type ParseTree,
|
||||
renderToText,
|
||||
} from "../../plug-api/lib/tree.ts";
|
||||
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
traverseTreeAsync,
|
||||
} from "@silverbulletmd/silverbullet/lib/tree";
|
||||
import {
|
||||
cleanAttributes,
|
||||
extractAttributes,
|
||||
} from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
import { rewritePageRefs } from "@silverbulletmd/silverbullet/lib/resolve";
|
||||
import type { ObjectValue } from "../../plug-api/types.ts";
|
||||
import { indexObjects } from "./api.ts";
|
||||
import { updateITags } from "@silverbulletmd/silverbullet/lib/tags";
|
||||
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
|
||||
import {
|
||||
cleanHashTags,
|
||||
extractHashTags,
|
||||
updateITags,
|
||||
} from "@silverbulletmd/silverbullet/lib/tags";
|
||||
import {
|
||||
extractFrontmatter,
|
||||
type FrontMatter,
|
||||
} from "@silverbulletmd/silverbullet/lib/frontmatter";
|
||||
import { deepClone } from "@silverbulletmd/silverbullet/lib/json";
|
||||
|
||||
export type ItemObject = ObjectValue<
|
||||
{
|
||||
|
@ -22,73 +33,127 @@ export type ItemObject = ObjectValue<
|
|||
>;
|
||||
|
||||
export async function indexItems({ name, tree }: IndexTreeEvent) {
|
||||
const items = await extractItems(name, tree);
|
||||
console.log("Found", items, "item(s)");
|
||||
await indexObjects(name, items);
|
||||
}
|
||||
|
||||
export async function extractItems(name: string, tree: ParseTree) {
|
||||
const items: ObjectValue<ItemObject>[] = [];
|
||||
|
||||
const frontmatter = await extractFrontmatter(tree);
|
||||
|
||||
const coll = collectNodesOfType(tree, "ListItem");
|
||||
await traverseTreeAsync(tree, async (n) => {
|
||||
if (n.type !== "ListItem") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const n of coll) {
|
||||
if (!n.children) {
|
||||
continue;
|
||||
}
|
||||
if (collectNodesOfType(n, "Task").length > 0) {
|
||||
// This is a task item, skip it
|
||||
continue;
|
||||
// Weird, let's jump out
|
||||
return true;
|
||||
}
|
||||
|
||||
const tags = new Set<string>();
|
||||
const item: ItemObject = await extractItemFromNode(
|
||||
name,
|
||||
n,
|
||||
frontmatter,
|
||||
);
|
||||
|
||||
items.push(item);
|
||||
|
||||
return false;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function extractItemFromNode(
|
||||
name: string,
|
||||
itemNode: ParseTree,
|
||||
frontmatter: FrontMatter,
|
||||
) {
|
||||
const item: ItemObject = {
|
||||
ref: `${name}@${n.from}`,
|
||||
ref: `${name}@${itemNode.from}`,
|
||||
tag: "item",
|
||||
name: "",
|
||||
text: "",
|
||||
page: name,
|
||||
pos: n.from!,
|
||||
pos: itemNode.from!,
|
||||
};
|
||||
|
||||
const textNodes: ParseTree[] = [];
|
||||
|
||||
const fullText = renderToText(n);
|
||||
|
||||
collectNodesOfType(n, "Hashtag").forEach((h) => {
|
||||
// Push tag to the list, removing the initial #
|
||||
tags.add(h.children![0].text!.substring(1));
|
||||
h.children = [];
|
||||
});
|
||||
|
||||
// Extract attributes and remove from tree
|
||||
// Now let's extract tags and attributes
|
||||
const tags = extractHashTags(itemNode);
|
||||
const extractedAttributes = await extractAttributes(
|
||||
["item", ...tags],
|
||||
n,
|
||||
true,
|
||||
itemNode,
|
||||
);
|
||||
|
||||
for (const child of n.children!.slice(1)) {
|
||||
const clonedTextNodes: ParseTree[] = [];
|
||||
|
||||
for (const child of itemNode.children!.slice(1)) {
|
||||
rewritePageRefs(child, name);
|
||||
|
||||
if (child.type === "OrderedList" || child.type === "BulletList") {
|
||||
break;
|
||||
}
|
||||
textNodes.push(child);
|
||||
clonedTextNodes.push(deepClone(child, ["parent"]));
|
||||
}
|
||||
|
||||
item.name = textNodes.map(renderToText).join("").trim();
|
||||
item.text = fullText;
|
||||
// Original text
|
||||
item.text = clonedTextNodes.map(renderToText).join("").trim();
|
||||
|
||||
if (tags.size > 0) {
|
||||
item.tags = [...tags];
|
||||
// Clean out attribtus and tags and render a clean item name
|
||||
for (const clonedTextNode of clonedTextNodes) {
|
||||
cleanHashTags(clonedTextNode);
|
||||
cleanAttributes(clonedTextNode);
|
||||
}
|
||||
|
||||
for (
|
||||
const [key, value] of Object.entries(extractedAttributes)
|
||||
) {
|
||||
item.name = clonedTextNodes.map(renderToText).join("").trim();
|
||||
|
||||
if (tags.length > 0) {
|
||||
item.tags = tags;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(extractedAttributes)) {
|
||||
item[key] = value;
|
||||
}
|
||||
|
||||
updateITags(item, frontmatter);
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
// console.log("Found", items, "item(s)");
|
||||
await indexObjects(name, items);
|
||||
await enrichItemFromParents(itemNode, item, name, frontmatter);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export async function enrichItemFromParents(
|
||||
n: ParseTree,
|
||||
item: ObjectValue<any>,
|
||||
pageName: string,
|
||||
frontmatter: FrontMatter,
|
||||
) {
|
||||
let directParent = true;
|
||||
let parentItemNode = findParentMatching(n, (n) => n.type === "ListItem");
|
||||
while (parentItemNode) {
|
||||
// console.log("Got parent", parentItemNode);
|
||||
const parentItem = await extractItemFromNode(
|
||||
pageName,
|
||||
parentItemNode,
|
||||
frontmatter,
|
||||
);
|
||||
if (directParent) {
|
||||
item.parent = parentItem.ref;
|
||||
directParent = false;
|
||||
}
|
||||
// Merge tags
|
||||
item.itags = [
|
||||
...new Set([
|
||||
...item.itags || [],
|
||||
...(parentItem.itags!.filter((t) => !["item", "task"].includes(t))),
|
||||
]),
|
||||
];
|
||||
|
||||
parentItemNode = findParentMatching(
|
||||
parentItemNode,
|
||||
(n) => n.type === "ListItem",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ export async function indexPage({ name, tree }: IndexTreeEvent) {
|
|||
const toplevelAttributes = await extractAttributes(
|
||||
["page", ...frontmatter.tags || []],
|
||||
tree,
|
||||
false,
|
||||
);
|
||||
|
||||
// Push them all into the page object
|
||||
|
|
|
@ -46,7 +46,7 @@ export async function indexParagraphs({ name: page, tree }: IndexTreeEvent) {
|
|||
});
|
||||
|
||||
// Extract attributes and remove from tree
|
||||
const attrs = await extractAttributes(["paragraph", ...tags], p, true);
|
||||
const attrs = await extractAttributes(["paragraph", ...tags], p);
|
||||
const text = renderToText(p);
|
||||
|
||||
if (!text.trim()) {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import "../../plug-api/lib/syscall_mock.ts";
|
||||
import { parseMarkdown } from "$common/markdown_parser/parser.ts";
|
||||
import { extractTasks } from "./task.ts";
|
||||
import { extractItems } from "../index/item.ts";
|
||||
import { assertEquals } from "@std/assert";
|
||||
|
||||
const itemsMd = `
|
||||
* Item 1 #tag1 #tag2 [age: 100]
|
||||
* [ ] Task 1 [age: 200]
|
||||
* [ ] Task 2 #tag3 #tag1
|
||||
* [x] Task 2.1
|
||||
`;
|
||||
|
||||
Deno.test("Test task extraction", async () => {
|
||||
const t = parseMarkdown(itemsMd);
|
||||
const tasks = await extractTasks("test", t);
|
||||
const items = await extractItems("test", t);
|
||||
|
||||
// Tasks are also indexed as items, because they are
|
||||
assertEquals(items.length, 4);
|
||||
|
||||
assertEquals(tasks.length, 3);
|
||||
assertEquals(tasks[0].name, "Task 1");
|
||||
assertEquals(tasks[0].age, 200);
|
||||
assertEquals(tasks[0].page, "test");
|
||||
assertEquals(tasks[0].text, "Task 1 [age: 200]");
|
||||
assertEquals(new Set(tasks[0].itags), new Set(["tag1", "tag2", "task"]));
|
||||
assertEquals(tasks[0].parent, "test@1");
|
||||
assertEquals(tasks[1].name, "Task 2");
|
||||
// Don't inherit attributes
|
||||
assertEquals(tasks[1].age, undefined);
|
||||
// But inherit tags through itags, not tags
|
||||
assertEquals(
|
||||
new Set(tasks[1].tags),
|
||||
new Set(["tag1", "tag3"]),
|
||||
);
|
||||
assertEquals(
|
||||
new Set(tasks[1].itags),
|
||||
new Set(["tag1", "tag3", "task", "tag2"]),
|
||||
);
|
||||
assertEquals(tasks[1].parent, "test@1");
|
||||
// Deeply
|
||||
assertEquals(tasks[2].name, "Task 2.1");
|
||||
assertEquals(tasks[2].tags, []);
|
||||
// Parent is * [ ] Task 2 #tag3 #tag1 list item
|
||||
assertEquals(tasks[2].parent, items[2].ref);
|
||||
assertEquals(
|
||||
new Set(tasks[2].itags),
|
||||
new Set(["tag1", "tag3", "task", "tag2"]),
|
||||
);
|
||||
});
|
|
@ -21,16 +21,25 @@ import {
|
|||
traverseTreeAsync,
|
||||
} from "../../plug-api/lib/tree.ts";
|
||||
import { niceDate } from "$lib/dates.ts";
|
||||
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
import {
|
||||
cleanAttributes,
|
||||
extractAttributes,
|
||||
} from "@silverbulletmd/silverbullet/lib/attribute";
|
||||
import { rewritePageRefs } from "@silverbulletmd/silverbullet/lib/resolve";
|
||||
import type { ObjectValue } from "../../plug-api/types.ts";
|
||||
import { indexObjects, queryObjects } from "../index/plug_api.ts";
|
||||
import { updateITags } from "@silverbulletmd/silverbullet/lib/tags";
|
||||
import {
|
||||
cleanHashTags,
|
||||
extractHashTags,
|
||||
updateITags,
|
||||
} from "@silverbulletmd/silverbullet/lib/tags";
|
||||
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
|
||||
import {
|
||||
parsePageRef,
|
||||
positionOfLine,
|
||||
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
||||
import { enrichItemFromParents } from "../index/item.ts";
|
||||
import { deepClone } from "@silverbulletmd/silverbullet/lib/json";
|
||||
|
||||
export type TaskObject = ObjectValue<
|
||||
{
|
||||
|
@ -57,7 +66,10 @@ function getDeadline(deadlineNode: ParseTree): string {
|
|||
const completeStates = ["x", "X"];
|
||||
const incompleteStates = [" "];
|
||||
|
||||
export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
||||
export async function extractTasks(
|
||||
name: string,
|
||||
tree: ParseTree,
|
||||
): Promise<TaskObject[]> {
|
||||
const tasks: ObjectValue<TaskObject>[] = [];
|
||||
const taskStates = new Map<string, { count: number; firstPos: number }>();
|
||||
const frontmatter = await extractFrontmatter(tree);
|
||||
|
@ -66,6 +78,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
|||
if (n.type !== "Task") {
|
||||
return false;
|
||||
}
|
||||
const listItemNode = n.parent!;
|
||||
const state = n.children![0].children![1].text!;
|
||||
if (!incompleteStates.includes(state) && !completeStates.includes(state)) {
|
||||
let currentState = taskStates.get(state);
|
||||
|
@ -76,6 +89,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
|||
currentState.count++;
|
||||
}
|
||||
const complete = completeStates.includes(state);
|
||||
|
||||
const task: TaskObject = {
|
||||
ref: `${name}@${n.from}`,
|
||||
tag: "task",
|
||||
|
@ -99,30 +113,27 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
|||
// Remove this node from the tree
|
||||
return null;
|
||||
}
|
||||
if (tree.type === "Hashtag") {
|
||||
// Push the tag to the list, removing the initial #
|
||||
const tagName = tree.children![0].text!.substring(1);
|
||||
if (!task.tags) {
|
||||
task.tags = [];
|
||||
}
|
||||
task.tags.push(tagName);
|
||||
tree.children = [];
|
||||
}
|
||||
});
|
||||
|
||||
// Extract attributes and remove from tree
|
||||
// Extract tags and attributes
|
||||
task.tags = extractHashTags(n);
|
||||
const extractedAttributes = await extractAttributes(
|
||||
["task", ...task.tags || []],
|
||||
n,
|
||||
true,
|
||||
);
|
||||
task.name = n.children!.slice(1).map(renderToText).join("").trim();
|
||||
|
||||
// Then clean them out
|
||||
const clonedNode = deepClone(n, ["parent"]);
|
||||
cleanHashTags(clonedNode);
|
||||
cleanAttributes(clonedNode);
|
||||
task.name = clonedNode.children!.slice(1).map(renderToText).join("").trim();
|
||||
|
||||
for (const [key, value] of Object.entries(extractedAttributes)) {
|
||||
task[key] = value;
|
||||
}
|
||||
|
||||
updateITags(task, frontmatter);
|
||||
await enrichItemFromParents(listItemNode, task, name, frontmatter);
|
||||
|
||||
tasks.push(task);
|
||||
return true;
|
||||
|
@ -141,10 +152,15 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
|||
})),
|
||||
);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
||||
const extractedTasks = await extractTasks(name, tree);
|
||||
|
||||
// Index tasks themselves
|
||||
if (tasks.length > 0) {
|
||||
await indexObjects(name, tasks);
|
||||
if (extractTasks.length > 0) {
|
||||
await indexObjects(name, extractedTasks);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue