+
),
- hr: ({ node, ...props }) =>
,
+ hr: ({ node, ...props }) =>
,
pre: ({ node, ...props }) => props.children,
code: ({ node, className, ref, style, ...props }) => (
@@ -84,20 +81,12 @@ function CodeComponent({
ref,
style,
...props
-}: {
- node: unknown;
- className?: string;
- ref?: unknown;
- style?: unknown;
- [key: string]: unknown;
-}) {
- const theme = useChangeTheme();
- const codetheme = theme === "tomorrow" ? tomorrow : atomOneDark;
+}: JSX.IntrinsicElements["code"] & ExtraProps) {
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
className || ""
);
if (match) {
- const runtimeLang = getRuntimeLang(match[1]);
+ const runtimeLang = getRuntimeLang(match[1] as MarkdownLang | undefined);
if (match[2] === "-exec" && match[3]) {
/*
```python-exec:main.py
@@ -111,13 +100,11 @@ function CodeComponent({
*/
if (runtimeLang) {
return (
-
-
-
+
);
}
} else if (match[2] === "-repl") {
@@ -129,57 +116,45 @@ function CodeComponent({
}
if (runtimeLang) {
return (
-
-
-
+
);
}
} else if (match[3]) {
// ファイル名指定がある場合、ファイルエディター
- const aceLang = getAceLang(match[1]);
+ const aceLang = getAceLang(match[1] as MarkdownLang | undefined);
return (
-
-
-
+
);
}
+ const syntaxHighlighterLang = getSyntaxHighlighterLang(
+ match[1] as MarkdownLang | undefined
+ );
return (
-
+
{String(props.children || "").replace(/\n$/, "")}
-
+
);
} else if (String(props.children).includes("\n")) {
// 言語指定なしコードブロック
return (
-
+
{String(props.children || "").replace(/\n$/, "")}
-
+
);
} else {
// inline
return (
);
diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx
index 59202da..c402020 100644
--- a/app/[docs_id]/page.tsx
+++ b/app/[docs_id]/page.tsx
@@ -1,52 +1,75 @@
+import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
-import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
+import { splitMarkdown } from "./splitMarkdown";
import { PageContent } from "./pageContent";
import { ChatHistoryProvider } from "./chatHistory";
import { getChatFromCache } from "@/lib/chatHistory";
+import { getLanguageName } from "@/pagesList";
-export default async function Page({
+async function getMarkdownContent(docs_id: string): Promise
{
+ try {
+ if (process.env.NODE_ENV === "development") {
+ return await readFile(
+ join(process.cwd(), "public", "docs", `${docs_id}.md`),
+ "utf-8"
+ );
+ } else {
+ const cfAssets = getCloudflareContext().env.ASSETS;
+ const res = await cfAssets!.fetch(
+ `https://assets.local/docs/${docs_id}.md`
+ );
+ if (!res.ok) {
+ notFound();
+ }
+ return await res.text();
+ }
+ } catch (e) {
+ console.error(e);
+ notFound();
+ }
+}
+
+export async function generateMetadata({
params,
}: {
params: Promise<{ docs_id: string }>;
-}) {
+}): Promise {
const { docs_id } = await params;
+ const mdContent = await getMarkdownContent(docs_id);
+ const splitMdContent = splitMarkdown(mdContent);
- let mdContent: Promise;
- if (process.env.NODE_ENV === "development") {
- mdContent = readFile(
- join(process.cwd(), "public", "docs", `${docs_id}.md`),
- "utf-8"
- ).catch((e) => {
- console.error(e);
- notFound();
- });
- } else {
- const cfAssets = getCloudflareContext().env.ASSETS;
- mdContent = cfAssets!
- .fetch(`https://assets.local/docs/${docs_id}.md`)
- .then(async (res) => {
- if (!res.ok) {
- notFound();
- }
- return res.text();
- })
- .catch((e) => {
- console.error(e);
- notFound();
- });
- }
+ // 先頭の 第n章: を除いたものをタイトルとする
+ const title = splitMdContent[0]?.title?.split(" ").slice(1).join(" ");
- const splitMdContent: Promise = mdContent.then((text) =>
- splitMarkdown(text)
- );
+ const description = splitMdContent[0].content;
+
+ const chapter = docs_id.split("-")[1];
+
+ return {
+ title: `${getLanguageName(docs_id)}-${chapter}. ${title}`,
+ description,
+ };
+}
+
+export default async function Page({
+ params,
+}: {
+ params: Promise<{ docs_id: string }>;
+}) {
+ const { docs_id } = await params;
+ const mdContent = getMarkdownContent(docs_id);
+ const splitMdContent = mdContent.then((text) => splitMarkdown(text));
const initialChatHistories = getChatFromCache(docs_id);
return (
-
+
{dynamicMdContent.map((section, index) => (
- <>
+
{
sectionRefs.current[index] = el;
@@ -103,14 +102,14 @@ export function PageContent(props: PageContentProps) {
{section.title}
-
+
{/* 右側に表示するチャット履歴欄 */}
{chatHistories
.filter((c) => c.sectionId === section.sectionId)
.map(({ chatId, messages }) => (
{messages.map((msg, index) => (
@@ -120,12 +119,10 @@ export function PageContent(props: PageContentProps) {
>
@@ -137,7 +134,7 @@ export function PageContent(props: PageContentProps) {
))}
- >
+
))}
{isFormVisible ? (
// sidebarの幅が80であることからleft-84 (sidebar.tsx参照)
diff --git a/app/[docs_id]/styledSyntaxHighlighter.tsx b/app/[docs_id]/styledSyntaxHighlighter.tsx
new file mode 100644
index 0000000..42b2e92
--- /dev/null
+++ b/app/[docs_id]/styledSyntaxHighlighter.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useChangeTheme } from "./themeToggle";
+import {
+ tomorrow,
+ tomorrowNight,
+} from "react-syntax-highlighter/dist/esm/styles/hljs";
+import { lazy, Suspense, useEffect, useState } from "react";
+
+// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
+const SyntaxHighlighter = lazy(() => {
+ if (typeof window !== "undefined") {
+ return import("react-syntax-highlighter");
+ } else {
+ throw new Error("should not try SSR");
+ }
+});
+
+// Markdownで指定される可能性のある言語名を列挙
+export type MarkdownLang =
+ | "python"
+ | "py"
+ | "ruby"
+ | "rb"
+ | "cpp"
+ | "c++"
+ | "javascript"
+ | "js"
+ | "typescript"
+ | "ts"
+ | "bash"
+ | "sh"
+ | "json"
+ | "csv"
+ | "text"
+ | "txt";
+
+// react-syntax-highliter (hljs版) が対応している言語
+// https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD を参照
+export type SyntaxHighlighterLang =
+ | "python"
+ | "ruby"
+ | "c"
+ | "cpp"
+ | "javascript"
+ | "typescript"
+ | "bash"
+ | "json";
+export function getSyntaxHighlighterLang(
+ lang: MarkdownLang | undefined
+): SyntaxHighlighterLang | undefined {
+ switch (lang) {
+ case "python":
+ case "py":
+ return "python";
+ case "ruby":
+ case "rb":
+ return "ruby";
+ case "cpp":
+ case "c++":
+ return "cpp";
+ case "javascript":
+ case "js":
+ return "javascript";
+ case "typescript":
+ case "ts":
+ return "typescript";
+ case "bash":
+ case "sh":
+ return "bash";
+ case "json":
+ return "json";
+ case "csv": // not supported
+ case "text":
+ case "txt":
+ case undefined:
+ return undefined;
+ default:
+ lang satisfies never;
+ console.warn(`Language not listed in MarkdownLang: ${lang}`);
+ return undefined;
+ }
+}
+export function StyledSyntaxHighlighter(props: {
+ children: string;
+ language: SyntaxHighlighterLang | undefined;
+}) {
+ const theme = useChangeTheme();
+ const codetheme = theme === "tomorrow" ? tomorrow : tomorrowNight;
+ const [initHighlighter, setInitHighlighter] = useState(false);
+ useEffect(() => {
+ setInitHighlighter(true);
+ }, []);
+ return initHighlighter ? (
+
{props.children}}>
+
+ {props.children}
+
+
+ ) : (
+
{props.children}
+ );
+}
+function FallbackPre({ children }: { children: string }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/[docs_id]/themeToggle.tsx b/app/[docs_id]/themeToggle.tsx
index 1b4fd4a..7bdce16 100644
--- a/app/[docs_id]/themeToggle.tsx
+++ b/app/[docs_id]/themeToggle.tsx
@@ -2,13 +2,14 @@
import { useState, useEffect } from "react";
export function useChangeTheme() {
- const [theme, setTheme] = useState("tomorrow");
+ const [theme, setTheme] = useState<"tomorrow" | "tomorrow_night">("tomorrow");
useEffect(() => {
const updateTheme = () => {
const theme = document.documentElement.getAttribute("data-theme");
- setTheme(theme === "dark" ? "twilight" : "tomorrow");
+ setTheme(theme === "mycdark" ? "tomorrow_night" : "tomorrow");
};
+ updateTheme();
const observer = new MutationObserver(updateTheme);
observer.observe(document.documentElement, {
attributes: true,
@@ -21,20 +22,19 @@ export function useChangeTheme() {
}
export function ThemeToggle() {
const theme = useChangeTheme();
- const isChecked = theme === "twilight";
+ const isChecked = theme === "tomorrow_night";
useEffect(() => {
const checkIsDarkSchemePreferred = () =>
window?.matchMedia?.("(prefers-color-scheme:dark)")?.matches ?? false;
- const initialTheme = checkIsDarkSchemePreferred() ? "dark" : "light";
+ const initialTheme = checkIsDarkSchemePreferred() ? "mycdark" : "myclight";
document.documentElement.setAttribute("data-theme", initialTheme);
}, []);
return (
-