@san-siva/blogkit-md 0.3.4 → 0.3.6
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 +2 -0
- package/dist/index.js +13 -3
- package/package.json +2 -2
- package/src/components/BlogPost.tsx +4 -2
- package/src/hooks/readMarkdownFile.ts +1 -1
- package/src/utils/renderMarkdown.test.tsx +111 -0
- package/src/utils/renderMarkdown.tsx +19 -1
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@ title: blogkit-md
|
|
|
3
3
|
description: A React component library that converts markdown files into rendered blog posts for `@san-siva/blogkit`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
# Hello world
|
|
7
|
+
|
|
6
8
|
A React component library that converts markdown files into rendered blog posts for [`@san-siva/blogkit`](https://blogkit.santhoshsiva.dev).
|
|
7
9
|
|
|
8
10
|
> Live preview of this README is available here [blogkit-md.santhoshsiva.dev](https://blogkit-md.santhoshsiva.dev)
|
package/dist/index.js
CHANGED
|
@@ -369,9 +369,18 @@ function renderSection(section, counters, key = -1) {
|
|
|
369
369
|
)
|
|
370
370
|
] }, key);
|
|
371
371
|
}
|
|
372
|
-
|
|
372
|
+
function stripRedundantSectionTitle(grouped, isTitleEmpty) {
|
|
373
|
+
if (!isTitleEmpty && grouped.length === 1) {
|
|
374
|
+
const [{ title, nodes, subsections }] = grouped;
|
|
375
|
+
if (title.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
|
|
376
|
+
grouped[0].title = "";
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
var renderMarkdownAst = (ast, isTitleEmpty) => {
|
|
373
381
|
const counters = { mermaid: 0 };
|
|
374
382
|
const grouped = groupSections(ast.children);
|
|
383
|
+
stripRedundantSectionTitle(grouped, isTitleEmpty);
|
|
375
384
|
return {
|
|
376
385
|
sections: grouped.map(
|
|
377
386
|
(section, index) => renderSection(section, counters, index)
|
|
@@ -404,9 +413,10 @@ var BlogPost = async ({ filePath, jsonLd }) => {
|
|
|
404
413
|
] }) });
|
|
405
414
|
}
|
|
406
415
|
const { ast, frontmatter } = parseMarkdown(content);
|
|
407
|
-
const rendered = renderMarkdownAst(ast);
|
|
408
416
|
const title = frontmatter.title;
|
|
409
417
|
const desc = frontmatter.description;
|
|
418
|
+
const isTitleEmpty = !title || title.trim() === "";
|
|
419
|
+
const rendered = renderMarkdownAst(ast, isTitleEmpty);
|
|
410
420
|
return /* @__PURE__ */ jsxs2(Blog, { jsonLd, children: [
|
|
411
421
|
title && /* @__PURE__ */ jsx3(BlogHeader, { title: [title], desc: desc ? [desc] : [] }),
|
|
412
422
|
/* @__PURE__ */ jsx3(MarkdownSections, { rendered })
|
|
@@ -435,7 +445,7 @@ var readMarkdownFile = async (filePath) => {
|
|
|
435
445
|
return { success: false, error: `File "${filePath}" is empty.` };
|
|
436
446
|
}
|
|
437
447
|
const { ast, frontmatter } = parseMarkdown(content);
|
|
438
|
-
const rendered = renderMarkdownAst(ast);
|
|
448
|
+
const rendered = renderMarkdownAst(ast, !frontmatter.title);
|
|
439
449
|
return __spreadValues({ success: true, rendered }, frontmatter);
|
|
440
450
|
};
|
|
441
451
|
export {
|
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.6",
|
|
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.29",
|
|
25
25
|
"@san-siva/stylekit": "^1.0.12",
|
|
26
26
|
"next": "^16.0.10",
|
|
27
27
|
"react": "^19.0.0",
|
|
@@ -41,11 +41,13 @@ const BlogPost = async ({ filePath, jsonLd }: BlogPostProperties) => {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const { ast, frontmatter } = parseMarkdown(content);
|
|
44
|
-
const rendered = renderMarkdownAst(ast);
|
|
45
|
-
|
|
46
44
|
const title = frontmatter.title;
|
|
47
45
|
const desc = frontmatter.description;
|
|
48
46
|
|
|
47
|
+
const isTitleEmpty = !title || title.trim() === '';
|
|
48
|
+
|
|
49
|
+
const rendered = renderMarkdownAst(ast, isTitleEmpty);
|
|
50
|
+
|
|
49
51
|
return (
|
|
50
52
|
<Blog jsonLd={jsonLd}>
|
|
51
53
|
{title && <BlogHeader title={[title]} desc={desc ? [desc] : []} />}
|
|
@@ -36,7 +36,7 @@ export const readMarkdownFile = async (
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const { ast, frontmatter } = parseMarkdown(content);
|
|
39
|
-
const rendered = renderMarkdownAst(ast);
|
|
39
|
+
const rendered = renderMarkdownAst(ast, !frontmatter.title);
|
|
40
40
|
|
|
41
41
|
return { success: true, rendered, ...frontmatter };
|
|
42
42
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkParse from 'remark-parse';
|
|
5
|
+
import { unified } from 'unified';
|
|
6
|
+
import type { Root } from 'mdast';
|
|
7
|
+
|
|
8
|
+
vi.mock('@san-siva/stylekit/styles/index.module.scss', () => ({ default: {} }));
|
|
9
|
+
vi.mock('@san-siva/blogkit', () => ({
|
|
10
|
+
BlogSection: 'BlogSection',
|
|
11
|
+
Callout: 'Callout',
|
|
12
|
+
CheckList: 'CheckList',
|
|
13
|
+
CodeBlock: 'CodeBlock',
|
|
14
|
+
Mermaid: 'Mermaid',
|
|
15
|
+
Table: 'Table',
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { renderMarkdownAst, stripRedundantSectionTitle } from './renderMarkdown';
|
|
19
|
+
import type { Section } from './groupSections';
|
|
20
|
+
|
|
21
|
+
const makeSection = (
|
|
22
|
+
title: string,
|
|
23
|
+
nodes: Section['nodes'] = [],
|
|
24
|
+
subsections: Section[] = []
|
|
25
|
+
): Section => ({
|
|
26
|
+
title,
|
|
27
|
+
headingLevel: 2,
|
|
28
|
+
nodes,
|
|
29
|
+
subsections,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const parse = (md: string): Root =>
|
|
33
|
+
unified().use(remarkParse).use(remarkGfm).parse(md) as Root;
|
|
34
|
+
|
|
35
|
+
describe('stripRedundantSectionTitle', () => {
|
|
36
|
+
it('clears the section title when isTitleEmpty is false and the section has nodes', () => {
|
|
37
|
+
const grouped = [makeSection('My Title', [{ type: 'paragraph', children: [] }])];
|
|
38
|
+
stripRedundantSectionTitle(grouped, false);
|
|
39
|
+
expect(grouped[0].title).toBe('');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('clears the section title when isTitleEmpty is false and the section has subsections', () => {
|
|
43
|
+
const sub = makeSection('Sub', [{ type: 'paragraph', children: [] }]);
|
|
44
|
+
const grouped = [makeSection('My Title', [], [sub])];
|
|
45
|
+
stripRedundantSectionTitle(grouped, false);
|
|
46
|
+
expect(grouped[0].title).toBe('');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not clear the title when isTitleEmpty is true', () => {
|
|
50
|
+
const grouped = [makeSection('My Title', [{ type: 'paragraph', children: [] }])];
|
|
51
|
+
stripRedundantSectionTitle(grouped, true);
|
|
52
|
+
expect(grouped[0].title).toBe('My Title');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does not clear the title when there are multiple sections', () => {
|
|
56
|
+
const grouped = [
|
|
57
|
+
makeSection('Title A', [{ type: 'paragraph', children: [] }]),
|
|
58
|
+
makeSection('Title B', [{ type: 'paragraph', children: [] }]),
|
|
59
|
+
];
|
|
60
|
+
stripRedundantSectionTitle(grouped, false);
|
|
61
|
+
expect(grouped[0].title).toBe('Title A');
|
|
62
|
+
expect(grouped[1].title).toBe('Title B');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not clear the title when the section has no nodes or subsections', () => {
|
|
66
|
+
const grouped = [makeSection('My Title')];
|
|
67
|
+
stripRedundantSectionTitle(grouped, false);
|
|
68
|
+
expect(grouped[0].title).toBe('My Title');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not clear the title when it is already empty', () => {
|
|
72
|
+
const grouped = [makeSection('', [{ type: 'paragraph', children: [] }])];
|
|
73
|
+
stripRedundantSectionTitle(grouped, false);
|
|
74
|
+
expect(grouped[0].title).toBe('');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('renderMarkdownAst', () => {
|
|
79
|
+
it('returns an empty sections array for empty markdown', () => {
|
|
80
|
+
const result = renderMarkdownAst(parse(''), false);
|
|
81
|
+
expect(result.sections).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns one section for single-section markdown', () => {
|
|
85
|
+
const result = renderMarkdownAst(parse('## Section\n\nSome content'), false);
|
|
86
|
+
expect(result.sections).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns multiple sections for multi-section markdown', () => {
|
|
90
|
+
const result = renderMarkdownAst(parse('## One\n\n## Two\n\n## Three'), false);
|
|
91
|
+
expect(result.sections).toHaveLength(3);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('strips the section title when isTitleEmpty is false and there is a single titled section', () => {
|
|
95
|
+
const result = renderMarkdownAst(parse('## Section\n\nSome content'), false);
|
|
96
|
+
const section = result.sections[0] as React.ReactElement;
|
|
97
|
+
expect(section.props.title).toBe('');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('preserves the section title when isTitleEmpty is true', () => {
|
|
101
|
+
const result = renderMarkdownAst(parse('## Section\n\nSome content'), true);
|
|
102
|
+
const section = result.sections[0] as React.ReactElement;
|
|
103
|
+
expect(section.props.title).toBe('Section');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('preserves titles of all sections when there are multiple sections', () => {
|
|
107
|
+
const result = renderMarkdownAst(parse('## One\n\n## Two'), false);
|
|
108
|
+
const titles = (result.sections as React.ReactElement[]).map(s => s.props.title);
|
|
109
|
+
expect(titles).toEqual(['One', 'Two']);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -221,9 +221,27 @@ export type RenderedMarkdown = {
|
|
|
221
221
|
sections: React.ReactNode[];
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
-
export
|
|
224
|
+
export function stripRedundantSectionTitle(
|
|
225
|
+
grouped: Section[],
|
|
226
|
+
isTitleEmpty: boolean
|
|
227
|
+
): void {
|
|
228
|
+
if (!isTitleEmpty && grouped.length === 1) {
|
|
229
|
+
const [{ title, nodes, subsections }] = grouped;
|
|
230
|
+
if (title.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
|
|
231
|
+
grouped[0].title = '';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const renderMarkdownAst = (
|
|
237
|
+
ast: Root,
|
|
238
|
+
isTitleEmpty: boolean
|
|
239
|
+
): RenderedMarkdown => {
|
|
225
240
|
const counters: Counters = { mermaid: 0 };
|
|
226
241
|
const grouped = groupSections(ast.children);
|
|
242
|
+
|
|
243
|
+
stripRedundantSectionTitle(grouped, isTitleEmpty);
|
|
244
|
+
|
|
227
245
|
return {
|
|
228
246
|
sections: grouped.map((section, index) =>
|
|
229
247
|
renderSection(section, counters, index)
|