168 lines
No EOL
5.7 KiB
JavaScript
168 lines
No EOL
5.7 KiB
JavaScript
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 processedLinkedContent;
|
|
if (item.type === "embed") {
|
|
// Replace embed link with embed content
|
|
const embedProcessor = context.embedProviders.find(provider => provider.shouldProcessLink(item.link));
|
|
if (embedProcessor) {
|
|
const embedContent = await embedProcessor.process(item.link);
|
|
if (embedContent) {
|
|
processedLinkedContent = embedContent;
|
|
} else {
|
|
processedLinkedContent = item.original;
|
|
}
|
|
} else {
|
|
processedLinkedContent = item.original;
|
|
}
|
|
} else {
|
|
// Replace regular link with linked content
|
|
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();
|
|
|
|
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 = await app.vault.getAbstractFileByPath(parseLinktext(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)
|
|
} |