This commit is contained in:
parent
98290ad230
commit
667638a9ce
2 changed files with 262 additions and 20 deletions
|
|
@ -11,5 +11,5 @@ port = 8000
|
||||||
# b, kb, mb, gb, tb
|
# b, kb, mb, gb, tb
|
||||||
# kib, mib, gib, tib
|
# kib, mib, gib, tib
|
||||||
max_size = "10MB"
|
max_size = "10MB"
|
||||||
extension_whitelist = [".docx", ".md", ".json", ".rs"]
|
# extension_whitelist = [".docx", ".md", ".json", ".rs"]
|
||||||
block_binary = true
|
block_binary = true
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>paste</title>
|
||||||
<style>
|
<style>
|
||||||
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
|
@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 {
|
.container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor {
|
#editor {
|
||||||
height: 100%;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
|
|
@ -29,6 +32,8 @@
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header ul {
|
header ul {
|
||||||
|
|
@ -37,6 +42,20 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 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 {
|
header button {
|
||||||
|
|
@ -47,6 +66,31 @@
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
@ -54,8 +98,33 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<ul>
|
<ul>
|
||||||
<li><button>[ new ]</button></li>
|
<li><button id="new-button" type="button">[ new ]</button></li>
|
||||||
<li><button>[ save ]</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>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
<div id="editor"></div>
|
<div id="editor"></div>
|
||||||
|
|
@ -67,63 +136,236 @@
|
||||||
<script id="initial-data" type="text/plain">$%{defaultText}%$</script>
|
<script id="initial-data" type="text/plain">$%{defaultText}%$</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { basicSetup, EditorView } from "https://esm.sh/codemirror";
|
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 {
|
import {
|
||||||
CodeMirror,
|
|
||||||
vim,
|
vim,
|
||||||
Vim,
|
Vim,
|
||||||
getCM,
|
|
||||||
} from "https://esm.sh/@replit/codemirror-vim";
|
} from "https://esm.sh/@replit/codemirror-vim";
|
||||||
|
|
||||||
let initialDoc =
|
const initialData = document.getElementById("initial-data");
|
||||||
document.getElementById("initial-data").textContent;
|
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({
|
let editor = new EditorView({
|
||||||
doc: initialDoc,
|
doc: initialDoc,
|
||||||
extensions: [
|
extensions: [
|
||||||
|
languageCompartment.of([]),
|
||||||
vim({ status: true }),
|
vim({ status: true }),
|
||||||
basicSetup,
|
basicSetup,
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
refreshSaveState();
|
||||||
|
}
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
parent: document.querySelector("#editor"),
|
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() {
|
async function saveFile() {
|
||||||
const currentContent = editor.state.doc.toString();
|
const currentContent = editor.state.doc.toString();
|
||||||
|
const currentExtension = normalizeExtension(extensionInput.value);
|
||||||
|
|
||||||
if (currentContent === initialDoc) {
|
if (currentContent === initialDoc && currentExtension === initialExtension) {
|
||||||
console.log("No changes detected.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/paste.txt", {
|
saveButton.disabled = true;
|
||||||
|
|
||||||
|
const response = await fetch(`/paste${currentExtension}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: currentContent,
|
body: currentContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const resultText = await response.text();
|
const resultText = await response.text();
|
||||||
console.log("Server returned path:", resultText);
|
const savedUrl = new URL(resultText, window.location.origin);
|
||||||
window.history.pushState({}, "", resultText);
|
|
||||||
|
window.history.pushState({}, "", `${savedUrl.pathname}${savedUrl.search}${savedUrl.hash}`);
|
||||||
initialDoc = currentContent;
|
initialDoc = currentContent;
|
||||||
|
initialExtension = currentExtension;
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to save! Status: " + response.status);
|
console.error("Save failed with status:", response.status);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Save error:", error);
|
console.error("Save error:", error);
|
||||||
|
} finally {
|
||||||
|
refreshSaveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.querySelectorAll("button").forEach((btn) => {
|
|
||||||
if (btn.textContent.includes("save")) {
|
saveButton.onclick = saveFile;
|
||||||
btn.onclick = saveFile;
|
newButton.onclick = () => (window.location.href = "/");
|
||||||
}
|
|
||||||
if (btn.textContent.includes("new")) {
|
languageSelect.addEventListener("change", (event) => {
|
||||||
btn.onclick = () => (window.location.href = "/");
|
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", () => {
|
Vim.defineEx("write", "w", () => {
|
||||||
saveFile();
|
saveFile();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
refreshSaveState();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue