Skip to content

Commit af257ac

Browse files
committed
next/dynamicからreactのlazyに変更し、読み込み中もpreで内容を表示
1 parent 8bdb978 commit af257ac

File tree

5 files changed

+158
-58
lines changed

5 files changed

+158
-58
lines changed

app/[docs_id]/styledSyntaxHighlighter.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
"use client";
2+
13
import { useChangeTheme } from "./themeToggle";
24
import {
35
tomorrow,
46
tomorrowNight,
57
} from "react-syntax-highlighter/dist/esm/styles/hljs";
6-
import dynamic from "next/dynamic";
8+
import { lazy, Suspense, useEffect, useState } from "react";
79

810
// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
9-
const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter"), {
10-
ssr: false,
11+
const SyntaxHighlighter = lazy(() => {
12+
if (typeof window !== "undefined") {
13+
return import("react-syntax-highlighter");
14+
} else {
15+
throw new Error("should not try SSR");
16+
}
1117
});
1218

1319
// Markdownで指定される可能性のある言語名を列挙
@@ -81,14 +87,29 @@ export function StyledSyntaxHighlighter(props: {
8187
}) {
8288
const theme = useChangeTheme();
8389
const codetheme = theme === "tomorrow" ? tomorrow : tomorrowNight;
90+
const [initHighlighter, setInitHighlighter] = useState(false);
91+
useEffect(() => {
92+
setInitHighlighter(true);
93+
}, []);
94+
return initHighlighter ? (
95+
<Suspense fallback={<FallbackPre>{props.children}</FallbackPre>}>
96+
<SyntaxHighlighter
97+
language={props.language}
98+
PreTag="div"
99+
className="border-2 border-current/20 mx-2 my-2 rounded-box p-4! bg-base-300! text-base-content!"
100+
style={codetheme}
101+
>
102+
{props.children}
103+
</SyntaxHighlighter>
104+
</Suspense>
105+
) : (
106+
<FallbackPre>{props.children}</FallbackPre>
107+
);
108+
}
109+
function FallbackPre({ children }: { children: string }) {
84110
return (
85-
<SyntaxHighlighter
86-
language={props.language}
87-
PreTag="div"
88-
className="border-2 border-current/20 mx-2 my-2 rounded-box p-4! bg-base-300! text-base-content!"
89-
style={codetheme}
90-
>
91-
{props.children}
92-
</SyntaxHighlighter>
111+
<pre className="border-2 border-current/20 mx-2 my-2 rounded-box p-4! bg-base-300! text-base-content!">
112+
{children}
113+
</pre>
93114
);
94115
}

app/terminal/editor.tsx

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
"use client";
22

3-
import dynamic from "next/dynamic";
3+
import { lazy, Suspense, useEffect, useState } from "react";
4+
import clsx from "clsx";
5+
import { useChangeTheme } from "../[docs_id]/themeToggle";
6+
import { useEmbedContext } from "./embedContext";
7+
import { langConstants } from "./runtime";
8+
import { MarkdownLang } from "@/[docs_id]/styledSyntaxHighlighter";
9+
410
// https://github.com/securingsincity/react-ace/issues/27 により普通のimportができない
5-
const AceEditor = dynamic(
6-
async () => {
11+
const AceEditor = lazy(async () => {
12+
if (typeof window !== "undefined") {
713
const ace = await import("react-ace");
14+
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
815
// テーマは色分けが今のTerminal側のハイライト(highlight.js)の実装に近いものを適当に選んだ
916
await import("ace-builds/src-min-noconflict/theme-tomorrow");
1017
await import("ace-builds/src-min-noconflict/theme-tomorrow_night");
@@ -19,16 +26,10 @@ const AceEditor = dynamic(
1926
await import("ace-builds/src-min-noconflict/mode-csv");
2027
await import("ace-builds/src-min-noconflict/mode-text");
2128
return ace;
22-
},
23-
{ ssr: false }
24-
);
25-
import { useEffect, useState } from "react";
26-
import clsx from "clsx";
27-
import { useChangeTheme } from "../[docs_id]/themeToggle";
28-
import { useEmbedContext } from "./embedContext";
29-
import { langConstants } from "./runtime";
30-
import { MarkdownLang } from "@/[docs_id]/styledSyntaxHighlighter";
31-
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
29+
} else {
30+
throw new Error("should not try SSR");
31+
}
32+
});
3233

3334
// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
3435
export type AceLang =
@@ -92,12 +93,16 @@ export function EditorComponent(props: EditorProps) {
9293
}
9394
}, [files, props.filename, props.initContent, writeFile]);
9495

