mirror of
https://github.com/JonasunderscoreJones/selfhosted-overleaf-project-share.git
synced 2025-10-25 18:09:18 +02:00
added base code
This commit is contained in:
parent
8ec95f5d7f
commit
4f717a05dd
6 changed files with 419 additions and 0 deletions
10
public/404.html
Normal file
10
public/404.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>404 Not Found</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
<p>This project is not available for static sharing.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
public/index.html
Normal file
27
public/index.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>LaTeX Projects Viewer</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: #121212;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Welcome</h1>
|
||||||
|
<p>This is a static interface for viewing LaTeX projects. If you have a direct link to a project, you can view its source and compiled output there.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
335
public/overleaf.html
Normal file
335
public/overleaf.html
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Project Viewer</title>
|
||||||
|
<link rel="stylesheet" href="/prism.css" />
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: white;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
height: 40px;
|
||||||
|
background: #1b222c;
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 200px;
|
||||||
|
background: #252525;
|
||||||
|
border-right: 1px solid #444;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar li {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar li:hover {
|
||||||
|
background: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar li.active {
|
||||||
|
background: #4a90e2;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
flex: 2;
|
||||||
|
background: #1e1e1e;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 200px;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
flex: 1;
|
||||||
|
background: #2d2d2d;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-error {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #f33;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #330000cc;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: none;
|
||||||
|
z-index: 10;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
height: 30px;
|
||||||
|
background: #222;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 5px;
|
||||||
|
color: #777;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draggable separator */
|
||||||
|
#dragbar {
|
||||||
|
width: 10px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
background: #444;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
#dragbar:hover {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
#open-in-editor-button {
|
||||||
|
margin-left: auto;
|
||||||
|
color: white;
|
||||||
|
background-color: #098842;
|
||||||
|
border-radius: 100px;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>Project Viewer (Read-only)<a id="open-in-editor-button">Open in Editor</a></header>
|
||||||
|
<main>
|
||||||
|
<nav class="sidebar">
|
||||||
|
<ul id="file-list"></ul>
|
||||||
|
</nav>
|
||||||
|
<div class="editor">
|
||||||
|
<pre class="line-numbers"><code id="code" class="language-latex">// Loading...</code></pre>
|
||||||
|
</div>
|
||||||
|
<div id="dragbar"></div>
|
||||||
|
<section class="preview-container">
|
||||||
|
<div class="preview">
|
||||||
|
<iframe id="pdf" title="PDF preview"></iframe>
|
||||||
|
<div id="pdf-error" class="pdf-error">PDF failed to load or was not compiled.</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer>Static viewer for LaTeX projects</footer>
|
||||||
|
|
||||||
|
<script src="/prism.js"></script>
|
||||||
|
<script>
|
||||||
|
const projectId = window.location.pathname.split("/")[2];
|
||||||
|
const base = `/projects/${projectId}`;
|
||||||
|
const fileListEl = document.getElementById("file-list");
|
||||||
|
const codeEl = document.getElementById("code");
|
||||||
|
const pdfEl = document.getElementById("pdf");
|
||||||
|
const pdfErrorEl = document.getElementById("pdf-error");
|
||||||
|
|
||||||
|
const sidebarEl = document.querySelector(".sidebar");
|
||||||
|
const editorEl = document.querySelector(".editor");
|
||||||
|
const previewContainerEl = document.querySelector(".preview-container");
|
||||||
|
const dragbar = document.getElementById("dragbar");
|
||||||
|
const mainEl = document.querySelector("main");
|
||||||
|
|
||||||
|
document.getElementById("open-in-editor-button").onclick = () => {
|
||||||
|
window.open(`https://tex.jonasjones.dev/project/${projectId}`, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
|
async function listFiles() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${base}/`);
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch file list");
|
||||||
|
const text = await res.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(text, "text/html");
|
||||||
|
const links = Array.from(doc.querySelectorAll("a"));
|
||||||
|
|
||||||
|
const sourceFiles = [];
|
||||||
|
let pdfFiles = [];
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
const name = link.getAttribute("href");
|
||||||
|
if (!name) continue;
|
||||||
|
if (name === '../' || name.endsWith('/')) continue;
|
||||||
|
if (/\.(tex|bib|cls|sty|txt)$/i.test(name)) {
|
||||||
|
sourceFiles.push(name);
|
||||||
|
} else if (name.toLowerCase().endsWith(".pdf")) {
|
||||||
|
pdfFiles.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceFiles.length === 0) {
|
||||||
|
codeEl.textContent = "// No source files found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add files to sidebar WITHOUT numbering
|
||||||
|
sourceFiles.forEach((file, idx) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = file;
|
||||||
|
if (idx === 0) li.classList.add("active");
|
||||||
|
li.onclick = () => {
|
||||||
|
document.querySelectorAll(".sidebar li").forEach(el => el.classList.remove("active"));
|
||||||
|
li.classList.add("active");
|
||||||
|
loadSourceFile(file);
|
||||||
|
};
|
||||||
|
fileListEl.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sourceFiles.length > 0) {
|
||||||
|
loadSourceFile(sourceFiles[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF fallback: output.pdf > main.pdf > any .pdf
|
||||||
|
let pdfToLoad = pdfFiles.find(f => f.toLowerCase() === "output.pdf")
|
||||||
|
|| pdfFiles.find(f => f.toLowerCase() === "main.pdf")
|
||||||
|
|| pdfFiles[0];
|
||||||
|
|
||||||
|
if (pdfToLoad) {
|
||||||
|
loadPDF(pdfToLoad);
|
||||||
|
} else {
|
||||||
|
showPDFError(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
codeEl.textContent = `// Error loading file list: ${err.message}`;
|
||||||
|
showPDFError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSourceFile(file) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${base}/${file}`);
|
||||||
|
if (!res.ok) throw new Error("Failed to load file");
|
||||||
|
const content = await res.text();
|
||||||
|
|
||||||
|
codeEl.textContent = content;
|
||||||
|
|
||||||
|
// Change prism language class based on file extension
|
||||||
|
const ext = file.split('.').pop().toLowerCase();
|
||||||
|
codeEl.className = `language-${ext === "tex" ? "latex" : ext}`;
|
||||||
|
Prism.highlightElement(codeEl);
|
||||||
|
} catch (err) {
|
||||||
|
codeEl.textContent = `// Error loading file: ${err.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPDF(pdfFile) {
|
||||||
|
showPDFError(false);
|
||||||
|
pdfEl.src = `${base}/${pdfFile}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPDFError(show) {
|
||||||
|
pdfErrorEl.style.display = show ? "block" : "none";
|
||||||
|
if (show) {
|
||||||
|
pdfEl.style.display = "none";
|
||||||
|
} else {
|
||||||
|
pdfEl.style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfEl.onerror = () => {
|
||||||
|
showPDFError(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
pdfEl.onload = () => {
|
||||||
|
showPDFError(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draggable vertical separator
|
||||||
|
let isDragging = false;
|
||||||
|
|
||||||
|
dragbar.addEventListener("mousedown", e => {
|
||||||
|
isDragging = true;
|
||||||
|
document.body.style.cursor = "ew-resize";
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("mouseup", e => {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", e => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const containerRect = mainEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
const sidebarWidth = sidebarEl.offsetWidth;
|
||||||
|
const availableWidth = containerRect.width - sidebarWidth - dragbar.offsetWidth;
|
||||||
|
|
||||||
|
let newEditorWidth = e.clientX - containerRect.left - sidebarWidth;
|
||||||
|
newEditorWidth = Math.max(200, Math.min(newEditorWidth, availableWidth - 200));
|
||||||
|
|
||||||
|
editorEl.style.flex = "none";
|
||||||
|
previewContainerEl.style.flex = "none";
|
||||||
|
|
||||||
|
editorEl.style.width = newEditorWidth + "px";
|
||||||
|
previewContainerEl.style.width = (availableWidth - newEditorWidth) + "px";
|
||||||
|
});
|
||||||
|
|
||||||
|
listFiles();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
public/prism.css
Normal file
5
public/prism.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* PrismJS 1.30.0
|
||||||
|
https://prismjs.com/download#themes=prism&languages=latex&plugins=line-highlight+line-numbers */
|
||||||
|
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
||||||
|
pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
|
||||||
|
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
|
||||||
6
public/prism.js
Normal file
6
public/prism.js
Normal file
File diff suppressed because one or more lines are too long
36
serve.py
Normal file
36
serve.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
import os
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
PORT = 8001
|
||||||
|
BASE_DIR = os.path.abspath("public")
|
||||||
|
|
||||||
|
class CustomHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def translate_path(self, path):
|
||||||
|
path = unquote(path)
|
||||||
|
|
||||||
|
# /project/<id> should always serve overleaf.html
|
||||||
|
if path.startswith("/project/"): #and ("/" not in path[len("/project/"):]):
|
||||||
|
print(f"Serving Overleaf for project: {path}")
|
||||||
|
return os.path.join(BASE_DIR, "overleaf.html")
|
||||||
|
|
||||||
|
# /project/<id>/file -> serve from projects/<id>/file
|
||||||
|
if path.startswith("/project/"):
|
||||||
|
sub_path = path[len("/project/"):]
|
||||||
|
return os.path.join(BASE_DIR, "projects", sub_path)
|
||||||
|
|
||||||
|
# Default: serve from public/
|
||||||
|
if path in ["/", ""]:
|
||||||
|
path = "/index.html"
|
||||||
|
|
||||||
|
return os.path.join(BASE_DIR, path.lstrip("/"))
|
||||||
|
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with socketserver.TCPServer(("", PORT), CustomHandler) as httpd:
|
||||||
|
print(f"Serving at http://localhost:{PORT}")
|
||||||
|
httpd.serve_forever()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue