Skip to content

Commit 431f6e1

Browse files
authored
Sairin
1 parent dace4e6 commit 431f6e1

11 files changed

Lines changed: 349 additions & 226 deletions

File tree

Build/package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Build/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@codemirror/state": "^6.6.0",
3030
"@codemirror/view": "^6.42.1",
3131
"@fortawesome/fontawesome-free": "^7.2.0",
32+
"@nisoku/sairin": "^0.1.2",
3233
"@prettier/plugin-xml": "^3.4.2",
3334
"@types/codemirror": "^5.60.17",
3435
"@uiw/codemirror-theme-bbedit": "^4.25.9",

Build/src/appState.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { effect, path, signal } from "@nisoku/sairin";
2+
import type { State } from "./types";
3+
import { defaultCss, defaultHtml, defaultJs } from "./defaultContent";
4+
import { editors } from "./editor";
5+
import { showError, switchOutput, switchTab } from "./ui";
6+
import { runCode } from "./runner";
7+
8+
function readPersistedState(): Partial<State> | null {
9+
try {
10+
const savedState = localStorage.getItem("htmlRunnerState");
11+
if (!savedState) {
12+
return null;
13+
}
14+
15+
return JSON.parse(savedState) as Partial<State>;
16+
} catch {
17+
return null;
18+
}
19+
}
20+
21+
const persistedState = readPersistedState();
22+
23+
const darkModeInitial =
24+
persistedState?.darkMode ?? localStorage.getItem("darkMode") === "true";
25+
const autoRunInitial =
26+
persistedState?.autoRun ?? localStorage.getItem("autoRun") === "true";
27+
const htmlInitial = persistedState?.html ?? defaultHtml;
28+
const cssInitial = persistedState?.css ?? defaultCss;
29+
const jsInitial = persistedState?.js ?? defaultJs;
30+
const activeTabInitial = persistedState?.activeTab ?? "html";
31+
const activeOutputInitial = persistedState?.activeOutput ?? "preview";
32+
const splitSizesInitial =
33+
Array.isArray(persistedState?.splitSizes) && persistedState.splitSizes.length === 2
34+
? persistedState.splitSizes
35+
: [50, 50];
36+
const STORAGE_KEY = "htmlRunnerState";
37+
38+
export const htmlState = signal(path("htmlrunner", "editor", "html"), htmlInitial);
39+
export const cssState = signal(path("htmlrunner", "editor", "css"), cssInitial);
40+
export const jsState = signal(path("htmlrunner", "editor", "js"), jsInitial);
41+
export const activeTabState = signal(path("htmlrunner", "ui", "activeTab"), activeTabInitial);
42+
export const activeOutputState = signal(path("htmlrunner", "ui", "activeOutput"), activeOutputInitial);
43+
export const splitSizesState = signal(path("htmlrunner", "layout", "splitSizes"), splitSizesInitial);
44+
export const darkModeState = signal(path("htmlrunner", "ui", "darkMode"), darkModeInitial);
45+
export const autoRunState = signal(path("htmlrunner", "editor", "autoRun"), autoRunInitial);
46+
export const stateHydrated = signal(path("htmlrunner", "meta", "stateHydrated"), false);
47+
48+
effect(() => {
49+
if (!stateHydrated.get()) {
50+
return;
51+
}
52+
53+
localStorage.setItem("htmlRunnerState", JSON.stringify(createStateSnapshot()));
54+
});
55+
56+
export function createStateSnapshot(): State {
57+
return {
58+
html: htmlState.get(),
59+
css: cssState.get(),
60+
js: jsState.get(),
61+
activeTab: activeTabState.get(),
62+
activeOutput: activeOutputState.get(),
63+
splitSizes: splitSizesState.get(),
64+
darkMode: darkModeState.get(),
65+
autoRun: autoRunState.get(),
66+
};
67+
}
68+
69+
export function applyStateSnapshot(snapshot: Partial<State>): void {
70+
if (typeof snapshot.html === "string") {
71+
htmlState.set(snapshot.html);
72+
}
73+
74+
if (typeof snapshot.css === "string") {
75+
cssState.set(snapshot.css);
76+
}
77+
78+
if (typeof snapshot.js === "string") {
79+
jsState.set(snapshot.js);
80+
}
81+
82+
if (typeof snapshot.activeTab === "string") {
83+
activeTabState.set(snapshot.activeTab);
84+
}
85+
86+
if (typeof snapshot.activeOutput === "string") {
87+
activeOutputState.set(snapshot.activeOutput);
88+
}
89+
90+
if (
91+
Array.isArray(snapshot.splitSizes) &&
92+
snapshot.splitSizes.length === 2 &&
93+
snapshot.splitSizes.every((size) => typeof size === "number")
94+
) {
95+
splitSizesState.set(snapshot.splitSizes);
96+
}
97+
98+
if (typeof snapshot.darkMode === "boolean") {
99+
darkModeState.set(snapshot.darkMode);
100+
}
101+
102+
if (typeof snapshot.autoRun === "boolean") {
103+
autoRunState.set(snapshot.autoRun);
104+
}
105+
}
106+
107+
export function markStateHydrated(): void {
108+
stateHydrated.set(true);
109+
}
110+
111+
export function loadState(): void {
112+
try {
113+
const savedState = localStorage.getItem(STORAGE_KEY);
114+
if (savedState) {
115+
const parsed = JSON.parse(savedState) as Partial<State>;
116+
applyStateSnapshot(parsed);
117+
118+
editors.html.view.dispatch({
119+
changes: {
120+
from: 0,
121+
to: editors.html.view.state.doc.length,
122+
insert: htmlState.get(),
123+
},
124+
});
125+
editors.css.view.dispatch({
126+
changes: {
127+
from: 0,
128+
to: editors.css.view.state.doc.length,
129+
insert: cssState.get(),
130+
},
131+
});
132+
editors.js.view.dispatch({
133+
changes: {
134+
from: 0,
135+
to: editors.js.view.state.doc.length,
136+
insert: jsState.get(),
137+
},
138+
});
139+
140+
if (["html", "css", "js"].includes(activeTabState.get())) {
141+
switchTab(activeTabState.get());
142+
}
143+
if (["preview", "console"].includes(activeOutputState.get())) {
144+
switchOutput(activeOutputState.get());
145+
}
146+
147+
runCode();
148+
markStateHydrated();
149+
} else {
150+
resetCode(true);
151+
markStateHydrated();
152+
}
153+
} catch (e: any) {
154+
showError("Failed to load state: " + e.message);
155+
resetCode(true);
156+
markStateHydrated();
157+
}
158+
}
159+
160+
export function resetCode(skipConfirmation: boolean = false): void {
161+
if (skipConfirmation || confirm("Are you sure you want to reset all code?")) {
162+
editors.html.view.dispatch({
163+
changes: {
164+
from: 0,
165+
to: editors.html.view.state.doc.length,
166+
insert: defaultHtml,
167+
},
168+
});
169+
170+
editors.css.view.dispatch({
171+
changes: {
172+
from: 0,
173+
to: editors.css.view.state.doc.length,
174+
insert: defaultCss,
175+
},
176+
});
177+
178+
editors.js.view.dispatch({
179+
changes: {
180+
from: 0,
181+
to: editors.js.view.state.doc.length,
182+
insert: defaultJs,
183+
},
184+
});
185+
186+
runCode();
187+
markStateHydrated();
188+
}
189+
}

