Merge pull request #167 from life-itself/152-rhs-toc
[Site/MDX]: Right hand side table of contents.
This commit is contained in:
commit
6631b23179
|
|
@ -192,7 +192,7 @@ Understand the deeper theoretical concepts behind the technical and economic cla
|
||||||
* [Predatory inclusion](../concepts/predatory-inclusion.md)
|
* [Predatory inclusion](../concepts/predatory-inclusion.md)
|
||||||
* [Enclosure](../concepts/enclosure.md)
|
* [Enclosure](../concepts/enclosure.md)
|
||||||
|
|
||||||
**Meta**
|
#### Meta
|
||||||
|
|
||||||
* [Value](../concepts/value.md)
|
* [Value](../concepts/value.md)
|
||||||
* [Risk](../concepts/risk.md)
|
* [Risk](../concepts/risk.md)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
export const Heading = ({ level, observer }) => (props) => {
|
||||||
|
useEffect(() => {
|
||||||
|
/* start observing heading's intersection with the bounding box
|
||||||
|
* set by observer's `rootMargin` */
|
||||||
|
if (!observer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
observer.observe(document.getElementById(props.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
return React.createElement(`h${level}`, {
|
||||||
|
...props,
|
||||||
|
className: "c-heading scroll-mt-16 cursor-pointer"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import Nav from './Nav'
|
|
||||||
import siteConfig from '../config/siteConfig'
|
import siteConfig from '../config/siteConfig'
|
||||||
import navLinks from '../config/navLinks.js'
|
import navLinks from '../config/navLinks'
|
||||||
|
import Nav from './Nav'
|
||||||
|
// import Sidebar from './Sidebar'
|
||||||
|
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -18,11 +21,11 @@ export default function Layout({ children }) {
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<footer className="w-full h-24 mt-16">
|
<footer className="w-full h-24 mt-16">
|
||||||
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 md:px-8 overflow-hidden">
|
||||||
<nav className="-mx-5 -my-2 flex flex-wrap justify-center" aria-label="Footer">
|
<nav className="-mx-5 -my-2 flex flex-wrap justify-center" aria-label="Footer">
|
||||||
{navLinks.map((item) => (
|
{navLinks.map((item) => (
|
||||||
<div key={item.name} className="px-5 py-2">
|
<div key={item.name} className="px-5 py-2">
|
||||||
<a href={item.href} className="text-base text-gray-500 hover:text-gray-900">
|
<a href={item.href} className="text-base text-gray-400 hover:text-gray-500">
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -36,21 +39,25 @@ export default function Layout({ children }) {
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="flex items-center justify-center mt-8">
|
<div className="flex flex-col items-center mt-8 text-gray-400">
|
||||||
Created by
|
<p>
|
||||||
<a
|
Created by
|
||||||
href={siteConfig.authorUrl}
|
<a
|
||||||
target="_blank"
|
href={siteConfig.authorUrl}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
<img src={siteConfig.authorLogo} alt={siteConfig.author} width="20" height="20" className="mx-2 h-6 inline-block" />
|
>
|
||||||
{siteConfig.author}
|
<img src={siteConfig.authorLogo} alt={siteConfig.author} width="20" height="20" className="mx-2 h-6 inline-block" />
|
||||||
{' '}
|
{siteConfig.author}
|
||||||
|
{' '}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
Licensed under a CC-By 4.0 International License
|
Licensed under a CC-By 4.0 International License
|
||||||
</a>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,15 @@
|
||||||
import Head from 'next/head'
|
import { NextSeo } from "next-seo";
|
||||||
import dynamic from 'next/dynamic'
|
import LiteYouTubeEmbed from "react-lite-youtube-embed";
|
||||||
import { NextSeo } from 'next-seo'
|
|
||||||
import siteConfig from "../config/siteConfig"
|
|
||||||
import LiteYouTubeEmbed from "react-lite-youtube-embed"
|
|
||||||
import { YOUTUBE_REGEX } from "../lib/constants"
|
|
||||||
|
|
||||||
const Anchor = dynamic(() => import('./Anchor').then(module => module.Anchor), {
|
import { YOUTUBE_REGEX } from "../lib/constants";
|
||||||
ssr: false
|
import siteConfig from "../config/siteConfig";
|
||||||
})
|
import MdxContent from "./MdxContent";
|
||||||
|
|
||||||
const Paragraph = dynamic(() => import("./Paragraph").then(mod => mod.Paragraph))
|
export default function MdxPage({ body, meta }) {
|
||||||
|
const { title, description, date, keywords, youtube, podcast, image, _raw } =
|
||||||
|
meta;
|
||||||
|
|
||||||
const components = {
|
let youtubeThumnbnail;
|
||||||
Head,
|
|
||||||
p: Paragraph,
|
|
||||||
a: Anchor
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MdxPage({ children }) {
|
|
||||||
const { Component, frontmatter: {
|
|
||||||
title, description, date, keywords, youtube, podcast, image, _raw
|
|
||||||
}} = children
|
|
||||||
|
|
||||||
let youtubeThumnbnail
|
|
||||||
|
|
||||||
const youtubeId =
|
const youtubeId =
|
||||||
youtube && YOUTUBE_REGEX.test(youtube) && youtube.split(/^|=|\//).pop();
|
youtube && YOUTUBE_REGEX.test(youtube) && youtube.split(/^|=|\//).pop();
|
||||||
|
|
@ -47,12 +34,14 @@ export default function MdxPage({ children }) {
|
||||||
const SeoTitle = title ?? titleFromUrl;
|
const SeoTitle = title ?? titleFromUrl;
|
||||||
const imageUrl = image
|
const imageUrl = image
|
||||||
? siteConfig.url + image
|
? siteConfig.url + image
|
||||||
: youtubeThumnbnail ? youtubeThumnbnail : null
|
: youtubeThumnbnail
|
||||||
|
? youtubeThumnbnail
|
||||||
|
: null;
|
||||||
|
|
||||||
// enable editing content only for claims, concepts, and guide for now
|
// enable editing content only for claims, concepts, and guide for now
|
||||||
const editUrl = ['claims', 'concepts', 'guide'].includes(_raw.sourceFileDir)
|
const editUrl = ["claims", "concepts", "guide"].includes(_raw.sourceFileDir)
|
||||||
? siteConfig.repoRoot + siteConfig.repoEditPath + _raw.sourceFilePath
|
? siteConfig.repoRoot + siteConfig.repoEditPath + _raw.sourceFilePath
|
||||||
: null
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -66,72 +55,89 @@ export default function MdxPage({ children }) {
|
||||||
url: `${siteConfig.url}/${_raw.flattenedPath}`,
|
url: `${siteConfig.url}/${_raw.flattenedPath}`,
|
||||||
type: "article",
|
type: "article",
|
||||||
article: {
|
article: {
|
||||||
tags: keywords ? keywords.split(",") : []
|
tags: keywords ? keywords.split(",") : [],
|
||||||
},
|
},
|
||||||
images: imageUrl
|
images: imageUrl
|
||||||
? ([
|
? [
|
||||||
{
|
{
|
||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 627,
|
height: 627,
|
||||||
alt: title,
|
alt: title,
|
||||||
type: "image/png"
|
type: "image/png",
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
: siteConfig.nextSeo.openGraph.images,
|
: siteConfig.nextSeo.openGraph.images,
|
||||||
}}
|
}}
|
||||||
additionalMetaTags={[
|
additionalMetaTags={[
|
||||||
{ name: "keywords", content: keywords ? keywords : "" }
|
{ name: "keywords", content: keywords ? keywords : "" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<article className="prose dark:prose-invert prose-a:break-all mx-auto p-6">
|
<div className="max-w-7xl mx-auto px-2 sm:px-6 md:px-8">
|
||||||
<header>
|
<article className="prose dark:prose-invert prose-a:break-all mx-auto lg:mr-[20rem] p-6">
|
||||||
<div className="mb-6">
|
<header>
|
||||||
{title && <h1 className="mb-0">{title}</h1>}
|
<div className="mb-6">
|
||||||
{date && (
|
{title && <h1 className="mb-0">{title}</h1>}
|
||||||
<p className="text-gray-900 dark:text-gray-500 text-sm pl-2">
|
{date && (
|
||||||
on {date}
|
<p className="text-gray-900 dark:text-gray-500 text-sm pl-2">
|
||||||
</p>
|
on {date}
|
||||||
)}
|
</p>
|
||||||
{description && (
|
)}
|
||||||
<p className="">{description}</p>
|
{description && <p className="">{description}</p>}
|
||||||
)}
|
{youtubeId && <LiteYouTubeEmbed id={youtubeId} />}
|
||||||
{youtubeId && (
|
{podcast && (
|
||||||
<LiteYouTubeEmbed id={youtubeId} />
|
<div className="pt-4">
|
||||||
)}
|
<ul className="list-disc">
|
||||||
{podcast && (
|
<li>
|
||||||
<div className="pt-4">
|
<a
|
||||||
<ul className="list-disc">
|
className="flex items-center"
|
||||||
<li>
|
target="_blank"
|
||||||
<a className="flex items-center" target="_blank" rel="noopener" href={podcast}>
|
rel="noopener"
|
||||||
<div className="w-4 mr-2">
|
href={podcast}
|
||||||
<PodcastIcon />
|
>
|
||||||
</div>
|
<div className="w-4 mr-2">
|
||||||
<p className="m-0">Listen to this podcast</p>
|
<PodcastIcon />
|
||||||
</a>
|
</div>
|
||||||
</li>
|
<p className="m-0">Listen to this podcast</p>
|
||||||
</ul>
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="my-6">
|
||||||
|
<MdxContent body={body} />
|
||||||
|
{editUrl && (
|
||||||
|
<div className="mt-12 mb-6">
|
||||||
|
<a
|
||||||
|
className="flex no-underline font-semibold text-yellow-li"
|
||||||
|
href={editUrl}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Edit this page
|
||||||
|
<span className="mx-1">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</main>
|
||||||
</header>
|
</article>
|
||||||
<main>
|
</div>
|
||||||
<div className="my-6">
|
|
||||||
<Component components={components} />
|
|
||||||
</div>
|
|
||||||
{editUrl && (
|
|
||||||
<div className='mt-12 mb-6'>
|
|
||||||
<a className="flex no-underline font-semibold text-yellow-li" href={editUrl} target="_blank">
|
|
||||||
Edit this page
|
|
||||||
<span className="mx-1">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>)}
|
|
||||||
</main>
|
|
||||||
</article>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useMDXComponent } from "next-contentlayer/hooks";
|
||||||
|
|
||||||
|
import { Heading } from "./Heading";
|
||||||
|
import useHeadingsObserver from "../hooks/useHeadingsObserver";
|
||||||
|
|
||||||
|
const Anchor = dynamic(
|
||||||
|
() => import("./Anchor").then((module) => module.Anchor)
|
||||||
|
// {
|
||||||
|
// ssr: false,
|
||||||
|
// }
|
||||||
|
);
|
||||||
|
|
||||||
|
const Paragraph = dynamic(() =>
|
||||||
|
import("./Paragraph").then((module) => module.Paragraph)
|
||||||
|
);
|
||||||
|
|
||||||
|
const MdxContent = ({ body }) => {
|
||||||
|
const observer = useHeadingsObserver();
|
||||||
|
|
||||||
|
const customComponents = {
|
||||||
|
Head,
|
||||||
|
p: Paragraph,
|
||||||
|
a: Anchor,
|
||||||
|
h1: Heading({ level: 1, observer }),
|
||||||
|
h2: Heading({ level: 2, observer }),
|
||||||
|
h3: Heading({ level: 3, observer }),
|
||||||
|
h4: Heading({ level: 4, observer }),
|
||||||
|
h5: Heading({ level: 5, observer }),
|
||||||
|
h6: Heading({ level: 6, observer }),
|
||||||
|
};
|
||||||
|
const Component = useMDXComponent(body.code);
|
||||||
|
|
||||||
|
return <Component components={customComponents} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MdxContent;
|
||||||
|
|
@ -4,6 +4,7 @@ import remarkGfm from 'remark-gfm'
|
||||||
import rehypeSlug from 'rehype-slug'
|
import rehypeSlug from 'rehype-slug'
|
||||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||||
import wikiLinkPlugin from "remark-wiki-link-plus"
|
import wikiLinkPlugin from "remark-wiki-link-plus"
|
||||||
|
import rehypeToc from "@jsdevtools/rehype-toc"
|
||||||
|
|
||||||
const isValidDate = dateObject => new Date(dateObject)
|
const isValidDate = dateObject => new Date(dateObject)
|
||||||
.toString() !== 'Invalid Date';
|
.toString() !== 'Invalid Date';
|
||||||
|
|
@ -58,8 +59,12 @@ export default makeSource({
|
||||||
mdx: {
|
mdx: {
|
||||||
remarkPlugins: [
|
remarkPlugins: [
|
||||||
remarkGfm,
|
remarkGfm,
|
||||||
[wikiLinkPlugin, { markdownFolder: 'content' }]
|
[ wikiLinkPlugin, { markdownFolder: 'content' } ]
|
||||||
],
|
],
|
||||||
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings]
|
rehypePlugins: [
|
||||||
|
rehypeSlug,
|
||||||
|
[ rehypeAutolinkHeadings, { behavior: 'wrap' } ],
|
||||||
|
[ rehypeToc, { position: 'afterend' } ]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
/* Creates an Intersection Observer to keep track of headings intersecting
|
||||||
|
* a section of the viewport defined by the rootMargin */
|
||||||
|
const getIntersectionObserver = (callback) => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useHeadingsObserver = () => {
|
||||||
|
const [activeHeading, setActiveHeading] = useState("");
|
||||||
|
const [observer, setObserver] = useState(null);
|
||||||
|
|
||||||
|
/* Runs only after the first render, in order to preserve the observer
|
||||||
|
* between component rerenderings (e.g. after activeHeading change). */
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
// entries.forEach((entry) => {
|
||||||
|
// if (entry.isIntersecting) {
|
||||||
|
// setActiveHeading(entry.target.id);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
const firstIntersectingHeading = entries.find(
|
||||||
|
(entry) => entry.isIntersecting
|
||||||
|
);
|
||||||
|
if (firstIntersectingHeading) {
|
||||||
|
setActiveHeading(firstIntersectingHeading.target.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
threshold: 0.55,
|
||||||
|
rootMargin: "-65px 0% -85% 0%", // 65px is a navbar height
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setObserver(observer);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeHeading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tocLink = document.querySelector(
|
||||||
|
`.toc-link[href="#${activeHeading}"]`
|
||||||
|
);
|
||||||
|
tocLink.classList.add("active");
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tocLink.classList.remove("active");
|
||||||
|
};
|
||||||
|
}, [activeHeading]);
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useHeadingsObserver;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -31,6 +31,7 @@
|
||||||
"postcss": "^8.3.5",
|
"postcss": "^8.3.5",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
|
"rehype-toc": "^3.0.2",
|
||||||
"rehype-slug": "^5.0.1",
|
"rehype-slug": "^5.0.1",
|
||||||
"remark-gfm": "^3.0.0",
|
"remark-gfm": "^3.0.0",
|
||||||
"remark-parse": "^10.0.1",
|
"remark-parse": "^10.0.1",
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,30 @@
|
||||||
import MdxPage from '../components/MDX';
|
import { allOtherPages } from "contentlayer/generated";
|
||||||
import { allOtherPages } from 'contentlayer/generated';
|
|
||||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
|
||||||
|
|
||||||
|
import MdxPage from "../components/MDX";
|
||||||
|
import siteConfig from "../config/siteConfig";
|
||||||
|
|
||||||
export default function Page({ body, ...rest }) {
|
export default function Page({ body, ...meta }) {
|
||||||
const Component = useMDXComponent(body.code);
|
return <MdxPage body={body} meta={meta} />;
|
||||||
const children = {
|
|
||||||
Component,
|
|
||||||
frontmatter: {
|
|
||||||
...rest
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MdxPage children={children} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async ({ params }) => {
|
export const getStaticProps = async ({ params }) => {
|
||||||
// All pages ending with .md in the /data folder are made available in allOtherPages
|
// All pages ending with .md in the /data folder are made available in allOtherPages
|
||||||
// Based on the specified slug, the correct page is selected
|
// Based on the specified slug, the correct page is selected
|
||||||
const urlPath = params.slug.join('/')
|
const urlPath = params.slug.join("/");
|
||||||
const page = allOtherPages.find(p => p._raw.flattenedPath === urlPath)
|
const page = allOtherPages.find((p) => p._raw.flattenedPath === urlPath);
|
||||||
return { props: page }
|
return { props: page };
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
export const getStaticPaths = async () => {
|
||||||
const paths = allOtherPages.map((page) => {
|
const paths = allOtherPages.map((page) => {
|
||||||
// demo => [demo]
|
// demo => [demo]
|
||||||
// abc/demo => [abc,demo]
|
// abc/demo => [abc,demo]
|
||||||
const parts = page._raw.flattenedPath.split('/')
|
const parts = page._raw.flattenedPath.split("/");
|
||||||
return { params: { slug: parts } }
|
return { params: { slug: parts } };
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths,
|
paths,
|
||||||
fallback: false,
|
fallback: false,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* OTHERS */
|
/* OTHERS */
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-heading a {
|
||||||
|
@apply no-underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
@apply text-yellow-li !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* bg-neutral-800
|
/* bg-neutral-800
|
||||||
@apply bg-slate-800
|
@apply bg-slate-800
|
||||||
*/
|
*/
|
||||||
|
|
@ -38,3 +45,64 @@ body {
|
||||||
width: 10rem;
|
width: 10rem;
|
||||||
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 100%);
|
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* rehype-toc classes */
|
||||||
|
/* nav element */
|
||||||
|
.toc {
|
||||||
|
@apply
|
||||||
|
hidden
|
||||||
|
lg:block
|
||||||
|
w-[20rem]
|
||||||
|
my-12
|
||||||
|
pt-12
|
||||||
|
px-8
|
||||||
|
fixed
|
||||||
|
top-16
|
||||||
|
bottom-0
|
||||||
|
right-[max(2rem,calc(50%-40rem+2rem))]
|
||||||
|
overflow-y-auto
|
||||||
|
/* border-l */
|
||||||
|
/* border-slate-800 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* toc title */
|
||||||
|
.toc::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "On this page";
|
||||||
|
@apply text-white text-xl font-semibold top-1
|
||||||
|
}
|
||||||
|
|
||||||
|
/* list (ol) element */
|
||||||
|
.toc-level {
|
||||||
|
@apply list-none p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-level:not(.toc-level-1) {
|
||||||
|
@apply pl-2
|
||||||
|
}
|
||||||
|
|
||||||
|
/* list item (li) element */
|
||||||
|
.toc-item {
|
||||||
|
@apply leading-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item-h1 {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link {
|
||||||
|
@apply transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* link (a) element */
|
||||||
|
.toc-item .toc-link {
|
||||||
|
@apply text-sm text-slate-400 no-underline break-normal
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item .toc-link:not(.active) {
|
||||||
|
@apply hover:text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link.active {
|
||||||
|
@apply text-yellow-li
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { unified } from 'unified'
|
import { unified } from 'unified'
|
||||||
import remarkParse from 'remark-parse'
|
import remarkParse from 'remark-parse'
|
||||||
import find from 'unist-util-find'
|
|
||||||
import { toString } from 'mdast-util-to-string'
|
import { toString } from 'mdast-util-to-string'
|
||||||
|
|
||||||
|
|
||||||
// get first paragraph found in the document
|
// get first paragraph found in the document
|
||||||
const documentExtract = (md) => {
|
const documentExtract = (md) => {
|
||||||
const mdast = unified().use(remarkParse).parse(md);
|
const mdast = unified().use(remarkParse).parse(md);
|
||||||
let paragraph = find(mdast, (node) => node.type === "paragraph");
|
let paragraph = mdast.children.find((node) => node.type === "paragraph");
|
||||||
return toString(paragraph);
|
return toString(paragraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue