improve frontend
All checks were successful
/ print-content (push) Successful in 2m0s

This commit is contained in:
암냥 2026-03-12 18:44:16 +09:00
commit 667638a9ce
No known key found for this signature in database
2 changed files with 262 additions and 20 deletions

View file

@ -11,5 +11,5 @@ port = 8000
# b, kb, mb, gb, tb
# kib, mib, gib, tib
max_size = "10MB"
extension_whitelist = [".docx", ".md", ".json", ".rs"]
# extension_whitelist = [".docx", ".md", ".json", ".rs"]
block_binary = true

View file

@ -4,6 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>paste</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
@ -16,10 +17,12 @@
.container {
height: 100vh;
display: flex;
flex-direction: column;
}
#editor {
height: 100%;
flex: 1;
}
.cm-editor {
@ -29,6 +32,8 @@
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
}
header ul {
@ -37,6 +42,20 @@
gap: 10px;
margin: 0;
padding: 0;
align-items: center;
flex-wrap: wrap;
}
header li {
display: flex;
align-items: center;
gap: 6px;
}
header button,
header select,
header input {
font: inherit;
}
header button {
@ -47,6 +66,31 @@
-moz-user-select: none;
-webkit-user-select: none;
}
header button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
header select,
header input {
border: none;
outline: none;
background: transparent;
min-width: 0;
}
header input {
width: 4ch;
}
footer {
padding: 0 8px 8px;
}
footer p {
margin: 0;
}
</style>
</head>
@ -54,8 +98,33 @@
<div class="container">
<header>
<ul>
<li><button>[ new ]</button></li>
<li><button>[ save ]</button></li>
<li><button id="new-button" type="button">[ new ]</button></li>
<li><button id="save-button" type="button">[ save ]</button></li>
</ul>
<ul>
<li>
<label for="language-select">[ lang ]</label>
<select id="language-select">
<option value="plain">plain text</option>
<option value="markdown">markdown</option>
<option value="rust">rust</option>
<option value="javascript">javascript</option>
<option value="typescript">typescript</option>
<option value="json">json</option>
<option value="html">html</option>
<option value="css">css</option>
<option value="python">python</option>
<option value="sql">sql</option>
<option value="yaml">yaml</option>
<option value="toml">toml</option>
</select>
</li>
<li>
<label for="extension-input">[ ext ]</label>
<span>.</span>
<input id="extension-input" type="text" inputmode="text" autocomplete="off" spellcheck="false"
placeholder="txt" />
</li>
</ul>
</header>
<div id="editor"></div>
@ -67,63 +136,236 @@
<script id="initial-data" type="text/plain">$%{defaultText}%$</script>
<script type="module">
import { basicSetup, EditorView } from "https://esm.sh/codemirror";
import { Compartment } from "https://esm.sh/@codemirror/state";
import { StreamLanguage } from "https://esm.sh/@codemirror/language";
import { markdown } from "https://esm.sh/@codemirror/lang-markdown";
import { javascript } from "https://esm.sh/@codemirror/lang-javascript";
import { json } from "https://esm.sh/@codemirror/lang-json";
import { html } from "https://esm.sh/@codemirror/lang-html";
import { css } from "https://esm.sh/@codemirror/lang-css";
import { python } from "https://esm.sh/@codemirror/lang-python";
import { rust } from "https://esm.sh/@codemirror/lang-rust";
import { sql } from "https://esm.sh/@codemirror/lang-sql";
import { yaml } from "https://esm.sh/@codemirror/lang-yaml";
import { toml } from "https://esm.sh/@codemirror/legacy-modes/mode/toml";
import {
CodeMirror,
vim,
Vim,
getCM,
} from "https://esm.sh/@replit/codemirror-vim";
let initialDoc =
document.getElementById("initial-data").textContent;
const initialData = document.getElementById("initial-data");
const languageSelect = document.getElementById("language-select");
const extensionInput = document.getElementById("extension-input");
const saveButton = document.getElementById("save-button");
const newButton = document.getElementById("new-button");
const languageCompartment = new Compartment();
const LANGUAGE_PRESETS = {
plain: {
extension: ".txt",
language: () => [],
},
markdown: {
extension: ".md",
language: () => markdown(),
},
rust: {
extension: ".rs",
language: () => rust(),
},
javascript: {
extension: ".js",
language: () => javascript(),
},
typescript: {
extension: ".ts",
language: () => javascript({ typescript: true }),
},
json: {
extension: ".json",
language: () => json(),
},
html: {
extension: ".html",
language: () => html(),
},
css: {
extension: ".css",
language: () => css(),
},
python: {
extension: ".py",
language: () => python(),
},
sql: {
extension: ".sql",
language: () => sql(),
},
yaml: {
extension: ".yml",
language: () => yaml(),
},
toml: {
extension: ".toml",
language: () => StreamLanguage.define(toml),
},
};
const EXTENSION_LANGUAGE_MAP = new Map([
[".txt", "plain"],
[".md", "markdown"],
[".markdown", "markdown"],
[".rs", "rust"],
[".js", "javascript"],
[".ts", "typescript"],
[".json", "json"],
[".html", "html"],
[".htm", "html"],
[".css", "css"],
[".py", "python"],
[".sql", "sql"],
[".yaml", "yaml"],
[".yml", "yaml"],
[".toml", "toml"],
]);
let initialDoc = initialData.textContent;
let initialExtension = ".txt";
let lastSuggestedExtension = ".txt";
function normalizeExtension(value, fallback = ".txt") {
const cleaned = value
.trim()
.toLowerCase()
.replace(/^\.+/, "")
.replace(/[^a-z0-9_+-]/g, "");
if (!cleaned) {
return fallback;
}
return `.${cleaned}`;
}
function getPathExtension() {
const path = decodeURIComponent(window.location.pathname || "/");
const trimmed = path.replace(/^\/+/, "");
const match = trimmed.match(/(\.[^.\/]+)$/);
return match ? match[1].toLowerCase() : "";
}
function isDirty() {
const currentContent = editor.state.doc.toString();
const currentExtension = normalizeExtension(extensionInput.value);
return currentContent !== initialDoc || currentExtension !== initialExtension;
}
function refreshSaveState() {
saveButton.disabled = !isDirty();
}
function applyLanguagePreset(languageKey) {
const preset = LANGUAGE_PRESETS[languageKey] ?? LANGUAGE_PRESETS.plain;
const currentExtension = normalizeExtension(extensionInput.value, "");
editor.dispatch({
effects: languageCompartment.reconfigure(preset.language()),
});
if (!currentExtension || currentExtension === lastSuggestedExtension) {
extensionInput.value = preset.extension.slice(1);
}
lastSuggestedExtension = preset.extension;
refreshSaveState();
}
let editor = new EditorView({
doc: initialDoc,
extensions: [
languageCompartment.of([]),
vim({ status: true }),
basicSetup,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
refreshSaveState();
}
}),
],
parent: document.querySelector("#editor"),
});
const inferredExtension = getPathExtension() || ".txt";
const inferredLanguage = EXTENSION_LANGUAGE_MAP.get(inferredExtension) ?? "plain";
languageSelect.value = inferredLanguage;
extensionInput.value = inferredExtension.slice(1);
initialExtension = inferredExtension;
lastSuggestedExtension = inferredExtension;
applyLanguagePreset(inferredLanguage);
async function saveFile() {
const currentContent = editor.state.doc.toString();
const currentExtension = normalizeExtension(extensionInput.value);
if (currentContent === initialDoc) {
console.log("No changes detected.");
if (currentContent === initialDoc && currentExtension === initialExtension) {
return;
}
try {
const response = await fetch("/paste.txt", {
saveButton.disabled = true;
const response = await fetch(`/paste${currentExtension}`, {
method: "PUT",
body: currentContent,
});
if (response.ok) {
const resultText = await response.text();
console.log("Server returned path:", resultText);
window.history.pushState({}, "", resultText);
const savedUrl = new URL(resultText, window.location.origin);
window.history.pushState({}, "", `${savedUrl.pathname}${savedUrl.search}${savedUrl.hash}`);
initialDoc = currentContent;
initialExtension = currentExtension;
} else {
alert("Failed to save! Status: " + response.status);
console.error("Save failed with status:", response.status);
}
} catch (error) {
console.error("Save error:", error);
} finally {
refreshSaveState();
}
}
document.querySelectorAll("button").forEach((btn) => {
if (btn.textContent.includes("save")) {
btn.onclick = saveFile;
}
if (btn.textContent.includes("new")) {
btn.onclick = () => (window.location.href = "/");
saveButton.onclick = saveFile;
newButton.onclick = () => (window.location.href = "/");
languageSelect.addEventListener("change", (event) => {
applyLanguagePreset(event.target.value);
});
extensionInput.addEventListener("input", () => {
extensionInput.value = extensionInput.value
.toLowerCase()
.replace(/^\.+/, "")
.replace(/[^a-z0-9_+-]/g, "");
refreshSaveState();
});
window.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
event.preventDefault();
saveFile();
}
});
Vim.defineEx("write", "w", () => {
saveFile();
});
refreshSaveState();
</script>
</body>