95-
const [fontSize, setFontSize] = useState(16);
96+
const [fontSize, setFontSize] = useState<number>();
97+
const [initAce, setInitAce] = useState(false);
9698
useEffect(() => {
9799
setFontSize(
98100
parseFloat(getComputedStyle(document.documentElement).fontSize)
99101
); // 1rem
102+
setInitAce(true);
100103
}, []);
104+
// 最小8行 or 初期内容+1行
105+
const editorHeight = Math.max(props.initContent.split("\n").length + 1, 8);
101106

102107
return (
103108
<div className="border border-accent border-2 shadow-md m-2 rounded-box overflow-hidden">
@@ -137,26 +142,55 @@ export function EditorComponent(props: EditorProps) {
137142
元の内容に戻す
138143
</button>
139144
</div>
140-
<AceEditor
141-
name={`ace-editor-${props.filename}`}
142-
mode={props.language}
143-
theme={theme}
144-
tabSize={langConstants(props.language || "text").tabSize}
145-
width="100%"
146-
height={
147-
Math.max((props.initContent.split("\n").length + 2) * fontSize, 128) +
148-
"px"
149-
}
150-
className="font-mono!" // Aceのデフォルトフォントを上書き
151-
readOnly={props.readonly}
152-
fontSize={fontSize}
153-
showPrintMargin={false}
154-
enableBasicAutocompletion={false}
155-
enableLiveAutocompletion={false}
156-
enableSnippets={false}
157-
value={code}
158-
onChange={(code: string) => writeFile({ [props.filename]: code })}
159-
/>
145+
{fontSize !== undefined && initAce ? (
146+
<Suspense
147+
fallback={
148+
<FallbackPre editorHeight={editorHeight}>{code}</FallbackPre>
149+
}
150+
>
151+
<AceEditor
152+
name={`ace-editor-${props.filename}`}
153+
mode={props.language}
154+
theme={theme}
155+
tabSize={langConstants(props.language || "text").tabSize}
156+
width="100%"
157+
height={editorHeight * (fontSize + 1) + "px"}
158+
className="font-mono!" // Aceのデフォルトフォントを上書き
159+
readOnly={props.readonly}
160+
fontSize={fontSize}
161+
showPrintMargin={false}
162+
enableBasicAutocompletion={false}
163+
enableLiveAutocompletion={false}
164+
enableSnippets={false}
165+
value={code}
166+
onChange={(code: string) => writeFile({ [props.filename]: code })}
167+
/>
168+
</Suspense>
169+
) : (
170+
<FallbackPre editorHeight={editorHeight}>{code}</FallbackPre>
171+
)}
160172
</div>
161173
);
162174
}
175+
176+
function FallbackPre({
177+
children,
178+
editorHeight,
179+
}: {
180+
children: string;
181+
editorHeight: number;
182+
}) {
183+
// AceEditorはなぜかline-heightが小さい
184+
// fontSize + 1px になるっぽい?
185+
return (
186+
<pre
187+
className="font-mono overflow-auto bg-base-300 px-2 cursor-wait"
188+
style={{
189+
height: `calc((1em + 1px) * ${editorHeight})`,
190+
lineHeight: "calc(1em + 1px)",
191+
}}
192+
>
193+
{children}
194+
</pre>
195+
);
196+
}

app/terminal/exec.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { writeOutput } from "./repl";
1111
import { useEffect, useState } from "react";
1212
import { useEmbedContext } from "./embedContext";
1313
import { RuntimeLang, useRuntime } from "./runtime";
14+
import clsx from "clsx";
1415

