Skip to content

Commit 59b5865

Browse files
committed
implement configuration options
add `injectStyles` and `classPrefix` options to provide more control over styles Signed-off-by: rishichawda <[email protected]>
1 parent 976e822 commit 59b5865

File tree

4 files changed

+161
-51
lines changed

4 files changed

+161
-51
lines changed

index.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@ import type { Blockquote, Paragraph, Text } from 'mdast'
44
import { styles } from './lib/styles.js'
55
import { createNoteStructure } from './lib/node-structure.js'
66
import { VALID_NOTE_TYPES, ValidNoteType } from './lib/icons-hast.js'
7+
import type { RemarkNotesOptions } from './lib/types/options.js'
8+
9+
// Export types for public API
10+
export type { RemarkNotesOptions } from './lib/types/options.js'
11+
export type { ValidNoteType } from './lib/icons-hast.js'
12+
13+
export default function remarkNotes(options: RemarkNotesOptions = {}) {
14+
// Extract options with defaults
15+
const classPrefix = options.classPrefix ?? ''
16+
const injectStyles = options.injectStyles ?? true
717

8-
export default function remarkNotes() {
918
let hasInjectedStyles = false
1019

1120
return (tree: Node) => {
12-
// Inject styles at the beginning of the document (only once)
13-
if (!hasInjectedStyles) {
21+
// Inject styles at the beginning of the document (only once) if enabled
22+
if (injectStyles && !hasInjectedStyles) {
1423
const root = tree as Parent
1524
if (root.children) {
1625
root.children.unshift({
@@ -24,46 +33,46 @@ export default function remarkNotes() {
2433
visit(tree, 'blockquote', (node: Blockquote, index, parent) => {
2534
// Validate we have parent and index for proper node replacement
2635
if (!parent || index === null || index === undefined) return
27-
36+
2837
const firstParagraph = node.children[0]
2938
if (!firstParagraph || firstParagraph.type !== 'paragraph') return
30-
39+
3140
const firstChild = firstParagraph.children[0]
3241
if (!firstChild || firstChild.type !== 'text') return
33-
42+
3443
const textNode = firstChild as Text
35-
44+
3645
// Match [!type] pattern
3746
const match = textNode.value.match(/^\[!(\w+)\]/)
3847
if (!match) return
39-
48+
4049
const noteType = match[1].toLowerCase() as ValidNoteType
41-
50+
4251
// Only transform if it's a recognized note type
4352
if (!VALID_NOTE_TYPES.includes(noteType)) return
44-
53+
4554
// Clone children to preserve original markdown structure
4655
const children = [...node.children]
47-
56+
4857
// Process the first paragraph to remove the note marker
4958
if (children.length > 0 && children[0].type === 'paragraph') {
5059
const firstPara = children[0] as Paragraph
5160
const firstTextNode = firstPara.children[0] as Text
52-
61+
5362
if (firstTextNode && firstTextNode.type === 'text') {
5463
// Remove the [!type] marker and any trailing whitespace
5564
firstTextNode.value = firstTextNode.value.replace(/^\[!\w+\]\s*/, '')
56-
65+
5766
// If the text node is now empty and it's the only child, remove the paragraph
5867
if (firstTextNode.value === '' && firstPara.children.length === 1) {
5968
children.shift()
6069
}
6170
}
6271
}
63-
72+
6473
// Create the note structure using proper mdast nodes (with hast-converted SVG icons)
65-
const noteContainer = createNoteStructure(noteType, children);
66-
74+
const noteContainer = createNoteStructure(noteType, children, classPrefix);
75+
6776
// Replace the blockquote with the container
6877
(parent as Parent).children[index] = noteContainer;
6978
})

lib/node-structure.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,41 @@ import { ICON_HAST_MAP, ValidNoteType } from './icons-hast.js'
2121
*
2222
* @param noteType - The type of note (note, tip, important, etc.)
2323
* @param children - The content nodes (markdown) to include in the note
24+
* @param classPrefix - The prefix for all CSS class names (default: 'remark-note')
2425
* @returns A mdast node that will be transformed to the note HTML structure
2526
*/
2627
export function createNoteStructure(
2728
noteType: ValidNoteType,
28-
children: Node[]
29+
children: Node[],
30+
classPrefix: string = ''
2931
): any {
32+
// Build class names: prefix is prepended to standard 'remark-note' names
33+
// Default: 'remark-note-icon', With prefix 'my': 'my-remark-note-icon'
34+
const baseClass = classPrefix ? `${classPrefix}-remark-note` : 'remark-note'
35+
const makeClass = (suffix: string) => classPrefix ? `${classPrefix}-remark-note-${suffix}` : `remark-note-${suffix}`
3036
// Get the hast representation of the icon
3137
const iconHast = ICON_HAST_MAP[noteType]
32-
38+
3339
// Create icon span with embedded hast node
3440
// Using data.hast tells remark-rehype to use this hast node directly
3541
const iconNode: any = {
3642
type: 'paragraph',
3743
data: {
3844
hName: 'span',
3945
hProperties: {
40-
className: ['remark-note-icon']
46+
className: [makeClass('icon')]
4147
},
4248
hChildren: [iconHast] // Embed hast node directly
4349
},
4450
}
45-
51+
4652
// Create title span
4753
const titleNode: any = {
4854
type: 'paragraph',
4955
data: {
5056
hName: 'span',
5157
hProperties: {
52-
className: ['remark-note-title']
58+
className: [makeClass('title')]
5359
}
5460
},
5561
children: [
@@ -59,43 +65,43 @@ export function createNoteStructure(
5965
}
6066
]
6167
}
62-
68+
6369
// Create header div
6470
const headerNode: any = {
6571
type: 'paragraph',
6672
data: {
6773
hName: 'div',
6874
hProperties: {
69-
className: ['remark-note-header']
75+
className: [makeClass('header')]
7076
}
7177
},
7278
children: [iconNode, titleNode]
7379
}
74-
80+
7581
// Create content wrapper div
7682
const contentNode: any = {
7783
type: 'paragraph',
7884
data: {
7985
hName: 'div',
8086
hProperties: {
81-
className: ['remark-note-content']
87+
className: [makeClass('content')]
8288
}
8389
},
8490
children: children
8591
}
86-
92+
8793
// Build the structure: wrap everything in a blockquote container with data hints
8894
// Using blockquote for semantic HTML since the source is a blockquote
8995
const noteContainer: any = {
9096
type: 'paragraph', // Use a standard mdast type
9197
data: {
9298
hName: 'blockquote', // Transform to blockquote for semantic HTML
9399
hProperties: {
94-
className: ['remark-note', noteType]
100+
className: [baseClass, makeClass(noteType)]
95101
}
96102
},
97103
children: [headerNode, contentNode]
98104
}
99-
105+
100106
return noteContainer
101107
}

lib/types/options.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Configuration options for the remark-notes plugin
3+
*
4+
* @module lib/types/options
5+
*/
6+
7+
/**
8+
* Options for configuring the remark-notes plugin behavior
9+
*
10+
* @example
11+
* ```typescript
12+
* import remarkNotes from 'remark-notes-plugin'
13+
*
14+
* // Using default options
15+
* unified().use(remarkNotes)
16+
*
17+
* // With custom class prefix
18+
* unified().use(remarkNotes, { classPrefix: 'my-callout' })
19+
*
20+
* // Disable automatic style injection
21+
* unified().use(remarkNotes, { injectStyles: false })
22+
*
23+
* // Both options
24+
* unified().use(remarkNotes, {
25+
* classPrefix: 'custom-note',
26+
* injectStyles: false
27+
* })
28+
* ```
29+
*/
30+
export interface RemarkNotesOptions {
31+
/**
32+
* Custom prefix for all generated CSS class names
33+
*
34+
* This prefix is **prepended** to the standard 'remark-note' class names.
35+
*
36+
* **Default (no prefix):**
37+
* - Container: `remark-note` and `remark-note-{noteType}`
38+
* - Elements: `remark-note-header`, `remark-note-icon`, etc.
39+
*
40+
* **With prefix (e.g., 'my'):**
41+
* - Container: `my-remark-note` and `my-remark-note-{noteType}`
42+
* - Elements: `my-remark-note-header`, `my-remark-note-icon`, etc.
43+
*
44+
* @default '' (empty string - no prefix)
45+
*
46+
* @example
47+
* ```typescript
48+
* // No prefix (default)
49+
* { classPrefix: '' }
50+
* // Generates: class="remark-note remark-note-tip"
51+
*
52+
* // Custom prefix
53+
* { classPrefix: 'my' }
54+
* // Generates: class="my-remark-note my-remark-note-tip"
55+
* ```
56+
*
57+
* @remarks
58+
* The shipped CSS uses attribute selectors (e.g., `[class*="remark-note-icon"]`)
59+
* and will work with any prefix automatically.
60+
*/
61+
classPrefix?: string
62+
63+
/**
64+
* Controls whether the plugin automatically injects styles into the document
65+
*
66+
* When `true` (default), the plugin injects a `<style>` tag containing the note styles
67+
* directly into the AST. This is convenient for most use cases.
68+
*
69+
* When `false`, styles are not injected and you must manually import the CSS file:
70+
* `import 'remark-notes-plugin/styles.css'`
71+
*
72+
* Set to `false` when:
73+
* - Using Server-Side Rendering (SSR) with separate CSS extraction
74+
* - Building with tools that handle CSS imports separately (Vite, Webpack, etc.)
75+
* - Providing completely custom styles
76+
* - You want more control over style loading order
77+
*
78+
* @default true
79+
*
80+
* @example
81+
* ```typescript
82+
* // Automatic style injection (default)
83+
* { injectStyles: true }
84+
*
85+
* // Manual CSS import
86+
* { injectStyles: false }
87+
* // Then in your code:
88+
* // import 'remark-notes-plugin/styles.css'
89+
* ```
90+
*
91+
* @remarks
92+
* The CSS file is available at the package export: `remark-notes-plugin/styles.css`
93+
*/
94+
injectStyles?: boolean
95+
}

0 commit comments

Comments
 (0)