@san-siva/blogkit-md 0.3.18 → 0.3.20

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/dist/index.d.mts CHANGED
@@ -8,11 +8,6 @@ type BlogPostProperties = {
8
8
  };
9
9
  declare const BlogPost: ({ filePath, jsonLd }: BlogPostProperties) => Promise<react_jsx_runtime.JSX.Element>;
10
10
 
11
- type Frontmatter = {
12
- title?: string;
13
- description?: string;
14
- };
15
-
16
11
  type RenderedMarkdown = {
17
12
  sections: React.ReactNode[];
18
13
  };
@@ -20,13 +15,15 @@ declare const MarkdownSections: ({ rendered, }: {
20
15
  rendered: RenderedMarkdown;
21
16
  }) => React.ReactNode;
22
17
 
23
- type MarkdownFileResult = ({
18
+ type MarkdownFileResult = {
24
19
  success: true;
25
20
  rendered: RenderedMarkdown;
26
- } & Frontmatter) | {
21
+ title: string;
22
+ description: string;
23
+ } | {
27
24
  success: false;
28
25
  error: string;
29
26
  };
30
27
  declare const readMarkdownFile: (filePath: string | undefined) => Promise<MarkdownFileResult>;
31
28
 
32
- export { BlogPost, type Frontmatter, MarkdownSections, readMarkdownFile };
29
+ export { BlogPost, MarkdownSections, readMarkdownFile };
package/dist/index.js CHANGED
@@ -1,23 +1,3 @@
1
- var __defProp = Object.defineProperty;
2
- var __defProps = Object.defineProperties;
3
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
- var __spreadValues = (a, b) => {
9
- for (var prop in b || (b = {}))
10
- if (__hasOwnProp.call(b, prop))
11
- __defNormalProp(a, prop, b[prop]);
12
- if (__getOwnPropSymbols)
13
- for (var prop of __getOwnPropSymbols(b)) {
14
- if (__propIsEnum.call(b, prop))
15
- __defNormalProp(a, prop, b[prop]);
16
- }
17
- return a;
18
- };
19
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
-
21
1
  // src/components/BlogPost.tsx
22
2
  import { Blog, BlogHeader, Callout as Callout2 } from "@san-siva/blogkit";
23
3
  import { readFile } from "fs/promises";
@@ -39,35 +19,44 @@ var extractText = (nodes) => nodes.map((node) => {
39
19
  }).join("");
40
20
 
41
21
  // src/utils/parseMarkdown.ts
42
- var consumeH1Title = (ast, frontmatter) => {
22
+ var getPageInfoFromSections = (ast) => {
43
23
  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 });
24
+ const firstSection = (_a = ast == null ? void 0 : ast.children) == null ? void 0 : _a[0];
25
+ if (!firstSection) return;
26
+ const isFirstSectionHeading = firstSection.type === "heading";
27
+ if (!isFirstSectionHeading) return;
28
+ const isFirstSectionAValidHeader = firstSection.depth === 1;
29
+ if (!isFirstSectionAValidHeader) return;
30
+ const title = extractText(ast.children[0].children);
31
+ ast.children.shift();
32
+ return { title };
33
+ };
34
+ var getPageInfoFromYaml = (ast) => {
35
+ var _a, _b, _c;
36
+ if (((_b = (_a = ast == null ? void 0 : ast.children) == null ? void 0 : _a[0]) == null ? void 0 : _b.type) !== "yaml") return;
37
+ const raw = ast.children[0].value;
38
+ ast.children.shift();
39
+ try {
40
+ const parsed = parseYaml(raw);
41
+ const title = (_c = parsed.title) != null ? _c : parsed.name;
42
+ return {
43
+ title: typeof title === "string" ? title : void 0,
44
+ description: typeof parsed.description === "string" ? parsed.description : void 0
45
+ };
46
+ } catch (e) {
47
+ return;
48
48
  }
49
- return frontmatter;
50
49
  };
