Skip to content

Commit

Permalink
refactor: link transformation
Browse files Browse the repository at this point in the history
  • Loading branch information
yy4382 committed Oct 22, 2024
1 parent 0bf1433 commit 16055ce
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 90 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"github-slugger": "^2.0.0",
"isomorphic-git": "^1.27.1",
"ramda": "^0.30.1",
"yaml": "^2.6.0"
"yaml": "^2.6.0",
"zod": "^3.23.8"
},
"packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b"
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 49 additions & 23 deletions src/transform/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import type { TFile, Pos } from "obsidian";
import type {
TFile,
Pos,
CachedMetadata,
FrontMatterCache,
App,
Notice,
} from "obsidian";
import { ImageTransformer } from "@/Image/base";
import type { TransformCtx, Entry, Post } from "@/type";
import { processLinks } from "./link";
import { transformTag } from "./tag";
import { getImageTransformer } from "@/Image";
import { validateMeta } from "./validate";
import { z } from "zod";
import { SSSettings } from "@/type";

/*
Transform pipeline:
Expand All @@ -20,6 +29,34 @@ Transform pipeline:
- Run all functions in "actions" array
*/

export type CachedMetadataPost = Omit<
CachedMetadata,
"frontmatter" | "frontmatterPosition"
> & {
frontmatter: z.infer<typeof frontmatterSchema> & FrontMatterCache;
frontmatterPosition: NonNullable<CachedMetadata["frontmatterPosition"]>;
};
export const frontmatterSchema = z
.object({
slug: z.string(),
published: z.literal(true),
})
.passthrough();

export interface TransformCtx {
cachedRead: App["vault"]["cachedRead"];
readBinary: App["vault"]["readBinary"];
getFileMetadata: App["metadataCache"]["getFileCache"];
resolveLink: App["metadataCache"]["getFirstLinkpathDest"];
notice: (...args: ConstructorParameters<typeof Notice>) => void;
settings: SSSettings;
}

export interface Post {
tFile: TFile;
content: string;
meta: CachedMetadataPost;
}
export type TransformCtxWithImage = TransformCtx & {
imageTf: ImageTransformer;
};
Expand All @@ -46,9 +83,11 @@ export async function transform(
))(ctx);
await imageTf.onBeforeTransform();
const ctxWithImage: TransformCtxWithImage = { ...ctx, imageTf };

const transformedPosts = await Promise.all(
originalPosts.map((post) => normalizePost(post, ctxWithImage)),
);

await imageTf.onFinish();
return transformedPosts;
}
Expand All @@ -69,29 +108,16 @@ const readAndFilterValidPosts = async (
postFiles.map(async (tFile: TFile) => ({
tFile: tFile,
content: await ctx.cachedRead(tFile),
meta: ctx.getFileMetadata(tFile),
meta: validateMeta(tFile, ctx),
})),
)
)
.map((post) => validateEntry(post, ctx))
.filter((post): post is Post => post !== undefined);

/**
* Validates the given entry to ensure it has the necessary metadata.
*
* @param post - The entry to validate.
* @returns The validated post if it contains the required metadata, otherwise `undefined`.
*/
function validateEntry(post: Entry, ctx: TransformCtx): Post | undefined {
if (!(post.meta && post.meta.frontmatterPosition && post.meta.frontmatter))
return undefined;
if (!post.meta.frontmatter.published) return undefined;
if (!post.meta.frontmatter.slug) {
ctx.notice(`Post "${post.tFile.name}" does not have slug`);
return undefined;
}
return post as Post;
}
).filter((post): post is Post => {
if (post.meta instanceof Error) {
ctx.notice(`Error validating "${post.tFile.path}": ${post.meta.message}`);
return false;
}
return true;
});

async function normalizePost(
post: Post,
Expand Down
78 changes: 43 additions & 35 deletions src/transform/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type { Post } from "@/type";
import type { ReferenceCache, TFile } from "obsidian";
import { slug as slugger } from "github-slugger";
import type { TransformAction, TransformCtxWithImage } from "@/transform/index";
import { validateMeta } from "./validate";

const IMAGE_EXT = ["png", "jpg", "jpeg", "gif", "svg", "webp"];

export async function processLinks(
post: Post,
Expand All @@ -25,58 +28,63 @@ export async function processLinks(
},
];
}

