Skip to content

Commit 0e3e113

Browse files
committed
add validation and more tests
Signed-off-by: rishichawda <[email protected]>
1 parent 82f49e0 commit 0e3e113

File tree

7 files changed

+232
-8
lines changed

7 files changed

+232
-8
lines changed

__tests__/custom-prefix.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,4 @@ test('remark-notes: combining custom prefix with injectStyles false', async () =
131131
'Should not inject styles')
132132
})
133133

134-
console.log('\n✅ All custom prefix tests completed!')
134+
console.log('All custom prefix tests completed!')

__tests__/generate-fixtures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async function generateFixtures() {
3737
console.log(`Generated fixture for ${type}`)
3838
}
3939

40-
console.log('\nAll fixtures generated successfully!')
40+
console.log('All fixtures generated successfully!')
4141
}
4242

4343
generateFixtures().catch(console.error)

__tests__/validation.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Test validation behavior
3+
*/
4+
5+
import { test } from 'node:test'
6+
import assert from 'node:assert/strict'
7+
import { unified } from 'unified'
8+
import remarkParse from 'remark-parse'
9+
import remarkRehype from 'remark-rehype'
10+
import rehypeRaw from 'rehype-raw'
11+
import rehypeStringify from 'rehype-stringify'
12+
import remarkNotes from '../index.js'
13+
14+
test('remark-notes: invalid note type is ignored (graceful degradation)', async () => {
15+
const markdown = '> [!warning]\n> This is an invalid note type'
16+
17+
const file = await unified()
18+
.use(remarkParse)
19+
.use(remarkNotes)
20+
.use(remarkRehype, { allowDangerousHtml: true })
21+
.use(rehypeRaw)
22+
.use(rehypeStringify)
23+
.process(markdown)
24+
25+
const output = String(file)
26+
27+
// Should remain as a regular blockquote
28+
assert.ok(output.includes('<blockquote>'),
29+
'Invalid note type should remain as blockquote')
30+
31+
// Should NOT be transformed into a note
32+
assert.ok(!output.includes('class="remark-note'),
33+
'Invalid note type should not be transformed into a note')
34+
35+
// Should still contain the original marker
36+
assert.ok(output.includes('[!warning]'),
37+
'Original marker should be preserved for invalid types')
38+
})
39+
40+
test('remark-notes: valid note types are transformed', async () => {
41+
const validTypes = ['note', 'tip', 'important', 'quote', 'bonus']
42+
43+
for (const type of validTypes) {
44+
const markdown = `> [!${type}]\n> Test content`
45+
46+
const file = await unified()
47+
.use(remarkParse)
48+
.use(remarkNotes)
49+
.use(remarkRehype, { allowDangerousHtml: true })
50+
.use(rehypeRaw)
51+
.use(rehypeStringify)
52+
.process(markdown)
53+
54+
const output = String(file)
55+
56+
// Should be transformed into a note
57+
assert.ok(output.includes(`class="remark-note remark-note-${type}"`),
58+
`Valid type '${type}' should be transformed into a note`)
59+
60+
// Should NOT remain as blockquote with marker
61+
assert.ok(!output.includes(`[!${type}]`),
62+
`Marker should be removed for valid type '${type}'`)
63+
}
64+
})
65+
66+
test('remark-notes: case insensitive note type matching', async () => {
67+
const variations = ['TIP', 'Tip', 'tIp', 'tiP']
68+
69+
for (const variant of variations) {
70+
const markdown = `> [!${variant}]\n> Test content`
71+
72+
const file = await unified()
73+
.use(remarkParse)
74+
.use(remarkNotes)
75+
.use(remarkRehype, { allowDangerousHtml: true })
76+
.use(rehypeRaw)
77+
.use(rehypeStringify)
78+
.process(markdown)
79+
80+
const output = String(file)
81+
82+
// Should be transformed (case insensitive)
83+
assert.ok(output.includes('class="remark-note remark-note-tip"'),
84+
`Case variant '${variant}' should be recognized as valid`)
85+
}
86+
})
87+
88+
test('remark-notes: multiple notes with mixed valid and invalid types', async () => {
89+
const markdown = `
90+
> [!note]
91+
> Valid note
92+
93+
> [!warning]
94+
> Invalid type
95+
96+
> [!tip]
97+
> Another valid note
98+
`
99+
100+
const file = await unified()
101+
.use(remarkParse)
102+
.use(remarkNotes)
103+
.use(remarkRehype, { allowDangerousHtml: true })
104+
.use(rehypeRaw)
105+
.use(rehypeStringify)
106+
.process(markdown)
107+
108+
const output = String(file)
109+
110+
// Valid notes should be transformed
111+
assert.ok(output.includes('class="remark-note remark-note-note"'),
112+
'First valid note should be transformed')
113+
assert.ok(output.includes('class="remark-note remark-note-tip"'),
114+
'Second valid note should be transformed')
115+
116+
// Invalid note should remain as blockquote
117+
assert.ok(output.includes('[!warning]'),
118+
'Invalid note marker should be preserved')
119+
120+
// Count blockquotes (should have 1 for the invalid type)
121+
const blockquoteMatches = output.match(/<blockquote>/g)
122+
assert.ok(blockquoteMatches && blockquoteMatches.length >= 1,
123+
'At least one blockquote should remain for invalid type')
124+
})
125+
126+
test('remark-notes: typos in note types are not transformed', async () => {
127+
const typos = ['notte', 'tipp', 'importnt', 'qoute', 'bouns']
128+
129+
for (const typo of typos) {
130+
const markdown = `> [!${typo}]\n> Test content`
131+
132+
const file = await unified()
133+
.use(remarkParse)
134+
.use(remarkNotes)
135+
.use(remarkRehype, { allowDangerousHtml: true })
136+
.use(rehypeRaw)
137+
.use(rehypeStringify)
138+
.process(markdown)
139+
140+
const output = String(file)
141+
142+
// Should remain as blockquote with marker
143+
assert.ok(output.includes(`[!${typo}]`),
144+
`Typo '${typo}' should remain as blockquote`)
145+
146+
// Should NOT be transformed
147+
assert.ok(!output.includes('class="remark-note'),
148+
`Typo '${typo}' should not be transformed`)
149+
}
150+
})
151+
152+
console.log('All validation tests completed!')