51
50
  var parseMarkdown = (content) => {
52
51
  var _a, _b;
53
52
  const processor = unified().use(remarkParse).use(remarkGfm).use(remarkFrontmatter, ["yaml"]);
54
53
  const ast = processor.parse(content);
55
- let frontmatter = {};
56
- if (((_a = ast.children[0]) == null ? void 0 : _a.type) === "yaml") {
57
- const raw = ast.children[0].value;
58
- try {
59
- const parsed = parseYaml(raw);
60
- const title = (_b = parsed.title) != null ? _b : parsed.name;
61
- frontmatter = {
62
- title: typeof title === "string" ? title : void 0,
63
- description: typeof parsed.description === "string" ? parsed.description : void 0
64
- };
65
- } catch (e) {
66
- }
67
- ast.children.shift();
68
- }
69
- frontmatter = consumeH1Title(ast, frontmatter);
70
- return { ast, frontmatter };
54
+ const { title, description } = (_b = (_a = getPageInfoFromYaml(ast)) != null ? _a : getPageInfoFromSections(ast)) != null ? _b : {};
55
+ return {
56
+ ast,
57
+ title,
58
+ description
59
+ };
71
60
  };
72
61
 
73
62
  // src/utils/renderMarkdown.tsx
@@ -369,18 +358,9 @@ function renderSection(section, counters, key = -1) {
369
358
  )
370
359
  ] }, key);
371
360
  }
372
- function stripRedundantSectionTitle(grouped, isTitleEmpty) {
373
- if (!isTitleEmpty && grouped.length === 1) {
374
- const [{ titleNodes, nodes, subsections }] = grouped;
375
- if (titleNodes.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
376
- grouped[0].titleNodes = [];
377
- }
378
- }
379
- }
380
- var renderMarkdownAst = (ast, isTitleEmpty) => {
361
+ var renderMarkdownAst = (ast) => {
381
362
  const counters = { mermaid: 0 };
382
363
  const grouped = groupSections(ast.children);
383
- stripRedundantSectionTitle(grouped, isTitleEmpty);
384
364
  return {
385
365
  sections: grouped.map(
386
366
  (section, index) => renderSection(section, counters, index)
@@ -412,11 +392,8 @@ var BlogPost = async ({ filePath, jsonLd }) => {
412
392
  '" is empty.'
413
393
  ] }) });
414
394
  }
415
- const { ast, frontmatter } = parseMarkdown(content);
416
- const title = frontmatter.title;
417
- const desc = frontmatter.description;
418
- const isTitleEmpty = !title || title.trim() === "";
419
- const rendered = renderMarkdownAst(ast, isTitleEmpty);
395
+ const { ast, title, description: desc } = parseMarkdown(content);
396
+ const rendered = renderMarkdownAst(ast);
420
397
  return /* @__PURE__ */ jsxs2(Blog, { jsonLd, children: [
421
398
  title && /* @__PURE__ */ jsx3(BlogHeader, { title: [title], desc: desc ? [desc] : [] }),
422
399
  /* @__PURE__ */ jsx3(MarkdownSections, { rendered })
@@ -444,9 +421,9 @@ var readMarkdownFile = async (filePath) => {
444
421
  if (!content.trim()) {
445
422
  return { success: false, error: `File "${filePath}" is empty.` };
446
423
  }
447
- const { ast, frontmatter } = parseMarkdown(content);
448
- const rendered = renderMarkdownAst(ast, !frontmatter.title);
449
- return __spreadValues({ success: true, rendered }, frontmatter);
424
+ const { ast, title = "", description = "" } = parseMarkdown(content);
425
+ const rendered = renderMarkdownAst(ast);
426
+ return { success: true, rendered, title, description };
450
427
  };
451
428
  export {
452
429
  BlogPost_default as BlogPost,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@san-siva/blogkit-md",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
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.41",
24
+ "@san-siva/blogkit": "^1.1.42",
25
25
  "@san-siva/stylekit": "^1.0.13",
26
26
  "next": "^16.0.10",
27
27
  "react": "^19.0.0",
@@ -40,13 +40,9 @@ const BlogPost = async ({ filePath, jsonLd }: BlogPostProperties) => {
40
40
  );
41
41
  }
42
42
 
43
- const { ast, frontmatter } = parseMarkdown(content);
44
- const title = frontmatter.title;
45
- const desc = frontmatter.description;
43
+ const { ast, title, description: desc } = parseMarkdown(content);
46
44
 
47
- const isTitleEmpty = !title || title.trim() === '';
48
-
49
- const rendered = renderMarkdownAst(ast, isTitleEmpty);
45
+ const rendered = renderMarkdownAst(ast);
50
46
 
51
47
  return (
52
48
  <Blog jsonLd={jsonLd}>
@@ -1,13 +1,17 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
- import type { Frontmatter } from '../utils/parseMarkdown';
5
4
  import { parseMarkdown } from '../utils/parseMarkdown';
6
5
  import type { RenderedMarkdown } from '../utils/renderMarkdown';
7
6
  import { renderMarkdownAst } from '../utils/renderMarkdown';
8
7
 
9
8
  type MarkdownFileResult =
10
- | ({ success: true; rendered: RenderedMarkdown } & Frontmatter)
9
+ | {
10
+ success: true;
11
+ rendered: RenderedMarkdown;
12
+ title: string;
13
+ description: string;
14
+ }
11
15
  | { success: false; error: string };
12
16
 
13
17
  export const readMarkdownFile = async (
@@ -35,8 +39,8 @@ export const readMarkdownFile = async (
35
39
  return { success: false, error: `File "${filePath}" is empty.` };
36
40
  }
37
41
 
38
- const { ast, frontmatter } = parseMarkdown(content);
39
- const rendered = renderMarkdownAst(ast, !frontmatter.title);
42
+ const { ast, title = '', description = '' } = parseMarkdown(content);
43
+ const rendered = renderMarkdownAst(ast);
40
44
 
41
- return { success: true, rendered, ...frontmatter };
45
+ return { success: true, rendered, title, description };
42
46
  };
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { default as BlogPost } from './components/BlogPost';
2
2
  export { readMarkdownFile } from './hooks/readMarkdownFile';
3
- export type { Frontmatter } from './utils/parseMarkdown';
4
3
  export { MarkdownSections } from './utils/renderMarkdown';
@@ -5,8 +5,8 @@ import { parseMarkdown } from './parseMarkdown';
5
5
  describe('parseMarkdown', () => {
6
6
  describe('consumeH1Title', () => {
7
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');
8
+ const { title } = parseMarkdown('# My Title\n\nSome content');
9
+ expect(title).toBe('My Title');
10
10
  });
11
11
 
12
12
  it('removes the H1 from the AST after extracting it as the title', () => {
@@ -15,25 +15,25 @@ describe('parseMarkdown', () => {
15
15
  });
16
16
 
17
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');
18
+ const { title, ast } = parseMarkdown('---\ntitle: Frontmatter Title\n---\n\n# H1 Title');
19
+ expect(title).toBe('Frontmatter Title');
20
20
  expect(ast.children[0]?.type).toBe('heading');
21
21
  });
22
22
 
23
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();
24
+ const { title } = parseMarkdown('## Section Title');
25
+ expect(title).toBeUndefined();
26
26
  });
27
27
 
28
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();
29
+ const { title, ast } = parseMarkdown('Some intro\n\n# Title');
30
+ expect(title).toBeUndefined();
31
31
  expect(ast.children).toHaveLength(2);
32
32
  });
33
33
 
34
34
  it('handles inline formatting in the H1', () => {
35
- const { frontmatter } = parseMarkdown('# My **Bold** Title');
36
- expect(frontmatter.title).toBe('My Bold Title');
35
+ const { title } = parseMarkdown('# My **Bold** Title');
36
+ expect(title).toBe('My Bold Title');
37
37
  });
38
38
  });
39
39
  });
@@ -7,49 +7,58 @@ import { parse as parseYaml } from 'yaml';
7
7
 
8
8
  import { extractText } from './extractText';
9
9
 
10
- export type Frontmatter = {
11
- title?: string;
12
- description?: string;
13
- };
14
-
15
10
  export type ParseResult = {
16
11
  ast: Root;
17
- frontmatter: Frontmatter;
12
+ title: string | undefined;
13
+ description: string | undefined;
18
14
  };
19
15
 
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 };
16
+ const getPageInfoFromSections = (ast: Root) => {
17
+ const firstSection = ast?.children?.[0];
18
+ if (!firstSection) return;
19
+
20
+ const isFirstSectionHeading = firstSection.type === 'heading';
21
+ if (!isFirstSectionHeading) return;
22
+
23
+ const isFirstSectionAValidHeader = (firstSection as Heading).depth === 1;
24
+ if (!isFirstSectionAValidHeader) return;
25
+
26
+ const title = extractText((ast.children[0] as Heading).children);
27
+
28
+ // to avoid re-printing the same header twice;
29
+ ast.children.shift();
30
+
31
+ return { title };
32
+ };
33
+
34
+ const getPageInfoFromYaml = (ast: Root) => {
35
+ if (ast?.children?.[0]?.type !== 'yaml') return;
36
+ const raw = (ast.children[0] as Yaml).value;
37
+ ast.children.shift();
38
+ try {
39
+ const parsed = parseYaml(raw) as Record<string, unknown>;
40
+ const title = parsed.title ?? parsed.name;
41
+ return {
42
+ title: typeof title === 'string' ? title : undefined,
43
+ description:
44
+ typeof parsed.description === 'string' ? parsed.description : undefined,
45
+ };
46
+ } catch {
47
+ return;
27
48
  }
28
- return frontmatter;
29
49
  };
