Skip to content
Closed
51 changes: 46 additions & 5 deletions src/core/Templater.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
App,
getFrontMatterInfo,
MarkdownPostProcessorContext,
MarkdownView,
normalizePath,
Expand All @@ -13,6 +14,7 @@ import {
get_active_file,
get_folder_path_from_file_path,
resolve_tfile,
merge_front_matter,
} from "utils/Utils";
import TemplaterPlugin from "main";
import {
Expand All @@ -22,6 +24,7 @@ import {
import { errorWrapper, errorWrapperSync, TemplaterError } from "utils/Error";
import { Parser } from "./parser/Parser";
import { log_error } from "utils/Log";
import * as yaml from "js-yaml";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use parseYaml and stringifyYaml from 'obsidian' instead.


export enum RunMode {
CreateNewFromTemplate,
Expand Down Expand Up @@ -254,10 +257,20 @@ export class Templater {
return;
}

const front_matter_info = getFrontMatterInfo(output_content);
const frontmatter = yaml.load(front_matter_info.frontmatter);
await merge_front_matter(
this.plugin.app,
active_editor.file,
frontmatter
);

const editor = active_editor.editor;
const doc = editor.getDoc();
const oldSelections = doc.listSelections();
doc.replaceSelection(output_content);
doc.replaceSelection(
output_content.slice(front_matter_info.contentStart)
);
if (active_view) {
// Wait for view to finish rendering properties widget
await delay(100);
Expand Down Expand Up @@ -293,7 +306,7 @@ export class Templater {
file,
RunMode.OverwriteFile
);
const output_content = await errorWrapper(
let output_content = await errorWrapper(
async () => this.read_and_parse_template(running_config),
"Template parsing error, aborting."
);
Expand All @@ -302,7 +315,30 @@ export class Templater {
await this.end_templater_task(path);
return;
}
await this.plugin.app.vault.modify(file, output_content);

let existing_front_matter = null;
await delay(100); // Sometimes the front matter is not yet available if the file was just created
await this.plugin.app.fileManager.processFrontMatter(
file,
(front_matter) => {
existing_front_matter = front_matter;
}
);

const front_matter_info = getFrontMatterInfo(output_content);
const frontmatter = yaml.load(front_matter_info.frontmatter);

if (existing_front_matter) {
// Bases can create frontmatter, merge this into the template frontmatter
await merge_front_matter(this.plugin.app, file, frontmatter);
await this.plugin.app.vault.append(
file,
output_content.slice(front_matter_info.contentStart)
);
output_content = await this.plugin.app.vault.read(file);
} else {
await this.plugin.app.vault.modify(file, output_content);
}
Comment on lines +319 to +341
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of reading and writing going on that could potentially cause issues with other plugins. Ideally we should be doing a single atomic operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To counter this. What if you have a template that includes another template which sets a property, then you want to use the value of the property in the parent template. Each template should have access to the frontmatter introduced by the included templates. I think the most reliable way of doing that is merging and writing the frontmatter for each included template.

An example

  • Task template includes the Project template
  • The Project template is some JS that sets the project property in the note frontmatter based on some unrelated logic. This template does nothing else so it is portable
  • The Task template can then use tp.frontmatter["project"] for further logic in its template

Note

I think the above example doesn't work with either of our code since the tp.frontmatter is not updated after each template have been processed and can only access the frontmatter which exists at the time of starting to insert the template.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather update tp.frontmatter directly than modify the file over and over. But that can be a separate project, I don't think it should be part of this PR to try to solve this.

// Set cursor to first line of editor (below properties)
// https://github.com/SilentVoid13/Templater/issues/1231
if (
Expand Down Expand Up @@ -491,8 +527,13 @@ export class Templater {
return;
}

const file_content = await app.vault.read(file);
const frontmatter_info = getFrontMatterInfo(file_content);
const content_size =
file_content.length - frontmatter_info.contentStart;

if (
file.stat.size == 0 &&
content_size == 0 &&
templater.plugin.settings.enable_folder_templates
) {
const folder_template_match =
Expand All @@ -512,7 +553,7 @@ export class Templater {
}
await templater.write_template_to_file(template_file, file);
} else if (
file.stat.size == 0 &&
content_size == 0 &&
templater.plugin.settings.enable_file_templates
) {
const file_template_match =
Expand Down
11 changes: 10 additions & 1 deletion src/core/functions/internal_functions/file/InternalModuleFile.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { InternalModule } from "../InternalModule";
import { log_error } from "utils/Log";
import * as yaml from "js-yaml";
import {
FileSystemAdapter,
getAllTags,
getFrontMatterInfo,
moment,
normalizePath,
parseLinktext,
Expand All @@ -13,6 +15,7 @@ import {
} from "obsidian";
import { TemplaterError } from "utils/Error";
import { ModuleName } from "editor/TpDocumentation";
import { merge_front_matter } from "utils/Utils";

export const DEPTH_LIMIT = 10;

Expand Down Expand Up @@ -210,14 +213,20 @@ export class InternalModuleFile extends InternalModule {
}
}

const active_file = this.plugin.app.workspace.getActiveFile();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may not always be the active file, since you can use tp.file.create_new to create a new file without opening it.
I think we'll need to find a different solution for how to get tp.file.include to handle merging frontmatter.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can use the target file from the running config? Same place we get tp.config.target_file from.


try {
const parsed_content =
await this.plugin.templater.parser.parse_commands(
inc_file_content,
this.plugin.templater.current_functions_object
);
const front_matter_info = getFrontMatterInfo(parsed_content);
const frontmatter = yaml.load(front_matter_info.frontmatter);
await merge_front_matter(this.plugin.app, active_file, frontmatter);

this.include_depth -= 1;
return parsed_content;
return parsed_content.slice(front_matter_info.contentStart);
} catch (e) {
this.include_depth -= 1;
throw e;
Expand Down
151 changes: 106 additions & 45 deletions src/utils/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { DocBlock, DocNode, DocParamBlock, DocParamCollection, DocPlainText, DocSection, ParserContext, TSDocParser } from "@microsoft/tsdoc";
import {
DocBlock,
DocNode,
DocParamBlock,
DocParamCollection,
DocPlainText,
DocSection,
ParserContext,
TSDocParser,
} from "@microsoft/tsdoc";

import { TJDocFile, TJDocFileArgument } from "./TJDocFile";

Expand Down Expand Up @@ -80,82 +89,87 @@ export async function populate_docs_from_user_scripts(
app: App,
files: Array<TFile>
): Promise<TJDocFile[]> {
const docFiles = await Promise.all(files.map(async file => {
const docFiles = await Promise.all(
files.map(async (file) => {
// Get file contents
const content = await app.vault.cachedRead(file)
const content = await app.vault.cachedRead(file);

const newDocFile = generate_jsdoc(file, content);

return newDocFile;
}
));
})
);

return docFiles;
}

function generate_jsdoc(
file: TFile,
content: string
): TJDocFile{
function generate_jsdoc(file: TFile, content: string): TJDocFile {
// Parse the content
const tsdocParser = new TSDocParser();
const parsedDoc = tsdocParser.parseString(content);

// Copy and extract information into the TJDocFile
const newDocFile = new TJDocFile(file);

newDocFile.description = generate_jsdoc_description(parsedDoc.docComment.summarySection);
newDocFile.returns = generate_jsdoc_return(parsedDoc.docComment.returnsBlock);
newDocFile.arguments = generate_jsdoc_arguments(parsedDoc.docComment.params);

return newDocFile
newDocFile.description = generate_jsdoc_description(
parsedDoc.docComment.summarySection
);
newDocFile.returns = generate_jsdoc_return(
parsedDoc.docComment.returnsBlock
);
newDocFile.arguments = generate_jsdoc_arguments(
parsedDoc.docComment.params
);

return newDocFile;
}

function generate_jsdoc_description(
summarySection: DocSection
) : string {
function generate_jsdoc_description(summarySection: DocSection): string {
try {
const description = summarySection.nodes.map((node: DocNode) =>
node.getChildNodes()
const description = summarySection.nodes.map((node: DocNode) =>
node
.getChildNodes()
.filter((node: DocNode) => node instanceof DocPlainText)
.map((x: DocPlainText) => x.text)
.join("\n")
);
return description.join("\n");

return description.join("\n");
} catch (error) {
console.error('Failed to parse sumamry section');
throw error;
console.error("Failed to parse summary section");
}
}

function generate_jsdoc_return(
returnSection : DocBlock | undefined
): string {
function generate_jsdoc_return(returnSection: DocBlock | undefined): string {
if (!returnSection) return "";

try {
const returnValue = returnSection.content.nodes[0].getChildNodes()[0].text.trim();
return returnValue;
const returnValue = returnSection.content.nodes[0]
.getChildNodes()[0]
.text.trim();
return returnValue;
} catch (error) {
return "";
}
}

function generate_jsdoc_arguments(
paramSection: DocParamCollection
) : TJDocFileArgument[] {
): TJDocFileArgument[] {
try {
const blocks = paramSection.blocks;
const args = blocks.map((block) => {
const name = block.parameterName;
const description = block.content.getChildNodes()[0].getChildNodes()
.filter(x => x instanceof DocPlainText)
.map(x => x.text).join(" ")
return new TJDocFileArgument(name, description);
})

return args;
const name = block.parameterName;
const description = block.content
.getChildNodes()[0]
.getChildNodes()
.filter((x) => x instanceof DocPlainText)
.map((x) => x.text)
.join(" ");
return new TJDocFileArgument(name, description);
});

return args;
} catch (error) {
return [];
}
Expand Down Expand Up @@ -212,17 +226,64 @@ export function get_fn_params(func: (...args: unknown[]) => unknown) {
*/
export function append_bolded_label_with_value_to_parent(
parent: HTMLElement,
title: string,
value: string
): HTMLElement{
const tag = parent instanceof HTMLOListElement ? "li" : "p";
title: string,
value: string
): HTMLElement {
const tag = parent instanceof HTMLOListElement ? "li" : "p";

const para = parent.createEl(tag);
const bold = parent.createEl('b', {text: title});
const bold = parent.createEl("b", { text: title });
para.appendChild(bold);
para.appendChild(document.createTextNode(`: ${value}`))
para.appendChild(document.createTextNode(`: ${value}`));

// Returns a p or li element
// Resulting in <b>Title</b>: value
return para;
}

export async function merge_front_matter(
app: App,
file: TFile | null,
properties: Record<string, unknown>
): Promise<void> {
if (!file || !is_object(properties)) {
return;
}
try {
await app.fileManager.processFrontMatter(file, (frontmatter) => {
for (const prop in properties) {
const currentValue = frontmatter[prop];
const newValue = properties[prop];

if (currentValue === undefined) {
// If the property doesn't exist, add it
frontmatter[prop] = newValue;
} else if (
Array.isArray(currentValue) ||
Array.isArray(newValue)
) {
// If either is an array, merge them
frontmatter[prop] = Array.from(
new Set([
...(currentValue
? Array.isArray(currentValue)
? currentValue
: [currentValue]
: []),
...(newValue
? Array.isArray(newValue)
? newValue
: [newValue]
: []),
])
);
} else if (newValue !== currentValue && newValue != null) {
// If they are different, update the value
frontmatter[prop] = newValue;
}
}
});
} catch (error) {
console.error("Error in processing frontmatter: ", error);
}
}