Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API for use in other plugins or scripts #6

Merged
merged 1 commit into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 2 additions & 107 deletions src/BakeModal.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,15 @@
import {
App,
FileSystemAdapter,
Modal,
Platform,
Setting,
TFile,
parseLinktext,
resolveSubpath,
} from 'obsidian';

import EasyBake, { BakeSettings } from './main';
import EasyBake from './main';
import {
applyIndent,
extractSubpath,
getWordCount,
sanitizeBakedContent,
stripFirstBullet,
} from './util';

const lineStartRE = /(?:^|\n) *$/;
const listLineStartRE = /(?:^|\n)([ \t]*)(?:[-*+]|[0-9]+[.)]) +$/;
const lineEndRE = /^ *(?:\r?\n|$)/;

async function bake(
app: App,
file: TFile,
subpath: string | null,
ancestors: Set<TFile>,
settings: BakeSettings
) {
const { vault, metadataCache } = app;

let text = await vault.cachedRead(file);
const cache = metadataCache.getFileCache(file);

// No cache? Return the file as is...
if (!cache) return text;

// Get the target block or section if we have a subpath
const resolvedSubpath = subpath ? resolveSubpath(cache, subpath) : null;
if (resolvedSubpath) {
text = extractSubpath(text, resolvedSubpath, cache);
}

const links = settings.bakeLinks ? cache.links || [] : [];
const embeds = settings.bakeEmbeds ? cache.embeds || [] : [];
const targets = [...links, ...embeds];

// No links in the current file; we can stop here...
if (targets.length === 0) return text;

targets.sort((a, b) => a.position.start.offset - b.position.start.offset);

const newAncestors = new Set(ancestors);
newAncestors.add(file);

// This helps us keep track of edits we make to the text and sync them with
// position data held in the metadata cache
let posOffset = 0;
for (const target of targets) {
const { path, subpath } = parseLinktext(target.link);
const linkedFile = metadataCache.getFirstLinkpathDest(path, file.path);

if (!linkedFile) continue;

const start = target.position.start.offset + posOffset;
const end = target.position.end.offset + posOffset;
const prevLen = end - start;

const before = text.substring(0, start);
const after = text.substring(end);

const listMatch = settings.bakeInList
? before.match(listLineStartRE)
: null;
const isInline =
!(listMatch || lineStartRE.test(before)) || !lineEndRE.test(after);
const isMarkdownFile = linkedFile.extension === 'md';

const replaceTarget = (replacement: string) => {
text = before + replacement + after;
posOffset += replacement.length - prevLen;
};

if (!isMarkdownFile) {
// Skip link processing if we're not converting file links...
if (!settings.convertFileLinks) continue;

const adapter = app.vault.adapter as FileSystemAdapter;

// FYI: The mobile adapter also has getFullPath so this should work on mobile and desktop
// The mobile adapter isn't exported in the public API, however
if (!adapter.getFullPath) continue;
const fullPath = adapter.getFullPath(linkedFile.path);
const protocol = Platform.isWin ? 'file:///' : 'file://';
replaceTarget(`![](${protocol}${encodeURI(fullPath)})`);
continue;
}

// Replace the link with its text if the it's inline or would create an infinite loop
if (newAncestors.has(linkedFile) || isInline) {
replaceTarget(target.displayText || path);
continue;
}

// Recurse and bake the linked file...
const baked = sanitizeBakedContent(
await bake(app, linkedFile, subpath, newAncestors, settings)
);
replaceTarget(
listMatch ? applyIndent(stripFirstBullet(baked), listMatch[1]) : baked
);
}

return text;
}
import { bake } from "./bake";

function disableBtn(btn: HTMLButtonElement) {
btn.removeClass('mod-cta');
Expand Down
60 changes: 60 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { bake as bakeUtil } from "./bake";
import {
TFile,
} from 'obsidian';
import EasyBake, { BakeSettings } from './main';

export class EasyBakeApi {
private plugin: EasyBake;

constructor(plugin: EasyBake) {
this.plugin = plugin;
}

public async bakeToString(
inputPath: string,
settings: BakeSettings
) {
const app = this.plugin.app;
const file = app.vault.getAbstractFileByPath(inputPath);

if (!(file instanceof TFile)) {
console.error("Input file does not exist");
return;
}

return await bakeUtil(app, file, null, new Set(), settings);
}

public async bakeToFile(
inputPath: string,
outputPath: string,
settings: BakeSettings
) {
const baked = await this.bakeToString(inputPath, settings);
if (!baked) return;

const app = this.plugin.app;
let existing = app.vault.getAbstractFileByPath(outputPath);
if (existing instanceof TFile) {
await app.vault.modify(existing, baked);
} else {
existing = await app.vault.create(outputPath, baked);
}
}

public async bakeAndOpen(
inputPath: string,
outputPath: string,
settings: BakeSettings
) {
await this.bakeToFile(inputPath, outputPath, settings);

const app = this.plugin.app;
let existing = app.vault.getAbstractFileByPath(outputPath);
if (existing instanceof TFile) {
await app.workspace.getLeaf('tab').openFile(existing);
}
}
}

114 changes: 114 additions & 0 deletions src/bake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
App,
FileSystemAdapter,
Platform,
TFile,
parseLinktext,
resolveSubpath,
} from 'obsidian';

import { BakeSettings } from './main';
import {
applyIndent,
extractSubpath,
sanitizeBakedContent,
stripFirstBullet,
} from './util';

const lineStartRE = /(?:^|\n) *$/;
const listLineStartRE = /(?:^|\n)([ \t]*)(?:[-*+]|[0-9]+[.)]) +$/;
const lineEndRE = /^ *(?:\r?\n|$)/;

export async function bake(
app: App,
file: TFile,
subpath: string | null,
ancestors: Set<TFile>,
settings: BakeSettings
) {
const { vault, metadataCache } = app;

let text = await vault.cachedRead(file);
const cache = metadataCache.getFileCache(file);

// No cache? Return the file as is...
if (!cache) return text;

// Get the target block or section if we have a subpath
const resolvedSubpath = subpath ? resolveSubpath(cache, subpath) : null;
if (resolvedSubpath) {
text = extractSubpath(text, resolvedSubpath, cache);
}

const links = settings.bakeLinks ? cache.links || [] : [];
const embeds = settings.bakeEmbeds ? cache.embeds || [] : [];
const targets = [...links, ...embeds];

// No links in the current file; we can stop here...
if (targets.length === 0) return text;

targets.sort((a, b) => a.position.start.offset - b.position.start.offset);

const newAncestors = new Set(ancestors);
newAncestors.add(file);

// This helps us keep track of edits we make to the text and sync them with
// position data held in the metadata cache
let posOffset = 0;
for (const target of targets) {
const { path, subpath } = parseLinktext(target.link);
const linkedFile = metadataCache.getFirstLinkpathDest(path, file.path);

if (!linkedFile) continue;

const start = target.position.start.offset + posOffset;
const end = target.position.end.offset + posOffset;
const prevLen = end - start;

const before = text.substring(0, start);
const after = text.substring(end);

const listMatch = settings.bakeInList
? before.match(listLineStartRE)
: null;
const isInline =
!(listMatch || lineStartRE.test(before)) || !lineEndRE.test(after);
const isMarkdownFile = linkedFile.extension === 'md';

const replaceTarget = (replacement: string) => {
text = before + replacement + after;
posOffset += replacement.length - prevLen;
};

if (!isMarkdownFile) {
// Skip link processing if we're not converting file links...
if (!settings.convertFileLinks) continue;

const adapter = app.vault.adapter as FileSystemAdapter;

// FYI: The mobile adapter also has getFullPath so this should work on mobile and desktop
// The mobile adapter isn't exported in the public API, however
if (!adapter.getFullPath) continue;
const fullPath = adapter.getFullPath(linkedFile.path);
const protocol = Platform.isWin ? 'file:///' : 'file://';
replaceTarget(`![](${protocol}${encodeURI(fullPath)})`);
continue;
}

// Replace the link with its text if the it's inline or would create an infinite loop
if (newAncestors.has(linkedFile) || isInline) {
replaceTarget(target.displayText || path);
continue;
}

// Recurse and bake the linked file...
const baked = sanitizeBakedContent(
await bake(app, linkedFile, subpath, newAncestors, settings)
);
replaceTarget(
listMatch ? applyIndent(stripFirstBullet(baked), listMatch[1]) : baked
);
}

return text;
}
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ const DEFAULT_SETTINGS: BakeSettings = {
convertFileLinks: true,
};

import { EasyBakeApi } from "./api";

export default class EasyBake extends Plugin {
settings: BakeSettings;

public api = new EasyBakeApi(this);

async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
Expand Down