30
50
 
31
51
  export const parseMarkdown = (content: string): ParseResult => {
32
- const processor = unified().use(remarkParse).use(remarkGfm).use(remarkFrontmatter, ['yaml']);
52
+ const processor = unified()
53
+ .use(remarkParse)
54
+ .use(remarkGfm)
55
+ .use(remarkFrontmatter, ['yaml']);
33
56
  const ast = processor.parse(content) as Root;
34
-
35
- let frontmatter: Frontmatter = {};
36
-
37
- if (ast.children[0]?.type === 'yaml') {
38
- const raw = (ast.children[0] as Yaml).value;
39
- try {
40
- const parsed = parseYaml(raw) as Record<string, unknown>;
41
- const title = parsed.title ?? parsed.name;
42
- frontmatter = {
43
- title: typeof title === 'string' ? title : undefined,
44
- description: typeof parsed.description === 'string' ? parsed.description : undefined,
45
- };
46
- } catch {
47
- // Invalid YAML frontmatter — ignore and fall through to H1 title extraction
48
- }
49
- ast.children.shift();
50
- }
51
-
52
- frontmatter = consumeH1Title(ast, frontmatter);
53
-
54
- return { ast, frontmatter };
57
+ const { title, description } =
58
+ getPageInfoFromYaml(ast) ?? getPageInfoFromSections(ast) ?? {};
59
+ return {
60
+ ast,
61
+ title,
62
+ description,
63
+ };
55
64
  };
@@ -15,93 +15,29 @@ vi.mock('@san-siva/blogkit', () => ({
15
15
  Table: 'Table',
16
16
  }));
17
17
 
18
- import { renderMarkdownAst, stripRedundantSectionTitle } from './renderMarkdown';
19
- import type { Section } from './groupSections';
20
- import type { PhrasingContent } from 'mdast';
21
-
22
- const textNode = (value: string): PhrasingContent => ({ type: 'text', value });
23
-
24
- const makeSection = (
25
- titleNodes: PhrasingContent[] = [],
26
- nodes: Section['nodes'] = [],
27
- subsections: Section[] = []
28
- ): Section => ({
29
- titleNodes,
30
- headingLevel: 2,
31
- nodes,
32
- subsections,
33
- });
18
+ import { renderMarkdownAst } from './renderMarkdown';
34
19
 
35
20
  const parse = (md: string): Root =>
36
21
  unified().use(remarkParse).use(remarkGfm).parse(md) as Root;
37
22
 
