mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-09 12:57:45 +00:00
move rehype config out of astro.config.ts
This commit is contained in:
@@ -7,17 +7,7 @@ import oembedTransformer from "@remark-embedder/transformer-oembed";
|
|||||||
import { TwitchTransformer } from "./src/utils/markdown/remark-embedder-twitch";
|
import { TwitchTransformer } from "./src/utils/markdown/remark-embedder-twitch";
|
||||||
import remarkTwoslash from "remark-shiki-twoslash";
|
import remarkTwoslash from "remark-shiki-twoslash";
|
||||||
import { UserConfigSettings } from "shiki-twoslash";
|
import { UserConfigSettings } from "shiki-twoslash";
|
||||||
import rehypeSlug from "rehype-slug-custom-id";
|
import { createRehypePlugins } from "./src/utils/markdown";
|
||||||
import { rehypeHeaderText } from "./src/utils/markdown/rehype-header-text";
|
|
||||||
import { rehypeTabs } from "./src/utils/markdown/tabs";
|
|
||||||
import { rehypeAstroImageMd } from "./src/utils/markdown/rehype-astro-image-md";
|
|
||||||
import { rehypeUnicornElementMap } from "./src/utils/markdown/rehype-unicorn-element-map";
|
|
||||||
import { rehypeExcerpt } from "./src/utils/markdown/rehype-excerpt";
|
|
||||||
import { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post";
|
|
||||||
import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count";
|
|
||||||
import { rehypeUnicornGetSuggestedPosts } from "./src/utils/markdown/rehype-unicorn-get-suggested-posts";
|
|
||||||
import { rehypeUnicornIFrameClickToRun } from "./src/utils/markdown/iframes/rehype-transform";
|
|
||||||
import { rehypeHeadingLinks } from "./src/utils/markdown/heading-links/rehype-transform";
|
|
||||||
import preact from "@astrojs/preact";
|
import preact from "@astrojs/preact";
|
||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
import { EnumChangefreq as ChangeFreq } from "sitemap";
|
import { EnumChangefreq as ChangeFreq } from "sitemap";
|
||||||
@@ -25,7 +15,6 @@ import { siteUrl } from "./src/constants/site-config";
|
|||||||
|
|
||||||
// TODO: Create types
|
// TODO: Create types
|
||||||
import behead from "remark-behead";
|
import behead from "remark-behead";
|
||||||
import rehypeRaw from "rehype-raw";
|
|
||||||
|
|
||||||
import image from "@astrojs/image";
|
import image from "@astrojs/image";
|
||||||
import mdx from "@astrojs/mdx";
|
import mdx from "@astrojs/mdx";
|
||||||
@@ -84,51 +73,6 @@ export default defineConfig({
|
|||||||
} as UserConfigSettings,
|
} as UserConfigSettings,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
rehypePlugins: [
|
rehypePlugins: createRehypePlugins({ format: "html" }),
|
||||||
rehypeUnicornPopulatePost,
|
|
||||||
rehypeUnicornGetSuggestedPosts,
|
|
||||||
// This is required to handle unsafe HTML embedded into Markdown
|
|
||||||
[rehypeRaw, { passThrough: [`mdxjsEsm`] }],
|
|
||||||
// Do not add the tabs before the slug. We rely on some of the heading
|
|
||||||
// logic in order to do some of the subheading logic
|
|
||||||
[
|
|
||||||
rehypeSlug,
|
|
||||||
{
|
|
||||||
maintainCase: true,
|
|
||||||
removeAccents: true,
|
|
||||||
enableCustomId: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
rehypeTabs,
|
|
||||||
{
|
|
||||||
injectSubheaderProps: true,
|
|
||||||
tabSlugifyProps: {
|
|
||||||
enableCustomId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
rehypeHeaderText,
|
|
||||||
/**
|
|
||||||
* Insert custom HTML generation code here
|
|
||||||
*/
|
|
||||||
[
|
|
||||||
rehypeAstroImageMd,
|
|
||||||
{
|
|
||||||
maxHeight: 768,
|
|
||||||
maxWidth: 768,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
rehypeUnicornIFrameClickToRun,
|
|
||||||
rehypeHeadingLinks,
|
|
||||||
rehypeUnicornElementMap,
|
|
||||||
[
|
|
||||||
rehypeExcerpt,
|
|
||||||
{
|
|
||||||
maxLength: 150,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
rehypeWordCount,
|
|
||||||
],
|
|
||||||
} as AstroUserConfig["markdown"] as never,
|
} as AstroUserConfig["markdown"] as never,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
// default sizing used for iframes (MarkdownRenderer/media.tsx)
|
// default sizing used for iframes (MarkdownRenderer/media.tsx)
|
||||||
export const EMBED_SIZE = { w: "100%", h: 500 };
|
export const EMBED_SIZE = { w: "100%", h: 500 };
|
||||||
|
|
||||||
|
export interface MarkdownConfig {
|
||||||
|
format: "html" | "epub";
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Root, Element } from "hast";
|
|||||||
import { Plugin } from "unified";
|
import { Plugin } from "unified";
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
import { HeaderLink } from "./heading-link";
|
import { HeaderLink } from "./heading-link";
|
||||||
|
import { toString } from "hast-util-to-string";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rehype plugin that adds a link SVG icon adjacent to each heading
|
* Rehype plugin that adds a link SVG icon adjacent to each heading
|
||||||
@@ -19,7 +20,7 @@ export const rehypeHeadingLinks: Plugin<[], Root> = () => {
|
|||||||
// create an absolute link icon adjacent to the header contents
|
// create an absolute link icon adjacent to the header contents
|
||||||
const hastHeader = HeaderLink({
|
const hastHeader = HeaderLink({
|
||||||
slug: node.properties.id.toString(),
|
slug: node.properties.id.toString(),
|
||||||
title: node.properties["data-header-text"].toString(),
|
title: toString(node),
|
||||||
});
|
});
|
||||||
|
|
||||||
node.children = [hastHeader, ...node.children];
|
node.children = [hastHeader, ...node.children];
|
||||||
|
|||||||
49
src/utils/markdown/index.ts
Normal file
49
src/utils/markdown/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { RehypePlugins } from "astro";
|
||||||
|
import rehypeSlug from "rehype-slug-custom-id";
|
||||||
|
import rehypeRaw from "rehype-raw";
|
||||||
|
import { rehypeTabs } from "./tabs/rehype-transform";
|
||||||
|
import { rehypeAstroImageMd } from "./rehype-astro-image-md";
|
||||||
|
import { rehypeUnicornElementMap } from "./rehype-unicorn-element-map";
|
||||||
|
import { rehypeExcerpt } from "./rehype-excerpt";
|
||||||
|
import { rehypeUnicornPopulatePost } from "./rehype-unicorn-populate-post";
|
||||||
|
import { rehypeWordCount } from "./rehype-word-count";
|
||||||
|
import { rehypeUnicornGetSuggestedPosts } from "./rehype-unicorn-get-suggested-posts";
|
||||||
|
import { rehypeUnicornIFrameClickToRun } from "./iframes/rehype-transform";
|
||||||
|
import { rehypeHeadingLinks } from "./heading-links/rehype-transform";
|
||||||
|
import { MarkdownConfig } from "./constants";
|
||||||
|
|
||||||
|
export function createRehypePlugins(config: MarkdownConfig): RehypePlugins {
|
||||||
|
return [
|
||||||
|
rehypeUnicornPopulatePost,
|
||||||
|
rehypeUnicornGetSuggestedPosts,
|
||||||
|
// This is required to handle unsafe HTML embedded into Markdown
|
||||||
|
[rehypeRaw, { passThrough: [`mdxjsEsm`] }],
|
||||||
|
// Do not add the tabs before the slug. We rely on some of the heading
|
||||||
|
// logic in order to do some of the subheading logic
|
||||||
|
[
|
||||||
|
rehypeSlug,
|
||||||
|
{
|
||||||
|
maintainCase: true,
|
||||||
|
removeAccents: true,
|
||||||
|
enableCustomId: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...(config.format === "html" && [
|
||||||
|
/**
|
||||||
|
* Insert custom HTML generation code here
|
||||||
|
*/
|
||||||
|
rehypeTabs,
|
||||||
|
rehypeAstroImageMd,
|
||||||
|
rehypeUnicornIFrameClickToRun,
|
||||||
|
rehypeHeadingLinks,
|
||||||
|
rehypeUnicornElementMap,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
rehypeExcerpt,
|
||||||
|
{
|
||||||
|
maxLength: 150,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rehypeWordCount,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -17,15 +17,10 @@ import { getLargestSourceSetSrc } from "../get-largest-source-set-src";
|
|||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
interface RehypeAstroImageProps {
|
const MAX_WIDTH = 768;
|
||||||
maxHeight?: number;
|
const MAX_HEIGHT = 768;
|
||||||
maxWidth?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rehypeAstroImageMd: Plugin<
|
export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
||||||
[RehypeAstroImageProps | never],
|
|
||||||
Root
|
|
||||||
> = ({ maxHeight, maxWidth }) => {
|
|
||||||
return async (tree, file) => {
|
return async (tree, file) => {
|
||||||
const imgNodes: any[] = [];
|
const imgNodes: any[] = [];
|
||||||
visit(tree, (node: any) => {
|
visit(tree, (node: any) => {
|
||||||
@@ -79,14 +74,14 @@ export const rehypeAstroImageMd: Plugin<
|
|||||||
|
|
||||||
const imgRatioHeight = dimensions.height / dimensions.width;
|
const imgRatioHeight = dimensions.height / dimensions.width;
|
||||||
const imgRatioWidth = dimensions.width / dimensions.height;
|
const imgRatioWidth = dimensions.width / dimensions.height;
|
||||||
if (maxHeight && dimensions.height > maxHeight) {
|
if (dimensions.height > MAX_HEIGHT) {
|
||||||
dimensions.height = maxHeight;
|
dimensions.height = MAX_HEIGHT;
|
||||||
dimensions.width = maxHeight * imgRatioWidth;
|
dimensions.width = MAX_HEIGHT * imgRatioWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxWidth && dimensions.width > maxWidth) {
|
if (dimensions.width > MAX_WIDTH) {
|
||||||
dimensions.width = maxWidth;
|
dimensions.width = MAX_WIDTH;
|
||||||
dimensions.height = maxWidth * imgRatioHeight;
|
dimensions.height = MAX_WIDTH * imgRatioHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pictureResult = await getPicture({
|
const pictureResult = await getPicture({
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { headingRank } from "hast-util-heading-rank";
|
|
||||||
import { hasProperty } from "hast-util-has-property";
|
|
||||||
import { toString } from "hast-util-to-string";
|
|
||||||
import { Root, Parent } from "hast";
|
|
||||||
import { visit } from "unist-util-visit";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin to add `data-header-text`s to headings.
|
|
||||||
*/
|
|
||||||
export const rehypeHeaderText = () => {
|
|
||||||
return (tree: Root, file) => {
|
|
||||||
visit(tree, "element", (node: Parent["children"][number]) => {
|
|
||||||
if (
|
|
||||||
headingRank(node) &&
|
|
||||||
"properties" in node &&
|
|
||||||
node.properties &&
|
|
||||||
!hasProperty(node, "data-header-text")
|
|
||||||
) {
|
|
||||||
const headerText = toString(node);
|
|
||||||
node.properties["data-header-text"] = headerText;
|
|
||||||
|
|
||||||
const headingWithID = {
|
|
||||||
value: headerText,
|
|
||||||
depth: headingRank(node)!,
|
|
||||||
slug: node.properties["id"] as string,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (file.data.astro.frontmatter.headingsWithId) {
|
|
||||||
file.data.astro.frontmatter.headingsWithId.push(headingWithID);
|
|
||||||
} else {
|
|
||||||
file.data.astro.frontmatter.headingsWithId = [headingWithID];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,37 +1,19 @@
|
|||||||
import { Root } from "hast";
|
import { Root, Element } from "hast";
|
||||||
import { Plugin } from "unified";
|
import { Plugin } from "unified";
|
||||||
|
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
|
|
||||||
import { EMBED_SIZE } from "./constants";
|
|
||||||
import { getFullRelativePath, isRelativePath } from "../url-paths";
|
import { getFullRelativePath, isRelativePath } from "../url-paths";
|
||||||
import { fromHtml } from "hast-util-from-html";
|
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
interface RehypeUnicornElementMapProps {}
|
|
||||||
|
|
||||||
function escapeHTML(s) {
|
|
||||||
if (!s) return s;
|
|
||||||
return s
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add switch/case and dedicated files ala "Components"
|
// TODO: Add switch/case and dedicated files ala "Components"
|
||||||
export const rehypeUnicornElementMap: Plugin<
|
export const rehypeUnicornElementMap: Plugin<[], Root> = () => {
|
||||||
[RehypeUnicornElementMapProps | never],
|
|
||||||
Root
|
|
||||||
> = () => {
|
|
||||||
return async (tree, file) => {
|
return async (tree, file) => {
|
||||||
const splitFilePath = path.dirname(file.path).split(path.sep);
|
const splitFilePath = path.dirname(file.path).split(path.sep);
|
||||||
// "collections" | "blog"
|
// "collections" | "blog"
|
||||||
const parentFolder = splitFilePath.at(-2);
|
const parentFolder = splitFilePath.at(-2);
|
||||||
const slug = splitFilePath.at(-1);
|
const slug = splitFilePath.at(-1);
|
||||||
|
|
||||||
visit(tree, (node: any) => {
|
visit(tree, (node: Element) => {
|
||||||
if (node.tagName === "video") {
|
if (node.tagName === "video") {
|
||||||
node.properties.muted ??= true;
|
node.properties.muted ??= true;
|
||||||
node.properties.autoPlay ??= true;
|
node.properties.autoPlay ??= true;
|
||||||
@@ -43,13 +25,13 @@ export const rehypeUnicornElementMap: Plugin<
|
|||||||
"/content/",
|
"/content/",
|
||||||
parentFolder,
|
parentFolder,
|
||||||
slug,
|
slug,
|
||||||
node.properties.src
|
node.properties.src.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.tagName === "a") {
|
if (node.tagName === "a") {
|
||||||
const href = node.properties.href;
|
const href = node.properties.href;
|
||||||
const isInternalLink = isRelativePath(href || "");
|
const isInternalLink = isRelativePath(href?.toString() || "");
|
||||||
if (!isInternalLink) {
|
if (!isInternalLink) {
|
||||||
node.properties.target = "_blank";
|
node.properties.target = "_blank";
|
||||||
node.properties.rel = "nofollow noopener noreferrer";
|
node.properties.rel = "nofollow noopener noreferrer";
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./rehype-transform";
|
|
||||||
@@ -60,11 +60,6 @@ const getApproxLineCount = (nodes: Node[], inParagraph?: boolean): number => {
|
|||||||
return lines;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RehypeTabsProps {
|
|
||||||
injectSubheaderProps?: boolean;
|
|
||||||
tabSlugifyProps?: Parameters<typeof getHeaderNodeId>[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin to add Docsify's tab support.
|
* Plugin to add Docsify's tab support.
|
||||||
* @see https://jhildenbiddle.github.io/docsify-tabs/
|
* @see https://jhildenbiddle.github.io/docsify-tabs/
|
||||||
@@ -82,10 +77,7 @@ export interface RehypeTabsProps {
|
|||||||
* To align with React Tabs package:
|
* To align with React Tabs package:
|
||||||
* @see https://github.com/reactjs/react-tabs
|
* @see https://github.com/reactjs/react-tabs
|
||||||
*/
|
*/
|
||||||
export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({
|
export const rehypeTabs: Plugin<[], Root> = () => {
|
||||||
injectSubheaderProps = false,
|
|
||||||
tabSlugifyProps = {},
|
|
||||||
}) => {
|
|
||||||
return (tree) => {
|
return (tree) => {
|
||||||
const replaceTabNodes = (nodes: Node[]) => {
|
const replaceTabNodes = (nodes: Node[]) => {
|
||||||
let sectionStarted = false;
|
let sectionStarted = false;
|
||||||
@@ -102,10 +94,9 @@ export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({
|
|||||||
if (isNodeLargestHeading(localNode, largestSize)) {
|
if (isNodeLargestHeading(localNode, largestSize)) {
|
||||||
// Make sure that all tabs labeled "thing" aren't also labeled "thing2"
|
// Make sure that all tabs labeled "thing" aren't also labeled "thing2"
|
||||||
slugs.reset();
|
slugs.reset();
|
||||||
const { id: headerSlug } = getHeaderNodeId(
|
const { id: headerSlug } = getHeaderNodeId(localNode, {
|
||||||
localNode,
|
enableCustomId: true,
|
||||||
tabSlugifyProps
|
});
|
||||||
);
|
|
||||||
|
|
||||||
tabs.push({
|
tabs.push({
|
||||||
slug: headerSlug,
|
slug: headerSlug,
|
||||||
@@ -118,7 +109,7 @@ export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For any other heading found in the tab contents, append to the nested headers array
|
// For any other heading found in the tab contents, append to the nested headers array
|
||||||
if (isNodeHeading(localNode) && injectSubheaderProps) {
|
if (isNodeHeading(localNode)) {
|
||||||
const lastTab = tabs.at(-1);
|
const lastTab = tabs.at(-1);
|
||||||
|
|
||||||
// Store the related tab ID in the attributes of the header
|
// Store the related tab ID in the attributes of the header
|
||||||
|
|||||||
Reference in New Issue
Block a user