Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions server/utils/readme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,23 +139,24 @@ const ALLOWED_TAGS = [
]

const ALLOWED_ATTR: Record<string, string[]> = {
a: ['href', 'title', 'target', 'rel'],
img: ['src', 'alt', 'title', 'width', 'height', 'align'],
source: ['src', 'srcset', 'type', 'media'],
button: ['class', 'title', 'type', 'aria-label', 'data-copy'],
th: ['colspan', 'rowspan', 'align'],
td: ['colspan', 'rowspan', 'align'],
h3: ['id', 'data-level', 'align'],
h4: ['id', 'data-level', 'align'],
h5: ['id', 'data-level', 'align'],
h6: ['id', 'data-level', 'align'],
blockquote: ['data-callout'],
details: ['open'],
code: ['class'],
pre: ['class', 'style'],
span: ['class', 'style'],
div: ['class', 'style', 'align'],
p: ['align'],
'*': ['id'], // Allow id on all tags
'a': ['href', 'title', 'target', 'rel'],
'img': ['src', 'alt', 'title', 'width', 'height', 'align'],
'source': ['src', 'srcset', 'type', 'media'],
'button': ['class', 'title', 'type', 'aria-label', 'data-copy'],
'th': ['colspan', 'rowspan', 'align'],
'td': ['colspan', 'rowspan', 'align'],
'h3': ['data-level', 'align'],
'h4': ['data-level', 'align'],
'h5': ['data-level', 'align'],
'h6': ['data-level', 'align'],
'blockquote': ['data-callout'],
'details': ['open'],
'code': ['class'],
'pre': ['class', 'style'],
'span': ['class', 'style'],
'div': ['class', 'style', 'align'],
'p': ['align'],
}

// GitHub-style callout types
Expand Down Expand Up @@ -397,6 +398,14 @@ ${html}

const rawHtml = marked.parse(content) as string

// Helper to prefix id attributes with 'user-content-'
const prefixId = (tagName: string, attribs: sanitizeHtml.Attributes) => {
if (attribs.id && !attribs.id.startsWith('user-content-')) {
attribs.id = `user-content-${attribs.id}`
}
return { tagName, attribs }
}
Comment on lines +401 to +407
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make fragment normalisation idempotent to avoid double‑prefixing.

Links already using #user-content-… (common when copied from GitHub) will become #user-content-user-content-… because resolveUrl always prefixes hash fragments. Guard against that so those anchors still resolve.

🔧 Proposed fix
function resolveUrl(url: string, packageName: string, repoInfo?: RepositoryInfo): string {
  if (!url) return url
  if (url.startsWith('#')) {
    // Prefix anchor links to match heading IDs (avoids collision with page IDs)
-    return `#user-content-${url.slice(1)}`
+    if (url.startsWith('#user-content-')) return url
+    return `#user-content-${url.slice(1)}`
  }


const sanitized = sanitizeHtml(rawHtml, {
allowedTags: ALLOWED_TAGS,
allowedAttributes: ALLOWED_ATTR,
Expand Down Expand Up @@ -436,6 +445,11 @@ ${html}
}
return { tagName, attribs }
},
div: prefixId,
p: prefixId,
span: prefixId,
section: prefixId,
article: prefixId,
Comment on lines +448 to +452
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate the file and check its existence
find . -name "readme.ts" -path "*/server/utils/*" 2>/dev/null | head -20

Repository: npmx-dev/npmx.dev

Length of output: 85


🏁 Script executed:

# Check the file size and read the specific lines mentioned
if [ -f "server/utils/readme.ts" ]; then
  wc -l server/utils/readme.ts
  echo "=== Lines 440-460 ==="
  sed -n '440,460p' server/utils/readme.ts
fi

Repository: npmx-dev/npmx.dev

Length of output: 648


🏁 Script executed:

# Search for the transformTags configuration to understand the full context
rg "transformTags" server/utils/readme.ts -A 30 -B 5

Repository: npmx-dev/npmx.dev

Length of output: 1470


🏁 Script executed:

# Look for the prefixId function definition
rg "prefixId\s*[:=]|function prefixId" server/utils/readme.ts -B 2 -A 10

Repository: npmx-dev/npmx.dev

Length of output: 511


🏁 Script executed:

# Check package.json for sanitize-html version
cat package.json | grep -A 2 -B 2 "sanitize-html"

Repository: npmx-dev/npmx.dev

Length of output: 377


🌐 Web query:

sanitize-html 2.17.0 wildcard transform support transformTags

💡 Result:

In sanitize-html v2.17.0, transformTags supports a '*' wildcard key to apply a transform to all tags (not just a specific tag name). [1][2]

import sanitizeHtml from "sanitize-html";

const clean = sanitizeHtml(dirtyHtml, {
  transformTags: {
    "*": (tagName, attribs) => ({
      tagName,
      attribs: { ...attribs, "data-sanitized": "1" }
    })
  }
});

You can also use the wildcard with sanitizeHtml.simpleTransform(...) for simpler “add/override attributes” cases. [1][2]


🏁 Script executed:

# Also search for any usage of wildcard transforms in the codebase
rg "transformTags" . -A 50 | grep -E "(\*|wildcard)" || echo "No wildcard patterns found in transformTags"

Repository: npmx-dev/npmx.dev

Length of output: 104


🏁 Script executed:

# Let's check if there are any fragment link rewrites happening elsewhere
rg "user-content" server/utils/readme.ts -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 1054


🏁 Script executed:

# Check the ALLOWED_TAGS to see what tags are allowed through
rg "ALLOWED_TAGS\s*=" server/utils/readme.ts -A 30

Repository: npmx-dev/npmx.dev

Length of output: 366


Prefix IDs for anchor elements to match rewritten fragment links.

Fragment links are rewritten to #user-content-foo, but anchor IDs are not prefixed, creating a mismatch. Apply prefixId to the a transform to keep anchor jumps consistent.

♻️ Proposed fix (apply to `a`)
      a: (tagName, attribs) => {
        // Add security attributes for external links
        if (attribs.href && hasProtocol(attribs.href, { acceptRelative: true })) {
          attribs.rel = 'nofollow noreferrer noopener'
          attribs.target = '_blank'
        }
-        return { tagName, attribs }
+        return prefixId(tagName, attribs)
      },

Alternatively, sanitize-html 2.17.0 supports a wildcard '*' transform in transformTags if you prefer centralising ID prefixing.

},
})

Expand Down
Loading