module.exports = { // object that describes the step and its configuration description: { // the name of your step name: "Replace links", // short description of what it does description: "Replaces wikilinks with the linked content", // array. valid options are "Scene", "Manuscript", "Join". "Join" must be the only member if present. availableKinds: ["Scene"], // array of step options, or an empty array if step has no options options: [ // a boolean option follows as another example { id: "removeH1", name: "Remove h1 of linked content", description: "If checked, the h1 headers of the linked content will be removed.", type: "Boolean", default: true, }, { id: "removeFrontmatter", name: "Remove frontmatter of linked content", description: "If checked, the frontmatter of the linked content will be removed.", type: "Boolean", default: true, }, ], }, /** Function that is executed during compilation. It may be `async`. Errors encountered during execution should be thrown and will be handled by Longform. @param input If the step is of kind Scene or Join (see context), this will be *an array* containing elements of type: { path: string; // path to scene name: string; // file name of scene contents: string; // text contents of scene metadata: CachedMetadata; // Obsidian metadata of scene indentationLevel?: number; // The indent level (starting at zero) of the scene } where each element corresponds to a scene (and thus the step has access to all scenes at once in `input`). If the step is of kind Manuscript (see context), this will be of type: { // text contents of manuscript contents: string; } @param context The execution context of the step, including the step kind and option values: { kind: string; // "Scene" | "Join" | "Manuscript" optionValues: { [id: string]: unknown } // Map of option IDs to values projectPath: string; // path in vault to compiling project draft: Draft; // The Draft type describing your project app: App; // Obsidian app } @note For an example of using `context` to determine the shape of `input`, see https://github.com/kevboh/longform/blob/main/src/compile/steps/strip-frontmatter.ts @returns If of kind "Scene" or "Manuscript", the same shape as `input` with the appropriate changes made to `contents`. If of kind "Join", the same shape as a "Manuscript" step input. */ compile }; async function compile(input, context) { const files = await getAllFiles(context.app); console.info("All files: ", files); return Promise.all( input.map(async scene => ({ ...scene, contents: await replaceLinksWithContent(scene.contents, scene.metadata, context, files, 0) })) ) } function getAllFiles(app) { return app.vault.getFiles(); } async function replaceLinksWithContent(content, metadata, context, files, deep) { if (deep > 1) { return content; } console.info("Current file: ", metadata, "deep: ", deep); const allLinks = [...(metadata.links || []), ...(metadata.embeds || [])]; return allLinks.reduce(async (accProm, item) => { const acc = await accProm; const [linkedFile, linkedContent] = await getLinkedContent(item.link, context.app, files); if (!linkedContent) { return acc; } const linkedMetadata = getMetadata(linkedFile, context.app); let contentArray = Array.from(linkedContent); contentArray = removeFrontmatter(contentArray, linkedMetadata.frontmatter, context.optionValues); contentArray = removeHeaders(contentArray, linkedMetadata.headings, context.optionValues) const cleanedContent = contentArray.filter(val => val !== null) .join("") .trim(); const processedLinkedContent = await replaceLinksWithContent(cleanedContent, linkedMetadata, context, files, deep + 1) return acc.replaceAll(item.original, processedLinkedContent) }, Promise.resolve(content)); } async function getLinkedContent(linkText, app, files) { const file = files.find(file => file.basename === linkText); if(!file){ const file = files.find(file => file.path === linkText); } if(!file){ const file = files.find(file => file.basename === getFilenameWithoutExtension(linkText)); } return file ? [file, await app.vault.read(file)] : []; } function getMetadata(file, app) { return app.metadataCache.getFileCache(file) } function removeHeaders(content, headers, optionValues) { if (optionValues.removeH1 && headers) { return headers .filter(header => header.level === 1) .reduce((acc, header) => { return removeFromTextArray(acc, header.position) }, content) } else { return content; } } function removeFrontmatter(content, frontmatter, optionValues) { if (frontmatter && optionValues.removeFrontmatter) { return removeFromTextArray(content, frontmatter.position); } else { return content; } } function removeFromTextArray(text, position) { return text.map((val, i) => i >= position.start.offset && i < position.end.offset ? null : val) } function getFilenameWithoutExtension(filePath) { const fileName = filePath.split('/').pop(); return fileName.split('.').slice(0, -1).join('.'); }