Skip to content

Commit 0ebb489

Browse files
authored
Merge pull request #12 from ut-code/split-markdown
markdownをセクションごとに分割
2 parents 05e15b3 + 81a836f commit 0ebb489

File tree

4 files changed

+98
-19
lines changed

4 files changed

+98
-19
lines changed

app/[docs_id]/markdown.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Markdown, { Components } from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4+
import { Heading } from "./section";
45

56
export function StyledMarkdown({ content }: { content: string }) {
67
return (
@@ -12,18 +13,12 @@ export function StyledMarkdown({ content }: { content: string }) {
1213

1314
// TailwindCSSがh1などのタグのスタイルを消してしまうので、手動でスタイルを指定する必要がある
1415
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-
),
16+
h1: ({ children }) => <Heading level={1}>{children}</Heading>,
17+
h2: ({ children }) => <Heading level={2}>{children}</Heading>,
18+
h3: ({ children }) => <Heading level={3}>{children}</Heading>,
19+
h4: ({ children }) => <Heading level={4}>{children}</Heading>,
20+
h5: ({ children }) => <Heading level={5}>{children}</Heading>,
21+
h6: ({ children }) => <Heading level={6}>{children}</Heading>,
2722
p: ({ node, ...props }) => <p className="mx-2 my-2" {...props} />,
2823
ul: ({ node, ...props }) => (
2924
<ul className="list-disc list-outside ml-6 my-2" {...props} />

app/[docs_id]/page.tsx

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

88
export default async function Page({
99
params,
@@ -14,13 +14,13 @@ export default async function Page({
1414

1515
let mdContent: string;
1616
try {
17-
if (process.env.NODE_ENV === 'development') {
17+
if (process.env.NODE_ENV === "development") {
1818
mdContent = await readFile(
1919
join(process.cwd(), "public", "docs", `${docs_id}.md`),
2020
"utf-8"
2121
);
2222
} else {
23-
const cfAssets = getCloudflareContext().env.ASSETS;
23+
const cfAssets = getCloudflareContext().env.ASSETS;
2424
mdContent = await cfAssets!
2525
.fetch(`https://assets.local/docs/${docs_id}.md`)
2626
.then((res) => res.text());
@@ -30,10 +30,13 @@ export default async function Page({
3030
notFound();
3131
}
3232

33+
const splitMdContent: MarkdownSection[] = await splitMarkdown(mdContent);
34+
3335
return (
3436
<div className="p-4">
35-
<StyledMarkdown content={mdContent} />
36-
<ChatForm />
37+
{splitMdContent.map((section, index) => (
38+
<Section key={index} section={section} />
39+
))}
3740
</div>
3841
);
3942
}

app/[docs_id]/section.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import { ReactNode } from "react";
4+
import { type MarkdownSection } from "./splitMarkdown";
5+
import { StyledMarkdown } from "./markdown";
6+
import { ChatForm } from "./chatForm";
7+
8+
// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
9+
export function Section({ section }: { section: MarkdownSection }) {
10+
return (
11+
<div>
12+
<Heading level={section.level}>{section.title}</Heading>
13+
<StyledMarkdown content={section.content} />
14+
<ChatForm />
15+
</div>
16+
);
17+
}
18+
19+
export function Heading({ level, children }: { level: number; children: ReactNode }) {
20+
switch (level) {
21+
case 1:
22+
return <h1 className="text-2xl font-bold my-4">{children}</h1>;
23+
case 2:
24+
return <h2 className="text-xl font-bold mt-4 mb-3 ">{children}</h2>;
25+
case 3:
26+
return <h3 className="text-lg font-bold mt-4 mb-2">{children}</h3>;
27+
case 4:
28+
return <h4 className="text-base font-bold mt-3 mb-2">{children}</h4>;
29+
case 5:
30+
// TODO: これ以下は4との差がない (全体的に大きくする必要がある?)
31+
return <h5 className="text-base font-bold mt-3 mb-2">{children}</h5>;
32+
case 6:
33+
return <h6 className="text-base font-bold mt-3 mb-2">{children}</h6>;
34+
}
35+
}

app/[docs_id]/splitMarkdown.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
/**
13+
* Markdownコンテンツを見出しごとに分割し、
14+
* 見出しのレベルとタイトル、内容を含むオブジェクトの配列を返す。
15+
*/
16+
export async function splitMarkdown(
17+
content: string
18+
): Promise<MarkdownSection[]> {
19+
const tree = unified().use(remarkParse).use(remarkGfm).parse(content);
20+
// console.log(tree.children.map(({ type, position }) => ({ type, position: JSON.stringify(position) })));
21+
const headingNodes = tree.children.filter((node) => node.type === "heading");
22+
const splitContent = content.split("\n");
23+
const sections: MarkdownSection[] = [];
24+
for (let i = 0; i < headingNodes.length; i++) {
25+
const startLine = headingNodes.at(i)?.position?.start.line;
26+
if (startLine === undefined) {
27+
continue;
28+
}
29+
let endLine: number | undefined = undefined;
30+
for (let j = i + 1; j < headingNodes.length; j++) {
31+
if (headingNodes.at(j)?.position?.start.line !== undefined) {
32+
endLine = headingNodes.at(j)!.position!.start.line;
33+
break;
34+
}
35+
}
36+
sections.push({
37+
title: splitContent[startLine - 1].replace(/#+\s*/, "").trim(),
38+
content: splitContent
39+
.slice(startLine - 1 + 1, endLine ? endLine - 1 : undefined)
40+
.join("\n")
41+
.trim(),
42+
level: headingNodes.at(i)!.depth,
43+
});
44+
}
45+
return sections;
46+
}

0 commit comments

Comments
 (0)