Build/src/defaultContent.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export const defaultHtml = `<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>My Page</title>
5+
<link rel="stylesheet" href="styles.css">
6+
</head>
7+
<body>
8+
<script src="main.js"><\/script>
9+
<h1>Hello, HTMLRunner!</h1>
10+
<p>This is a demo page.</p>
11+
<button onclick="testFunction()">Click me!</button>
12+
<\/body>
13+
<\/html>`;
14+
15+
export const defaultCss = `body {
16+
font-family: Arial, sans-serif;
17+
margin: 20px;
18+
line-height: 1.6;
19+
}
20+
button {
21+
background: #2196F3;
22+
color: white;
23+
border: none;
24+
padding: 10px 15px;
25+
border-radius: 4px;
26+
cursor: pointer;
27+
font-size: 16px;
28+
}
29+
button:hover {
30+
background: #1976D2;
31+
}`;
32+
33+
export const defaultJs = `function testFunction() {
34+
console.log('Button clicked!');
35+
console.warn('This is a warning');
36+
console.error('This is an error');
37+
console.info('This is an info');
38+
console.log('Object:', { name: 'Alice', age: 25, hobbies: ['coding', 'reading'] });
39+
}`;

Build/src/editor.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,36 @@ import { CodeMirrorEditor, Editors } from "./types";
1111
import { runCode } from "./runner";
1212
import { toggleSearch } from "./main";
1313
import { debounce } from "./utils";
14-
import { saveState } from "./state";
14+
import { autoRunState, cssState, darkModeState, htmlState, jsState } from "./appState";
1515
import { search } from "@codemirror/search";
1616
import { toggleComment } from "@codemirror/commands"; // Ensure toggleComment is imported
1717

