-
-
Notifications
You must be signed in to change notification settings - Fork 133
Automatically generate table of contents in text #2213
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: master
Are you sure you want to change the base?
Changes from all commits
82acffa
0f6ef6d
239ef2c
d8078ab
742d1df
8be52dd
ad7d229
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -20,6 +20,7 @@ import rehypeSN from '@/lib/rehype-sn' | |||
import remarkUnicode from '@/lib/remark-unicode' | ||||
import Embed from './embed' | ||||
import remarkMath from 'remark-math' | ||||
import remarkToc from '@/lib/remark-toc' | ||||
|
||||
const rehypeSNStyled = () => rehypeSN({ | ||||
stylers: [{ | ||||
|
@@ -33,7 +34,11 @@ const rehypeSNStyled = () => rehypeSN({ | |||
}] | ||||
}) | ||||
|
||||
const remarkPlugins = [gfm, remarkUnicode, [remarkMath, { singleDollarTextMath: false }]] | ||||
const baseRemarkPlugins = [ | ||||
gfm, | ||||
remarkUnicode, | ||||
[remarkMath, { singleDollarTextMath: false }] | ||||
] | ||||
|
||||
export function SearchText ({ text }) { | ||||
return ( | ||||
|
@@ -49,6 +54,9 @@ export function SearchText ({ text }) { | |||
|
||||
// this is one of the slowest components to render | ||||
export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, children, tab, itemId, outlawed, topLevel }) { | ||||
// include remarkToc if topLevel | ||||
const remarkPlugins = topLevel ? [...baseRemarkPlugins, remarkToc] : baseRemarkPlugins | ||||
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. Unfortunately, 2025-07-25.02-08-24.mp4not sure how to best deal with this 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. Interesting. One possibility is to remove the topLevel check for giving headings node ids: stacker.news/components/text.js Line 116 in 4620160
I'm not sure why that check is there and if doing so would break anything. I suppose another possibility is to only process |
||||
|
||||
// would the text overflow on the current screen size? | ||||
const [overflowing, setOverflowing] = useState(false) | ||||
// should we show the full text? | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { SKIP, visit } from 'unist-util-visit' | ||
import { extractHeadings } from './toc' | ||
|
||
export default function remarkToc () { | ||
return function transformer (tree) { | ||
const headings = extractHeadings(tree) | ||
|
||
visit(tree, 'paragraph', (node, index, parent) => { | ||
if ( | ||
node.children?.length === 1 && | ||
node.children[0].type === 'text' && | ||
node.children[0].value.trim() === '{:toc}' | ||
) { | ||
parent.children.splice(index, 1, buildToc(headings)) | ||
return [SKIP, index] | ||
} | ||
}) | ||
} | ||
} | ||
|
||
function buildToc (headings) { | ||
const root = { type: 'list', ordered: false, spread: false, children: [] } | ||
const stack = [{ depth: 0, node: root }] // holds the current chain of parents | ||
|
||
for (const { heading, slug, depth } of headings) { | ||
// walk up the stack to find the parent of the current heading | ||
while (stack.length && depth <= stack[stack.length - 1].depth) { | ||
stack.pop() | ||
} | ||
let parent = stack[stack.length - 1].node | ||
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. Bug: Stack Underflow in
|
||
|
||
// if the parent is a li, gets its child ul | ||
if (parent.type === 'listItem') { | ||
let ul = parent.children.find(c => c.type === 'list') | ||
if (!ul) { | ||
ul = { type: 'list', ordered: false, spread: false, children: [] } | ||
parent.children.push(ul) | ||
} | ||
parent = ul | ||
} | ||
|
||
// build the li from the current heading | ||
const listItem = { | ||
type: 'listItem', | ||
spread: false, | ||
children: [{ | ||
type: 'paragraph', | ||
children: [{ | ||
type: 'link', | ||
url: `#${slug}`, | ||
children: [{ type: 'text', value: heading }] | ||
}] | ||
}] | ||
} | ||
|
||
parent.children.push(listItem) | ||
stack.push({ depth, node: listItem }) | ||
} | ||
|
||
return root | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,23 @@ | ||||
import { fromMarkdown } from 'mdast-util-from-markdown' | ||||
import { visit } from 'unist-util-visit' | ||||
import { toString } from 'mdast-util-to-string' | ||||
import { slug } from 'github-slugger' | ||||
|
||||
export function extractHeadings (markdownOrTree) { | ||||
const tree = typeof markdownOrTree === 'string' | ||||
? fromMarkdown(markdownOrTree) | ||||
: markdownOrTree | ||||
|
||||
const headings = [] | ||||
|
||||
visit(tree, 'heading', node => { | ||||
const str = toString(node) | ||||
headings.push({ | ||||
heading: str, | ||||
slug: slug(str.replace(/[^\w\-\s]+/gi, '')), | ||||
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. note for the future, unrelated to review: 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. We don't use 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. Ok, will leave this as is for now 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. what is the replacement for? 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. Was just copying from line 22 of the old
But is it redundant since we're already passing the string to |
||||
depth: node.depth | ||||
}) | ||||
}) | ||||
|
||||
return headings | ||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that we don't give IDs to nodes inside comments, only on full posts:
stacker.news/components/text.js
Line 116 in 4620160
Because of this, the table of contents won't work if used in comments. I personally think that the ToC doesn't make that much sense in comments, so we can just disable
{:toc}
for them withtopLevel
awareness. What do you think? ^^Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, it wouldn't make sense to use Toc in comments, so I changed it so that remarkToc is only processed for topLevel items