_includes/resources/playlist/PlaylistLayout.11ty.tsx (160 lines of code) (raw):

import { Playlist, PlaylistFrontmatter } from "./PlaylistModels"; import { LayoutContext, LayoutProps } from "../../../src/models"; import VideoPlayer from "../../video/VideoPlayer.11ty"; import { HTMLElement, parse } from "node-html-parser"; import { BaseLayout } from "../../layouts/BaseLayout.11ty"; import ArticleTitleSubtitle from "../common/ArticleTitleSubtitle.11ty"; import ArticleAuthor from "../common/ArticleAuthor.11ty"; import ArticleTopics from "../common/ArticleTopics.11ty"; import { Author } from "../author/AuthorModels"; import AnimatedGif from "../../animatedgif/AnimatedGif.11ty"; import { Fragment } from "jsx-async-runtime/jsx-dev-runtime"; import path from "upath"; import EditArticle from "../common/EditArticle.11ty"; export type PlaylistLayoutData = LayoutProps & PlaylistFrontmatter; function relativizeContentUrl(originalUrl: string, contentUrl: string) { if (!originalUrl) return originalUrl; // noinspection SuspiciousTypeOfGuard Guess what, we have videos that are an object, not a string if (typeof originalUrl !== "string") return originalUrl; // rewrite relative URL if (originalUrl.startsWith(".") && !originalUrl.startsWith("../")) { return contentUrl + originalUrl; } return originalUrl; } function relativize(originalUrl: string, content: string) { const prefix = originalUrl; //`../../${originalUrl}`; const doc = parse(content); const anchors = doc.getElementsByTagName("a"); const imgs = doc.getElementsByTagName("img"); function rewriteAttribute(element: HTMLElement, attribute: string) { const href = element.attrs[attribute]; // no value if (!href) return; // already good if (href.startsWith(prefix)) return; // data URI if (href.startsWith("data:")) return; // absolute urls if (href.startsWith("http://") || href.startsWith("https://")) return; // relative paths if (href.startsWith(".") && !href.startsWith("../")) return; // root link if (href.startsWith("/")) return; //anchor link if (href.startsWith("#")) return; // ignore VITE ASSETS if (href.startsWith("__VITE_ASSET__")) return; if (prefix && href) { element.setAttribute(attribute, path.join(prefix, href)); } } anchors.forEach((element) => { rewriteAttribute(element, "href"); }); imgs.forEach((element) => { rewriteAttribute(element, "src"); }); return doc.toString(); } export function PlaylistLayout( this: LayoutContext, data: PlaylistLayoutData, ): JSX.Element { const { collections, content, page } = data; const playlist = collections.resourceMap.get(page.url) as Playlist; if (!playlist) { throw new Error(`Playlist "${page.url}" not in collection`); } const { all } = data.collections; // Main content const author = playlist.references?.author as Author; if (!author) { throw new Error(`Author "${playlist.author}" not in collection`); } const main = ( <Fragment> <ArticleTitleSubtitle title={playlist.title} subtitle={playlist.subtitle} /> <EditArticle path={page.inputPath} /> <ArticleAuthor author={author} displayDate={playlist.displayDate} /> {playlist.references?.topics && ( <ArticleTopics topics={playlist.references?.topics} /> )} <div class="content" style="margin-bottom: 3rem"> {content} </div> {playlist.playlistResources.map((item: any, index: number) => { const thisItem = all.find((i) => i.page.url === item.url); const itemContent = thisItem ? relativize(thisItem.page.url, thisItem.content) : ""; const isVisible = index == 0 ? "" : "display:none"; // rewrite relative URLs const animatedGif = item.animatedGif; if (animatedGif) { animatedGif.file = relativizeContentUrl(animatedGif.file, item.url); } let screenshot = item.screenshot; if (screenshot) { screenshot = relativizeContentUrl(screenshot, item.url); } let video = item.video; if (video) { video = relativizeContentUrl(video, item.url); } return ( <div id={item.anchor} style={isVisible} class="playlist-item"> <h2 class="is-size-2">{item.title}</h2> {item.subtitle && <p class="subtitle is-4">{item.subtitle}</p>} {animatedGif && ( <AnimatedGif {...animatedGif} width="600" style="object-fit: contain; object-position: top" /> )} {screenshot && ( <img src={screenshot} alt="Tip Screenshot" width="600" style="object-fit: contain; object-position: top" /> )} {video && <VideoPlayer source={video}></VideoPlayer>} {item.linkURL && ( <p class={video ? "mt-4" : ""}> <a href={item.linkURL} class="link-external"> View at original site </a> </p> )} {itemContent && <div class="content mt-4">{itemContent}</div>} </div> ); })} </Fragment> ); // Sidebar const sidebarSteps = ( <div class="column is-3 is-full-touch"> <aside class="menu"> <p class="menu-label">Playlist</p> <ul class="menu-list playlist-toggles"> {playlist.playlistResources.map((step) => ( <li> <a aria-label="Playlist Item" href={`#${step.anchor}`}> {step.title} </a> </li> ))} </ul> </aside> </div> ); // data-meta will be processed out return ( <BaseLayout subtitle={playlist.subtitle} {...data}> <div class="section"> <div class="container"> <div class="columns is-multiline"> {sidebarSteps} <div class="column is-9"> <main class="content">{main}</main> </div> </div> </div> </div> </BaseLayout> ); } export const render = PlaylistLayout;