Skip to content

Commit

Permalink
add watcher for yaml statblocks
Browse files Browse the repository at this point in the history
  • Loading branch information
valentine195 committed Jan 21, 2022
1 parent 25cec2c commit 1c84df6
Show file tree
Hide file tree
Showing 2 changed files with 307 additions and 0 deletions.
198 changes: 198 additions & 0 deletions src/watcher/watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Component, TAbstractFile, TFile, TFolder } from "obsidian";
import type StatBlockPlugin from "src/main";
//have to ignore until i fix typing issue
//@ts-expect-error
import Worker, {
GetFileCacheMessage,
FileCacheMessage,
QueueMessage,
UpdateEventMessage,
SaveMessage,
FinishFileMessage
} from "./watcher.worker";

declare global {
interface Worker {
postMessage<T>(message: T, transfer?: Transferable[]): void;
}
}

export class Watcher extends Component {
get metadataCache() {
return this.plugin.app.metadataCache;
}
get vault() {
return this.plugin.app.vault;
}
constructor(public plugin: StatBlockPlugin) {
super();
}

watchPaths: Map<string, string> = new Map();

worker = new Worker();
onload() {
/** Metadata for a file has changed and the file should be checked. */
this.registerEvent(
this.metadataCache.on("changed", async (file) => {
if (!this.plugin.settings.autoParse) return;
const { frontmatter } =
this.metadataCache.getFileCache(file) ?? {};
if (!frontmatter || !frontmatter.statblock) {
if (this.watchPaths.has(file.path)) {
this.delete(file.path);
}
return;
}
this.parsePath(file);
})
);
/** A file has been renamed and should be checked for events.
* Could this be hashed?
*/
//TODO: Refactor
this.registerEvent(
this.vault.on("rename", async (abstractFile, oldPath) => {
if (!this.plugin.settings.autoParse) return;
if (!(abstractFile instanceof TFile)) return;
if (!this.watchPaths.has(oldPath)) return;

await this.delete(oldPath);
this.parsePath(abstractFile);
})
);
/** A file has been deleted and should be checked for events to unlink. */
//TODO: Refactor
this.registerEvent(
this.vault.on("delete", (abstractFile) => {
if (!this.plugin.settings.autoParse) return;
if (!(abstractFile instanceof TFile)) return;
if (!this.watchPaths.has(abstractFile.path)) return;
this.delete(abstractFile.path);
})
);

//worker messages
/** The worker will ask for file information from files in its queue here */
this.worker.addEventListener(
"message",
(event: MessageEvent<GetFileCacheMessage>) => {
if (event.data.type == "get") {
const { path } = event.data;
const data = this.getFileInformation(path);
//TODO: Add in file data parsing for events
//TODO: E.g., timelines
this.worker.postMessage<FileCacheMessage>({
type: "file",
path,
...data
});
}
}
);

/** The worker has found an event that should be updated. */
this.worker.addEventListener(
"message",
async (evt: MessageEvent<UpdateEventMessage>) => {
if (evt.data.type == "update") {
const { monster, path } = evt.data;
this.watchPaths.set(path, monster.name);
this.plugin.saveMonster(monster, false, false);
}
}
);

/** The worker has parsed all files in its queue. */
this.worker.addEventListener(
"message",
async (evt: MessageEvent<SaveMessage>) => {
if (evt.data.type == "save") {
await this.plugin.saveSettings();
if (this.startTime) {
console.info(
`TTRPG Statblocks: Frontmatter Parsing Complete in ${(
(Date.now() - this.startTime) /
1000
).toLocaleString()} seconds.`
);
this.startTime = 0;
}
}
}
);
if (!this.plugin.settings.autoParse) return;
this.plugin.app.workspace.onLayoutReady(() => this.start());
}
async delete(path: string) {
await this.plugin.deleteMonster(this.watchPaths.get(path));
this.watchPaths.delete(path);
}
startTime: number;
start() {
this.startTime = Date.now();
console.info("TTRPG Statblocks: Starting Frontmatter Parsing.");
const folder = this.vault.getAbstractFileByPath(
this.plugin.settings.path
);
this.parsePath(folder);
}
pathContainsFile(file: TAbstractFile) {
if (!this.plugin.settings.path || this.plugin.settings.path == "/")
return true;

return file.path.includes(this.plugin.settings.path, 1);
}
parsePath(folder: TAbstractFile) {
if (!this.pathContainsFile(folder)) return;
const parsing: Set<string> = new Set();
for (const path of this.getFiles(folder)) {
parsing.add(path);
}
this.startParsing([...parsing]);
}
startParsing(paths: string[]) {
if (paths.length) {
this.worker.postMessage<QueueMessage>({
type: "queue",
paths
});
}
}
getFileInformation(path: string) {
const file = this.plugin.app.vault.getAbstractFileByPath(path);
if (!(file instanceof TFile)) return;

const cache = this.metadataCache.getFileCache(file);
return {
cache,
file: { path: file.path, basename: file.basename }
};
}
getFiles(folder: TAbstractFile): string[] {
let files = [];
if (folder instanceof TFolder) {
for (const child of folder.children) {
files.push(...this.getFiles(child));
}
}
if (folder instanceof TFile) {
files.push(folder.path);
}
return files;
}
async reparseVault() {
for (const monster of this.watchPaths.values()) {
this.plugin.deleteMonster(monster, false, false);
}

const folder = this.vault.getAbstractFileByPath(
this.plugin.settings.path
);
this.parsePath(folder);
}
onunload() {
this.worker.terminate();
this.worker = null;
}
}
109 changes: 109 additions & 0 deletions src/watcher/watcher.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { Monster } from "@types";
import copy from "fast-copy";
import type { CachedMetadata } from "obsidian";
import { transformTraits } from "src/util/util";

export interface QueueMessage {
type: "queue";
paths: string[];
}
export interface FileCacheMessage {
type: "file";
path: string;
cache: CachedMetadata;
file: { path: string; basename: string };
}
export interface GetFileCacheMessage {
type: "get";
path: string;
}
export interface FinishFileMessage {
type: "done";
path: string;
}
export interface UpdateEventMessage {
type: "update";
monster: Monster;
path: string;
}
export interface SaveMessage {
type: "save";
}

const ctx: Worker = self as any;
class Parser {
queue: string[] = [];
parsing: boolean = false;

constructor() {
//Add Files to Queue
ctx.addEventListener("message", (event: MessageEvent<QueueMessage>) => {
if (event.data.type == "queue") {
this.add(...event.data.paths);
}
});
}
add(...paths: string[]) {
this.queue.push(...paths);
if (!this.parsing) this.parse();
}
async parse() {
this.parsing = true;
while (this.queue.length) {
const path = this.queue.shift();
const { file, cache } = await this.getFileData(path);
this.parseFileForCreatures(file, cache);
ctx.postMessage<FinishFileMessage>({ type: "done", path });
}
this.parsing = false;
ctx.postMessage<SaveMessage>({ type: "save" });
}
async getFileData(path: string): Promise<FileCacheMessage> {
return new Promise((resolve) => {
ctx.addEventListener(
"message",
(event: MessageEvent<FileCacheMessage>) => {
if (event.data.type == "file") {
resolve(event.data);
}
}
);
ctx.postMessage<GetFileCacheMessage>({ path, type: "get" });
});
}
parseFileForCreatures(
file: { path: string; basename: string },
cache: CachedMetadata
) {
if (!cache) return;
if (!cache.frontmatter) return;
if (!cache.frontmatter.statblock) return;
if (!cache.frontmatter.name) return;
const monster: Monster = Object.assign({}, copy(cache.frontmatter), {
note: file.path
});

if (monster.traits) {
monster.traits = transformTraits([], monster.traits);
}
if (monster.actions) {
monster.actions = transformTraits([], monster.actions);
}
if (monster.reactions) {
monster.reactions = transformTraits([], monster.reactions);
}
if (monster.legendary_actions) {
monster.legendary_actions = transformTraits(
[],
monster.legendary_actions
);
}

ctx.postMessage<UpdateEventMessage>({
type: "update",
monster,
path: file.path
});
}
}
new Parser();

0 comments on commit 1c84df6

Please sign in to comment.