/**
* Case 1: target not found / target is not md nor image / target not valid post (not published)
* - link.original
* Case 2: target is image
* - imageTf.onTransform
* Case 3: target is another post
* - normal link
* @param link
* @param sourceTFile
* @param ctx
* @returns
*/
async function transformLink(
link: ReferenceCache,
sourceTFile: TFile,
ctx: TransformCtxWithImage,
): Promise<string> {
const targetFile = ctx.resolveLink(link.link.split("#")[0], sourceTFile.path);

if (targetFile == null) {
console.warn(`link not found:`, link.original);
return link.original;
}
const imgExt = ["png", "jpg", "jpeg", "gif", "svg", "webp"];
if (imgExt.includes(targetFile.extension)) {
{
return await ctx.imageTf.onTransform(link, sourceTFile, targetFile);
}
} else if (targetFile.extension === "md") {
{
const slug = getSlug(targetFile, ctx);
const displayText = link.displayText ?? link.link;
if (slug) {
const fragment = link.link.includes("#")
? slugger(link.link.split("#")[1])
: undefined;
if (targetFile.path === sourceTFile.path && fragment) {
return `[${displayText}](#${fragment})`;
} else {
return `[${displayText}](${slugToPath(slug, ctx) + (fragment ? "#" + fragment : "")})`;
}
} else {
console.warn(
`link target not published: ${targetFile.name} from ${sourceTFile.name}`,
);
return link.original;
}
}
} else {
{
console.error(
`unknown ext ${targetFile.extension} for ${targetFile.name}`,
);

if (IMAGE_EXT.includes(targetFile.extension))
return await ctx.imageTf.onTransform(link, sourceTFile, targetFile);

if (targetFile.extension === "md") {
const slug = getSlug(targetFile, ctx);

// target not published
if (!slug) {
console.warn(
`link target not published: "${targetFile.name}" from "${sourceTFile.name}"`,
);
return link.original;
}

const displayText = link.displayText ?? link.link;
const fragment = link.link.includes("#")
? "#" + slugger(link.link.split("#").slice(1).join("#"))
: "";
if (targetFile.path === sourceTFile.path && fragment) {
return `[${displayText}](${fragment})`;
} else {
return `[${displayText}](${slugToPath(slug, ctx)}${fragment})`;
}
}

console.error(`unknown ext ${targetFile.extension} for ${targetFile.name}`);
return link.original;
}

function getSlug(file: TFile, ctx: TransformCtxWithImage): string | null {
const { published, slug } = ctx.getFileMetadata(file)?.frontmatter ?? {};

if (!published) return null;
return slug ?? null;
const meta = validateMeta(file, ctx);
if (meta instanceof Error) return null;
return meta.frontmatter.slug;
}

function slugToPath(slug: string, ctx: TransformCtxWithImage): string {
Expand Down
40 changes: 40 additions & 0 deletions src/transform/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CachedMetadataPost, frontmatterSchema } from ".";
import { App, TFile } from "obsidian";
export class PostValidationError extends Error {
tFile: TFile;
constructor(message: string, tFile: TFile) {
super(message);
this.tFile = tFile;
}
}

function formatZodError(
error: NonNullable<
ReturnType<(typeof frontmatterSchema)["safeParse"]>["error"]
>,
) {
const formattedError = error.issues.reduce((acc, issue) => {
const message = `"${issue.path.join(".")}": ${issue.message}`;
return acc + message + "\n";
}, "\n");
return formattedError;
}

export function validateMeta(
file: TFile,
ctx: {
getFileMetadata: App["metadataCache"]["getFileCache"];
},
): CachedMetadataPost | PostValidationError {
if (file.extension !== "md")
return new PostValidationError("Not a markdown file", file);

const meta = ctx.getFileMetadata(file);
if (!meta || !meta.frontmatter || !meta.frontmatterPosition)
return new PostValidationError("No frontmatter", file);

const { error } = frontmatterSchema.safeParse(meta.frontmatter);
if (error) return new PostValidationError(formatZodError(error), file);

return meta as CachedMetadataPost;
}
30 changes: 1 addition & 29 deletions src/type.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,2 @@
import { App, CachedMetadata, TFile, Notice } from "obsidian";
import type { SSSettings } from "@/Settings";

export interface Entry {
tFile: TFile;
content: string;
meta: CachedMetadata | null;
}

export interface TransformCtx {
cachedRead: App["vault"]["cachedRead"];
readBinary: App["vault"]["readBinary"];
getFileMetadata: App["metadataCache"]["getFileCache"];
resolveLink: App["metadataCache"]["getFirstLinkpathDest"];
notice: (...args: ConstructorParameters<typeof Notice>) => void;
settings: SSSettings;
}

type CachedMetadataPost = Omit<
CachedMetadata,
"frontmatter" | "frontmatterPosition"
> & {
frontmatter: NonNullable<CachedMetadata["frontmatter"]>;
frontmatterPosition: NonNullable<CachedMetadata["frontmatterPosition"]>;
};
export interface Post extends Entry {
meta: CachedMetadataPost;
}

export type { SSSettings } from "./Settings";
export type { Post } from "./transform";
3 changes: 1 addition & 2 deletions src/utils/stringifyPost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { stringify } from "yaml";
export const stringifyPost = (
post: Post,
): { filename: string; content: string } => {
const filename: string =
(post.meta.frontmatter.slug ?? post.tFile.basename) + ".md";
const filename: string = post.meta.frontmatter.slug + ".md";

const frontmatter = stringify(post.meta.frontmatter);
const content = `---\n${frontmatter}---\n\n` + post.content.trim();
Expand Down

0 comments on commit 16055ce

Please sign in to comment.