index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Node, Parent } from 'unist'
33
import type { Blockquote, Paragraph, Text } from 'mdast'
44
import { styles } from './lib/styles.js'
55
import { createNoteStructure } from './lib/node-structure.js'
6-
import { VALID_NOTE_TYPES, ValidNoteType } from './lib/icons-hast.js'
6+
import { isValidNoteType } from './lib/validation.js'
77
import type { RemarkNotesOptions } from './lib/types/options.js'
88

99
// Export types for public API
@@ -46,10 +46,10 @@ export default function remarkNotes(options: RemarkNotesOptions = {}) {
4646
const match = textNode.value.match(/^\[!(\w+)\]/)
4747
if (!match) return
4848

49-
const noteType = match[1].toLowerCase() as ValidNoteType
49+
const noteType = match[1].toLowerCase()
5050

51-
// Only transform if it's a recognized note type
52-
if (!VALID_NOTE_TYPES.includes(noteType)) return
51+
// Validate note type - skip transformation if invalid (graceful degradation)
52+
if (!isValidNoteType(noteType)) return
5353

5454
// Clone children to preserve original markdown structure
5555
const children = [...node.children]

lib/types/options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ export interface RemarkNotesOptions {
9292
* The CSS file is available at the package export: `remark-notes-plugin/styles.css`
9393
*/
9494
injectStyles?: boolean
95-
}
95+
}

lib/validation.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Validation utilities for note types and error message generation
3+
*
4+
* @module lib/validation
5+
*/
6+
7+
import type { Position } from 'unist'
8+
import { VALID_NOTE_TYPES, ValidNoteType } from './icons-hast.js'
9+
10+
/**
11+
* Type guard to check if a string is a valid note type
12+
*
13+
* @param type - The string to check
14+
* @returns True if the type is a valid note type, false otherwise
15+
*
16+
* @example
17+
* ```typescript
18+
* if (isValidNoteType(noteType)) {
19+
* // TypeScript now knows noteType is ValidNoteType
20+
* createNoteStructure(noteType, ...)
21+
* }
22+
* ```
23+
*/
24+
export function isValidNoteType(type: string): type is ValidNoteType {
25+
return VALID_NOTE_TYPES.includes(type as ValidNoteType)
26+
}
27+
28+
/**
29+
* Creates a descriptive error message for invalid note types
30+
*
31+
* @param invalidType - The invalid note type that was encountered
32+
* @param position - Optional position information from the AST node
33+
* @returns A formatted error message with context
34+
*
35+
* @example
36+
* ```typescript
37+
* const error = createInvalidTypeError('warn', node.position)
38+
* // Returns: "Invalid note type 'warn' at line 5, column 3. Valid types are: note, tip, important, quote, bonus"
39+
* ```
40+
*/
41+
export function createInvalidTypeError(invalidType: string, position?: Position): string {
42+
const validTypes = VALID_NOTE_TYPES.join(', ')
43+
const positionInfo = position?.start
44+
? ` at line ${position.start.line}, column ${position.start.column}`
45+
: ''
46+
47+
return `Invalid note type '${invalidType}'${positionInfo}. Valid types are: ${validTypes}`
48+
}
49+
50+
/**
51+
* Validates a note type and throws an error if invalid
52+
*
53+
* @param type - The note type to validate
54+
* @param position - Optional position information for error messages
55+
* @throws {Error} If the note type is invalid
56+
*
57+
* @example
58+
* ```typescript
59+
* try {
60+
* validateNoteType(noteType, node.position)
61+
* // If we get here, noteType is valid
62+
* } catch (error) {
63+
* console.error(error.message)
64+
* }
65+
* ```
66+
*/
67+
export function validateNoteType(type: string, position?: Position): asserts type is ValidNoteType {
68+
if (!isValidNoteType(type)) {
69+
throw new Error(createInvalidTypeError(type, position))
70+
}
71+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"test": "node --loader ts-node/esm __tests__/index.ts",
3737
"test:styles": "node --loader ts-node/esm __tests__/styles.ts",
3838
"test:custom-prefix": "node --loader ts-node/esm __tests__/custom-prefix.ts",
39-
"test:all": "yarn test && yarn test:styles && yarn test:custom-prefix",
39+
"test:validation": "node --loader ts-node/esm __tests__/validation.ts",
40+
"test:all": "yarn test && yarn test:styles && yarn test:custom-prefix && yarn test:validation",
4041
"generate:fixtures": "node --loader ts-node/esm __tests__/generate-fixtures.ts"
4142
},
4243
"keywords": [

0 commit comments

Comments
 (0)