Redoing item and task indexing

item-task-indexers
Zef Hemel 2024-08-21 20:13:40 +02:00
parent 0c4838e45c
commit 8bc7dee75e
15 changed files with 408 additions and 129 deletions

View File

@ -1,11 +1,10 @@
import { parse } from "./parse_tree.ts";
import { import {
collectNodesOfType, collectNodesOfType,
findNodeOfType, findNodeOfType,
renderToText, renderToText,
} from "@silverbulletmd/silverbullet/lib/tree"; } from "@silverbulletmd/silverbullet/lib/tree";
import { assertEquals, assertNotEquals } from "@std/assert"; import { assert, assertEquals, assertNotEquals } from "@std/assert";
import { extendedMarkdownLanguage } from "./parser.ts"; import { parseMarkdown } from "./parser.ts";
const sample1 = `--- const sample1 = `---
type: page type: page
@ -26,7 +25,7 @@ name: Zef
Supper`; Supper`;
Deno.test("Test parser", () => { Deno.test("Test parser", () => {
let tree = parse(extendedMarkdownLanguage, sample1); let tree = parseMarkdown(sample1);
// console.log("tree", JSON.stringify(tree, null, 2)); // console.log("tree", JSON.stringify(tree, null, 2));
// Check if rendering back to text works // Check if rendering back to text works
assertEquals(renderToText(tree), sample1); assertEquals(renderToText(tree), sample1);
@ -44,7 +43,7 @@ Deno.test("Test parser", () => {
// Find frontmatter // Find frontmatter
let node = findNodeOfType(tree, "FrontMatter"); let node = findNodeOfType(tree, "FrontMatter");
assertNotEquals(node, undefined); assertNotEquals(node, undefined);
tree = parse(extendedMarkdownLanguage, sampleInvalid1); tree = parseMarkdown(sampleInvalid1);
node = findNodeOfType(tree, "FrontMatter"); node = findNodeOfType(tree, "FrontMatter");
// console.log("Invalid node", node); // console.log("Invalid node", node);
assertEquals(node, undefined); assertEquals(node, undefined);
@ -61,7 +60,7 @@ And one with nested brackets: [array: [1, 2, 3]]
`; `;
Deno.test("Test inline attribute syntax", () => { Deno.test("Test inline attribute syntax", () => {
const tree = parse(extendedMarkdownLanguage, inlineAttributeSample); const tree = parseMarkdown(inlineAttributeSample);
// console.log("Attribute parsed", JSON.stringify(tree, null, 2)); // console.log("Attribute parsed", JSON.stringify(tree, null, 2));
const attributes = collectNodesOfType(tree, "Attribute"); const attributes = collectNodesOfType(tree, "Attribute");
let nameNode = findNodeOfType(attributes[0], "AttributeName"); let nameNode = findNodeOfType(attributes[0], "AttributeName");
@ -81,11 +80,8 @@ Deno.test("Test inline attribute syntax", () => {
}); });
Deno.test("Test template directive parsing", () => { Deno.test("Test template directive parsing", () => {
const tree = parse( const tree = parseMarkdown("Simple {{name}} and {{count({ page })}}");
extendedMarkdownLanguage, assert(findNodeOfType(tree, "TemplateDirective"));
"Simple {{name}} and {{count({ page })}}",
);
console.log("Template directive", JSON.stringify(tree, null, 2));
}); });
const multiStatusTaskExample = ` const multiStatusTaskExample = `
@ -95,7 +91,7 @@ const multiStatusTaskExample = `
`; `;
Deno.test("Test multi-status tasks", () => { Deno.test("Test multi-status tasks", () => {
const tree = parse(extendedMarkdownLanguage, multiStatusTaskExample); const tree = parseMarkdown(multiStatusTaskExample);
// console.log("Tasks parsed", JSON.stringify(tree, null, 2)); // console.log("Tasks parsed", JSON.stringify(tree, null, 2));
const tasks = collectNodesOfType(tree, "Task"); const tasks = collectNodesOfType(tree, "Task");
assertEquals(tasks.length, 3); assertEquals(tasks.length, 3);
@ -112,7 +108,7 @@ const commandLinkSample = `
`; `;
Deno.test("Test command links", () => { Deno.test("Test command links", () => {
const tree = parse(extendedMarkdownLanguage, commandLinkSample); const tree = parseMarkdown(commandLinkSample);
const commands = collectNodesOfType(tree, "CommandLink"); const commands = collectNodesOfType(tree, "CommandLink");
// console.log("Command links parsed", JSON.stringify(commands, null, 2)); // console.log("Command links parsed", JSON.stringify(commands, null, 2));
assertEquals(commands.length, 3); assertEquals(commands.length, 3);
@ -129,7 +125,7 @@ const commandLinkArgsSample = `
`; `;
Deno.test("Test command link arguments", () => { Deno.test("Test command link arguments", () => {
const tree = parse(extendedMarkdownLanguage, commandLinkArgsSample); const tree = parseMarkdown(commandLinkArgsSample);
const commands = collectNodesOfType(tree, "CommandLink"); const commands = collectNodesOfType(tree, "CommandLink");
assertEquals(commands.length, 2); assertEquals(commands.length, 2);
@ -142,22 +138,22 @@ Deno.test("Test command link arguments", () => {
Deno.test("Test directive parser", () => { Deno.test("Test directive parser", () => {
const simpleExample = `Simple {{.}}`; const simpleExample = `Simple {{.}}`;
let tree = parse(extendedMarkdownLanguage, simpleExample); let tree = parseMarkdown(simpleExample);
assertEquals(renderToText(tree), simpleExample); assertEquals(renderToText(tree), simpleExample);
const eachExample = `{{#each .}}Sup{{/each}}`; const eachExample = `{{#each .}}Sup{{/each}}`;
tree = parse(extendedMarkdownLanguage, eachExample); tree = parseMarkdown(eachExample);
const ifExample = `{{#if true}}Sup{{/if}}`; const ifExample = `{{#if true}}Sup{{/if}}`;
tree = parse(extendedMarkdownLanguage, ifExample); tree = parseMarkdown(ifExample);
assertEquals(renderToText(tree), ifExample); assertEquals(renderToText(tree), ifExample);
const ifElseExample = `{{#if true}}Sup{{else}}Sup2{{/if}}`; const ifElseExample = `{{#if true}}Sup{{else}}Sup2{{/if}}`;
tree = parse(extendedMarkdownLanguage, ifElseExample); tree = parseMarkdown(ifElseExample);
assertEquals(renderToText(tree), 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}}`; const letExample = `{{#let @p = true}}{{/let}}`;
tree = parse(extendedMarkdownLanguage, letExample); tree = parseMarkdown(letExample);
assertEquals(renderToText(tree), letExample); assertEquals(renderToText(tree), letExample);
}); });

View File

@ -541,6 +541,8 @@ const NamedAnchor = regexParser({
import { Table } from "./table_parser.ts"; import { Table } from "./table_parser.ts";
import { foldNodeProp } from "@codemirror/language"; import { foldNodeProp } from "@codemirror/language";
import { pWikiLinkRegex, tagRegex } from "$common/markdown_parser/constants.ts"; 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 // FrontMatter parser
@ -661,3 +663,7 @@ export const extendedMarkdownLanguage = markdown({
}, },
], ],
}).language; }).language;
export function parseMarkdown(text: string): ParseTree {
return parse(extendedMarkdownLanguage, text);
}

View File

@ -1,6 +1,9 @@
import "./syscall_mock.ts"; import "./syscall_mock.ts";
import { parse } from "$common/markdown_parser/parse_tree.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 { assertEquals } from "@std/assert";
import { renderToText } from "./tree.ts"; import { renderToText } from "./tree.ts";
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts"; import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
@ -19,22 +22,22 @@ const cleanedInlineAttributeSample = `
# My document # My document
Top level attributes: Top level attributes:
* [ ] Attribute in a task [tag:: foo] * [ ] Attribute in a task
* Regular item [tag:: bar] * Regular item
1. Itemized list [tag:: baz] 1. Itemized list
`; `;
Deno.test("Test attribute extraction", async () => { Deno.test("Test attribute extraction", async () => {
const tree = parse(extendedMarkdownLanguage, inlineAttributeSample); const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
const toplevelAttributes = await extractAttributes(["test"], tree, false); const toplevelAttributes = await extractAttributes(["test"], tree);
// console.log("All attributes", toplevelAttributes); // console.log("All attributes", toplevelAttributes);
assertEquals(toplevelAttributes.name, "sup"); assertEquals(toplevelAttributes.name, "sup");
assertEquals(toplevelAttributes.age, 42); assertEquals(toplevelAttributes.age, 42);
assertEquals(toplevelAttributes.children, ["pete", "john", "mary"]); assertEquals(toplevelAttributes.children, ["pete", "john", "mary"]);
// Check if the attributes are still there // Check if the attributes are still there
assertEquals(renderToText(tree), inlineAttributeSample); assertEquals(renderToText(tree), inlineAttributeSample);
// Now once again with cleaning // And now clean
await extractAttributes(["test"], tree, true); cleanAttributes(tree);
assertEquals(renderToText(tree), cleanedInlineAttributeSample); assertEquals(renderToText(tree), cleanedInlineAttributeSample);
}); });

View File

@ -2,7 +2,8 @@ import {
findNodeOfType, findNodeOfType,
type ParseTree, type ParseTree,
renderToText, renderToText,
replaceNodesMatchingAsync, replaceNodesMatching,
traverseTreeAsync,
} from "./tree.ts"; } from "./tree.ts";
import { cleanupJSON } from "@silverbulletmd/silverbullet/lib/json"; import { cleanupJSON } from "@silverbulletmd/silverbullet/lib/json";
@ -10,21 +11,19 @@ import { cleanupJSON } from "@silverbulletmd/silverbullet/lib/json";
import { system, YAML } from "../syscalls.ts"; 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 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 * @returns mapping from attribute name to attribute value
*/ */
export async function extractAttributes( export async function extractAttributes(
tags: string[], tags: string[],
tree: ParseTree, tree: ParseTree,
clean: boolean,
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
let attributes: Record<string, any> = {}; let attributes: Record<string, any> = {};
await replaceNodesMatchingAsync(tree, async (n) => { await traverseTreeAsync(tree, async (n) => {
if (n.type === "ListItem") { if (tree !== n && n.type === "ListItem") {
// Find top-level only, no nested lists // Find top-level only, no nested lists
return n; return true;
} }
if (n.type === "Attribute") { if (n.type === "Attribute") {
const nameNode = findNodeOfType(n, "AttributeName"); const nameNode = findNodeOfType(n, "AttributeName");
@ -38,15 +37,10 @@ export async function extractAttributes(
console.error("Error parsing attribute value as YAML", val, e); console.error("Error parsing attribute value as YAML", val, e);
} }
} }
// Remove from tree return true;
if (clean) {
return null;
} else {
return n;
}
} }
// Go on... // Go on...
return undefined; return false;
}); });
const text = renderToText(tree); const text = renderToText(tree);
const spaceScriptAttributes = await system.applyAttributeExtractors( const spaceScriptAttributes = await system.applyAttributeExtractors(
@ -60,3 +54,16 @@ export async function extractAttributes(
}; };
return attributes; 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;
});
}

View File

@ -9,7 +9,10 @@ import {
* Feed parsing functionality (WIP) * Feed parsing functionality (WIP)
*/ */
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute"; import {
cleanAttributes,
extractAttributes,
} from "@silverbulletmd/silverbullet/lib/attribute";
export type FeedItem = { export type FeedItem = {
id: string; id: string;
@ -29,7 +32,6 @@ export async function extractFeedItems(tree: ParseTree): Promise<FeedItem[]> {
for (const node of tree.children!) { for (const node of tree.children!) {
if (node.type === "FrontMatter") { if (node.type === "FrontMatter") {
// Not interested // Not interested
console.log("Ignoring", node);
continue; continue;
} }
if (node.type === "HorizontalRule") { if (node.type === "HorizontalRule") {
@ -51,7 +53,8 @@ async function nodesToFeedItem(nodes: ParseTree[]): Promise<FeedItem> {
const wrapperNode: ParseTree = { const wrapperNode: ParseTree = {
children: nodes, children: nodes,
}; };
const attributes = await extractAttributes(["feed"], wrapperNode, true); const attributes = await extractAttributes(["feed"], wrapperNode);
cleanAttributes(wrapperNode);
let id = attributes.id; let id = attributes.id;
delete attributes.id; delete attributes.id;
if (!id) { if (!id) {

View File

@ -1,5 +1,5 @@
import { assertEquals } from "@std/assert"; import { assertEquals } from "@std/assert";
import { cleanupJSON, deepEqual, deepObjectMerge } from "./json.ts"; import { cleanupJSON, deepClone, deepEqual, deepObjectMerge } from "./json.ts";
Deno.test("JSON utils", () => { Deno.test("JSON utils", () => {
assertEquals(deepEqual({ a: 1 }, { a: 1 }), true); 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"); 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
});

View File

@ -139,3 +139,39 @@ export function deepObjectMerge(a: any, b: any, reverseArrays = false): any {
} }
return b; 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.");
}

View File

@ -1,5 +1,9 @@
import type { FrontMatter } from "./frontmatter.ts"; import type { FrontMatter } from "./frontmatter.ts";
import type { ObjectValue } from "../types.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) { export function updateITags<T>(obj: ObjectValue<T>, frontmatter: FrontMatter) {
const itags = [obj.tag, ...frontmatter.tags || []]; const itags = [obj.tag, ...frontmatter.tags || []];
@ -12,3 +16,37 @@ export function updateITags<T>(obj: ObjectValue<T>, frontmatter: FrontMatter) {
} }
obj.itags = itags; 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;
});
}

View File

@ -43,7 +43,6 @@ export async function indexHeaders({ name: pageName, tree }: IndexTreeEvent) {
const extractedAttributes = await extractAttributes( const extractedAttributes = await extractAttributes(
["header", ...tags], ["header", ...tags],
n, n,
true,
); );
const name = n.children!.slice(1).map(renderToText).join("").trim(); const name = n.children!.slice(1).map(renderToText).join("").trim();

33
plugs/index/item.test.ts Normal file
View File

@ -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);
});

View File

@ -1,16 +1,27 @@
import type { IndexTreeEvent } from "../../plug-api/types.ts"; import type { IndexTreeEvent, ObjectValue } from "../../plug-api/types.ts";
import { import {
collectNodesOfType, findParentMatching,
type ParseTree, type ParseTree,
renderToText, renderToText,
} from "../../plug-api/lib/tree.ts"; traverseTreeAsync,
import { extractAttributes } from "@silverbulletmd/silverbullet/lib/attribute"; } from "@silverbulletmd/silverbullet/lib/tree";
import {
cleanAttributes,
extractAttributes,
} from "@silverbulletmd/silverbullet/lib/attribute";
import { rewritePageRefs } from "@silverbulletmd/silverbullet/lib/resolve"; import { rewritePageRefs } from "@silverbulletmd/silverbullet/lib/resolve";
import type { ObjectValue } from "../../plug-api/types.ts";
import { indexObjects } from "./api.ts"; import { indexObjects } from "./api.ts";
import { updateITags } from "@silverbulletmd/silverbullet/lib/tags"; import {
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter"; 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< export type ItemObject = ObjectValue<
{ {
@ -22,73 +33,127 @@ export type ItemObject = ObjectValue<
>; >;
export async function indexItems({ name, tree }: IndexTreeEvent) { 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 items: ObjectValue<ItemObject>[] = [];
const frontmatter = await extractFrontmatter(tree); 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) { if (!n.children) {
continue; // Weird, let's jump out
} return true;
if (collectNodesOfType(n, "Task").length > 0) {
// This is a task item, skip it
continue;
} }
const tags = new Set<string>(); const item: ItemObject = await extractItemFromNode(
const item: ItemObject = { name,
ref: `${name}@${n.from}`,
tag: "item",
name: "",
text: "",
page: name,
pos: n.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
const extractedAttributes = await extractAttributes(
["item", ...tags],
n, n,
true, frontmatter,
); );
for (const child of n.children!.slice(1)) {
rewritePageRefs(child, name);
if (child.type === "OrderedList" || child.type === "BulletList") {
break;
}
textNodes.push(child);
}
item.name = textNodes.map(renderToText).join("").trim();
item.text = fullText;
if (tags.size > 0) {
item.tags = [...tags];
}
for (
const [key, value] of Object.entries(extractedAttributes)
) {
item[key] = value;
}
updateITags(item, frontmatter);
items.push(item); items.push(item);
}
// console.log("Found", items, "item(s)"); return false;
await indexObjects(name, items); });
return items;
}
export async function extractItemFromNode(
name: string,
itemNode: ParseTree,
frontmatter: FrontMatter,
) {
const item: ItemObject = {
ref: `${name}@${itemNode.from}`,
tag: "item",
name: "",
text: "",
page: name,
pos: itemNode.from!,
};
// Now let's extract tags and attributes
const tags = extractHashTags(itemNode);
const extractedAttributes = await extractAttributes(
["item", ...tags],
itemNode,
);
const clonedTextNodes: ParseTree[] = [];
for (const child of itemNode.children!.slice(1)) {
rewritePageRefs(child, name);
if (child.type === "OrderedList" || child.type === "BulletList") {
break;
}
clonedTextNodes.push(deepClone(child, ["parent"]));
}
// Original text
item.text = clonedTextNodes.map(renderToText).join("").trim();
// Clean out attribtus and tags and render a clean item name
for (const clonedTextNode of clonedTextNodes) {
cleanHashTags(clonedTextNode);
cleanAttributes(clonedTextNode);
}
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);
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",
);
}
} }

View File

@ -27,7 +27,6 @@ export async function indexPage({ name, tree }: IndexTreeEvent) {
const toplevelAttributes = await extractAttributes( const toplevelAttributes = await extractAttributes(
["page", ...frontmatter.tags || []], ["page", ...frontmatter.tags || []],
tree, tree,
false,
); );
// Push them all into the page object // Push them all into the page object

View File

@ -46,7 +46,7 @@ export async function indexParagraphs({ name: page, tree }: IndexTreeEvent) {
}); });
// Extract attributes and remove from tree // 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); const text = renderToText(p);
if (!text.trim()) { if (!text.trim()) {

51
plugs/tasks/task.test.ts Normal file
View File

@ -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"]),
);
});

View File

@ -21,16 +21,25 @@ import {
traverseTreeAsync, traverseTreeAsync,
} from "../../plug-api/lib/tree.ts"; } from "../../plug-api/lib/tree.ts";
import { niceDate } from "$lib/dates.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 { rewritePageRefs } from "@silverbulletmd/silverbullet/lib/resolve";
import type { ObjectValue } from "../../plug-api/types.ts"; import type { ObjectValue } from "../../plug-api/types.ts";
import { indexObjects, queryObjects } from "../index/plug_api.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 { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
import { import {
parsePageRef, parsePageRef,
positionOfLine, positionOfLine,
} from "@silverbulletmd/silverbullet/lib/page_ref"; } from "@silverbulletmd/silverbullet/lib/page_ref";
import { enrichItemFromParents } from "../index/item.ts";
import { deepClone } from "@silverbulletmd/silverbullet/lib/json";
export type TaskObject = ObjectValue< export type TaskObject = ObjectValue<
{ {
@ -57,7 +66,10 @@ function getDeadline(deadlineNode: ParseTree): string {
const completeStates = ["x", "X"]; const completeStates = ["x", "X"];
const incompleteStates = [" "]; const incompleteStates = [" "];
export async function indexTasks({ name, tree }: IndexTreeEvent) { export async function extractTasks(
name: string,
tree: ParseTree,
): Promise<TaskObject[]> {
const tasks: ObjectValue<TaskObject>[] = []; const tasks: ObjectValue<TaskObject>[] = [];
const taskStates = new Map<string, { count: number; firstPos: number }>(); const taskStates = new Map<string, { count: number; firstPos: number }>();
const frontmatter = await extractFrontmatter(tree); const frontmatter = await extractFrontmatter(tree);
@ -66,6 +78,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
if (n.type !== "Task") { if (n.type !== "Task") {
return false; return false;
} }
const listItemNode = n.parent!;
const state = n.children![0].children![1].text!; const state = n.children![0].children![1].text!;
if (!incompleteStates.includes(state) && !completeStates.includes(state)) { if (!incompleteStates.includes(state) && !completeStates.includes(state)) {
let currentState = taskStates.get(state); let currentState = taskStates.get(state);
@ -76,6 +89,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
currentState.count++; currentState.count++;
} }
const complete = completeStates.includes(state); const complete = completeStates.includes(state);
const task: TaskObject = { const task: TaskObject = {
ref: `${name}@${n.from}`, ref: `${name}@${n.from}`,
tag: "task", tag: "task",
@ -99,30 +113,27 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
// Remove this node from the tree // Remove this node from the tree
return null; 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( const extractedAttributes = await extractAttributes(
["task", ...task.tags || []], ["task", ...task.tags || []],
n, 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)) { for (const [key, value] of Object.entries(extractedAttributes)) {
task[key] = value; task[key] = value;
} }
updateITags(task, frontmatter); updateITags(task, frontmatter);
await enrichItemFromParents(listItemNode, task, name, frontmatter);
tasks.push(task); tasks.push(task);
return true; 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 // Index tasks themselves
if (tasks.length > 0) { if (extractTasks.length > 0) {
await indexObjects(name, tasks); await indexObjects(name, extractedTasks);
} }
} }