Files
mize/packages/marts/task/obsidian-otask-parse.ts
2026-03-14 15:33:27 +01:00

539 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import remarkParse from "remark-parse";
import remarkStringify from "remark-stringify";
import { unified } from "unified";
import { visit } from "unist-util-visit";
import { Root, Content, Heading, List, ListItem, Text } from "mdast";
import remarkFrontmatter from "remark-frontmatter";
import remarkGfm from "remark-gfm";
import wikiLinkPlugin from "@flowershow/remark-wiki-link";
import { TFile } from "obsidian";
const OTASK_PROP_NAMES = ["short", "outcome", "priority"];
const ACTION_PROP_NAMES = [];
const REMARK = unified()
.use(remarkParse)
.use(remarkFrontmatter)
.use(remarkGfm)
.use(remarkStringify, { bullet: "-" });
export function task_obsidian_otask_parse(mize: any) {
const obsidian_plugin = mize.get_part_native("obsidian");
obsidian_plugin.addCommand({
id: "full-parse",
name: "Parse all otask files into the db",
callback: async () => {
await full_parse();
},
});
}
async function full_parse() {
console.log("full parse.......................... yay");
let otasks = [];
OTASK_PARSE_QUEUE = [];
OTASK_LIST = [];
const root_file = app.vault.getAbstractFileByPath("task/my-projects.md");
const content = await app.vault.read(root_file);
const list = await getListUnderHeading(content, "root");
for (const entry of list) {
queue_otasks_from_list_item(entry, []);
}
// go through the queue of otaskParseTasks
// using a queue instead of recursive calls, to make it more debuggable...
do {
const task = OTASK_PARSE_QUEUE.shift();
dbg("task", task);
if (task.file) {
const otask = await otasks_from_file(
task.file,
task.otask_path,
task.priority,
);
dbg("otask", otask);
if (otask) {
OTASK_LIST.push(otask);
}
} else {
const otask = await otasks_from_list_item(
task.listItem,
task.otask_path,
task.priority,
);
dbg("otask", otask);
if (otask) {
OTASK_LIST.push(otask);
}
}
} while (OTASK_PARSE_QUEUE.length > 0);
console.log("otasks::", OTASK_LIST);
render_sub0_file(OTASK_LIST);
}
let OTASK_PARSE_QUEUE: any = [];
let OTASK_LIST = [];
let DBG = false;
function dbg() {
if (DBG) {
console.log(arguments);
}
}
function queue_otasks_from_list_item(listItem, otask_path, priority = 0) {
OTASK_PARSE_QUEUE.push({
listItem,
otask_path,
priority,
});
}
function queue_otasks_from_file(file, otask_path, priority = 0) {
OTASK_PARSE_QUEUE.push({
file,
otask_path,
priority,
});
}
// parsing helper functions
async function render_sub0_file(otasks) {
const sub0_file = app.vault.getAbstractFileByPath("private/sub0-tasks.md");
if (!(sub0_file instanceof TFile)) {
console.error(
"SystemC2: sub0_file not found:",
"private/sub0-tasks.md",
sub0_file,
);
return;
}
//render sub0
const sub0_otasks = otasks.filter(
(otask) => otask.priority == 0 && !otask.is_file,
);
console.log("sub0 otasks...", sub0_otasks);
let rendered_otasks_string = "";
for (const otask of sub0_otasks) {
const path_part = otask.path.length > 0 ? otask.path.join(": ") + ": " : "";
let content_part = otask.content ? otask.content + "\n" : "";
content_part = content_part
.split("\n")
.map((line) => "\t" + line)
.join("\n"); // add a \t in front of every otask.content line
rendered_otasks_string += `- ${path_part}${otask.longName}\n${content_part}\n`;
}
//console.log("newContent...", rendered_otasks_string)
await replaceHeading(sub0_file, "sub0", rendered_otasks_string);
//render sub-1
}
async function replaceHeading(
file: TFile,
heading: string,
newContent: string,
) {
const data = await this.app.vault.read(file);
// Parse the Markdown into an AST
const tree = REMARK.parse(data) as Root;
// Split new content into AST nodes
const newContentAst = REMARK.parse(newContent) as Root;
const children = tree.children;
const newChildren: Content[] = [];
let insideTarget = false;
let targetLevel = 0;
for (let i = 0; i < children.length; i++) {
const node = children[i];
if (node.type === "heading") {
const headingNode = node as Heading;
const text = headingNode.children
.filter((c) => c.type === "text")
.map((c: any) => c.value)
.join("");
// If we encounter our target heading
if (text.trim() === heading) {
insideTarget = true;
targetLevel = headingNode.depth;
newChildren.push(node); // Keep the heading itself
// Insert new content right after
newChildren.push(...newContentAst.children);
continue;
}
// If we’re currently inside target section and see a heading of same or higher level
if (insideTarget && headingNode.depth <= targetLevel) {
insideTarget = false;
}
}
// If we’re not inside the target section, keep the node
if (!insideTarget) {
newChildren.push(node);
}
}
const newTree: Root = { ...tree, children: newChildren };
// Convert AST back to Markdown
const newMarkdown = await REMARK.stringify(newTree);
await this.app.vault.modify(file, newMarkdown);
}
async function otasks_from_list_item(listItem, otask_path, priority = 0) {
try {
// the main otask this listItem is about
let otask = {
description: [],
actions: [],
path: [],
priority,
};
dbg("SystemC2: otasks_from_list_item", listItem, otask_path);
if (!listItem.children[0].type === "paragraph") {
dbg(
"listItem[0] of a otask item is not a paragraph... the otask:",
listItem,
);
return null;
}
let longName = nodeToString(listItem.children[0]);
dbg("longName:", longName);
// check for [[anotherotask]], call otasks_from_file, return those
const match = longName.match(/\[\[([^\]]+)\]\]/);
if (match) {
const file = app.metadataCache.getFirstLinkpathDest(match[1], "");
if (file) {
const metadata = app.metadataCache.getFileCache(file);
if (metadata?.frontmatter?.tags?.includes("t/otask")) {
queue_otasks_from_file(file, otask_path);
return null;
} else {
dbg(
"SystemC2: listItem with Link to '",
match[1],
"', which is not an otask",
longName,
);
}
}
}
//check for [x] and remove from name
if (longName.startsWith("[ ") || longName.startsWith("[x")) {
longName = longName.slice(4);
}
//check for "ot:" and remove from name
if (longName.startsWith("ot:")) {
longName = longName.slice(4);
}
otask.longName = longName;
otask.short = longName;
otask.path = otask_path;
let new_otask_path = [...otask_path, otask.short];
// go through sub list items
// - if it's a prop for the otask... handle that
// - if it's an "ac:" handle that
// - if it's "ot: " handle as another otask -> recursive call
// - if it's "[ ]" handle as another otask -> recursive call
// - if it's a link to a otask file... handle as another otask -> call otasks_from_file()
// - else add it as description item list to otask
if (!listItem.children || listItem.children.length < 2) {
return otask;
}
// add content as text to otask
if (otask.longName == "hosting updates") {
console.log("listItem... ", listItem.children[1]);
let hii = REMARK.stringify(listItem.children[1]);
console.log("text...", hii);
}
let content_text = REMARK.stringify(listItem.children[1]);
otask.content = content_text;
for (const subListItem of listItem.children[1].children) {
let text = nodeToString(subListItem.children[0]);
// check prop
if (check_otask_prop_from_list_item(subListItem, otask)) {
new_otask_path = [...otask_path, otask.short]; // have to redo it here, in case short changed from aprop
continue; // don't do any other processing on this sub-listItem
}
// remove "[ ]"
if (text.startsWith("[ ") || text.startsWith("[x")) {
text = text.slice(4);
}
// check ac
if (text.startsWith("ac:")) {
otask.actions = [
...otask.actions,
action_from_list_item(subListItem, new_otask_path, otask.priority),
];
continue; // don't do any other processing on this sub-listItem
}
// check ot
if (text.startsWith("ot:")) {
queue_otasks_from_list_item(
subListItem,
new_otask_path,
otask.priority,
);
continue; // don't do any other processing on this sub-listItem
}
// check [ ]
if (text.startsWith("[ ] ") || text.startsWith("[x] ")) {
queue_otasks_from_list_item(
subListItem,
new_otask_path,
otask.priority,
);
continue; // don't do any other processing on this sub-listItem
}
// check link to otask file
const match = text.match(/\[\[([^\]]+)\]\]/);
if (match) {
const file = app.metadataCache.getFirstLinkpathDest(match[1], "");
if (file) {
const metadata = app.metadataCache.getFileCache(file);
if (metadata?.frontmatter?.tags?.includes("t/otask")) {
queue_otasks_from_file(file, new_otask_path, otask.priority);
continue;
} else {
dbg(
"SystemC2: listItem with Link to '",
match[1],
"', which is not an otask",
longName,
);
}
continue; // don't do any other processing on this sub-listItem
}
}
// add as description
const desc = REMARK.stringify(subListItem);
otask.description.push(desc);
}
return otask;
} catch (error) {
console.log("failed to parse list item:", listItem);
}
}
async function otasks_from_file(file: TFile, otask_path, priority = 0) {
try {
dbg("SystemC2: otasks_from_file", file, otask_path);
// the main otask this file is about
let otask = {
description: [],
actions: [],
path: [],
priority,
is_file: true,
};
otask.longName = file.basename;
otask.short = otask.longName;
otask.path = otask_path;
let new_otask_path = [...otask_path, otask.short];
const content = await app.vault.read(file);
// parse listItems at the top... for props
const topListItems = getListItemsBeforeFirstHeading(content);
for (const topListItem of topListItems) {
const text = nodeToString(topListItem);
// check for prop
if (check_otask_prop_from_list_item(topListItem, otask)) {
new_otask_path = [...otask_path, otask.short];
continue; // don't do any other processing on this sub-listItem
}
}
// parse all the sub sections
const SUB_COUNT = 10;
for (let sub_number = 0; sub_number < SUB_COUNT; sub_number++) {
let listItems = await getListUnderHeading(content, "sub" + sub_number);
// "sub" without a number should have priority 5
if (sub_number == 5) {
listItems = listItems.concat(await getListUnderHeading(content, "sub"));
}
//dbg(`sub section ${sub_number} has items:`, listItems)
for (const listItem of listItems) {
// check if listItem is an action...
let text = nodeToString(listItem.children[0]);
if (
text.startsWith("ac:") ||
text.startsWith("[ ] ac:") ||
text.startsWith("[x] ac:")
) {
const action = action_from_list_item(listItem, otask_path);
otask.actions.push(action);
}
queue_otasks_from_list_item(
listItem,
new_otask_path,
otask.priority + sub_number,
);
}
}
return otask;
} catch (error) {
console.log("failed to parse file: ", file.path);
}
}
function action_from_list_item(listItem, otask_path) {
dbg("SystemC2: action_from_list_item", listItem, otask_path);
let action = {};
let text = nodeToString(listItem.children[0]).slice(4);
if (text.startsWith("[")) {
text = text.slice(4);
}
action.name = text;
return action;
}
function check_otask_prop_from_list_item(listItem, otask) {
let text = nodeToString(listItem.children[0]);
for (const prop_name of OTASK_PROP_NAMES) {
if (text.startsWith(prop_name + ":")) {
const value = text.slice(prop_name.length + 2);
if (prop_name == "priority") {
// parse priority as int
otask[prop_name] = parseInt(value);
} else if (prop_name == "outcome") {
// add sub items to it
let fullText = REMARK.stringify(listItem).slice(prop_name.length + 4);
otask[prop_name] = fullText;
} else {
otask[prop_name] = value;
}
return true;
}
}
return false;
}
// Extract plain text from a node
function nodeToString(node: any): string {
let result = "";
visit(node, "text", (textNode: Text) => {
result += textNode.value;
});
return result.trim();
}
async function getListUnderHeading(
markdown: string,
headingText: string,
): string[] {
const tree = REMARK.parse(markdown);
//dbg("tree", tree)
let capture = false;
let items: string[] = [];
for (let i = 0; i < tree.children.length; i++) {
const node: Content = tree.children[i];
// Detect the target heading
if (
node.type === "heading" &&
nodeToString(node).toLowerCase() === headingText.toLowerCase()
) {
capture = true;
continue;
}
// Stop if another heading of the same or higher level comes
if (capture && node.type === "heading") {
break;
}
// Collect list items if capturing
if (capture && node.type === "list") {
for (const li of (node as List).children) {
if (li.type === "listItem") {
items.push(li);
}
}
}
}
return items;
}
function getListItemsBeforeFirstHeading(markdown: string) {
const tree = REMARK.parse(markdown);
let items = [];
for (const node of tree.children) {
if (node.type === "heading") {
// stop at first heading
break;
}
if (node.type === "list") {
for (const li of (node as List).children) {
if (li.type === "listItem") {
items.push(li);
}
}
}
}
return items;
}