38
- describe('stripRedundantSectionTitle', () => {
39
- it('clears titleNodes when isTitleEmpty is false and the section has nodes', () => {
40
- const grouped = [makeSection([textNode('My Title')], [{ type: 'paragraph', children: [] }])];
41
- stripRedundantSectionTitle(grouped, false);
42
- expect(grouped[0].titleNodes).toHaveLength(0);
43
- });
44
-
45
- it('clears titleNodes when isTitleEmpty is false and the section has subsections', () => {
46
- const sub = makeSection([textNode('Sub')], [{ type: 'paragraph', children: [] }]);
47
- const grouped = [makeSection([textNode('My Title')], [], [sub])];
48
- stripRedundantSectionTitle(grouped, false);
49
- expect(grouped[0].titleNodes).toHaveLength(0);
50
- });
51
-
52
- it('does not clear titleNodes when isTitleEmpty is true', () => {
53
- const grouped = [makeSection([textNode('My Title')], [{ type: 'paragraph', children: [] }])];
54
- stripRedundantSectionTitle(grouped, true);
55
- expect(grouped[0].titleNodes).toEqual([textNode('My Title')]);
56
- });
57
-
58
- it('does not clear titleNodes when there are multiple sections', () => {
59
- const grouped = [
60
- makeSection([textNode('Title A')], [{ type: 'paragraph', children: [] }]),
61
- makeSection([textNode('Title B')], [{ type: 'paragraph', children: [] }]),
62
- ];
63
- stripRedundantSectionTitle(grouped, false);
64
- expect(grouped[0].titleNodes).toEqual([textNode('Title A')]);
65
- expect(grouped[1].titleNodes).toEqual([textNode('Title B')]);
66
- });
67
-
68
- it('does not clear titleNodes when the section has no nodes or subsections', () => {
69
- const grouped = [makeSection([textNode('My Title')])];
70
- stripRedundantSectionTitle(grouped, false);
71
- expect(grouped[0].titleNodes).toEqual([textNode('My Title')]);
72
- });
73
-
74
- it('does not clear titleNodes when they are already empty', () => {
75
- const grouped = [makeSection([], [{ type: 'paragraph', children: [] }])];
76
- stripRedundantSectionTitle(grouped, false);
77
- expect(grouped[0].titleNodes).toHaveLength(0);
78
- });
79
- });
80
-
81
23
  describe('renderMarkdownAst', () => {
82
24
  it('returns an empty sections array for empty markdown', () => {
83
- const result = renderMarkdownAst(parse(''), false);
25
+ const result = renderMarkdownAst(parse(''));
84
26
  expect(result.sections).toHaveLength(0);
85
27
  });
86
28
 
87
29
  it('returns one section for single-section markdown', () => {
88
- const result = renderMarkdownAst(parse('## Section\n\nSome content'), false);
30
+ const result = renderMarkdownAst(parse('## Section\n\nSome content'));
89
31
  expect(result.sections).toHaveLength(1);
90
32
  });
91
33
 
92
34
  it('returns multiple sections for multi-section markdown', () => {
93
- const result = renderMarkdownAst(parse('## One\n\n## Two\n\n## Three'), false);
35
+ const result = renderMarkdownAst(parse('## One\n\n## Two\n\n## Three'));
94
36
  expect(result.sections).toHaveLength(3);
95
37
  });
96
38
 
97
- it('strips the section title when isTitleEmpty is false and there is a single titled section', () => {
98
- const result = renderMarkdownAst(parse('## Section\n\nSome content'), false);
99
- const section = result.sections[0] as React.ReactElement;
100
- expect(section.props.title).toBe('');
101
- });
102
-
103
- it('preserves the section title when isTitleEmpty is true', () => {
104
- const result = renderMarkdownAst(parse('## Section\n\nSome content'), true);
39
+ it('preserves the section title', () => {
40
+ const result = renderMarkdownAst(parse('## Section\n\nSome content'));
105
41
  const section = result.sections[0] as React.ReactElement;
106
42
  const titleEl = section.props.title as React.ReactElement;
107
43
  expect(titleEl.type).toBe('p');
@@ -109,7 +45,7 @@ describe('renderMarkdownAst', () => {
109
45
  });
110
46
 
111
47
  it('preserves titles of all sections when there are multiple sections', () => {
112
- const result = renderMarkdownAst(parse('## One\n\n## Two'), false);
48
+ const result = renderMarkdownAst(parse('## One\n\n## Two'));
113
49
  const titleEls = (result.sections as React.ReactElement[]).map(
114
50
  s => s.props.title as React.ReactElement
115
51
  );
@@ -229,27 +229,10 @@ export type RenderedMarkdown = {
229
229
  sections: React.ReactNode[];
230
230
  };
231
231
 
232
- export function stripRedundantSectionTitle(
233
- grouped: Section[],
234
- isTitleEmpty: boolean
235
- ): void {
236
- if (!isTitleEmpty && grouped.length === 1) {
237
- const [{ titleNodes, nodes, subsections }] = grouped;
238
- if (titleNodes.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
239
- grouped[0].titleNodes = [];
240
- }
241
- }
242
- }
243
-
244
- export const renderMarkdownAst = (
245
- ast: Root,
246
- isTitleEmpty: boolean
247
- ): RenderedMarkdown => {
232
+ export const renderMarkdownAst = (ast: Root): RenderedMarkdown => {
248
233
  const counters: Counters = { mermaid: 0 };
249
234
  const grouped = groupSections(ast.children);
250
235
 
251
- stripRedundantSectionTitle(grouped, isTitleEmpty);
252
-
253
236
  return {
254
237
  sections: grouped.map((section, index) =>
255
238
  renderSection(section, counters, index)