Skip to content

Commit 231f265

Browse files
committed
markdownをセクションごとに分割
1 parent 05e15b3 commit 231f265

File tree

4 files changed

+100
-17
lines changed

4 files changed

+100
-17
lines changed

app/[docs_id]/markdown.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,25 @@ export function StyledMarkdown({ content }: { content: string }) {
1212

1313
// TailwindCSSがh1などのタグのスタイルを消してしまうので、手動でスタイルを指定する必要がある
1414
const components: Components = {
15-
h1: ({ node, ...props }) => (
16-
<h1 className="text-2xl font-bold my-4" {...props} />
17-
),
18-
h2: ({ node, ...props }) => (
19-
<h2 className="text-xl font-bold mt-4 mb-3 " {...props} />
20-
),
21-
h3: ({ node, ...props }) => (
22-
<h3 className="text-lg font-bold mt-4 mb-2" {...props} />
23-
),
24-
h4: ({ node, ...props }) => (
25-
<h4 className="text-base font-bold mt-3 mb-2" {...props} />
26-
),
15+
// Headingはすべて事前にパースされているはずなので、ここでレンダリングはしない
16+
h1: ({ node }) => {
17+
throw new Error(`Heading 1 not parsed: ${JSON.stringify(node)}`);
18+
},
19+
h2: ({ node }) => {
20+
throw new Error(`Heading 2 not parsed: ${JSON.stringify(node)}`);
21+
},
22+
h3: ({ node }) => {
23+
throw new Error(`Heading 3 not parsed: ${JSON.stringify(node)}`);
24+
},
25+
h4: ({ node }) => {
26+
throw new Error(`Heading 4 not parsed: ${JSON.stringify(node)}`);
27+
},
28+
h5: ({ node }) => {
29+
throw new Error(`Heading 5 not parsed: ${JSON.stringify(node)}`);
30+
},
31+
h6: ({ node }) => {
32+
throw new Error(`Heading 6 not parsed: ${JSON.stringify(node)}`);
33+
},
2734
p: ({ node, ...props }) => <p className="mx-2 my-2" {...props} />,
2835
ul: ({ node, ...props }) => (
2936
<ul className="list-disc list-outside ml-6 my-2" {...props} />

app/[docs_id]/page.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { notFound } from "next/navigation";
22
import { ChatForm } from "./chatForm";
3-
import { StyledMarkdown } from "./markdown";
43
import { getCloudflareContext } from "@opennextjs/cloudflare";
5-
import { readFile } from "node:fs/promises";
4+
import { readFile } from "node:fs/promises";
65
import { join } from "node:path";
6+
import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
7+
import { Section } from "./section";
78

89
export default async function Page({
910
params,
@@ -14,13 +15,13 @@ export default async function Page({
1415

1516
let mdContent: string;
1617
try {
17-
if (process.env.NODE_ENV === 'development') {
18+
if (process.env.NODE_ENV === "development") {
1819
mdContent = await readFile(
1920
join(process.cwd(), "public", "docs", `${docs_id}.md`),
2021
"utf-8"
2122
);
2223
} else {
23-
const cfAssets = getCloudflareContext().env.ASSETS;
24+
const cfAssets = getCloudflareContext().env.ASSETS;
2425
mdContent = await cfAssets!
2526
.fetch(`https://assets.local/docs/${docs_id}.md`)
2627
.then((res) => res.text());
@@ -30,9 +31,13 @@ export default async function Page({
3031
notFound();
3132
}
3233

34+
const splitMdContent: MarkdownSection[] = await splitMarkdown(mdContent);
35+
3336
return (
3437
<div className="p-4">
35-
<StyledMarkdown content={mdContent} />
38+
{splitMdContent.map((section, index) => (
39+
<Section key={index} section={section} />
40+
))}
3641
<ChatForm />
3742
</div>
3843
);

app/[docs_id]/section.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use client";
2+
3+
import { ReactNode } from "react";
4+
import { type MarkdownSection } from "./splitMarkdown";
5+
import { StyledMarkdown } from "./markdown";
6+
7+
export function Section({ section }: { section: MarkdownSection }) {
8+
return (
9+
<>
10+
<Heading level={section.level}>{section.title}</Heading>
11+
<StyledMarkdown content={section.content} />
12+
</>
13+
);
14+
}
15+
16+
function Heading({ level, children }: { level: number; children: ReactNode }) {
17+
switch (level) {
18+
case 1:
19+
return <h1 className="text-2xl font-bold my-4">{children}</h1>;
20+
case 2:
21+
return <h2 className="text-xl font-bold mt-4 mb-3 ">{children}</h2>;
22+
case 3:
23+
return <h3 className="text-lg font-bold mt-4 mb-2">{children}</h3>;
24+
case 4:
25+
return <h4 className="text-base font-bold mt-3 mb-2">{children}</h4>;
26+
default:
27+
throw new Error(`Unsupported heading level: ${level}`);
28+
}
29+
}

app/[docs_id]/splitMarkdown.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use server";
2+
3+
import { unified } from "unified";
4+
import remarkParse from "remark-parse";
5+
import remarkGfm from "remark-gfm";
6+
7+
export interface MarkdownSection {
8+
level: number;
9+
title: string;
10+
content: string;
11+
}
12+
export async function splitMarkdown(
13+
content: string
14+
): Promise<MarkdownSection[]> {
15+
const tree = unified().use(remarkParse).use(remarkGfm).parse(content);
16+
// console.log(tree.children.map(({ type, position }) => ({ type, position: JSON.stringify(position) })));
17+
const headingNodes = tree.children.filter((node) => node.type === "heading");
18+
const splitContent = content.split("\n");
19+
const sections: MarkdownSection[] = [];
20+
for (let i = 0; i < headingNodes.length; i++) {
21+
const startLine = headingNodes.at(i)?.position?.start.line;
22+
if (startLine === undefined) {
23+
continue;
24+
}
25+
let endLine: number | undefined = undefined;
26+
for (let j = i + 1; j < headingNodes.length; j++) {
27+
if (headingNodes.at(j)?.position?.start.line !== undefined) {
28+
endLine = headingNodes.at(j)!.position!.start.line;
29+
break;
30+
}
31+
}
32+
sections.push({
33+
title: splitContent[startLine - 1].replace(/#+\s*/, "").trim(),
34+
content: splitContent
35+
.slice(startLine - 1 + 1, endLine ? endLine - 1 : undefined)
36+
.join("\n")
37+
.trim(),
38+
level: headingNodes.at(i)!.depth,
39+
});
40+
}
41+
return sections;
42+
}

0 commit comments

Comments
 (0)