1516
interface ExecProps {
1617
/*
@@ -95,8 +96,28 @@ export function ExecFile(props: ExecProps) {
9596
{getCommandlineStr?.(props.filenames)}
9697
</code>
9798
</div>
98-
<div className="bg-base-300 p-4 pt-2">
99-
<div ref={terminalRef} />
99+
<div className="bg-base-300 p-4 pt-2 relative">
100+
{/*
101+
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
102+
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
103+
*/}
104+
<pre
105+
className={clsx(
106+
"font-mono overflow-auto cursor-wait",
107+
"min-h-26", // xterm.jsで5行分の高さ
108+
termReady && "hidden"
109+
)}
110+
>
111+
{props.content}
112+
</pre>
113+
<div
114+
className={clsx(
115+
!termReady &&
116+
/* "hidden" だとterminalがdivのサイズを取得しようとしたときにバグる*/
117+
"absolute invisible"
118+
)}
119+
ref={terminalRef}
120+
/>
100121
</div>
101122
{executionState !== "idle" && (
102123
<div className="absolute z-10 inset-0 cursor-wait" />

app/terminal/repl.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import type { Terminal } from "@xterm/xterm";
1515
import { useEmbedContext } from "./embedContext";
1616
import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime";
17+
import clsx from "clsx";
1718

1819
export interface ReplOutput {
1920
type: "stdout" | "stderr" | "error" | "return" | "trace" | "system"; // 出力の種類
@@ -309,7 +310,8 @@ export function ReplTerminal({
309310
runtimeReady &&
310311
initCommandState === "triggered"
311312
) {
312-
// ユーザーがクリックした時(triggered) && ランタイムが準備できた時に、実際にinitCommandを実行する(executing) setInitCommandState("executing");
313+
// ユーザーがクリックした時(triggered) && ランタイムが準備できた時に、実際にinitCommandを実行する(executing)
314+
setInitCommandState("executing");
313315
(async () => {
314316
if (initCommand) {
315317
// 初期化時に実行するコマンドがある場合はそれを実行
@@ -353,6 +355,19 @@ export function ReplTerminal({
353355

354356
return (
355357
<div className="bg-base-300 border border-accent border-2 shadow-md m-2 p-4 pr-1 rounded-box relative h-max">
358+
{/*
359+
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
360+
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
361+
*/}
362+
<pre
363+
className={clsx(
364+
"font-mono overflow-auto cursor-wait",
365+
"min-h-26", // xterm.jsで5行分の高さ
366+
initCommandState !== "initializing" && "hidden"
367+
)}
368+
>
369+
{initContent + "\n\n"}
370+
</pre>
356371
{terminalInstanceRef.current &&
357372
termReady &&
358373
initCommandState === "idle" && (
@@ -376,7 +391,14 @@ export function ReplTerminal({
376391
initCommandState === "executing") && (
377392
<div className="absolute z-10 inset-0 cursor-wait" />
378393
)}
379-
<div ref={terminalRef} />
394+
<div
395+
className={clsx(
396+
initCommandState === "initializing" &&
397+
/* "hidden" だとterminalがdivのサイズを取得しようとしたときにバグる*/
398+
"absolute invisible"
399+
)}
400+
ref={terminalRef}
401+
/>
380402
</div>
381403
);
382404
}

app/terminal/terminal.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ export function useTerminal(props: TerminalProps) {
102102
useEffect(() => {
103103
if (typeof window !== "undefined") {
104104
const abortController = new AbortController();
105+
const resizeTerminal = () => {
106+
// fitAddon.fit();
107+
const dims = fitAddonRef.current?.proposeDimensions();
108+
if (dims && !isNaN(dims.cols)) {
109+
const rows = Math.max(5, getRowsRef.current?.(dims.cols) ?? 0);
110+
terminalInstanceRef.current?.resize(dims.cols, rows);
111+
}
112+
}
105113
/*
106114
globals.cssでフォントを指定し読み込んでいるが、
107115
それが読み込まれる前にterminalを初期化してしまうとバグるので、
@@ -131,7 +139,8 @@ export function useTerminal(props: TerminalProps) {
131139

132140
fitAddonRef.current = new FitAddon();
133141
term.loadAddon(fitAddonRef.current);
134-
// fitAddon.fit();
142+
// fitAddonRef.current.fit();
143+
resizeTerminal();
135144

136145
term.open(terminalRef.current);
137146

@@ -160,14 +169,7 @@ export function useTerminal(props: TerminalProps) {
160169
}
161170
});
162171

163-
const observer = new ResizeObserver(() => {
164-
// fitAddon.fit();
165-
const dims = fitAddonRef.current?.proposeDimensions();
166-
if (dims && !isNaN(dims.cols)) {
167-
const rows = Math.max(5, getRowsRef.current?.(dims.cols) ?? 0);
168-
terminalInstanceRef.current?.resize(dims.cols, rows);
169-
}
170-
});
172+
const observer = new ResizeObserver(resizeTerminal);
171173
observer.observe(terminalRef.current);
172174

173175
return () => {

0 commit comments

Comments
 (0)