@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 +5 -8
- package/dist/index.js +37 -60
- package/package.json +2 -2
- package/src/components/BlogPost.tsx +2 -6
- package/src/hooks/readMarkdownFile.ts +9 -5
- package/src/index.ts +0 -1
- package/src/utils/parseMarkdown.test.ts +10 -10
- package/src/utils/parseMarkdown.ts +45 -36
- package/src/utils/renderMarkdown.test.tsx +7 -71
- package/src/utils/renderMarkdown.tsx +1 -18
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
|
-
|
|
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,
|
|
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
|
|
22
|
+
var getPageInfoFromSections = (ast) => {
|
|
43
23
|
var _a;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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,
|
|
416
|
-
const
|
|
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,
|
|
448
|
-
const rendered = renderMarkdownAst(ast
|
|
449
|
-
return
|
|
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.
|
|
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.
|
|
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,
|
|
44
|
-
const title = frontmatter.title;
|
|
45
|
-
const desc = frontmatter.description;
|
|
43
|
+
const { ast, title, description: desc } = parseMarkdown(content);
|
|
46
44
|
|
|
47
|
-
const
|
|
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
|
-
|
|
|
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,
|
|
39
|
-
const rendered = renderMarkdownAst(ast
|
|
42
|
+
const { ast, title = '', description = '' } = parseMarkdown(content);
|
|
43
|
+
const rendered = renderMarkdownAst(ast);
|
|
40
44
|
|
|
41
|
-
return { success: true, rendered,
|
|
45
|
+
return { success: true, rendered, title, description };
|
|
42
46
|
};
|
package/src/index.ts
CHANGED
|
@@ -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 {
|
|
9
|
-
expect(
|
|
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 {
|
|
19
|
-
expect(
|
|
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 {
|
|
25
|
-
expect(
|
|
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 {
|
|
30
|
-
expect(
|
|
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 {
|
|
36
|
-
expect(
|
|
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
|
-
|
|
12
|
+
title: string | undefined;
|
|
13
|
+
description: string | undefined;
|
|
18
14
|
};
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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()
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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('')
|
|
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')
|
|
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')
|
|
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('
|
|
98
|
-
const result = renderMarkdownAst(parse('## Section\n\nSome content')
|
|
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')
|
|
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
|
|
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)
|