Skip to content

Commit f296ac1

Browse files
committed
Add a mechanism for grabbing the stats of a draft.
1 parent e7b70f1 commit f296ac1

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
export interface MdImage {
2+
url: string
3+
alt?: string
4+
}
5+
6+
export interface MdLink {
7+
text: string
8+
url: string
9+
}
10+
11+
export interface MdCodeBlock {
12+
language?: string
13+
code: string
14+
}
15+
16+
export interface DraftStats {
17+
charCount: number
18+
images: MdImage[]
19+
links: MdLink[]
20+
codeBlocks: MdCodeBlock[]
21+
}
22+
export function statsFor(md: string): DraftStats {
23+
const charCount = md.length
24+
25+
const images: MdImage[] = []
26+
const links: MdLink[] = []
27+
const codeBlocks: MdCodeBlock[] = []
28+
29+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g
30+
let imageMatch: RegExpExecArray | null
31+
while ((imageMatch = imageRegex.exec(md)) !== null) {
32+
images.push({
33+
...(imageMatch[1] && { alt: imageMatch[1] }),
34+
url: imageMatch[2]!,
35+
})
36+
}
37+
38+
const linkRegex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g
39+
let linkMatch: RegExpExecArray | null
40+
while ((linkMatch = linkRegex.exec(md)) !== null) {
41+
links.push({
42+
text: linkMatch[1]!,
43+
url: linkMatch[2]!,
44+
})
45+
}
46+
47+
const codeBlockRegex = /```(\w*)\n?([\s\S]*?)```/g
48+
let codeMatch: RegExpExecArray | null
49+
while ((codeMatch = codeBlockRegex.exec(md)) !== null) {
50+
codeBlocks.push({
51+
...(codeMatch[1] && { language: codeMatch[1] }),
52+
code: codeMatch[2]!,
53+
})
54+
}
55+
56+
return {
57+
charCount,
58+
codeBlocks,
59+
images,
60+
links,
61+
}
62+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { statsFor } from '../../../src/lib/enhancers/draftStats'
3+
4+
describe('statsFor', () => {
5+
it('should handle empty markdown', () => {
6+
expect(statsFor('')).toMatchInlineSnapshot(`
7+
{
8+
"charCount": 0,
9+
"codeBlocks": [],
10+
"images": [],
11+
"links": [],
12+
}
13+
`)
14+
})
15+
16+
it('should count characters', () => {
17+
expect(statsFor('Hello world')).toMatchInlineSnapshot(`
18+
{
19+
"charCount": 11,
20+
"codeBlocks": [],
21+
"images": [],
22+
"links": [],
23+
}
24+
`)
25+
})
26+
27+
it('should extract images with alt text', () => {
28+
expect(statsFor('![Alt text](https://example.com/image.png)')).toMatchInlineSnapshot(`
29+
{
30+
"charCount": 42,
31+
"codeBlocks": [],
32+
"images": [
33+
{
34+
"alt": "Alt text",
35+
"url": "https://example.com/image.png",
36+
},
37+
],
38+
"links": [],
39+
}
40+
`)
41+
})
42+
43+
it('should extract images without alt text', () => {
44+
expect(statsFor('![](https://example.com/image.png)')).toMatchInlineSnapshot(`
45+
{
46+
"charCount": 34,
47+
"codeBlocks": [],
48+
"images": [
49+
{
50+
"url": "https://example.com/image.png",
51+
},
52+
],
53+
"links": [],
54+
}
55+
`)
56+
})
57+
58+
it('should extract links', () => {
59+
expect(statsFor('[Link text](https://example.com)')).toMatchInlineSnapshot(`
60+
{
61+
"charCount": 32,
62+
"codeBlocks": [],
63+
"images": [],
64+
"links": [
65+
{
66+
"text": "Link text",
67+
"url": "https://example.com",
68+
},
69+
],
70+
}
71+
`)
72+
})
73+
74+
it('should extract code blocks with language', () => {
75+
expect(statsFor('```javascript\nconsole.log("hello")\n```')).toMatchInlineSnapshot(`
76+
{
77+
"charCount": 38,
78+
"codeBlocks": [
79+
{
80+
"code": "console.log("hello")
81+
",
82+
"language": "javascript",
83+
},
84+
],
85+
"images": [],
86+
"links": [],
87+
}
88+
`)
89+
})
90+
91+
it('should extract code blocks without language', () => {
92+
expect(statsFor('```\nconsole.log("hello")\n```')).toMatchInlineSnapshot(`
93+
{
94+
"charCount": 28,
95+
"codeBlocks": [
96+
{
97+
"code": "console.log("hello")
98+
",
99+
},
100+
],
101+
"images": [],
102+
"links": [],
103+
}
104+
`)
105+
})
106+
107+
it('should handle complex markdown with multiple elements', () => {
108+
const markdown = `# Title
109+
110+
Here's some text with a [link](https://example.com) and an ![image](https://example.com/img.png).
111+
112+
\`\`\`typescript
113+
function hello() {
114+
return "world"
115+
}
116+
\`\`\`
117+
118+
More text with another ![alt text](https://example.com/img2.jpg) and [another link](https://test.com).
119+
120+
\`\`\`
121+
plain code block
122+
\`\`\``
123+
124+
expect(statsFor(markdown)).toMatchInlineSnapshot(`
125+
{
126+
"charCount": 293,
127+
"codeBlocks": [
128+
{
129+
"code": "function hello() {
130+
return "world"
131+
}
132+
",
133+
"language": "typescript",
134+
},
135+
{
136+
"code": "plain code block
137+
",
138+
},
139+
],
140+
"images": [
141+
{
142+
"alt": "image",
143+
"url": "https://example.com/img.png",
144+
},
145+
{
146+
"alt": "alt text",
147+
"url": "https://example.com/img2.jpg",
148+
},
149+
],
150+
"links": [
151+
{
152+
"text": "link",
153+
"url": "https://example.com",
154+
},
155+
{
156+
"text": "another link",
157+
"url": "https://test.com",
158+
},
159+
],
160+
}
161+
`)
162+
})
163+
})

0 commit comments

Comments
 (0)