Merge pull request #167 from life-itself/152-rhs-toc

[Site/MDX]: Right hand side table of contents.
This commit is contained in:
Khalil Ali 2022-06-07 15:23:32 +03:00 committed by GitHub
commit 6631b23179
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 6607 additions and 244 deletions

View File

@ -192,7 +192,7 @@ Understand the deeper theoretical concepts behind the technical and economic cla
* [Predatory inclusion](../concepts/predatory-inclusion.md)
* [Enclosure](../concepts/enclosure.md)
**Meta**
#### Meta
* [Value](../concepts/value.md)
* [Risk](../concepts/risk.md)

View File

@ -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"
})
}

View File

@ -1,7 +1,10 @@
import Head from 'next/head'
import Nav from './Nav'
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 }) {
return (
@ -18,11 +21,11 @@ export default function Layout({ children }) {
{children}
</main>
<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">
{navLinks.map((item) => (
<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}
</a>
</div>
@ -36,21 +39,25 @@ export default function Layout({ children }) {
</a>
))}
</div>
<p className="flex items-center justify-center mt-8">
Created by
<a
href={siteConfig.authorUrl}
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}
{' '}
<div className="flex flex-col items-center mt-8 text-gray-400">
<p>
Created by
<a
href={siteConfig.authorUrl}
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}
{' '}
</a>
</p>
<p>
Licensed under a CC-By 4.0 International License
</a>
</p>
</p>
</div>
</div>
</footer>
</>
)
}
}

View File

