mirror of
https://github.com/decaf-dev/obsidian-vault-explorer.git
synced 2026-07-22 10:10:31 +00:00
347 lines
8 KiB
TypeScript
347 lines
8 KiB
TypeScript
import Logger from "js-logger";
|
|
import { App, type FrontMatterCache, TFile } from "obsidian";
|
|
import {
|
|
type FileTextProperties,
|
|
loadPropertyValue,
|
|
loadTextProperties,
|
|
} from "src/svelte/shared/services/load-property-value";
|
|
import {
|
|
getTimeMillis,
|
|
isDateSupported,
|
|
} from "src/svelte/shared/services/time-utils";
|
|
import {
|
|
type CoverImageFit,
|
|
PropertyType,
|
|
type VaultExplorerPluginSettings,
|
|
} from "src/types";
|
|
import type { FileRenderData } from "../types";
|
|
import {
|
|
getFirstExternalEmbed,
|
|
getFirstInternalEmbed,
|
|
} from "./link-utils/link-getters";
|
|
import {
|
|
getExternalEmbedTarget,
|
|
getInternalEmbedTarget,
|
|
getWikiLinkTarget,
|
|
} from "./link-utils/link-target-getters";
|
|
import { isUrl, isWikiLink } from "./link-utils/link-validators";
|
|
import { removeFrontmatter } from "./utils/content-utils";
|
|
import { isImageExtension } from "./utils/image-utils";
|
|
import { getURIForWikiLinkTarget } from "./utils/wiki-link-utils";
|
|
|
|
/**
|
|
* Formats the file's data for rendering
|
|
* @param app - The Obsidian app
|
|
* @param settings - The plugin's settings
|
|
* @param file - The file to format
|
|
* @param fileId - The file's id. This is a randomly generated identifier for the file
|
|
* @param fileFrontmatter - The file's frontmatter
|
|
* @param fileContent - The file's content
|
|
* @param fileFavorite - The file's favorite status. This will be null if the file is a markdown file
|
|
* @returns A FileRenderData object that contains the file's data formatted for rendering
|
|
*/
|
|
export const formatFileDataForRender = ({
|
|
app,
|
|
settings,
|
|
file,
|
|
fileId,
|
|
fileContent,
|
|
}: {
|
|
app: App;
|
|
settings: VaultExplorerPluginSettings;
|
|
file: TFile;
|
|
fileId: string;
|
|
fileContent: string | null;
|
|
}): FileRenderData => {
|
|
const { name, basename, extension, path } = file;
|
|
|
|
const fileFrontmatter = app.metadataCache.getFileCache(file)?.frontmatter;
|
|
|
|
const { loadBodyTags } = settings;
|
|
const { coverImageSources } = settings.views.grid;
|
|
const {
|
|
createdDate: createdDateProp,
|
|
modifiedDate: modifiedDateProp,
|
|
url: urlProp,
|
|
image: imageProp,
|
|
coverImageFit: coverImageFitProp,
|
|
custom1: custom1Prop,
|
|
custom2: custom2Prop,
|
|
custom3: custom3Prop,
|
|
} = settings.properties;
|
|
|
|
let tags: string[] | null = loadPropertyValue<string[]>(
|
|
fileFrontmatter,
|
|
"tags",
|
|
PropertyType.LIST
|
|
);
|
|
|
|
if (loadBodyTags) {
|
|
const bodyTags = app.metadataCache.getFileCache(file)?.tags;
|
|
|
|
//Keep the tags array null if there are no tags in the frontmatter or body
|
|
if (bodyTags) {
|
|
//Remove the hash from the tags
|
|
const tagsWithoutHash = bodyTags.map((t) => t.tag.slice(1));
|
|
tags = Array.from(new Set([...(tags ?? []), ...tagsWithoutHash]));
|
|
}
|
|
}
|
|
|
|
const url: string | null = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
urlProp,
|
|
PropertyType.TEXT
|
|
);
|
|
|
|
let coverImageFit: CoverImageFit | null = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
coverImageFitProp,
|
|
PropertyType.TEXT
|
|
) as CoverImageFit | null;
|
|
if (coverImageFit === null) {
|
|
coverImageFit = settings.views.grid.coverImageFit;
|
|
}
|
|
|
|
const creationDate: string | null = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
createdDateProp,
|
|
PropertyType.DATE || PropertyType.DATETIME
|
|
);
|
|
const modifiedDate: string | null = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
modifiedDateProp,
|
|
PropertyType.DATE || PropertyType.DATETIME
|
|
);
|
|
|
|
const custom1: string | null = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
custom1Prop,
|
|
PropertyType.TEXT
|
|
);
|
|
const custom2: string | null = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
custom2Prop,
|
|
PropertyType.TEXT
|
|
);
|
|
const custom3: string | null = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
custom3Prop,
|
|
PropertyType.TEXT
|
|
);
|
|
|
|
let createdMillis = file.stat.ctime;
|
|
if (creationDate != null) {
|
|
//It's possible that the creation date is stored in the frontmatter in an unsupported date format
|
|
if (isDateSupported(creationDate)) {
|
|
createdMillis = getTimeMillis(creationDate);
|
|
} else {
|
|
Logger.warn(
|
|
`Property value has unsupported date format: ${creationDate}`
|
|
);
|
|
}
|
|
}
|
|
|
|
let modifiedMillis = file.stat.mtime;
|
|
if (modifiedDate != null) {
|
|
//It's possible that the modified date is stored in the frontmatter in an unsupported date format
|
|
if (isDateSupported(modifiedDate)) {
|
|
modifiedMillis = getTimeMillis(modifiedDate);
|
|
} else {
|
|
Logger.warn(
|
|
`Property value has unsupported date format: ${creationDate}`
|
|
);
|
|
}
|
|
}
|
|
|
|
let imageUrl: string | null = null;
|
|
|
|
//Handle image extension
|
|
if (isImageExtension(extension)) {
|
|
imageUrl = app.vault.getResourcePath(file);
|
|
}
|
|
|
|
for (const imageSource of coverImageSources) {
|
|
const { type, isEnabled } = imageSource;
|
|
if (isEnabled) {
|
|
switch (type) {
|
|
case "image-property": {
|
|
const loadedUrl = handleImagePropertyCoverSource(
|
|
app,
|
|
fileFrontmatter,
|
|
path,
|
|
imageProp
|
|
);
|
|
if (loadedUrl !== null) {
|
|
imageUrl = loadedUrl;
|
|
}
|
|
break;
|
|
}
|
|
case "url-property": {
|
|
const loadedUrl = handleUrlPropertyCoverSource(
|
|
fileFrontmatter,
|
|
urlProp
|
|
);
|
|
if (loadedUrl !== null) {
|
|
imageUrl = loadedUrl;
|
|
}
|
|
break;
|
|
}
|
|
case "frontmatter": {
|
|
const loadedUrl = handleFrontmatterCoverSource(
|
|
app,
|
|
fileFrontmatter,
|
|
path
|
|
);
|
|
if (loadedUrl !== null) {
|
|
imageUrl = loadedUrl;
|
|
}
|
|
break;
|
|
}
|
|
case "body": {
|
|
const loadedUrl = handleBodyCoverSource(
|
|
app,
|
|
path,
|
|
fileContent
|
|
);
|
|
if (loadedUrl !== null) {
|
|
imageUrl = loadedUrl;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error(
|
|
`Unhandled cover image source type: ${type}`
|
|
);
|
|
}
|
|
if (imageUrl !== null) {
|
|
break; // Exit the loop if a non-null URL is found
|
|
}
|
|
}
|
|
}
|
|
|
|
const displayName = extension === "md" ? basename : name;
|
|
const basePath = path.includes("/")
|
|
? path.substring(0, path.lastIndexOf("/"))
|
|
: "/";
|
|
|
|
return {
|
|
id: fileId,
|
|
displayName,
|
|
path,
|
|
baseName: basename,
|
|
coverImageFit,
|
|
basePath,
|
|
extension,
|
|
url,
|
|
content: fileContent,
|
|
tags,
|
|
imageUrl,
|
|
createdMillis,
|
|
modifiedMillis,
|
|
custom1,
|
|
custom2,
|
|
custom3,
|
|
};
|
|
};
|
|
|
|
const handleImagePropertyCoverSource = (
|
|
app: App,
|
|
fileFrontmatter: FrontMatterCache | undefined,
|
|
filePath: string,
|
|
imageProperty: string
|
|
) => {
|
|
const propertyValue = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
imageProperty,
|
|
PropertyType.TEXT
|
|
);
|
|
return getImageUrlFromProperty(app, filePath, propertyValue);
|
|
};
|
|
|
|
const handleUrlPropertyCoverSource = (
|
|
fileFrontmatter: FrontMatterCache | undefined,
|
|
urlProperty: string
|
|
) => {
|
|
const propertyValue = loadPropertyValue<string>(
|
|
fileFrontmatter,
|
|
urlProperty,
|
|
PropertyType.TEXT
|
|
);
|
|
if (propertyValue !== null) {
|
|
if (isUrl(propertyValue)) {
|
|
return propertyValue;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const handleFrontmatterCoverSource = (
|
|
app: App,
|
|
fileFrontmatter: FrontMatterCache | undefined,
|
|
filePath: string
|
|
) => {
|
|
const textProperties: FileTextProperties = loadTextProperties(
|
|
app,
|
|
fileFrontmatter
|
|
);
|
|
|
|
for (const property of textProperties) {
|
|
const { value } = property;
|
|
const imageUrl = getImageUrlFromProperty(app, filePath, value);
|
|
if (imageUrl) {
|
|
return imageUrl;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const handleBodyCoverSource = (
|
|
app: App,
|
|
filePath: string,
|
|
fileContent: string | null
|
|
) => {
|
|
if (fileContent === null) return null;
|
|
|
|
const body = removeFrontmatter(fileContent);
|
|
const firstInternalLink = getFirstInternalEmbed(body);
|
|
if (firstInternalLink) {
|
|
const target = getInternalEmbedTarget(firstInternalLink);
|
|
if (target) {
|
|
const uri = getURIForWikiLinkTarget(app, target, filePath);
|
|
if (uri) {
|
|
return uri;
|
|
}
|
|
}
|
|
}
|
|
|
|
const firstExternalLink = getFirstExternalEmbed(body);
|
|
if (firstExternalLink) {
|
|
const target = getExternalEmbedTarget(firstExternalLink);
|
|
if (target) {
|
|
return target;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const getImageUrlFromProperty = (
|
|
app: App,
|
|
filePath: string,
|
|
propertyValue: string | null
|
|
) => {
|
|
if (propertyValue === null) return null;
|
|
|
|
if (isWikiLink(propertyValue)) {
|
|
const target = getWikiLinkTarget(propertyValue);
|
|
if (target) {
|
|
const uri = getURIForWikiLinkTarget(app, target, filePath);
|
|
if (uri) {
|
|
return uri;
|
|
}
|
|
}
|
|
} else if (isUrl(propertyValue)) {
|
|
return propertyValue;
|
|
}
|
|
return null;
|
|
};
|