Skip to content

Commit

Permalink
iteration
Browse files Browse the repository at this point in the history
  • Loading branch information
elringus committed Dec 6, 2023
1 parent a3c658b commit 0a8a2a0
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 57 deletions.
29 changes: 26 additions & 3 deletions docs/.vitepress/imgit/plugin/youtube.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Plugin } from "../server";
import { BuiltAsset, ResolvedAsset } from "../server/asset";
import { Cache, cache, std, cfg } from "../server";

type YouTubeCache = Cache & {
/** Resolved thumbnail URLs mapped by YouTube video ID. */
youtube: Record<string, string>;
}

/** YouTube thumbnail variants; each video is supposed to have at least "default". */
const thumbs = ["maxresdefault", "hqdefault", "mqdefault", "sddefault", "default"];

/** Allows embedding YouTube videos with imgit.
* @example ![](https://www.youtube.com/watch?v=J7UwSVsiwzI) */
Expand All @@ -20,11 +29,10 @@ function getYouTubeId(url: string): string {
return new URL(url).searchParams.get("v")!;
}

function resolve(asset: ResolvedAsset): boolean {
async function resolve(asset: ResolvedAsset): Promise<boolean> {
if (!isYouTube(asset.syntax.url)) return false;
const id = getYouTubeId(asset.syntax.url);
asset.poster = { src: `https://img.youtube.com/vi/${id}/maxresdefault.jpg` };
asset.spec = {};
asset.content = { src: await resolveThumbnailUrl(id) };
return true;
}

Expand All @@ -38,3 +46,18 @@ function build(asset: BuiltAsset): boolean {
</div>`;
return true;
}

async function resolveThumbnailUrl(id: string): Promise<string> {
if ((<YouTubeCache>cache).youtube.hasOwnProperty(id))
return (<YouTubeCache>cache).youtube[id];
let response: Response;
for (const variant of thumbs)
if ((response = await std.fetch(buildThumbnailUrl(id, variant))).ok) break;
if (!response!.ok) cfg.log?.warn?.(`Failed to resolve thumbnail for YouTube video with ID '${id}'.`);
else (<YouTubeCache>cache).youtube[id] = response!.url;
return response!.url;
}

function buildThumbnailUrl(id: string, variant: string): string {
return `https://i.ytimg.com/vi_webp/${id}/${variant}.webp`;
}
28 changes: 10 additions & 18 deletions docs/.vitepress/imgit/server/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ export type AssetSyntax = {

/** Asset with resolved source content locations and specs. */
export type ResolvedAsset = CapturedAsset & {
/** Main source content of the asset, when applicable. */
main?: ResolvedContent;
/** Poster source content of the asset, when applicable. */
poster?: ResolvedContent;
/** Source content of the asset, when applicable. */
content?: ResolvedContent;
/** Optional user-defined asset specifications resolved (parsed) from captured syntax. */
spec: AssetSpec;
spec?: AssetSpec;
}

/** Source content of an asset resolved from captured syntax. */
Expand Down Expand Up @@ -59,10 +57,8 @@ export type AssetSpec = {

/** Asset with all the applicable source content files available on the local file system. */
export type FetchedAsset = ResolvedAsset & {
/** Main source content of the asset, when applicable. */
main?: FetchedContent;
/** Poster source content of the asset, when applicable. */
poster?: FetchedContent;
/** Source content of the asset, when applicable. */
content?: FetchedContent;
/** Whether any of the source content files were modified since last build. */
dirty?: boolean;
};
Expand All @@ -75,10 +71,8 @@ export type FetchedContent = ResolvedContent & {

/** Asset with identified source content. */
export type ProbedAsset = FetchedAsset & {
/** Main source content of the asset, when applicable. */
main?: ProbedContent;
/** Poster source content of the asset, when applicable. */
poster?: ProbedContent;
/** Source content of the asset, when applicable. */
content?: ProbedContent;
};

/** Identified source content of an asset. */
Expand All @@ -89,17 +83,15 @@ export type ProbedContent = FetchedContent & {

/** Asset with all the applicable encoded/generated content available on local file system. */
export type EncodedAsset = ProbedAsset & {
/** Main source content of the asset, when applicable. */
main?: EncodedContent;
/** Poster source content of the asset, when applicable. */
poster?: EncodedContent;
/** Source content of the asset, when applicable. */
content?: EncodedContent;
};

/** Optimized source of an asset with optional generated content. */
export type EncodedContent = ProbedContent & {
/** Full path to the asset's encoded/optimized content file on local file system. */
encoded: string;
/** Generate variant of the source content for compatibility/fallback, when applicable. */
/** Generated variant of the source content for compatibility/fallback, when applicable. */
safe?: string;
/** Generated variant of the source content for high-dpi displays, when applicable. */
dense?: string;
Expand Down
7 changes: 3 additions & 4 deletions docs/.vitepress/imgit/server/transform/2-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ export async function resolve(assets: CapturedAsset[]): Promise<ResolvedAsset[]>
async function resolveAsset(asset: ResolvedAsset): Promise<void> {
for (const resolver of cfg.resolvers)
if (await resolver(asset)) return;
asset.main = { src: asset.syntax.url };
asset.spec = resolveSpec(asset.syntax.spec);
asset.content = { src: asset.syntax.url };
asset.spec = asset.syntax.spec ? resolveSpec(asset.syntax.spec) : undefined;
}

function resolveSpec(query?: string): AssetSpec {
if (!query) return {};
function resolveSpec(query: string): AssetSpec {
const params = new URLSearchParams(query);
return {
eager: params.has("eager") ? true : undefined,
Expand Down
3 changes: 1 addition & 2 deletions docs/.vitepress/imgit/server/transform/3-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export async function fetch(assets: ResolvedAsset[]): Promise<FetchedAsset[]> {
}

async function fetchAsset(asset: FetchedAsset): Promise<void> {
if (asset.main) await fetchContent(asset.main, asset);
if (asset.poster) await fetchContent(asset.poster, asset);
if (asset.content) await fetchContent(asset.content, asset);
}

async function fetchContent(content: FetchedContent, asset: FetchedAsset): Promise<void> {
Expand Down
3 changes: 1 addition & 2 deletions docs/.vitepress/imgit/server/transform/4-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export async function probe(assets: FetchedAsset[]): Promise<ProbedAsset[]> {
}

async function probeAsset(asset: ProbedAsset): Promise<void> {
if (asset.main) await probeContent(asset.main, asset.dirty);
if (asset.poster) await probeContent(asset.poster, asset.dirty);
if (asset.content) await probeContent(asset.content, asset.dirty);
}

async function probeContent(content: ProbedContent, dirty?: boolean): Promise<void> {
Expand Down
22 changes: 10 additions & 12 deletions docs/.vitepress/imgit/server/transform/5-encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,15 @@ async function everythingIsFetched(): Promise<void> {
}

async function encodeAsset(asset: EncodedAsset): Promise<void> {
if (asset.main) {
await encodeMain(asset.main, asset);
await encodeSafe(asset.main, asset);
await encodeDense(asset.main, asset);
}
if (asset.poster) await encodeCover(asset.poster, asset);
else if (asset.main) await encodeCover(asset.main, asset);
if (!asset.content) return;
await encodeMain(asset.content, asset);
await encodeSafe(asset.content, asset);
await encodeDense(asset.content, asset);
await encodeCover(asset.content, asset);
}

async function encodeMain(content: EncodedContent, asset: EncodedAsset): Promise<void> {
const spec = getSpec(content.info.type, content.info, asset.spec.width);
const spec = getSpec(content.info.type, content.info, asset.spec?.width);
if (!spec) throw Error(`Failed to get encoding spec for ${content.src}.`);
const ext = content.info.type.startsWith("image/") ? "avif" : "mp4";
content.encoded = buildEncodedPath(content, ext);
Expand All @@ -43,7 +41,7 @@ async function encodeMain(content: EncodedContent, asset: EncodedAsset): Promise
async function encodeSafe(content: EncodedContent, asset: EncodedAsset): Promise<void> {
if (!cfg.encode.safe || isSafe(content.info.type, cfg.encode.safe.types)) return;
const type = content.info.type.startsWith("image/") ? "image/webp" : "video/mp4";
const spec = getSpec(type, content.info, asset.spec.width);
const spec = getSpec(type, content.info, asset.spec?.width);
if (!spec) return;
spec.codec = undefined;
const ext = type.substring(type.indexOf("/") + 1);
Expand All @@ -53,9 +51,9 @@ async function encodeSafe(content: EncodedContent, asset: EncodedAsset): Promise

async function encodeDense(content: EncodedContent, asset: EncodedAsset): Promise<void> {
if (!cfg.encode.dense || !content.info.type.startsWith("image/")) return;
const threshold = getThreshold(asset.spec.width);
const threshold = getThreshold(asset.spec?.width);
if (!threshold || content.info.width < threshold * 2) return;
const spec = getSpec(content.info.type, content.info, asset.spec.width);
const spec = getSpec(content.info.type, content.info, asset.spec?.width);
if (!spec) return;
spec.scale = undefined;
content.dense = buildEncodedPath(content, "avif", cfg.encode.dense.suffix);
Expand All @@ -64,7 +62,7 @@ async function encodeDense(content: EncodedContent, asset: EncodedAsset): Promis

async function encodeCover(content: EncodedContent, asset: EncodedAsset): Promise<void> {
if (!cfg.cover || !cfg.encode.cover) return;
const scale = getScale(cfg.encode.cover, content.info, asset.spec.width);
const scale = getScale(cfg.encode.cover, content.info, asset.spec?.width);
const spec = { ...cfg.encode.cover, scale };
content.cover = buildEncodedPath(content, "avif", cfg.encode.cover.suffix);
await encodeContent(`${content.src}@cover`, content.local, content.cover, content.info, spec);
Expand Down
30 changes: 14 additions & 16 deletions docs/.vitepress/imgit/server/transform/6-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { std, cfg, cache } from "../common";
export async function build(assets: EncodedAsset[]): Promise<BuiltAsset[]> {
const merges = new Array<BuiltAsset>;
for (let i = assets.length - 1; i >= 0; i--)
if (assets[i].spec.merge) merges.push(<BuiltAsset>assets[i]);
if (assets[i].spec?.merge) merges.push(<BuiltAsset>assets[i]);
else await buildAsset(<BuiltAsset>assets[i], merges).then(() => merges.length = 0);
return <BuiltAsset[]>assets;
}
Expand All @@ -15,10 +15,10 @@ async function buildAsset(asset: BuiltAsset, merges: BuiltAsset[]): Promise<void
for (const builder of cfg.builders)
if (await builder(asset, merges)) return;
for (const merge of merges) merge.html = "";
if (!asset.main) throw Error(`Failed to build HTML for '${asset.syntax.url}': missing content.`);
if (asset.main.info.type.startsWith("image/")) return buildPicture(asset.main, asset, merges);
if (asset.main.info.type.startsWith("video/")) return buildVideo(asset.main, asset, merges);
throw Error(`Failed to build HTML for '${asset.syntax.url}': unknown type (${asset.main.info.type}).`);
if (!asset.content) throw Error(`Failed to build HTML for '${asset.syntax.url}': missing content.`);
if (asset.content.info.type.startsWith("image/")) return buildPicture(asset.content, asset, merges);
if (asset.content.info.type.startsWith("video/")) return buildVideo(asset.content, asset, merges);
throw Error(`Failed to build HTML for '${asset.syntax.url}': unknown type (${asset.content.info.type}).`);
}

async function buildPicture(content: EncodedContent, asset: BuiltAsset, merges: BuiltAsset[]): Promise<void> {
Expand All @@ -27,7 +27,7 @@ async function buildPicture(content: EncodedContent, asset: BuiltAsset, merges:
const load = lazy ? `loading="lazy" decoding="async"` : `decoding="sync"`;
let sourcesHtml = await buildPictureSources(content, asset);
for (const merge of merges)
if (merge.main) sourcesHtml += await buildPictureSources(merge.main, merge);
if (merge.content) sourcesHtml += await buildPictureSources(merge.content, merge);
sourcesHtml += `<img data-imgit-loadable alt="${asset.syntax.alt}" ${size} ${load}/>`;
asset.html = `
<div class="imgit-picture" data-imgit-container>
Expand All @@ -40,8 +40,8 @@ async function buildPictureSources(content: EncodedContent, asset: BuiltAsset) {
const safe = await serve(content.safe ?? content.local, asset);
const encoded = await serve(content.encoded, asset);
const dense = content.dense && await serve(content.dense, asset);
return buildPictureSource(encoded, "image/avif", dense, asset.spec.media) +
buildPictureSource(safe, undefined, undefined, asset.spec.media);
return buildPictureSource(encoded, "image/avif", dense, asset.spec?.media) +
buildPictureSource(safe, undefined, undefined, asset.spec?.media);
}

function buildPictureSource(src: string, type?: string, dense?: string, media?: string): string {
Expand All @@ -55,7 +55,7 @@ async function buildVideo(content: EncodedContent, asset: BuiltAsset, merges: Bu
const safe = await serve(content.safe ?? content.local, asset);
const encoded = await serve(content.encoded, asset);
const size = buildSizeAttributes(content.info);
const media = asset.spec.media ? `media="${asset.spec.media}"` : "";
const media = asset.spec?.media ? `media="${asset.spec.media}"` : "";
// https://jakearchibald.com/2022/html-codecs-parameter-for-av1
const codec = "av01.0.04M.08"; // TODO: Resolve actual spec at the encoding stage.
asset.html = `
Expand All @@ -70,19 +70,17 @@ async function buildVideo(content: EncodedContent, asset: BuiltAsset, merges: Bu

async function buildCover(asset: EncodedAsset, size: string, merges: BuiltAsset[]): Promise<string> {
if (!cfg.cover) return "";
const path = asset.poster?.cover ?? asset.main?.cover;
let sourcesHtml = path ? await buildCoverSource(path, asset) : "";
for (const merge of merges) {
const path = merge.poster?.cover ?? merge.main?.cover;
if (path && merge.main) sourcesHtml += await buildCoverSource(path, merge);
}
let sourcesHtml = asset.content?.cover ? await buildCoverSource(asset.content.cover, asset) : "";
for (const merge of merges)
if (merge.content?.cover && merge.content)
sourcesHtml += await buildCoverSource(merge.content?.cover, merge);
sourcesHtml += `<img src="${cfg.cover}" alt="cover" ${size} decoding="sync"/>`;
return `<picture class="imgit-cover">${sourcesHtml}</picture>`;
}

async function buildCoverSource(path: string, asset: EncodedAsset): Promise<string> {
const avif = await getCoverBase64(asset.syntax.url, path, asset.dirty);
const mediaAttr = asset.spec.media ? `media="${asset.spec.media}"` : "";
const mediaAttr = asset.spec?.media ? `media="${asset.spec.media}"` : "";
return `<source srcset="${avif}" type="image/avif" ${mediaAttr}/>`;
}

Expand Down

0 comments on commit 0a8a2a0

Please sign in to comment.