Skip to content

Commit 2d0c38a

Browse files
committed
Fix doublewrapping in formatDescription and add unit tests
1 parent eef1be1 commit 2d0c38a

File tree

3 files changed

+98
-38
lines changed

3 files changed

+98
-38
lines changed

β€Žpackage.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
"postbuild": "next-sitemap",
1818
"prebuild": "tsx src/get-github-info.ts",
1919
"serve": "pnpx serve out",
20-
"test": "playwright test",
20+
"test": "playwright test && pnpm test:unit",
21+
"test:e2e": "playwright test",
2122
"test:ui": "playwright test --ui",
23+
"test:unit": "node --import=tsx --test 'src/**/*.test.tsx'",
2224
"validate:snippets": "node scripts/validate-snippets.js"
2325
},
2426
"dependencies": {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { it } from "node:test"
2+
import { strict as assert } from "node:assert"
3+
4+
import { formatDescription } from "./format-description"
5+
6+
it("does not double-wrap links", () => {
7+
assert.equal(
8+
formatDescription(`Check out Y! <a href="https://y.dev">https://y.dev</a>`),
9+
`Check out Y! <a href="https://y.dev" rel="noopener noreferrer" target="_blank" class=" typography-link">y.dev</a>`,
10+
)
11+
})
12+
13+
it("enriches plain URLs", () => {
14+
assert.equal(
15+
formatDescription(`Visit https://example.com for more info`),
16+
`Visit <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="typography-link">example.com</a> for more info`,
17+
)
18+
})
19+
20+
it("adds attributes to existing links without URL content", () => {
21+
assert.equal(
22+
formatDescription(`<a href="https://example.com">Click here</a>`),
23+
`<a href="https://example.com" rel="noopener noreferrer" target="_blank" class=" typography-link">Click here</a>`,
24+
)
25+
})
26+
27+
it("handles mixed content", () => {
28+
assert.equal(
29+
formatDescription(
30+
`Check <a href="https://y.dev">Y site</a> and https://example.com`,
31+
),
32+
`Check <a href="https://y.dev" rel="noopener noreferrer" target="_blank" class=" typography-link">Y site</a> and <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="typography-link">example.com</a>`,
33+
)
34+
})
35+
36+
it("handles multiple URLs in one text", () => {
37+
assert.equal(
38+
formatDescription(
39+
`Visit https://github.com and https://example.org for info`,
40+
),
41+
`Visit <a href="https://github.com" target="_blank" rel="noopener noreferrer" class="typography-link">github.com</a> and <a href="https://example.org" target="_blank" rel="noopener noreferrer" class="typography-link">example.org</a> for info`,
42+
)
43+
})
44+
45+
it("handles existing link with existing class", () => {
46+
assert.equal(
47+
formatDescription(
48+
`<a href="https://example.com" class="custom">Click here</a>`,
49+
),
50+
`<a href="https://example.com" class="custom typography-link" rel="noopener noreferrer" target="_blank">Click here</a>`,
51+
)
52+
})
Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,51 @@
1-
const URL_REGEX = /https?:\/\/[^\s]+/g
2-
const LINK_REGEX = /<a\s+([^>]*href\s*=\s*[^>]*)>/gi
1+
// Combined regex that matches either existing anchor tags OR standalone URLs
2+
const COMBINED_REGEX =
3+
/(<a\s+[^>]*href\s*=\s*[^>]*>.*?<\/a>)|(https?:\/\/[^\s]+)/gi
34

45
export function formatDescription(text: string): string {
5-
// we coerce all existing anchor tags to have target="_blank" rel="noopener noreferrer" and typography-link class
6-
const result = text.replace(LINK_REGEX, (_, attributes) => {
7-
let attrs = attributes
8-
9-
if (!attrs.includes("target=")) {
10-
attrs += ' target="_blank"'
11-
}
12-
13-
if (!attrs.includes("rel=")) {
14-
attrs += ' rel="noopener noreferrer"'
15-
}
16-
17-
if (!attrs.includes("class=")) {
18-
attrs += ' class="typography-link"'
19-
} else if (!attrs.includes("typography-link")) {
20-
attrs = attrs.replace(
21-
/class\s*=\s*["']([^"']*)/gi,
22-
'class="$1 typography-link',
6+
return text.replace(COMBINED_REGEX, (match, anchorTag, standaloneUrl) => {
7+
if (anchorTag) {
8+
// Handle existing anchor tag
9+
const linkMatch = anchorTag.match(
10+
/<a\s+([^>]*href\s*=\s*[^>]*)>(.*?)<\/a>/i,
11+
)
12+
if (!linkMatch) return anchorTag
13+
14+
const [, attributes, content] = linkMatch
15+
let attrs = attributes
16+
17+
if (!attrs.includes("rel=")) {
18+
attrs += ' rel="noopener noreferrer"'
19+
}
20+
21+
if (!attrs.includes("target=")) {
22+
attrs += ' target="_blank"'
23+
}
24+
25+
if (!attrs.includes("class=")) {
26+
attrs += ' class=" typography-link"'
27+
} else if (!attrs.includes("typography-link")) {
28+
attrs = attrs.replace(
29+
/class\s*=\s*["']([^"']*)/gi,
30+
'class="$1 typography-link',
31+
)
32+
}
33+
34+
// Format URL content to show just domain
35+
const urlContent = content.replace(
36+
/https?:\/\/[^\s]+/g,
37+
(url: string) => {
38+
return url.replace(/^https?:\/\//, "")
39+
},
2340
)
24-
}
25-
26-
return `<a ${attrs}>`
27-
})
28-
29-
// then we format plain URLs that are not already inside an anchor tag
30-
return result.replace(URL_REGEX, (url, offset) => {
31-
const beforeUrl = result.slice(0, offset)
32-
const afterUrl = result.slice(offset + url.length)
33-
34-
const lastOpenTag = beforeUrl.lastIndexOf("<")
35-
const lastCloseTag = beforeUrl.lastIndexOf(">")
36-
const nextCloseTag = afterUrl.indexOf(">")
3741

38-
if (lastOpenTag > lastCloseTag && nextCloseTag !== -1) {
39-
return url
42+
return `<a ${attrs}>${urlContent}</a>`
43+
} else if (standaloneUrl) {
44+
// Handle standalone URL
45+
const displayUrl = standaloneUrl.replace(/^https?:\/\//, "")
46+
return `<a href="${standaloneUrl}" target="_blank" rel="noopener noreferrer" class="typography-link">${displayUrl}</a>`
4047
}
4148

42-
const displayUrl = url.replace(/^https?:\/\//, "")
43-
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="typography-link">${displayUrl}</a>`
49+
return match
4450
})
4551
}

0 commit comments

Comments
 (0)