@san-siva/blogkit-md 0.3.17 → 0.3.19
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 +42 -61
- 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 +12 -22
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
|
|
@@ -332,7 +321,11 @@ function renderNode({
|
|
|
332
321
|
const items = node.children.map((item, index) => {
|
|
333
322
|
const para = item.children.find((c) => c.type === "paragraph");
|
|
334
323
|
const children = (para == null ? void 0 : para.type) === "paragraph" ? /* @__PURE__ */ jsx2("p", { children: renderPhrasingContent(para.children) }) : /* @__PURE__ */ jsx2("p", {});
|
|
335
|
-
return {
|
|
324
|
+
return {
|
|
325
|
+
id: String(index),
|
|
326
|
+
isChecked: item.checked === true,
|
|
327
|
+
children
|
|
328
|
+
};
|
|
336
329
|
});
|
|
337
330
|
return /* @__PURE__ */ jsx2(CheckList, { items, hasMarginUp: true, hasMarginDown: true }, key);
|
|
338
331
|
}
|
|
@@ -365,18 +358,9 @@ function renderSection(section, counters, key = -1) {
|
|
|
365
358
|
)
|
|
366
359
|
] }, key);
|
|
367
360
|
}
|
|
368
|
-
|
|
369
|
-
if (!isTitleEmpty && grouped.length === 1) {
|
|
370
|
-
const [{ titleNodes, nodes, subsections }] = grouped;
|
|
371
|
-
if (titleNodes.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
|
|
372
|
-
grouped[0].titleNodes = [];
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
var renderMarkdownAst = (ast, isTitleEmpty) => {
|
|
361
|
+
var renderMarkdownAst = (ast) => {
|
|
377
362
|
const counters = { mermaid: 0 };
|
|
378
363
|
const grouped = groupSections(ast.children);
|
|
379
|
-
stripRedundantSectionTitle(grouped, isTitleEmpty);
|
|
380
364
|
return {
|
|
381
365
|
sections: grouped.map(
|
|
382
366
|
(section, index) => renderSection(section, counters, index)
|
|
@@ -408,11 +392,8 @@ var BlogPost = async ({ filePath, jsonLd }) => {
|
|
|
408
392
|
'" is empty.'
|
|
409
393
|
] }) });
|
|
410
394
|
}
|
|
411
|
-
const { ast,
|
|
412
|
-
const
|
|
413
|
-
const desc = frontmatter.description;
|
|
414
|
-
const isTitleEmpty = !title || title.trim() === "";
|
|
415
|
-
const rendered = renderMarkdownAst(ast, isTitleEmpty);
|
|
395
|
+
const { ast, title, description: desc } = parseMarkdown(content);
|
|
396
|
+
const rendered = renderMarkdownAst(ast);
|
|
416
397
|
return /* @__PURE__ */ jsxs2(Blog, { jsonLd, children: [
|
|
417
398
|
title && /* @__PURE__ */ jsx3(BlogHeader, { title: [title], desc: desc ? [desc] : [] }),
|
|
418
399
|
/* @__PURE__ */ jsx3(MarkdownSections, { rendered })
|
|
@@ -440,9 +421,9 @@ var readMarkdownFile = async (filePath) => {
|
|
|
440
421
|
if (!content.trim()) {
|
|
441
422
|
return { success: false, error: `File "${filePath}" is empty.` };
|
|
442
423
|
}
|
|
443
|
-
const { ast,
|
|
444
|
-
const rendered = renderMarkdownAst(ast
|
|
445
|
-
return
|
|
424
|
+
const { ast, title = "", description = "" } = parseMarkdown(content);
|
|
425
|
+
const rendered = renderMarkdownAst(ast);
|
|
426
|
+
return { success: true, rendered, title, description };
|
|
446
427
|
};
|
|
447
428
|
export {
|
|
448
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.19",
|
|
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.41",
|
|
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
|
);
|
|
@@ -163,7 +163,11 @@ function renderNode({
|
|
|
163
163
|
) : (
|
|
164
164
|
<p />
|
|
165
165
|
);
|
|
166
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
id: String(index),
|
|
168
|
+
isChecked: item.checked === true,
|
|
169
|
+
children,
|
|
170
|
+
};
|
|
167
171
|
});
|
|
168
172
|
return <CheckList key={key} items={items} hasMarginUp hasMarginDown />;
|
|
169
173
|
}
|
|
@@ -205,9 +209,12 @@ function renderSection(
|
|
|
205
209
|
counters: Counters,
|
|
206
210
|
key = -1
|
|
207
211
|
): React.ReactNode {
|
|
208
|
-
const title =
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
const title =
|
|
213
|
+
section.titleNodes.length > 0 ? (
|
|
214
|
+
<p>{renderPhrasingContent(section.titleNodes)}</p>
|
|
215
|
+
) : (
|
|
216
|
+
''
|
|
217
|
+
);
|
|
211
218
|
return (
|
|
212
219
|
<BlogSection key={key} title={title}>
|
|
213
220
|
{renderNodes(section.nodes, counters)}
|
|
@@ -222,27 +229,10 @@ export type RenderedMarkdown = {
|
|
|
222
229
|
sections: React.ReactNode[];
|
|
223
230
|
};
|
|
224
231
|
|
|
225
|
-
export
|
|
226
|
-
grouped: Section[],
|
|
227
|
-
isTitleEmpty: boolean
|
|
228
|
-
): void {
|
|
229
|
-
if (!isTitleEmpty && grouped.length === 1) {
|
|
230
|
-
const [{ titleNodes, nodes, subsections }] = grouped;
|
|
231
|
-
if (titleNodes.length > 0 && (nodes.length > 0 || subsections.length > 0)) {
|
|
232
|
-
grouped[0].titleNodes = [];
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export const renderMarkdownAst = (
|
|
238
|
-
ast: Root,
|
|
239
|
-
isTitleEmpty: boolean
|
|
240
|
-
): RenderedMarkdown => {
|
|
232
|
+
export const renderMarkdownAst = (ast: Root): RenderedMarkdown => {
|
|
241
233
|
const counters: Counters = { mermaid: 0 };
|
|
242
234
|
const grouped = groupSections(ast.children);
|
|
243
235
|
|
|
244
|
-
stripRedundantSectionTitle(grouped, isTitleEmpty);
|
|
245
|
-
|
|
246
236
|
return {
|
|
247
237
|
sections: grouped.map((section, index) =>
|
|
248
238
|
renderSection(section, counters, index)
|