Absolue-Necessite/Projets/Scripts/replaceLinks.js

165 lines
5.7 KiB
JavaScript
Raw Normal View History

2023-04-03 20:35:26 +00:00
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)
}))
)
}
2023-04-03 21:57:57 +00:00
async function getAllFiles(app) {
const vaultDir = app.vault.adapter.basePath;
const fileData = await app.vault.adapter.fs.readdir(vaultDir, { withFileTypes: true });
const files = await Promise.all(fileData.map(async (file) => {
const filePath = path.join(vaultDir, file.name);
if (file.isFile() && path.extname(filePath) === '.md') {
const contents = await app.vault.read(file.name);
return { name: file.name, contents };
}
}));
return files.filter(file => file !== undefined);
2023-04-03 20:35:26 +00:00
}
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;
2023-04-03 21:57:57 +00:00
// Extract the link text and the link path from the link
const linkText = item.link;
const linkPath = path.isAbsolute(linkText) ? linkText.slice(1) : path.join(path.dirname(metadata.path), linkText);
const [linkedFile, linkedContent] = await getLinkedContent(linkPath, files);
2023-04-03 20:35:26 +00:00
if (!linkedContent) {
return acc;
}
2023-04-03 21:14:39 +00:00
2023-04-03 21:57:57 +00:00
const linkedMetadata = getMetadata(linkedFile.name, 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);
2023-04-03 20:35:26 +00:00
}, Promise.resolve(content));
}
2023-04-03 21:57:57 +00:00
async function getLinkedContent(linkPath, files) {
const linkedFile = files.find(file => file.name === linkPath);
return linkedFile
? [linkedFile, linkedFile.contents]
2023-04-03 20:35:26 +00:00
: [];
}
2023-04-03 21:57:57 +00:00
function getMetadata(fileName, app) {
const file = app.metadataCache.getFirstLinkpathDest(fileName, '');
return app.metadataCache.getFileCache(file);
2023-04-03 20:35:26 +00:00
}
2023-04-03 21:57:57 +00:00
function removeHeaders(content, headings, optionValues) {
if (optionValues.removeH1 && headings) {
return headings
2023-04-03 20:35:26 +00:00
.filter(header => header.level === 1)
.reduce((acc, header) => {
2023-04-03 21:57:57 +00:00
return removeFromTextArray(acc, header.position);
}, content);
2023-04-03 20:35:26 +00:00
} else {
return content;
}
}
function removeFrontmatter(content, frontmatter, optionValues) {
if (frontmatter && optionValues.removeFrontmatter) {
return removeFromTextArray(content, frontmatter.position);
} else {
return content;
}
}
function removeFromTextArray(text, position) {
2023-04-03 21:57:57 +00:00
return text.map((val, i) => i >= position.start.offset && i < position.end.offset ? null : val);
}