@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 CHANGED
@@ -79,13 +79,12 @@ description: A short description shown below the title
79
79
  | Image | `![alt](url)` |
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 | `> text` — renders as info callout |
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. The callout type is controlled by a prefix on the first word of the quote.
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
- > This is an info callout. Use `> text` for general information.
219
+ | Syntax | Callout type |
220
+ | :------------- | :----------- |
221
+ | `[!NOTE]` | info |
222
+ | `[!TIP]` | info |
223
+ | `[!IMPORTANT]` | info |
224
+ | `[!WARNING]` | warning |
225
+ | `[!CAUTION]` | error |
221
226
 
222
- > ~This is a warning callout. Use `> ~text` to flag caution.
223
-
224
- > !This is an error callout. Use `> !text` for errors or destructive actions.
227
+ ```markdown
228
+ > [!WARNING]
229
+ >
230
+ > Do not clone this repository into system folders.
231
+ ```
225
232
 
226
- The prefix character is stripped from the rendered output — only the callout style changes.
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
- if (firstInline.value.startsWith("!")) {
293
- calloutType = "error";
294
- const trimmed = firstInline.value.slice(1).trimStart();
295
- strippedChildren = [
296
- __spreadProps(__spreadValues({}, firstChild), {
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.1",
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",
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) => groupSections(parseMarkdown(md).ast.children);
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
- if (firstInline.value.startsWith('!')) {
121
- calloutType = 'error';
122
- const trimmed = firstInline.value.slice(1).trimStart();
123
- strippedChildren = [
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
  }