18-
export let isDarkMode: boolean = localStorage.getItem("darkMode") === "true";
19-
export let isAutoRun: boolean = localStorage.getItem("autoRun") === "true";
20-
2118
export const editors: Editors = {
2219
html: null as unknown as CodeMirrorEditor,
2320
css: null as unknown as CodeMirrorEditor,
2421
js: null as unknown as CodeMirrorEditor,
2522
};
2623

2724
export function setDarkMode(value: boolean): void {
28-
isDarkMode = value;
25+
darkModeState.set(value);
2926
Object.values(editors).forEach((editor) => {
3027
editor.view.dispatch({
3128
effects: editor.themeCompartment.reconfigure(
32-
isDarkMode ? monokai : bbedit
29+
darkModeState.get() ? monokai : bbedit
3330
),
3431
});
3532
});
3633
}
3734

3835
export function setAutoRun(value: boolean): void {
39-
isAutoRun = value;
36+
autoRunState.set(value);
4037
}
4138

4239
function createEditorConfig(
4340
language: Extension,
4441
container: HTMLElement,
45-
content: string
42+
content: string,
43+
contentState: typeof htmlState | typeof cssState | typeof jsState
4644
): CodeMirrorEditor {
4745
const themeCompartment = new Compartment();
4846
const autoRunCompartment = new Compartment();
@@ -53,7 +51,7 @@ function createEditorConfig(
5351
extensions: [
5452
lineNumbers(),
5553
language,
56-
themeCompartment.of(isDarkMode ? monokai : bbedit),
54+
themeCompartment.of(darkModeState.get() ? monokai : bbedit),
5755
EditorView.lineWrapping,
5856
EditorState.tabSize.of(2),
5957
EditorView.theme({
@@ -84,18 +82,18 @@ function createEditorConfig(
8482
},
8583
]),
8684
autoRunCompartment.of(
87-
isAutoRun
85+
autoRunState.get()
8886
? EditorView.updateListener.of((update) => {
8987
if (update.docChanged) {
88+
contentState.set(update.state.doc.toString());
9089
debounce(runCode, 1000)();
9190
}
9291
})
9392
: []
9493
),
95-
// Autosave listener
9694
EditorView.updateListener.of((update) => {
9795
if (update.docChanged) {
98-
debounce(saveState, 1000)();
96+
contentState.set(update.state.doc.toString());
9997
}
10098
}),
10199
],
@@ -133,7 +131,7 @@ export function initializeEditors(): void {
133131
throw new Error("Editor containers not found");
134132
}
135133

136-
editors.html = createEditorConfig(html(), htmlContainer, "");
137-
editors.css = createEditorConfig(css(), cssContainer, "");
138-
editors.js = createEditorConfig(javascript(), jsContainer, "");
134+
editors.html = createEditorConfig(html(), htmlContainer, htmlState.get(), htmlState);
135+
editors.css = createEditorConfig(css(), cssContainer, cssState.get(), cssState);
136+
editors.js = createEditorConfig(javascript(), jsContainer, jsState.get(), jsState);
139137
}

Build/src/global.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare module "*.css";
2+
declare module "@fortawesome/fontawesome-free/css/all.min.css" {
3+
const css: string;
4+
export default css;
5+
}

0 commit comments

Comments
 (0)