@san-siva/blogkit-md 0.3.7 → 0.3.8

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.js CHANGED
@@ -93,7 +93,7 @@ var consumeNode = (nodes, index, sections, section) => {
93
93
  if (isIncrementingHeading) {
94
94
  if (section.nodes.length > 0) {
95
95
  const subSection = {
96
- title: "",
96
+ titleNodes: [],
97
97
  headingLevel: section.headingLevel,
98
98
  nodes: section.nodes,
99
99
  subsections: [],
@@ -103,7 +103,7 @@ var consumeNode = (nodes, index, sections, section) => {
103
103
  section.nodes = [];
104
104
  }
105
105
  const subsection = {
106
- title: extractText(node.children),
106
+ titleNodes: node.children,
107
107
  headingLevel,
108
108
  nodes: [],
109
109
  subsections: [],
@@ -114,7 +114,7 @@ var consumeNode = (nodes, index, sections, section) => {
114
114
  }
115
115
  if (!section.previousSection) {
116
116
  const subSection = {
117
- title: extractText(node.children),
117
+ titleNodes: node.children,
118
118
  headingLevel,
119
119
  nodes: [],
120
120
  subsections: [],
@@ -127,7 +127,7 @@ var consumeNode = (nodes, index, sections, section) => {
127
127
  };
128
128
  var groupSections = (nodes) => {
129
129
  const initialSection = {
130
- title: "",
130
+ titleNodes: [],
131
131
  headingLevel: Infinity,
132
132
  nodes: [],
133
133
  subsections: [],
@@ -135,7 +135,7 @@ var groupSections = (nodes) => {
135
135
  };
136
136
  const sections = [initialSection];
137
137
  consumeNode(nodes, 0, sections, initialSection);
138
- return sections.filter((s) => s.title !== "" || s.nodes.length > 0);
138
+ return sections.filter((s) => s.titleNodes.length > 0 || s.nodes.length > 0);
139
139
  };
140
140
 
141
141
  // src/utils/renderPhrasingContent.tsx
@@ -326,18 +326,11 @@ function renderNode({
326
326
  (item) => item.checked !== null && item.checked !== void 0
327
327
  );
328
328
  if (isTaskList) {
329
- const items = node.children.map((item, index) => ({
330
- id: String(index),
331
- isChecked: item.checked === true,
332
- children: item.children.map(
333
- (child, childIndex) => renderNode({
334
- node: child,
335
- key: childIndex,
336
- inList: true,
337
- counters
338
- })
339
- )
340
- }));
329
+ const items = node.children.map((item, index) => {
330
+ const para = item.children.find((c) => c.type === "paragraph");
331
+ const children = (para == null ? void 0 : para.type) === "paragraph" ? /* @__PURE__ */ jsx2("p", { children: renderPhrasingContent(para.children) }) : /* @__PURE__ */ jsx2("p", {});
332
+ return { id: String(index), isChecked: item.checked === true, children };
333
+ });
341
334
  return /* @__PURE__ */ jsx2(CheckList, { items, hasMarginUp: true, hasMarginDown: true }, key);
342
335
  }
343
336
  const Tag = node.ordered ? "ol" : "ul";
@@ -361,8 +354,8 @@ function renderNodes(nodes, counters) {
361
354
  );
362
355
  }
363
356
  function renderSection(section, counters, key = -1) {
364
- var _a;
365
- return /* @__PURE__ */ jsxs(BlogSection, { title: (_a = section == null ? void 0 : section.title) != null ? _a : "", children: [
357
+ const title = section.titleNodes.length > 0 ? /* @__PURE__ */ jsx2("p", { children: renderPhrasingContent(section.titleNodes) }) : "";
358
+ return /* @__PURE__ */ jsxs(BlogSection, { title, children: [
366
359
  renderNodes(section.nodes, counters),
367
360
  section.subsections.map(
368
361
  (subsection, index) => renderSection(subsection, counters, index)
@@ -371,9 +364,9 @@ function renderSection(section, counters, key = -1) {
371
364
  }
372
365
  function stripRedundantSectionTitle(grouped, isTitleEmpty) {
373
366
  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 = "";
367
+ const [{ titleNodes, nodes, subsections }] = grouped;
368
+ if (titleNodes.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
369
+ grouped[0].titleNodes = [];
377
370
  }
378
371
  }
379
372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@san-siva/blogkit-md",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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.30",
24
+ "@san-siva/blogkit": "^1.1.31",
25
25
  "@san-siva/stylekit": "^1.0.12",
26
26
  "next": "^16.0.10",
27
27
  "react": "^19.0.0",
@@ -4,6 +4,7 @@ import remarkParse from 'remark-parse';
4
4
  import { unified } from 'unified';
5
5
  import type { Root } from 'mdast';
6
6
 
7
+ import { extractText } from './extractText';
7
8
  import { groupSections } from './groupSections';
8
9
 
9
10
  const sections = (md: string) => {
@@ -11,6 +12,9 @@ const sections = (md: string) => {
11
12
  return groupSections(ast.children);
12
13
  };
13
14
 
15
+ const title = (s: ReturnType<typeof sections>[number]) =>
16
+ extractText(s.titleNodes);
17
+
14
18
  describe('groupSections', () => {
15
19
  describe('empty input', () => {
16
20
  it('returns an empty array', () => {
@@ -22,7 +26,7 @@ describe('groupSections', () => {
22
26
  it('returns a single untitled section containing all nodes', () => {
23
27
  const result = sections('some text\n\nmore text');
24
28
  expect(result).toHaveLength(1);
25
- expect(result[0].title).toBe('');
29
+ expect(result[0].titleNodes).toHaveLength(0);
26
30
  expect(result[0].nodes).toHaveLength(2);
27
31
  });
28
32
  });
@@ -31,21 +35,21 @@ describe('groupSections', () => {
31
35
  it('creates a section for H1', () => {
32
36
  const result = sections('# Title');
33
37
  expect(result).toHaveLength(1);
34
- expect(result[0].title).toBe('Title');
38
+ expect(title(result[0])).toBe('Title');
35
39
  expect(result[0].headingLevel).toBe(1);
36
40
  });
37
41
 
38
42
  it('creates a section for H2', () => {
39
43
  const result = sections('## Section');
40
44
  expect(result).toHaveLength(1);
41
- expect(result[0].title).toBe('Section');
45
+ expect(title(result[0])).toBe('Section');
42
46
  expect(result[0].headingLevel).toBe(2);
43
47
  });
44
48
 
45
49
  it('creates multiple sections for sequential H2s', () => {
46
50
  const result = sections('## One\n\n## Two\n\n## Three');
47
51
  expect(result).toHaveLength(3);
48
- expect(result.map(s => s.title)).toEqual(['One', 'Two', 'Three']);
52
+ expect(result.map(s => title(s))).toEqual(['One', 'Two', 'Three']);
49
53
  });
50
54
 
51
55
  it('groups content nodes under their section', () => {
@@ -59,9 +63,9 @@ describe('groupSections', () => {
59
63
  it('returns an untitled intro section followed by headed sections', () => {
60
64
  const result = sections('intro text\n\n## Section');
61
65
  expect(result).toHaveLength(2);
62
- expect(result[0].title).toBe('');
66
+ expect(result[0].titleNodes).toHaveLength(0);
63
67
  expect(result[0].nodes).toHaveLength(1);
64
- expect(result[1].title).toBe('Section');
68
+ expect(title(result[1])).toBe('Section');
65
69
  });
66
70
  });
67
71
 
@@ -69,20 +73,20 @@ describe('groupSections', () => {
69
73
  it('moves content preceding a subsection into its own untitled subsection', () => {
70
74
  const result = sections('## Parent\n\nsome text\n\n### Child');
71
75
  expect(result).toHaveLength(1);
72
- expect(result[0].title).toBe('Parent');
76
+ expect(title(result[0])).toBe('Parent');
73
77
  expect(result[0].nodes).toHaveLength(0);
74
78
  expect(result[0].subsections).toHaveLength(2);
75
- expect(result[0].subsections[0].title).toBe('');
79
+ expect(result[0].subsections[0].titleNodes).toHaveLength(0);
76
80
  expect(result[0].subsections[0].nodes).toHaveLength(1);
77
- expect(result[0].subsections[1].title).toBe('Child');
81
+ expect(title(result[0].subsections[1])).toBe('Child');
78
82
  });
79
83
 
80
84
  it('skips the untitled subsection when no content precedes the deeper heading', () => {
81
85
  const result = sections('## Parent\n\n### Child');
82
86
  expect(result).toHaveLength(1);
83
- expect(result[0].title).toBe('Parent');
87
+ expect(title(result[0])).toBe('Parent');
84
88
  expect(result[0].subsections).toHaveLength(1);
85
- expect(result[0].subsections[0].title).toBe('Child');
89
+ expect(title(result[0].subsections[0])).toBe('Child');
86
90
  });
87
91
  });
88
92
 
@@ -91,19 +95,19 @@ describe('groupSections', () => {
91
95
  const result = sections('## Parent\n\n### Child');
92
96
  expect(result).toHaveLength(1);
93
97
  expect(result[0].subsections).toHaveLength(1);
94
- expect(result[0].subsections[0].title).toBe('Child');
98
+ expect(title(result[0].subsections[0])).toBe('Child');
95
99
  });
96
100
 
97
101
  it('promotes H3 to top-level when no parent section exists', () => {
98
102
  const result = sections('### Orphan');
99
103
  expect(result).toHaveLength(1);
100
- expect(result[0].title).toBe('Orphan');
104
+ expect(title(result[0])).toBe('Orphan');
101
105
  });
102
106
 
103
107
  it('creates multiple subsections under one parent', () => {
104
108
  const result = sections('## Parent\n\n### A\n\n### B');
105
109
  expect(result).toHaveLength(1);
106
- expect(result[0].subsections.map(s => s.title)).toEqual(['A', 'B']);
110
+ expect(result[0].subsections.map(s => title(s))).toEqual(['A', 'B']);
107
111
  });
108
112
  });
109
113
 
@@ -1,9 +1,7 @@
1
- import type { RootContent } from 'mdast';
2
-
3
- import { extractText } from './extractText';
1
+ import type { PhrasingContent, RootContent } from 'mdast';
4
2
 
5
3
  export type Section = {
6
- title: string;
4
+ titleNodes: PhrasingContent[];
7
5
  headingLevel: number;
8
6
  nodes: RootContent[];
9
7
  subsections: Section[];
@@ -37,7 +35,7 @@ const consumeNode = (
37
35
  // instead of: Parent { nodes: [...], subsections: [Child] }
38
36
  if (section.nodes.length > 0) {
39
37
  const subSection: Section = {
40
- title: '',
38
+ titleNodes: [],
41
39
  headingLevel: section.headingLevel,
42
40
  nodes: section.nodes,
43
41
  subsections: [],
@@ -47,7 +45,7 @@ const consumeNode = (
47
45
  section.nodes = [];
48
46
  }
49
47
  const subsection: Section = {
50
- title: extractText(node.children),
48
+ titleNodes: node.children,
51
49
  headingLevel,
52
50
  nodes: [],
53
51
  subsections: [],
@@ -59,7 +57,7 @@ const consumeNode = (
59
57
 
60
58
  if (!section.previousSection) {
61
59
  const subSection: Section = {
62
- title: extractText(node.children),
60
+ titleNodes: node.children,
63
61
  headingLevel,
64
62
  nodes: [],
65
63
  subsections: [],
@@ -74,7 +72,7 @@ const consumeNode = (
74
72
 
75
73
  export const groupSections = (nodes: RootContent[]): Section[] => {
76
74
  const initialSection: Section = {
77
- title: '',
75
+ titleNodes: [],
78
76
  headingLevel: Infinity,
79
77
  nodes: [],
80
78
  subsections: [],
@@ -82,5 +80,5 @@ export const groupSections = (nodes: RootContent[]): Section[] => {
82
80
  };
83
81
  const sections: Section[] = [initialSection];
84
82
  consumeNode(nodes, 0, sections, initialSection);
85
- return sections.filter(s => s.title !== '' || s.nodes.length > 0);
83
+ return sections.filter(s => s.titleNodes.length > 0 || s.nodes.length > 0);
86
84
  };
@@ -17,13 +17,16 @@ vi.mock('@san-siva/blogkit', () => ({
17
17
 
18
18
  import { renderMarkdownAst, stripRedundantSectionTitle } from './renderMarkdown';
19
19
  import type { Section } from './groupSections';
20
+ import type { PhrasingContent } from 'mdast';
21
+
22
+ const textNode = (value: string): PhrasingContent => ({ type: 'text', value });
20
23
 
21
24
  const makeSection = (
22
- title: string,
25
+ titleNodes: PhrasingContent[] = [],
23
26
  nodes: Section['nodes'] = [],
24
27
  subsections: Section[] = []
25
28
  ): Section => ({
26
- title,
29
+ titleNodes,
27
30
  headingLevel: 2,
28
31
  nodes,
29
32
  subsections,
@@ -33,45 +36,45 @@ const parse = (md: string): Root =>
33
36
  unified().use(remarkParse).use(remarkGfm).parse(md) as Root;
34
37
 
35
38
  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: [] }])];
39
+ it('clears titleNodes when isTitleEmpty is false and the section has nodes', () => {
40
+ const grouped = [makeSection([textNode('My Title')], [{ type: 'paragraph', children: [] }])];
38
41
  stripRedundantSectionTitle(grouped, false);
39
- expect(grouped[0].title).toBe('');
42
+ expect(grouped[0].titleNodes).toHaveLength(0);
40
43
  });
41
44
 
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
+ 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])];
45
48
  stripRedundantSectionTitle(grouped, false);
46
- expect(grouped[0].title).toBe('');
49
+ expect(grouped[0].titleNodes).toHaveLength(0);
47
50
  });
48
51
 
49
- it('does not clear the title when isTitleEmpty is true', () => {
50
- const grouped = [makeSection('My Title', [{ type: 'paragraph', children: [] }])];
52
+ it('does not clear titleNodes when isTitleEmpty is true', () => {
53
+ const grouped = [makeSection([textNode('My Title')], [{ type: 'paragraph', children: [] }])];
51
54
  stripRedundantSectionTitle(grouped, true);
52
- expect(grouped[0].title).toBe('My Title');
55
+ expect(grouped[0].titleNodes).toEqual([textNode('My Title')]);
53
56
  });
54
57
 
55
- it('does not clear the title when there are multiple sections', () => {
58
+ it('does not clear titleNodes when there are multiple sections', () => {
56
59
  const grouped = [
57
- makeSection('Title A', [{ type: 'paragraph', children: [] }]),
58
- makeSection('Title B', [{ type: 'paragraph', children: [] }]),
60
+ makeSection([textNode('Title A')], [{ type: 'paragraph', children: [] }]),
61
+ makeSection([textNode('Title B')], [{ type: 'paragraph', children: [] }]),
59
62
  ];
60
63
  stripRedundantSectionTitle(grouped, false);
61
- expect(grouped[0].title).toBe('Title A');
62
- expect(grouped[1].title).toBe('Title B');
64
+ expect(grouped[0].titleNodes).toEqual([textNode('Title A')]);
65
+ expect(grouped[1].titleNodes).toEqual([textNode('Title B')]);
63
66
  });
64
67
 
65
- it('does not clear the title when the section has no nodes or subsections', () => {
66
- const grouped = [makeSection('My Title')];
68
+ it('does not clear titleNodes when the section has no nodes or subsections', () => {
69
+ const grouped = [makeSection([textNode('My Title')])];
67
70
  stripRedundantSectionTitle(grouped, false);
68
- expect(grouped[0].title).toBe('My Title');
71
+ expect(grouped[0].titleNodes).toEqual([textNode('My Title')]);
69
72
  });
70
73
 
71
- it('does not clear the title when it is already empty', () => {
72
- const grouped = [makeSection('', [{ type: 'paragraph', children: [] }])];
74
+ it('does not clear titleNodes when they are already empty', () => {
75
+ const grouped = [makeSection([], [{ type: 'paragraph', children: [] }])];
73
76
  stripRedundantSectionTitle(grouped, false);
74
- expect(grouped[0].title).toBe('');
77
+ expect(grouped[0].titleNodes).toHaveLength(0);
75
78
  });
76
79
  });
77
80
 
@@ -100,12 +103,19 @@ describe('renderMarkdownAst', () => {
100
103
  it('preserves the section title when isTitleEmpty is true', () => {
101
104
  const result = renderMarkdownAst(parse('## Section\n\nSome content'), true);
102
105
  const section = result.sections[0] as React.ReactElement;
103
- expect(section.props.title).toBe('Section');
106
+ const titleEl = section.props.title as React.ReactElement;
107
+ expect(titleEl.type).toBe('p');
108
+ expect(titleEl.props.children).toEqual(['Section']);
104
109
  });
105
110
 
106
111
  it('preserves titles of all sections when there are multiple sections', () => {
107
112
  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']);
113
+ const titleEls = (result.sections as React.ReactElement[]).map(
114
+ s => s.props.title as React.ReactElement
115
+ );
116
+ expect(titleEls[0].type).toBe('p');
117
+ expect(titleEls[0].props.children).toEqual(['One']);
118
+ expect(titleEls[1].type).toBe('p');
119
+ expect(titleEls[1].props.children).toEqual(['Two']);
110
120
  });
111
121
  });
@@ -155,18 +155,16 @@ function renderNode({
155
155
  item => item.checked !== null && item.checked !== undefined
156
156
  );
157
157
  if (isTaskList) {
158
- const items: CheckListItem[] = node.children.map((item, index) => ({
159
- id: String(index),
160
- isChecked: item.checked === true,
161
- children: item.children.map((child, childIndex) =>
162
- renderNode({
163
- node: child as RootContent,
164
- key: childIndex,
165
- inList: true,
166
- counters,
167
- })
168
- ),
169
- }));
158
+ const items: CheckListItem[] = node.children.map((item, index) => {
159
+ const para = item.children.find(c => c.type === 'paragraph');
160
+ const children =
161
+ para?.type === 'paragraph' ? (
162
+ <p>{renderPhrasingContent(para.children)}</p>
163
+ ) : (
164
+ <p />
165
+ );
166
+ return { id: String(index), isChecked: item.checked === true, children };
167
+ });
170
168
  return <CheckList key={key} items={items} hasMarginUp hasMarginDown />;
171
169
  }
172
170
  const Tag = node.ordered ? 'ol' : 'ul';
@@ -207,8 +205,11 @@ function renderSection(
207
205
  counters: Counters,
208
206
  key = -1
209
207
  ): React.ReactNode {
208
+ const title = section.titleNodes.length > 0
209
+ ? <p>{renderPhrasingContent(section.titleNodes)}</p>
210
+ : '';
210
211
  return (
211
- <BlogSection key={key} title={section?.title ?? ''}>
212
+ <BlogSection key={key} title={title}>
212
213
  {renderNodes(section.nodes, counters)}
213
214
  {section.subsections.map((subsection, index) =>
214
215
  renderSection(subsection, counters, index)
@@ -226,9 +227,9 @@ export function stripRedundantSectionTitle(
226
227
  isTitleEmpty: boolean
227
228
  ): void {
228
229
  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 = '';
230
+ const [{ titleNodes, nodes, subsections }] = grouped;
231
+ if (titleNodes.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
232
+ grouped[0].titleNodes = [];
232
233
  }
233
234
  }
234
235
  }