Compare commits
2 commits
8fa40dc2ff
...
667638a9ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
667638a9ce |
|||
|
98290ad230 |
3 changed files with 285 additions and 29 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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
use crate::config::{AppConfig, normalize_extension};
|
use crate::config::{AppConfig, normalize_extension};
|
||||||
use content_inspector::ContentType;
|
use content_inspector::ContentType;
|
||||||
use rocket::data::{Data, ToByteUnit};
|
use rocket::data::{Data, ToByteUnit};
|
||||||
use rocket::{State, http::Status, response::content::RawHtml};
|
use rocket::http::{Accept, MediaType, Status};
|
||||||
|
use rocket::response::content::{RawHtml, RawText};
|
||||||
|
use rocket::{Responder, State};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tokio::fs as async_fs;
|
use tokio::fs as async_fs;
|
||||||
|
|
||||||
|
#[derive(Responder)]
|
||||||
|
pub enum FileResponse {
|
||||||
|
Html(RawHtml<String>),
|
||||||
|
Text(RawText<String>),
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub fn index() -> Option<RawHtml<String>> {
|
pub fn index() -> Option<RawHtml<String>> {
|
||||||
let raw: &str = include_str!("assets/editor.html");
|
let raw: &str = include_str!("assets/editor.html");
|
||||||
|
|
@ -15,22 +23,28 @@ pub fn index() -> Option<RawHtml<String>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<id>?<raw>")]
|
#[get("/<id>?<raw>")]
|
||||||
pub async fn get_file(id: &str, raw: Option<bool>, config: &State<AppConfig>) -> Result<String, Status> {
|
pub async fn get_file(
|
||||||
|
id: &str,
|
||||||
|
raw: Option<bool>,
|
||||||
|
accept: Option<&Accept>,
|
||||||
|
config: &State<AppConfig>,
|
||||||
|
) -> Result<FileResponse, Status> {
|
||||||
let file_path = Path::new(&config.upload_dir).join(id);
|
let file_path = Path::new(&config.upload_dir).join(id);
|
||||||
|
|
||||||
if file_path.exists() {
|
if file_path.exists() {
|
||||||
if let Ok(content) = async_fs::read_to_string(file_path).await {
|
if let Ok(content) = async_fs::read_to_string(file_path).await {
|
||||||
let is_mozilla = std::env::var("HTTP_USER_AGENT")
|
let wants_html = accept
|
||||||
.as_deref()
|
.map(|accept| accept.preferred().media_type() == &MediaType::HTML)
|
||||||
.unwrap_or("")
|
.unwrap_or(false);
|
||||||
.contains("Mozilla");
|
|
||||||
|
|
||||||
if raw.unwrap_or(false) || !is_mozilla {
|
if raw.unwrap_or(false) || !wants_html {
|
||||||
return Ok(content);
|
return Ok(FileResponse::Text(RawText(content)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let html: &str = include_str!("assets/editor.html");
|
let html: &str = include_str!("assets/editor.html");
|
||||||
return Ok(html.replace("$%{defaultText}%$", &content));
|
return Ok(FileResponse::Html(RawHtml(
|
||||||
|
html.replace("$%{defaultText}%$", &content),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue