Skip to content

Commit b61dff8

Browse files
committed
fix(ui): headers - prevent xss when no-html is set and allow markdown
1 parent 668271d commit b61dff8

File tree

1 file changed

+26
-25
lines changed

1 file changed

+26
-25
lines changed

Diff for: ui/src/util/extendHeading.js

+26-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import slugify from './slugify'
22

3+
// Helper function to transform emoji tokens
34
function unemoji(TokenConstructor, token) {
45
if (token.type === 'emoji') {
56
return Object.assign(new TokenConstructor(), token, { content: token.markup })
@@ -13,13 +14,11 @@ export default function extendHeading(
1314
toc = false,
1415
tocStart = 1,
1516
tocEnd = 3,
16-
noHeadingAnchorLinks = false,
17+
noHeadingAnchorLinks = false
1718
) {
1819
let Token
1920
md.core.ruler.push('headingLinks', function (state) {
20-
// save the Token constructor because we'll be building a few instances at render
21-
// time; that's sort of outside the intended markdown-it parsing sequence, but
22-
// since we have tight control over what we're creating (a link), we're safe
21+
// Save the Token constructor for later use when building new token instances.
2322
if (!Token) {
2423
Token = state.Token
2524
}
@@ -28,13 +27,16 @@ export default function extendHeading(
2827
md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
2928
const token = tokens[idx]
3029

31-
// get the token number
30+
// Get the numeric heading level (e.g., 1 for h1, 2 for h2, etc.)
3231
const tokenNumber = parseInt(token.tag[1])
3332

33+
// Get the children tokens (which represent the inline content)
3434
const children = tokens[idx + 1].children
3535

36+
// Build a plain text label by concatenating all child token content.
3637
const label = children.reduce((acc, t) => acc + t.content, '')
3738

39+
// Build the CSS classes for the heading
3840
const classes = []
3941
classes.push('q-markdown--heading')
4042
classes.push(`q-markdown--heading-${token.tag}`)
@@ -45,6 +47,7 @@ export default function extendHeading(
4547
classes.push('q-markdown--title-light')
4648
}
4749

50+
// If heading anchor links are enabled, add the specific class.
4851
if (
4952
noHeadingAnchorLinks !== true &&
5053
tocStart &&
@@ -56,19 +59,23 @@ export default function extendHeading(
5659
classes.push('q-markdown--heading--anchor-link')
5760
}
5861

62+
// Transform emoji tokens and render the inline content.
5963
const unemojiWithToken = unemoji.bind(null, Token)
6064
const renderedLabel = md.renderer.renderInline(children.map(unemojiWithToken), options, env)
6165

66+
// Create a slug from the rendered label for the heading id.
6267
const id = slugify(
6368
renderedLabel
64-
.replace(/[<>]/g, '') // In case the heading contains `<stuff>`
65-
.toLowerCase(), // should be lowercase
69+
.replace(/[<>]/g, '') // Remove any '<' or '>' characters.
70+
.toLowerCase() // Convert to lowercase.
6671
)
6772

73+
// Set attributes for the heading token.
6874
token.attrSet('id', id)
6975
token.attrSet('name', id)
7076
token.attrSet('class', classes.join(' '))
7177

78+
// If a table of contents is enabled, add this heading to the TOC data.
7279
if (toc) {
7380
if (
7481
tocStart &&
@@ -81,34 +88,28 @@ export default function extendHeading(
8188
}
8289
}
8390

91+
// If anchor links are enabled and the heading level is within the TOC range,
92+
// wrap the inline children with anchor link tokens to preserve formatting.
8493
if (noHeadingAnchorLinks !== true && tokenNumber <= tocEnd) {
85-
// add 3 new token objects link_open, text, link_close
94+
// Create the opening link token with the necessary attributes.
8695
const linkOpen = new Token('link_open', 'a', 1)
87-
const text = new Token('html_inline', '', 0)
88-
if (options.enableHeadingLinkIcons) {
89-
text.content = options.linkIcon
90-
}
91-
text.content = label
92-
93-
const linkClose = new Token('link_close', 'a', -1)
94-
95-
// add some link attributes
96-
// linkOpen.attrSet('id', id)
97-
// linkOpen.attrSet('class', '')
9896
linkOpen.attrSet('href', '#' + id)
9997
linkOpen.attrSet('aria-hidden', 'true')
10098

101-
// remove previous children
102-
while (children.length > 0) children.pop()
99+
// Create the closing link token.
100+
const linkClose = new Token('link_close', 'a', -1)
101+
102+
// Preserve the original inline tokens (to keep formatting like **bold**).
103+
const originalChildren = children.slice()
103104

104-
// add new token objects as children of heading
105-
children.unshift(linkClose)
106-
children.unshift(text)
107-
children.unshift(linkOpen)
105+
// Replace children with the new tokens wrapping the original content.
106+
tokens[idx + 1].children = [linkOpen, ...originalChildren, linkClose]
108107

108+
// Render the modified token.
109109
return md.renderer.renderToken(tokens, idx, options, env, self)
110110
}
111111

112+
// Render the token as usual if no modifications were made.
112113
return self.renderToken(tokens, idx, options)
113114
}
114115
}

0 commit comments

Comments
 (0)