@san-siva/blogkit-md 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/dist/index.js +25 -32
- package/package.json +2 -2
- package/src/utils/groupSections.test.ts +8 -2
- package/src/utils/parseMarkdown.test.ts +39 -0
- package/src/utils/parseMarkdown.ts +16 -1
- package/src/utils/renderMarkdown.tsx +5 -26
package/README.md
CHANGED
|
@@ -79,13 +79,12 @@ description: A short description shown below the title
|
|
|
79
79
|
| Image | `` |
|
|
80
80
|
| Ordered list | `1. item` |
|
|
81
81
|
| Unordered list | `- item` |
|
|
82
|
+
| Task list | `- [ ] item` / `- [x] item` |
|
|
82
83
|
| Table | `\| col \| col \|` — headers and rows only |
|
|
83
84
|
| Code block | ` ```lang ` |
|
|
84
85
|
| Mermaid diagram | ` ```mermaid ` |
|
|
85
86
|
| Thematic break | `---` |
|
|
86
|
-
| Blockquote
|
|
87
|
-
| Blockquote (warning) | `> ~text` — renders as warning callout |
|
|
88
|
-
| Blockquote (error) | `> !text` — renders as error callout |
|
|
87
|
+
| Blockquote / Callout | `> text` or `> [!TYPE]` — renders as callout |
|
|
89
88
|
|
|
90
89
|
## Philosophy
|
|
91
90
|
|
|
@@ -215,15 +214,23 @@ Result content.
|
|
|
215
214
|
|
|
216
215
|
### Callouts
|
|
217
216
|
|
|
218
|
-
Blockquotes are rendered as styled callout banners.
|
|
217
|
+
Blockquotes are rendered as styled callout banners. Plain blockquotes render as info callouts. Use GitHub-style alert syntax to control the type:
|
|
219
218
|
|
|
220
|
-
|
|
219
|
+
| Syntax | Callout type |
|
|
220
|
+
| :------------- | :----------- |
|
|
221
|
+
| `[!NOTE]` | info |
|
|
222
|
+
| `[!TIP]` | info |
|
|
223
|
+
| `[!IMPORTANT]` | info |
|
|
224
|
+
| `[!WARNING]` | warning |
|
|
225
|
+
| `[!CAUTION]` | error |
|
|
221
226
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
>
|
|
227
|
+
```markdown
|
|
228
|
+
> [!WARNING]
|
|
229
|
+
>
|
|
230
|
+
> Do not clone this repository into system folders.
|
|
231
|
+
```
|
|
225
232
|
|
|
226
|
-
The
|
|
233
|
+
The `[!TYPE]` line is stripped from the rendered output.
|
|
227
234
|
|
|
228
235
|
## Want more customization?
|
|
229
236
|
|
|
@@ -238,3 +245,7 @@ Contributions are welcome!
|
|
|
238
245
|
|
|
239
246
|
- **Author:** [Santhosh Siva](https://www.santhoshsiva.dev)
|
|
240
247
|
- **License:** [MIT](https://github.com/san-siva/blogkit-md/blob/main/LICENSE)
|
|
248
|
+
|
|
249
|
+
> [!WARNING]
|
|
250
|
+
>
|
|
251
|
+
> This project is in early development.
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,25 @@ import remarkGfm from "remark-gfm";
|
|
|
29
29
|
import remarkParse from "remark-parse";
|
|
30
30
|
import { unified } from "unified";
|
|
31
31
|
import { parse as parseYaml } from "yaml";
|
|
32
|
+
|
|
33
|
+
// src/utils/extractText.ts
|
|
34
|
+
var extractText = (nodes) => nodes.map((node) => {
|
|
35
|
+
if (node.type === "text") return node.value;
|
|
36
|
+
if ("children" in node)
|
|
37
|
+
return extractText(node.children);
|
|
38
|
+
return "";
|
|
39
|
+
}).join("");
|
|
40
|
+
|
|
41
|
+
// src/utils/parseMarkdown.ts
|
|
42
|
+
var consumeH1Title = (ast, frontmatter) => {
|
|
43
|
+
var _a;
|
|
44
|
+
if (!frontmatter.title && ((_a = ast.children[0]) == null ? void 0 : _a.type) === "heading" && ast.children[0].depth === 1) {
|
|
45
|
+
const title = extractText(ast.children[0].children);
|
|
46
|
+
ast.children.shift();
|
|
47
|
+
return __spreadProps(__spreadValues({}, frontmatter), { title });
|
|
48
|
+
}
|
|
49
|
+
return frontmatter;
|
|
50
|
+
};
|
|
32
51
|
var parseMarkdown = (content) => {
|
|
33
52
|
var _a, _b;
|
|
34
53
|
const processor = unified().use(remarkParse).use(remarkGfm).use(remarkFrontmatter, ["yaml"]);
|
|
@@ -44,6 +63,7 @@ var parseMarkdown = (content) => {
|
|
|
44
63
|
};
|
|
45
64
|
ast.children.shift();
|
|
46
65
|
}
|
|
66
|
+
frontmatter = consumeH1Title(ast, frontmatter);
|
|
47
67
|
return { ast, frontmatter };
|
|
48
68
|
};
|
|
49
69
|
|
|
@@ -57,14 +77,6 @@ import {
|
|
|
57
77
|
Table
|
|
58
78
|
} from "@san-siva/blogkit";
|
|
59
79
|
|
|
60
|
-
// src/utils/extractText.ts
|
|
61
|
-
var extractText = (nodes) => nodes.map((node) => {
|
|
62
|
-
if (node.type === "text") return node.value;
|
|
63
|
-
if ("children" in node)
|
|
64
|
-
return extractText(node.children);
|
|
65
|
-
return "";
|
|
66
|
-
}).join("");
|
|
67
|
-
|
|
68
80
|
// src/utils/groupSections.ts
|
|
69
81
|
var consumeNode = (nodes, index, sections, section) => {
|
|
70
82
|
const node = nodes.at(index);
|
|
@@ -289,30 +301,11 @@ function renderNode({
|
|
|
289
301
|
if ((firstChild == null ? void 0 : firstChild.type) === "paragraph") {
|
|
290
302
|
const firstInline = firstChild.children[0];
|
|
291
303
|
if ((firstInline == null ? void 0 : firstInline.type) === "text") {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
children: [
|
|
298
|
-
__spreadProps(__spreadValues({}, firstInline), { value: trimmed }),
|
|
299
|
-
...firstChild.children.slice(1)
|
|
300
|
-
]
|
|
301
|
-
}),
|
|
302
|
-
...children.slice(1)
|
|
303
|
-
];
|
|
304
|
-
} else if (firstInline.value.startsWith("~")) {
|
|
305
|
-
calloutType = "warning";
|
|
306
|
-
const trimmed = firstInline.value.slice(1).trimStart();
|
|
307
|
-
strippedChildren = [
|
|
308
|
-
__spreadProps(__spreadValues({}, firstChild), {
|
|
309
|
-
children: [
|
|
310
|
-
__spreadProps(__spreadValues({}, firstInline), { value: trimmed }),
|
|
311
|
-
...firstChild.children.slice(1)
|
|
312
|
-
]
|
|
313
|
-
}),
|
|
314
|
-
...children.slice(1)
|
|
315
|
-
];
|
|
304
|
+
const githubAlertMatch = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/i.exec(firstInline.value);
|
|
305
|
+
if (githubAlertMatch) {
|
|
306
|
+
const alertType = githubAlertMatch[1].toUpperCase();
|
|
307
|
+
calloutType = alertType === "WARNING" ? "warning" : alertType === "CAUTION" ? "error" : "info";
|
|
308
|
+
strippedChildren = children.slice(1);
|
|
316
309
|
}
|
|
317
310
|
}
|
|
318
311
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@san-siva/blogkit-md",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Converts markdown files into JSX blog posts for Blogkit",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"fix": "eslint . --config eslint.config.ts --fix"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@san-siva/blogkit": "^1.1.
|
|
24
|
+
"@san-siva/blogkit": "^1.1.28",
|
|
25
25
|
"@san-siva/stylekit": "^1.0.12",
|
|
26
26
|
"next": "^16.0.10",
|
|
27
27
|
"react": "^19.0.0",
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import remarkParse from 'remark-parse';
|
|
4
|
+
import { unified } from 'unified';
|
|
5
|
+
import type { Root } from 'mdast';
|
|
2
6
|
|
|
3
|
-
import { parseMarkdown } from './parseMarkdown';
|
|
4
7
|
import { groupSections } from './groupSections';
|
|
5
8
|
|
|
6
|
-
const sections = (md: string) =>
|
|
9
|
+
const sections = (md: string) => {
|
|
10
|
+
const ast = unified().use(remarkParse).use(remarkGfm).parse(md) as Root;
|
|
11
|
+
return groupSections(ast.children);
|
|
12
|
+
};
|
|
7
13
|
|
|
8
14
|
describe('groupSections', () => {
|
|
9
15
|
describe('empty input', () => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { parseMarkdown } from './parseMarkdown';
|
|
4
|
+
|
|
5
|
+
describe('parseMarkdown', () => {
|
|
6
|
+
describe('consumeH1Title', () => {
|
|
7
|
+
it('extracts a leading H1 as the title when no frontmatter title is set', () => {
|
|
8
|
+
const { frontmatter, ast } = parseMarkdown('# My Title\n\nSome content');
|
|
9
|
+
expect(frontmatter.title).toBe('My Title');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('removes the H1 from the AST after extracting it as the title', () => {
|
|
13
|
+
const { ast } = parseMarkdown('# My Title\n\nSome content');
|
|
14
|
+
expect(ast.children[0]?.type).not.toBe('heading');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('does not consume the H1 when frontmatter title is already set', () => {
|
|
18
|
+
const { frontmatter, ast } = parseMarkdown('---\ntitle: Frontmatter Title\n---\n\n# H1 Title');
|
|
19
|
+
expect(frontmatter.title).toBe('Frontmatter Title');
|
|
20
|
+
expect(ast.children[0]?.type).toBe('heading');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('does not extract a title when the first heading is not H1', () => {
|
|
24
|
+
const { frontmatter } = parseMarkdown('## Section Title');
|
|
25
|
+
expect(frontmatter.title).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('does not extract a title when H1 is not the first node', () => {
|
|
29
|
+
const { frontmatter, ast } = parseMarkdown('Some intro\n\n# Title');
|
|
30
|
+
expect(frontmatter.title).toBeUndefined();
|
|
31
|
+
expect(ast.children).toHaveLength(2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('handles inline formatting in the H1', () => {
|
|
35
|
+
const { frontmatter } = parseMarkdown('# My **Bold** Title');
|
|
36
|
+
expect(frontmatter.title).toBe('My Bold Title');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { Root, Yaml } from 'mdast';
|
|
1
|
+
import type { Heading, Root, Yaml } from 'mdast';
|
|
2
2
|
import remarkFrontmatter from 'remark-frontmatter';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import remarkParse from 'remark-parse';
|
|
5
5
|
import { unified } from 'unified';
|
|
6
6
|
import { parse as parseYaml } from 'yaml';
|
|
7
7
|
|
|
8
|
+
import { extractText } from './extractText';
|
|
9
|
+
|
|
8
10
|
export type Frontmatter = {
|
|
9
11
|
title?: string;
|
|
10
12
|
description?: string;
|
|
@@ -15,6 +17,17 @@ export type ParseResult = {
|
|
|
15
17
|
frontmatter: Frontmatter;
|
|
16
18
|
};
|
|
17
19
|
|
|
20
|
+
// When a document has no frontmatter title, treat a leading H1 as the page title
|
|
21
|
+
// and remove it from the AST so it isn't also rendered as a section.
|
|
22
|
+
const consumeH1Title = (ast: Root, frontmatter: Frontmatter): Frontmatter => {
|
|
23
|
+
if (!frontmatter.title && ast.children[0]?.type === 'heading' && (ast.children[0] as Heading).depth === 1) {
|
|
24
|
+
const title = extractText((ast.children[0] as Heading).children);
|
|
25
|
+
ast.children.shift();
|
|
26
|
+
return { ...frontmatter, title };
|
|
27
|
+
}
|
|
28
|
+
return frontmatter;
|
|
29
|
+
};
|
|
30
|
+
|
|
18
31
|
export const parseMarkdown = (content: string): ParseResult => {
|
|
19
32
|
const processor = unified().use(remarkParse).use(remarkGfm).use(remarkFrontmatter, ['yaml']);
|
|
20
33
|
const ast = processor.parse(content) as Root;
|
|
@@ -32,5 +45,7 @@ export const parseMarkdown = (content: string): ParseResult => {
|
|
|
32
45
|
ast.children.shift();
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
frontmatter = consumeH1Title(ast, frontmatter);
|
|
49
|
+
|
|
35
50
|
return { ast, frontmatter };
|
|
36
51
|
};
|
|
@@ -117,32 +117,11 @@ function renderNode({
|
|
|
117
117
|
if (firstChild?.type === 'paragraph') {
|
|
118
118
|
const firstInline = firstChild.children[0];
|
|
119
119
|
if (firstInline?.type === 'text') {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
...firstChild,
|
|
126
|
-
children: [
|
|
127
|
-
{ ...firstInline, value: trimmed },
|
|
128
|
-
...firstChild.children.slice(1),
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
...children.slice(1),
|
|
132
|
-
];
|
|
133
|
-
} else if (firstInline.value.startsWith('~')) {
|
|
134
|
-
calloutType = 'warning';
|
|
135
|
-
const trimmed = firstInline.value.slice(1).trimStart();
|
|
136
|
-
strippedChildren = [
|
|
137
|
-
{
|
|
138
|
-
...firstChild,
|
|
139
|
-
children: [
|
|
140
|
-
{ ...firstInline, value: trimmed },
|
|
141
|
-
...firstChild.children.slice(1),
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
...children.slice(1),
|
|
145
|
-
];
|
|
120
|
+
const githubAlertMatch = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/i.exec(firstInline.value);
|
|
121
|
+
if (githubAlertMatch) {
|
|
122
|
+
const alertType = githubAlertMatch[1].toUpperCase();
|
|
123
|
+
calloutType = alertType === 'WARNING' ? 'warning' : alertType === 'CAUTION' ? 'error' : 'info';
|
|
124
|
+
strippedChildren = children.slice(1);
|
|
146
125
|
}
|
|
147
126
|
}
|
|
148
127
|
}
|