@ -1,28 +1,15 @@
import Head from 'next/head'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import siteConfig from "../config/siteConfig"
import LiteYouTubeEmbed from "react-lite-youtube-embed"
import { YOUTUBE_REGEX } from "../lib/constants"
import { NextSeo } from "next-seo";
import LiteYouTubeEmbed from "react-lite-youtube-embed";
const Anchor = dynamic(() => import('./Anchor').then(module => module.Anchor), {
ssr: false
})
import { YOUTUBE_REGEX } from "../lib/constants";
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 = {
Head,
p: Paragraph,
a: Anchor
}
export default function MdxPage({ children }) {
const { Component, frontmatter: {
title, description, date, keywords, youtube, podcast, image, _raw
}} = children
let youtubeThumnbnail
let youtubeThumnbnail;
const youtubeId =
youtube && YOUTUBE_REGEX.test(youtube) && youtube.split(/^|=|\//).pop();
@ -47,12 +34,14 @@ export default function MdxPage({ children }) {
const SeoTitle = title ?? titleFromUrl;
const imageUrl = image
? siteConfig.url + image
: youtubeThumnbnail ? youtubeThumnbnail : null
: youtubeThumnbnail
? youtubeThumnbnail
: null;
// enable editing content only for claims, concepts, and guide for now
const editUrl = ['claims', 'concepts', 'guide'].includes(_raw.sourceFileDir)
? siteConfig.repoRoot + siteConfig.repoEditPath + _raw.sourceFilePath
: null
const editUrl = ["claims", "concepts", "guide"].includes(_raw.sourceFileDir)
? siteConfig.repoRoot + siteConfig.repoEditPath + _raw.sourceFilePath
: null;
return (
<>
@ -66,72 +55,89 @@ export default function MdxPage({ children }) {
url: `${siteConfig.url}/${_raw.flattenedPath}`,
type: "article",
article: {
tags: keywords ? keywords.split(",") : []
tags: keywords ? keywords.split(",") : [],
},
images: imageUrl
? ([
? [
{
url: imageUrl,
width: 1200,
height: 627,
alt: title,
type: "image/png"
type: "image/png",
},
])
]
: siteConfig.nextSeo.openGraph.images,
}}
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">
<header>
<div className="mb-6">
{title && <h1 className="mb-0">{title}</h1>}
{date && (
<p className="text-gray-900 dark:text-gray-500 text-sm pl-2">
on {date}
</p>
)}
{description && (
<p className="">{description}</p>
)}
{youtubeId && (
<LiteYouTubeEmbed id={youtubeId} />
)}
{podcast && (
<div className="pt-4">
<ul className="list-disc">
<li>
<a className="flex items-center" target="_blank" rel="noopener" href={podcast}>
<div className="w-4 mr-2">
<PodcastIcon />
</div>
<p className="m-0">Listen to this podcast</p>
</a>
</li>
</ul>
<div className="max-w-7xl mx-auto px-2 sm:px-6 md:px-8">
<article className="prose dark:prose-invert prose-a:break-all mx-auto lg:mr-[20rem] p-6">
<header>
<div className="mb-6">
{title && <h1 className="mb-0">{title}</h1>}
{date && (
<p className="text-gray-900 dark:text-gray-500 text-sm pl-2">
on {date}
</p>
)}
{description && <p className="">{description}</p>}
{youtubeId && <LiteYouTubeEmbed id={youtubeId} />}
{podcast && (
<div className="pt-4">
<ul className="list-disc">
<li>
<a
className="flex items-center"
target="_blank"
rel="noopener"
href={podcast}
>
<div className="w-4 mr-2">
<PodcastIcon />
</div>
<p className="m-0">Listen to this podcast</p>
</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>
</header>
<main>
<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>
</main>
</article>
</div>
</>
);
}

View File

@ -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;

View File

@ -4,6 +4,7 @@ import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import wikiLinkPlugin from "remark-wiki-link-plus"
import rehypeToc from "@jsdevtools/rehype-toc"
const isValidDate = dateObject => new Date(dateObject)
.toString() !== 'Invalid Date';
@ -58,8 +59,12 @@ export default makeSource({
mdx: {
remarkPlugins: [
remarkGfm,
[wikiLinkPlugin, { markdownFolder: 'content' }]
[ wikiLinkPlugin, { markdownFolder: 'content' } ]
],
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings]
rehypePlugins: [
rehypeSlug,
[ rehypeAutolinkHeadings, { behavior: 'wrap' } ],
[ rehypeToc, { position: 'afterend' } ]
]
}
})
})

View File

@ -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;

6403
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@
"postcss": "^8.3.5",
"prettier": "^2.6.2",
"rehype-autolink-headings": "^6.1.1",
"rehype-toc": "^3.0.2",
"rehype-slug": "^5.0.1",
"remark-gfm": "^3.0.0",
"remark-parse": "^10.0.1",

View File

@ -1,41 +1,30 @@
import MdxPage from '../components/MDX';
import { allOtherPages } from 'contentlayer/generated';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { allOtherPages } from "contentlayer/generated";
import MdxPage from "../components/MDX";
import siteConfig from "../config/siteConfig";
export default function Page({ body, ...rest }) {
const Component = useMDXComponent(body.code);
const children = {
Component,
frontmatter: {
...rest
},
};
return (
<MdxPage children={children} />
);
export default function Page({ body, ...meta }) {
return <MdxPage body={body} meta={meta} />;
}
export const getStaticProps = async ({ params }) => {
// All pages ending with .md in the /data folder are made available in allOtherPages
// Based on the specified slug, the correct page is selected
const urlPath = params.slug.join('/')
const page = allOtherPages.find(p => p._raw.flattenedPath === urlPath)
return { props: page }
}
const urlPath = params.slug.join("/");
const page = allOtherPages.find((p) => p._raw.flattenedPath === urlPath);
return { props: page };
};
export const getStaticPaths = async () => {
const paths = allOtherPages.map((page) => {
// demo => [demo]
// abc/demo => [abc,demo]
const parts = page._raw.flattenedPath.split('/')
return { params: { slug: parts } }
})
const parts = page._raw.flattenedPath.split("/");
return { params: { slug: parts } };
});
return {
paths,
fallback: false,
}
}
};
};

View File

@ -11,11 +11,18 @@
}
/* OTHERS */
html {
scroll-behavior: smooth;
}
.c-heading a {
@apply no-underline !important;
}
a:hover {
@apply text-yellow-li !important;
}
/* bg-neutral-800
@apply bg-slate-800
*/
@ -38,3 +45,64 @@ body {
width: 10rem;
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
}

View File

@ -1,13 +1,12 @@
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import find from 'unist-util-find'
import { toString } from 'mdast-util-to-string'
// get first paragraph found in the document
const documentExtract = (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);
}