Copilot/turn repo into obsidian plugin#10
Conversation
|
@copilot resolve the merge conflicts in this pull request |
There was a problem hiding this comment.
Code Review
This pull request transitions the repository into an Obsidian community plugin, adding configuration files, a build script, and compiled plugin code. The code review identifies several critical issues and areas for improvement. Notably, the TypeScript source file src/main.ts is missing from the commit, and the node_modules/ directory was accidentally committed. Additionally, there are major logic discrepancies in main.js where the code attempts to parse metadata using fragile regex on the document body instead of leveraging Obsidian's robust metadataCache to read from the YAML frontmatter. Other recommendations include adding proper error handling to async commands, enabling sourcemaps for development, and catching potential esbuild context errors.
|
|
||
| ## Repository Contents | ||
|
|
||
| - `src/main.ts` – plugin source code |
There was a problem hiding this comment.
| async generateEvidenceMatrix() { | ||
| const evidenceFiles = this.getMarkdownFilesInFolder("Evidence"); | ||
| const rows = []; | ||
| for (const file of evidenceFiles.sort((a, b) => a.basename.localeCompare(b.basename))) { | ||
| const content = await this.app.vault.cachedRead(file); | ||
| const evidence = `[[${file.basename}]]`; | ||
| const facts = this.extractLinkedSection(content, "Related Facts").join(", "); | ||
| const issues = this.extractLinkedSection(content, "Related Issues").join(", "); | ||
| const people = this.extractLinkedSection(content, "Related People").join(", "); | ||
| const cases = this.extractLinkedSection(content, "Related Cases").join(", "); | ||
| rows.push(`| ${evidence} | ${facts} | ${issues} | ${people} | ${cases} | |`); | ||
| } | ||
| const matrix = [ | ||
| "# Evidence Matrix", | ||
| "", | ||
| "| Evidence | Fact(s) Related | Issue(s) Related | Person(s) Related | Case(s) Related | Notes |", | ||
| "|----------|------------------|------------------|-------------------|-----------------|-------|", | ||
| ...rows, | ||
| "" | ||
| ].join("\n"); | ||
| await this.upsertFile("Matrix/evidence_matrix.md", matrix); | ||
| } |
There was a problem hiding this comment.
There is a major discrepancy between the Evidence Template.md and the generateEvidenceMatrix implementation. The template defines relationships like RelatedFacts, RelatedPeople, RelatedCases, and RelatedIssues in the YAML frontmatter, but generateEvidenceMatrix attempts to extract them from markdown headers (e.g., ## Related Facts) in the body using extractLinkedSection. Since these headers do not exist in the template, the matrix columns will always be empty.
You should update this to read the relationships directly from the frontmatter using Obsidian's metadataCache.
async generateEvidenceMatrix() {
const evidenceFiles = this.getMarkdownFilesInFolder("Evidence");
const rows = [];
for (const file of evidenceFiles.sort((a, b) => a.basename.localeCompare(b.basename))) {
const cache = this.app.metadataCache.getFileCache(file);
const frontmatter = cache?.frontmatter;
const evidence = "[[" + file.basename + "]]";
const facts = this.extractLinksFromFrontmatter(frontmatter, "RelatedFacts").map((l) => "[[" + l + "]]").join(", ");
const issues = this.extractLinksFromFrontmatter(frontmatter, "RelatedIssues").map((l) => "[[" + l + "]]").join(", ");
const people = this.extractLinksFromFrontmatter(frontmatter, "RelatedPeople").map((l) => "[[" + l + "]]").join(", ");
const cases = this.extractLinksFromFrontmatter(frontmatter, "RelatedCases").map((l) => "[[" + l + "]]").join(", ");
rows.push("| " + evidence + " | " + facts + " | " + issues + " | " + people + " | " + cases + " | |");
}
const matrix = [
"# Evidence Matrix",
"",
"| Evidence | Fact(s) Related | Issue(s) Related | Person(s) Related | Case(s) Related | Notes |",
"|----------|------------------|------------------|-------------------|-----------------|-------|",
...rows,
""
].join("\n");
await this.upsertFile("Matrix/evidence_matrix.md", matrix);
}| async collectChronologyEntries(folder, type) { | ||
| const files = this.getMarkdownFilesInFolder(folder); | ||
| const entries = []; | ||
| for (const file of files) { | ||
| const content = await this.app.vault.cachedRead(file); | ||
| const date = content.match(/\*\*Date\/Time:\*\*\s*([^\n\r]+)/)?.[1]?.trim() ?? ""; | ||
| const title = content.match(/^#\s*(.+)$/m)?.[1]?.trim() ?? file.basename; | ||
| entries.push({ date, type, link: `[[${file.basename}]]`, note: title }); | ||
| } | ||
| return entries; | ||
| } |
There was a problem hiding this comment.
The templates define DateTime in the YAML frontmatter, but collectChronologyEntries attempts to parse **Date/Time:** from the body content using regex. This will fail to find any dates. Using Obsidian's metadataCache is much faster and 100% reliable.
| async collectChronologyEntries(folder, type) { | |
| const files = this.getMarkdownFilesInFolder(folder); | |
| const entries = []; | |
| for (const file of files) { | |
| const content = await this.app.vault.cachedRead(file); | |
| const date = content.match(/\*\*Date\/Time:\*\*\s*([^\n\r]+)/)?.[1]?.trim() ?? ""; | |
| const title = content.match(/^#\s*(.+)$/m)?.[1]?.trim() ?? file.basename; | |
| entries.push({ date, type, link: `[[${file.basename}]]`, note: title }); | |
| } | |
| return entries; | |
| } | |
| async collectChronologyEntries(folder, type) { | |
| const files = this.getMarkdownFilesInFolder(folder); | |
| const entries = []; | |
| for (const file of files) { | |
| const cache = this.app.metadataCache.getFileCache(file); | |
| const date = String(cache?.frontmatter?.DateTime ?? ""); | |
| const title = cache?.headings?.find((h) => h.level === 1)?.heading ?? file.basename; | |
| entries.push({ date, type, link: "[[" + file.basename + "]]", note: title }); | |
| } | |
| return entries; | |
| } |
| extractLinkedSection(content, section) { | ||
| const rgx = new RegExp(`## ${section}[\\n\\r]+((?:- \\[\\[.*?\\]\\][\\n\\r]+)*)`); | ||
| const match = content.match(rgx); | ||
| if (!match) return []; | ||
| return [...match[1].matchAll(/\[\[(.*?)\]\]/g)].map((item) => item[1]); | ||
| } |
There was a problem hiding this comment.
Since relationships are defined in the frontmatter rather than markdown headers, we can replace extractLinkedSection with a helper that parses wikilinks from frontmatter fields (supporting both string and array formats).
| extractLinkedSection(content, section) { | |
| const rgx = new RegExp(`## ${section}[\\n\\r]+((?:- \\[\\[.*?\\]\\][\\n\\r]+)*)`); | |
| const match = content.match(rgx); | |
| if (!match) return []; | |
| return [...match[1].matchAll(/\[\[(.*?)\]\]/g)].map((item) => item[1]); | |
| } | |
| extractLinksFromFrontmatter(frontmatter, key) { | |
| const value = frontmatter?.[key]; | |
| if (!value) return []; | |
| const str = Array.isArray(value) ? value.join(" ") : String(value); | |
| return [...str.matchAll(/\[\[(.*?)\]\]/g)].map((m) => m[1]); | |
| } |
| @@ -0,0 +1,2 @@ | |||
| node_modules/ | |||
There was a problem hiding this comment.
| format: "cjs", | ||
| platform: "browser", | ||
| target: "es2020", | ||
| sourcemap: false, |
There was a problem hiding this comment.
| if (isWatch) { | ||
| esbuild.context(buildOptions).then((ctx) => ctx.watch()); | ||
| } else { | ||
| esbuild.build(buildOptions).catch(() => process.exit(1)); | ||
| } |
There was a problem hiding this comment.
If esbuild.context fails (for example, if there is a configuration error or the entry point is missing), the promise rejection will go unhandled. It is safer to add a .catch() block to handle errors in watch mode as well.
| if (isWatch) { | |
| esbuild.context(buildOptions).then((ctx) => ctx.watch()); | |
| } else { | |
| esbuild.build(buildOptions).catch(() => process.exit(1)); | |
| } | |
| if (isWatch) { | |
| esbuild.context(buildOptions).then((ctx) => ctx.watch()).catch(() => process.exit(1)); | |
| } else { | |
| esbuild.build(buildOptions).catch(() => process.exit(1)); | |
| } |
| async onload() { | ||
| this.addCommand({ | ||
| id: "initialize-casemap-database", | ||
| name: "Initialize CaseMap database structure", | ||
| callback: async () => { | ||
| await this.initializeStructure(); | ||
| new import_obsidian.Notice("CaseMap database structure initialized."); | ||
| } | ||
| }); | ||
| this.addCommand({ | ||
| id: "generate-casemap-indexes", | ||
| name: "Generate CaseMap indexes", | ||
| callback: async () => { | ||
| await this.generateIndexes(); | ||
| new import_obsidian.Notice("CaseMap indexes updated."); | ||
| } | ||
| }); | ||
| this.addCommand({ | ||
| id: "generate-master-chronology", | ||
| name: "Generate master chronology", | ||
| callback: async () => { | ||
| await this.generateChronology(); | ||
| new import_obsidian.Notice("Master chronology updated."); | ||
| } | ||
| }); | ||
| this.addCommand({ | ||
| id: "generate-evidence-matrix", | ||
| name: "Generate evidence matrix", | ||
| callback: async () => { | ||
| await this.generateEvidenceMatrix(); | ||
| new import_obsidian.Notice("Evidence matrix updated."); | ||
| } | ||
| }); | ||
| this.addCommand({ | ||
| id: "generate-fileclass-index", | ||
| name: "Generate FileClass field index", | ||
| callback: async () => { | ||
| await this.generateFileClassIndex(); | ||
| new import_obsidian.Notice("FileClass field index updated."); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
Async command callbacks should be wrapped in try/catch blocks. If any of these operations fail (e.g., due to file system permissions, locked files, or invalid metadata), the error will go unhandled, and the user will not be notified of the failure. Showing an error Notice on failure improves the user experience.
async onload() {
const runSafe = async (fn, successMsg, errorMsg) => {
try {
await fn();
new import_obsidian.Notice(successMsg);
} catch (error) {
console.error(error);
new import_obsidian.Notice(errorMsg);
}
};
this.addCommand({
id: "initialize-casemap-database",
name: "Initialize CaseMap database structure",
callback: () => runSafe(
() => this.initializeStructure(),
"CaseMap database structure initialized.",
"Failed to initialize CaseMap database structure."
)
});
this.addCommand({
id: "generate-casemap-indexes",
name: "Generate CaseMap indexes",
callback: () => runSafe(
() => this.generateIndexes(),
"CaseMap indexes updated.",
"Failed to update CaseMap indexes."
)
});
this.addCommand({
id: "generate-master-chronology",
name: "Generate master chronology",
callback: () => runSafe(
() => this.generateChronology(),
"Master chronology updated.",
"Failed to update master chronology."
)
});
this.addCommand({
id: "generate-evidence-matrix",
name: "Generate evidence matrix",
callback: () => runSafe(
() => this.generateEvidenceMatrix(),
"Evidence matrix updated.",
"Failed to update evidence matrix."
)
});
this.addCommand({
id: "generate-fileclass-index",
name: "Generate FileClass field index",
callback: () => runSafe(
() => this.generateFileClassIndex(),
"FileClass field index updated.",
"Failed to update FileClass field index."
)
});
}| async generateFileClassIndex() { | ||
| const rows = ["# FileClass Field Index", "", "| FileClass | Fields |", "|-----------|--------|"]; | ||
| const fileClassFiles = this.getMarkdownFilesInFolder("FileClasses").filter((file) => file.name.endsWith(".fileclass.md")).sort((a, b) => a.basename.localeCompare(b.basename)); | ||
| for (const file of fileClassFiles) { | ||
| const content = await this.app.vault.cachedRead(file); | ||
| const fileClass = content.match(/fileClass:\s*([^\n\r]+)/)?.[1]?.trim() ?? file.basename; | ||
| const fields = [...content.matchAll(/-\s*([A-Za-z0-9_]+)/g)].map((match) => match[1]).join(", "); | ||
| rows.push(`| ${fileClass} | ${fields} |`); | ||
| } | ||
| rows.push(""); | ||
| await this.upsertFile("FileClasses/_fileclass_index.md", rows.join("\n")); | ||
| } |
There was a problem hiding this comment.
Using raw regex matching on the file content to parse YAML frontmatter is fragile and can easily break if the file contains lists or other matching patterns in the body. Since Obsidian already parses and caches YAML frontmatter, you can use this.app.metadataCache.getFileCache(file) to retrieve the fileClass and fields array directly and safely.
async generateFileClassIndex() {
const rows = ["# FileClass Field Index", "", "| FileClass | Fields |", "|-----------|--------|"];
const fileClassFiles = this.getMarkdownFilesInFolder("FileClasses").filter((file) => file.name.endsWith(".fileclass.md")).sort((a, b) => a.basename.localeCompare(b.basename));
for (const file of fileClassFiles) {
const cache = this.app.metadataCache.getFileCache(file);
const fileClass = cache?.frontmatter?.fileClass ?? file.basename;
const fieldsArray = cache?.frontmatter?.fields;
const fields = Array.isArray(fieldsArray) ? fieldsArray.join(", ") : "";
rows.push("| " + fileClass + " | " + fields + " |");
}
rows.push("");
await this.upsertFile("FileClasses/_fileclass_index.md", rows.join("\n"));
}
No description provided.