-
Notifications
You must be signed in to change notification settings - Fork 1
feature: Table of Contents #202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
68b0a12
0778478
944baa9
e017a01
0c8bf8c
f1eab5e
a366605
171d071
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| import type { HTMLTag } from 'astro/types'; | ||
| import type { Heading } from 'datocms-structured-text-utils'; | ||
| import { htmlIdSlug } from '@components/HtmlIdSlugProvider/'; | ||
|
|
||
| interface Props { | ||
| id: string; | ||
| level: Heading['level']; | ||
| } | ||
| const { id, level = 2, ...props } = Astro.props; | ||
|
|
||
| const slug = htmlIdSlug({ | ||
| id, | ||
| html: await Astro.slots.render('default') | ||
| }); | ||
|
|
||
| const Tag = `h${level}` as HTMLTag; | ||
| --- | ||
|
|
||
| <Tag {...props} id={ slug } tabindex="-1"><slot /></Tag> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| import { resetHtmlIdSlugger } from './index'; | ||
|
|
||
| resetHtmlIdSlugger(); | ||
| --- | ||
|
|
||
| <slot /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import GithubSlugger from 'github-slugger'; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Placing this script and export the May need to use a different mechanism, like https://docs.astro.build/en/recipes/sharing-state/ ?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe we could use an actual |
||
| import { fromHtml } from 'hast-util-from-html'; | ||
| import { toString } from 'hast-util-to-string'; | ||
|
|
||
| const slugger = new GithubSlugger(); | ||
|
|
||
| export const resetHtmlIdSlugger = () => { | ||
| slugger.reset(); | ||
| }; | ||
|
|
||
| export const htmlIdSlug = ({ id, html }: { id?: string; html: string }) => { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should add test coverage for these methods |
||
| if (id) { | ||
| return slugger.slug(id); | ||
| } | ||
| const tree = fromHtml(html, { fragment: true }); | ||
| const text = toString(tree); | ||
| return slugger.slug(text); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| --- | ||
| import type { TreeItem } from './index'; | ||
|
|
||
| interface Props { | ||
| items: TreeItem[]; | ||
| } | ||
| const { items } = Astro.props; | ||
| --- | ||
|
|
||
| <ol> | ||
| { items.map((item) => ( | ||
| <li class:list={[ `level-${item.level}` ]}> | ||
| <a href={ `#${ item.id }` }>{ item.text }</a> | ||
| { item.items && ( | ||
| <Astro.self items={ item.items } /> | ||
| ) } | ||
| </li> | ||
| )) } | ||
| </ol> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Table of Contents | ||
|
|
||
| **Render a list of links to headings in a given HTML string.** |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --- | ||
| import { t } from '@lib/i18n'; | ||
| import { extractTocItemsFromHtml } from './index'; | ||
| import List from './List.astro'; | ||
|
|
||
| const html = await Astro.slots.render('default'); | ||
| const items = extractTocItemsFromHtml(html); | ||
| --- | ||
|
|
||
| { | ||
| items.length > 0 && ( | ||
| <nav> | ||
| <h2>{t('table_of_contents')}</h2> | ||
| <List items={items} /> | ||
| </nav> | ||
| ) | ||
| } | ||
| <Fragment set:html={html} /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { fromHtml } from 'hast-util-from-html'; | ||
| import { toString } from 'hast-util-to-string'; | ||
| import { visit } from 'unist-util-visit'; | ||
|
|
||
| export type TreeItem = { | ||
| id: string; | ||
| level: number; | ||
| text: string; | ||
| items?: TreeItem[]; | ||
| }; | ||
|
|
||
| export const extractTocItemsFromHtml = (html: string) => { | ||
|
|
||
| const hast = fromHtml(html, { fragment: true }); | ||
| const tagNames = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']); | ||
| const items: TreeItem[] = []; | ||
|
|
||
| visit(hast, 'element', (node) => { | ||
| if (tagNames.has(node.tagName) && node.properties?.id) { | ||
| const text = toString(node); | ||
| const item: TreeItem = { | ||
| id: String(node.properties.id), | ||
| level: parseInt(node.tagName.slice(1), 10), | ||
| text, | ||
| }; | ||
| const lastItem = items[items.length - 1]; | ||
| if (lastItem && lastItem.level < item.level) { | ||
| lastItem.items = lastItem.items ?? []; | ||
| lastItem.items.push(item); | ||
| } else { | ||
| items.push(item); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| return items; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.