Upgrade JS and Go deps versions (#517)

This commit is contained in:
Thomas Miceli
2025-10-07 16:59:37 +02:00
committed by GitHub
parent f0a596aed0
commit f653179cbf
81 changed files with 2487 additions and 6227 deletions

46
public/ts/admin.ts Normal file
View File

@@ -0,0 +1,46 @@
document.addEventListener('DOMContentLoaded', () => {
let elems = Array.from(document.getElementsByClassName("toggle-button"));
for (let elem of elems) {
elem.addEventListener('click', () => {
registerDomSetting(elem as HTMLElement)
})
}
let copyInviteButtons = Array.from(document.getElementsByClassName("copy-invitation-link"));
for (let button of copyInviteButtons) {
button.addEventListener('click', () => {
navigator.clipboard.writeText((button as HTMLElement).dataset.link).catch((err) => {
console.error('Could not copy text: ', err);
});
})
}
});
const setSetting = (key: string, value: string) => {
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
const data = new URLSearchParams();
data.append('key', key);
data.append('value', value);
if (document.getElementsByName('_csrf').length !== 0) {
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
}
return fetch(`${baseUrl}/admin-panel/set-config`, {
method: 'PUT',
credentials: 'same-origin',
body: data,
});
};
const registerDomSetting = (el: HTMLElement) => {
// @ts-ignore
el.dataset["bool"] = !(el.dataset["bool"] === 'true');
setSetting(el.id, el.dataset["bool"] === 'true' ? '1' : '0')
.then(() => {
el.classList.toggle("bg-primary-600");
el.classList.toggle("dark:bg-gray-400");
el.classList.toggle("bg-gray-300");
(el.childNodes.item(1) as HTMLElement).classList.toggle("translate-x-5");
});
};

1
public/ts/auto.ts Normal file
View File

@@ -0,0 +1 @@
import '../css/auto.css'

1
public/ts/dark.ts Normal file
View File

@@ -0,0 +1 @@
import '../css/dark.css'

461
public/ts/editor.ts Normal file
View File

@@ -0,0 +1,461 @@
import {EditorView, gutter, keymap, lineNumbers} from "@codemirror/view";
import {Compartment, EditorState, Facet, Line, SelectionRange} from "@codemirror/state";
import {defaultKeymap, indentLess} from "@codemirror/commands";
document.addEventListener("DOMContentLoaded", () => {
EditorView.theme({}, {dark: true});
let editorsjs: EditorView[] = [];
let editorsParentdom = document.getElementById("editors")!;
let allEditorsdom = document.querySelectorAll("#editors > .editor");
let firstEditordom = allEditorsdom[0];
const txtFacet = Facet.define<string>({
combine(values) {
return values;
},
});
let indentSize = new Compartment(),
wrapMode = new Compartment(),
indentType = new Compartment();
const newEditor = (dom: HTMLElement, value: string = ""): EditorView => {
let editor = new EditorView({
doc: value,
parent: dom,
extensions: [
lineNumbers(),
gutter({class: "cm-mygutter"}),
keymap.of([
{key: "Tab", run: customIndentMore, shift: indentLess},
...defaultKeymap,
]),
indentSize.of(EditorState.tabSize.of(2)),
wrapMode.of([]),
indentType.of(txtFacet.of("space")),
],
});
let mdpreview = dom.querySelector(".md-preview") as HTMLElement;
let formfilename = dom.querySelector<HTMLInputElement>(".form-filename");
// check if file ends with .md on pageload
if (formfilename!.value.endsWith(".md")) {
mdpreview!.classList.remove("hidden");
} else {
mdpreview!.classList.add("hidden");
}
// event if the filename ends with .md; trigger event
formfilename!.onkeyup = (e) => {
let filename = (e.target as HTMLInputElement).value;
if (filename.endsWith(".md")) {
mdpreview!.classList.remove("hidden");
} else {
mdpreview!.classList.add("hidden");
}
};
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
let previewShown = false;
mdpreview.onclick = () => {
previewShown = !previewShown;
let divpreview = dom.querySelector("div.preview") as HTMLElement;
let cmeditor = dom.querySelector(".cm-editor") as HTMLElement;
if (!previewShown) {
divpreview!.classList.add("hidden");
cmeditor!.classList.remove("hidden-important");
return;
} else {
const formData = new FormData();
formData.append('content', editor.state.doc.toString());
let csrf = document.querySelector<HTMLInputElement>('form#create input[name="_csrf"]').value
fetch(`${baseUrl}/preview`, {
method: 'POST',
credentials: 'same-origin',
body: formData,
headers: {
'X-CSRF-Token': csrf
}
}).then(r => r.text()).then(r => {
let divpreview = dom.querySelector("div.preview") as HTMLElement;
divpreview!.innerHTML = r;
divpreview!.classList.remove("hidden");
cmeditor!.classList.add("hidden-important");
})
}
}
dom.querySelector<HTMLInputElement>(".editor-indent-type")!.onchange = (e) => {
let newTabType = (e.target as HTMLInputElement).value;
setIndentType(editor, !["tab", "space"].includes(newTabType) ? "space" : newTabType);
};
dom.querySelector<HTMLInputElement>(".editor-indent-size")!.onchange = (e) => {
let newTabSize = parseInt((e.target as HTMLInputElement).value);
setIndentSize(editor, ![2, 4, 8].includes(newTabSize) ? 2 : newTabSize);
};
dom.querySelector<HTMLInputElement>(".editor-wrap-mode")!.onchange = (e) => {
let newWrapMode = (e.target as HTMLInputElement).value;
setLineWrapping(editor, newWrapMode === "soft");
};
dom.addEventListener("drop", (e) => {
e.preventDefault(); // prevent the browser from opening the dropped file
(e.target as HTMLInputElement)
.closest(".editor")
.querySelector<HTMLInputElement>("input.form-filename")!.value =
e.dataTransfer.files[0].name;
});
// remove editor on delete
let deleteBtns = dom.querySelector<HTMLButtonElement>("button.delete-file");
if (deleteBtns !== null) {
deleteBtns.onclick = () => {
// For both text and binary files, just remove from DOM
if (!dom.hasAttribute('data-binary-original-name')) {
// Only remove from editors array for text files
editorsjs.splice(editorsjs.indexOf(editor), 1);
}
dom.remove();
checkForFirstDeleteButton();
};
}
editor.dom.addEventListener("input", function inputConfirmLeave() {
if (!editor.inView) return; // skip events outside the viewport
editor.dom.removeEventListener("input", inputConfirmLeave);
window.onbeforeunload = () => {
return "Are you sure you want to quit?";
};
});
return editor;
};
function getIndentation(state: EditorState): string {
// @ts-ignore
if (indentType.get(state).value === "tab") {
return "\t";
}
// @ts-ignore
return " ".repeat(indentSize.get(state).value);
}
function customIndentMore({state, dispatch,}: { state: EditorState; dispatch: (value: any) => void; }): boolean {
let indentation = getIndentation(state);
dispatch({
...state.update(changeBySelectedLine(state, (line, changes) => {
changes.push({from: state.selection.ranges[0].from, insert: indentation,});
})),
selection: {
anchor: state.selection.ranges[0].from + indentation.length,
head: state.selection.ranges[0].from + indentation.length,
},
});
return true;
}
function changeBySelectedLine(state: EditorState, f: (line: Line, changes: any[]) => void): any {
let atLine = -1;
return state.changeByRange((range) => {
let changes: any[] = [];
for (let line = state.doc.lineAt(range.from); ;) {
if (line.number > atLine) {
f(line, changes);
atLine = line.number;
}
if (range.to <= line.to) break;
line = state.doc.lineAt(line.number + 1);
}
let changeSet = state.changes(changes);
return {
changes,
// @ts-ignore
range: new SelectionRange(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)),
};
});
}
function setIndentType(view: EditorView, type: string): void {
view.dispatch({effects: indentType.reconfigure(txtFacet.of(type))});
}
function setIndentSize(view: EditorView, size: number): void {
view.dispatch({effects: indentSize.reconfigure(EditorState.tabSize.of(size))});
}
function setLineWrapping(view: EditorView, enable: boolean): void {
view.dispatch({
effects: wrapMode.reconfigure(enable ? EditorView.lineWrapping : []),
});
}
let arr = Array.from(allEditorsdom);
arr.forEach((el: HTMLElement) => {
// in case we edit the gist contents
let formFileContent =el.querySelector<HTMLInputElement>(".form-filecontent")
if (formFileContent !== null) {
let currEditor = newEditor(el, el.querySelector<HTMLInputElement>(".form-filecontent")!.value);
editorsjs.push(currEditor);
} else if (el.hasAttribute('data-binary-original-name')) {
// For binary files, just set up the delete button
let deleteBtn = el.querySelector<HTMLButtonElement>("button.delete-file");
if (deleteBtn) {
deleteBtn.onclick = () => {
el.remove();
checkForFirstDeleteButton();
};
}
}
});
checkForFirstDeleteButton();
document.getElementById("add-file")!.onclick = () => {
const template = document.getElementById("editor-template")!;
const newEditorDom = template.firstElementChild!.cloneNode(true) as HTMLElement;
// creating the new codemirror editor and append it in the editor div
editorsjs.push(newEditor(newEditorDom));
editorsParentdom.append(newEditorDom);
showDeleteButton(newEditorDom);
};
document.querySelector<HTMLFormElement>("form#create")!.onsubmit = () => {
let j = 0;
document.querySelectorAll<HTMLInputElement>(".form-filecontent").forEach((el) => {
if (j < editorsjs.length) {
el.value = encodeURIComponent(editorsjs[j++].state.doc.toString());
}
});
const fileInput = document.getElementById("file-upload") as HTMLInputElement;
if (fileInput) {
fileInput.remove();
}
const form = document.querySelector<HTMLFormElement>("form#create")!;
uploadedFileUUIDs.forEach((fileData) => {
const uuidInput = document.createElement('input');
uuidInput.type = 'hidden';
uuidInput.name = 'uploadedfile_uuid';
uuidInput.value = fileData.uuid;
form.appendChild(uuidInput);
const filenameInput = document.createElement('input');
filenameInput.type = 'hidden';
filenameInput.name = 'uploadedfile_filename';
filenameInput.value = fileData.filename;
form.appendChild(filenameInput);
});
const binaryFiles = document.querySelectorAll('[data-binary-original-name]');
binaryFiles.forEach((fileDiv) => {
const originalName = fileDiv.getAttribute('data-binary-original-name');
const fileNameInput = fileDiv.querySelector('.form-filename') as HTMLInputElement;
if (fileNameInput) {
fileNameInput.removeAttribute('name');
}
const oldNameInput = document.createElement('input');
oldNameInput.type = 'hidden';
oldNameInput.name = 'binary_old_name';
oldNameInput.value = originalName || '';
form.appendChild(oldNameInput);
const newNameInput = document.createElement('input');
newNameInput.type = 'hidden';
newNameInput.name = 'binary_new_name';
newNameInput.value = fileNameInput?.value || '';
form.appendChild(newNameInput);
});
window.onbeforeunload = null;
};
document.getElementById('gist-metadata-btn')!.onclick = (el) => {
let metadata = document.getElementById('gist-metadata')!;
metadata.classList.toggle('hidden');
let btn = el.target as HTMLButtonElement;
if (btn.innerText.endsWith('▼')) {
btn.innerText = btn.innerText.replace('▼', '▲');
} else {
btn.innerText = btn.innerText.replace('▲', '▼');
}
}
function checkForFirstDeleteButton() {
// Count total files (both text and binary)
const totalFiles = editorsParentdom.querySelectorAll('.editor').length;
// Hide/show all delete buttons based on total file count
const deleteButtons = editorsParentdom.querySelectorAll<HTMLButtonElement>("button.delete-file");
deleteButtons.forEach(deleteBtn => {
if (totalFiles <= 1) {
deleteBtn.classList.add("hidden");
deleteBtn.previousElementSibling?.classList.remove("rounded-l-md");
deleteBtn.previousElementSibling?.classList.add("rounded-md");
} else {
deleteBtn.classList.remove("hidden");
deleteBtn.previousElementSibling?.classList.add("rounded-l-md");
deleteBtn.previousElementSibling?.classList.remove("rounded-md");
}
});
}
function showDeleteButton(editorDom: HTMLElement) {
let deleteBtn = editorDom.querySelector<HTMLButtonElement>("button.delete-file")!;
deleteBtn.classList.remove("hidden");
deleteBtn.previousElementSibling.classList.add("rounded-l-md");
deleteBtn.previousElementSibling.classList.remove("rounded-md");
checkForFirstDeleteButton();
}
// File upload functionality
let uploadedFileUUIDs: {uuid: string, filename: string}[] = [];
const fileUploadInput = document.getElementById("file-upload") as HTMLInputElement;
const uploadedFilesContainer = document.getElementById("uploaded-files")!;
const fileUploadZone = document.getElementById("file-upload-zone")!.querySelector('.border-dashed') as HTMLElement;
// Handle file selection
const handleFiles = (files: FileList) => {
Array.from(files).forEach(file => {
if (!uploadedFileUUIDs.find(f => f.filename === file.name)) {
uploadFile(file);
}
});
};
// Upload file to server
const uploadFile = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
const csrf = document.querySelector<HTMLInputElement>('form#create input[name="_csrf"]')?.value;
try {
const response = await fetch(`${baseUrl}/upload`, {
method: 'POST',
credentials: 'same-origin',
body: formData,
headers: {
'X-CSRF-Token': csrf || ''
}
});
if (response.ok) {
const result = await response.json();
uploadedFileUUIDs.push({uuid: result.uuid, filename: result.filename});
addFileToUI(result.filename, result.uuid, file.size);
} else {
console.error('Upload failed:', response.statusText);
}
} catch (error) {
console.error('Upload error:', error);
}
};
// Add file to UI
const addFileToUI = (filename: string, uuid: string, fileSize: number) => {
const fileElement = document.createElement('div');
fileElement.className = 'flex items-stretch bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden';
fileElement.dataset.uuid = uuid;
fileElement.innerHTML = `
<div class="flex items-center space-x-3 px-3 py-1 flex-1">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"></path>
</svg>
<div>
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">${filename}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">${formatFileSize(fileSize)}</p>
</div>
</div>
<button type="button" class="remove-file flex items-center justify-center px-4 border-l-1 dark:border-l-1 text-rose-600 dark:text-rose-400 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
`;
// Remove file handler
fileElement.querySelector('.remove-file')!.addEventListener('click', async () => {
// Remove from server
try {
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
const csrf = document.querySelector<HTMLInputElement>('form#create input[name="_csrf"]')?.value;
await fetch(`${baseUrl}/upload/${uuid}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: {
'X-CSRF-Token': csrf || ''
}
});
} catch (error) {
console.error('Error deleting file:', error);
}
// Remove from UI and local array
uploadedFileUUIDs = uploadedFileUUIDs.filter(f => f.uuid !== uuid);
fileElement.remove();
});
uploadedFilesContainer.appendChild(fileElement);
};
// Format file size
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};
// File input change handler
fileUploadInput.addEventListener('change', (e) => {
const files = (e.target as HTMLInputElement).files;
if (files) {
handleFiles(files);
// Clear the input value immediately so it doesn't get submitted with the form
(e.target as HTMLInputElement).value = '';
}
});
// Drag and drop handlers
fileUploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
fileUploadZone.classList.add('border-primary-400', 'dark:border-primary-500');
});
fileUploadZone.addEventListener('dragleave', (e) => {
e.preventDefault();
fileUploadZone.classList.remove('border-primary-400', 'dark:border-primary-500');
});
fileUploadZone.addEventListener('drop', (e) => {
e.preventDefault();
fileUploadZone.classList.remove('border-primary-400', 'dark:border-primary-500');
const files = e.dataTransfer?.files;
if (files) {
handleFiles(files);
}
});
});

1
public/ts/embed.ts Normal file
View File

@@ -0,0 +1 @@
import "../css/embed.css"

83
public/ts/gist.ts Normal file
View File

@@ -0,0 +1,83 @@
import '../ts/ipynb.ts';
import PDFObject from 'pdfobject';
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
el.addEventListener('click', event => {
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
const nextSibling = (event.target as HTMLElement).nextSibling;
if (nextSibling instanceof HTMLElement) {
nextSibling.classList.add('selected');
}
const filename = el.dataset.filenameSlug;
const line = (event.target as HTMLElement).textContent;
const url = location.protocol + '//' + location.host + location.pathname;
const hash = '#file-' + filename + '-' + line;
window.history.pushState(null, null, url + hash);
location.hash = hash;
}
});
});
let copybtnhtml = `<button type="button" style="top: 1em !important; right: 1em !important;" class="md-code-copy-btn absolute focus-within:z-auto rounded-md dark:border-gray-600 px-2 py-2 opacity-80 font-medium text-slate-700 bg-gray-100 dark:bg-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" /></svg></button>`;
document.querySelectorAll<HTMLElement>('.markdown-body pre').forEach((el) => {
if (el.classList.contains("mermaid")) {
return;
}
el.innerHTML = copybtnhtml + `<span class="code-div">` + el.innerHTML + `</span>`;
});
document.querySelectorAll('.md-code-copy-btn').forEach(button => {
button.addEventListener('click', function() {
let code = this.nextElementSibling.textContent;
navigator.clipboard.writeText(code).catch((err) => {
console.error('Could not copy text: ', err);
});
});
});
let checkboxes = document.querySelectorAll('li[data-checkbox-nb] input[type=checkbox]');
if (document.getElementById('gist').dataset.own) {
document.querySelectorAll<HTMLElement>('li[data-checkbox-nb]').forEach((el) => {
let input: HTMLButtonElement = el.querySelector('input[type=checkbox]');
input.disabled = false;
let checkboxNb = (el as HTMLElement).dataset.checkboxNb;
let filename = input.closest<HTMLElement>('div[data-file]').dataset.file;
input.addEventListener('change', function () {
const data = new URLSearchParams();
data.append('checkbox', checkboxNb);
data.append('file', filename);
if (document.getElementsByName('_csrf').length !== 0) {
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
}
checkboxes.forEach((el: HTMLButtonElement) => {
el.disabled = true;
el.classList.add('text-gray-400')
});
fetch(window.location.href.split('#')[0] + '/checkbox', {
method: 'PUT',
credentials: 'same-origin',
body: data,
}).then((response) => {
if (response.status === 200) {
checkboxes.forEach((el: HTMLButtonElement) => {
el.disabled = false;
el.classList.remove('text-gray-400')
});
}
});
});
});
} else {
checkboxes.forEach((el: HTMLButtonElement) => {
el.disabled = true;
});
}
document.querySelectorAll(".pdf").forEach((el) => {
PDFObject.embed(el.dataset.src || "", el);
})

126
public/ts/ipynb.ts Normal file
View File

@@ -0,0 +1,126 @@
import hljs from 'highlight.js';
import latex from './latex';
import { marked } from 'marked';
class IPynb {
private element: HTMLElement;
private cells: HTMLElement[] = [];
private language: string = 'python';
private notebook: any;
constructor(element: HTMLElement) {
this.element = element;
let notebookContent = element.innerText;
try {
this.notebook = JSON.parse(notebookContent);
} catch (e) {
console.error('Failed to parse Jupyter notebook content:', e);
return;
}
if (!this.notebook) {
console.error('Failed to parse Jupyter notebook content:', notebookContent);
return;
}
this.language = this.notebook.metadata.kernelspec?.language || 'python';
this.cells = this.createCells();
}
mount() {
const parent = this.element.parentElement as HTMLElement;
parent.removeChild(this.element);
parent.innerHTML = this.cells
.filter((cell: HTMLElement) => !!cell?.outerHTML)
.map((cell: HTMLElement) => cell.outerHTML)
.join('');
}
private getOutputs(cell: any): HTMLElement[] {
return (cell.outputs || []).map((output: any) => {
const outputElement = document.createElement('div');
outputElement.classList.add('jupyter-output');
if (output.output_type === 'stream') {
const textElement = document.createElement('pre');
textElement.classList.add('stream-output');
textElement.textContent = output.text.join('');
outputElement.appendChild(textElement);
} else if (output.output_type === 'display_data' || output.output_type === 'execute_result') {
if (output.data['text/plain']) {
outputElement.innerHTML += `\n<pre>${output.data['text/plain']}</pre>`;
}
if (output.data['text/html']) {
outputElement.innerHTML += '\n' + output.data['text/html'];
}
const images = Object.keys(output.data).filter(key => key.startsWith('image/'));
if (images.length > 0) {
const imgEl = document.createElement('img');
const imgType = images[0]; // Use the first image type found
imgEl.src = `data:${imgType};base64,${output.data[imgType]}`;
outputElement.innerHTML += imgEl.outerHTML;
}
} else if (output.output_type === 'execute_result') {
if (output.data['text/plain']) {
outputElement.innerHTML += `<pre>${output.data['text/plain']}</pre>`;
}
if (output.data['text/html']) {
outputElement.innerHTML += output.data['text/html'];
}
if (output.data['image/png']) {
const imgEl = document.createElement('img');
imgEl.src = `data:image/png;base64,${output.data['image/png']}`;
outputElement.appendChild(imgEl);
}
} else if (output.output_type === 'error') {
outputElement.classList.add('error');
outputElement.textContent = `Error: ${output.ename}: ${output.evalue}`;
}
return outputElement;
});
}
private createCellElement(cell: any): HTMLElement {
const cellElement = document.createElement('div');
const source = cell.source.join('');
cellElement.classList.add('jupyter-cell');
switch (cell.cell_type) {
case 'markdown':
cellElement.classList.add('markdown-cell');
cellElement.innerHTML = `<div class="markdown-body">${marked.parse(latex.render(source))}</div>`;
break;
case 'code':
cellElement.classList.add('code-cell');
cellElement.innerHTML = `<pre class="hljs"><code class="language-${this.language}">${source}</code></pre>`;
hljs.highlightElement(cellElement.querySelector('code') as HTMLElement);
break;
default:
break;
}
return cellElement;
}
private createCells(): HTMLElement[] {
return (this.notebook.cells || []).map((cell: any) => {
const container = document.createElement('div');
const cellElement = this.createCellElement(cell);
const outputs = this.getOutputs(cell);
container.classList.add('jupyter-cell-container');
container.appendChild(cellElement);
outputs.forEach((output: HTMLElement) => container.appendChild(output));
return container;
});
}
}
// Process Jupyter notebooks
document.querySelectorAll<HTMLElement>('.jupyter.notebook pre').forEach((el) => {
new IPynb(el).mount();
});

66
public/ts/latex.ts Normal file
View File

@@ -0,0 +1,66 @@
import katex from 'katex';
const delimiters = [
{ left: '\\\$\\\$', right: '\\\$\\\$', multiline: true },
{ left: '\\\$', right: '\\\$', multiline: false },
{ left: '\\\\\[', right: '\\\\\]', multiline: true },
{ left: '\\\\\(', right: '\\\\\)', multiline: false },
];
const delimiterMatchers = delimiters.map(
(delimiter) => new RegExp(
`${delimiter.left}(.*?)${delimiter.right}`,
`g${delimiter.multiline ? 'ms' : ''}`
)
);
// Replace LaTeX delimiters in a string with KaTeX rendering
function render(text: string): string {
// Step 1: Replace all LaTeX expressions with placeholders
const expressions: Array<{ placeholder: string; latex: string; displayMode: boolean }> = [];
let modifiedText = text;
let placeholderIndex = 0;
// Process each delimiter type
delimiters.forEach((delimiter, i) => {
// Find all matches and replace with placeholders
modifiedText = modifiedText.replace(delimiterMatchers[i], (match, latex) => {
if (!latex.trim()) {
return match; // Return original if content is empty
}
const placeholder = `__KATEX_PLACEHOLDER_${placeholderIndex++}__`;
expressions.push({
placeholder,
latex,
displayMode: delimiter.multiline,
});
return placeholder;
});
});
// Step 2: Replace placeholders with rendered LaTeX
for (const { placeholder, latex, displayMode } of expressions) {
try {
const rendered = katex.renderToString(latex, {
throwOnError: false,
displayMode,
});
modifiedText = modifiedText.replace(placeholder, rendered);
} catch (error) {
console.error('KaTeX rendering error:', error);
// Replace placeholder with original LaTeX if rendering fails
modifiedText = modifiedText.replace(
placeholder,
displayMode ? `$$${latex}$$` : `$${latex}$`
);
}
}
return modifiedText;
}
export default {
render,
};

1
public/ts/light.ts Normal file
View File

@@ -0,0 +1 @@
import '../css/light.css'

185
public/ts/main.ts Normal file
View File

@@ -0,0 +1,185 @@
import '../css/tailwind.css';
import '../img/favicon-32.png';
import '../img/opengist.svg';
import jdenticon from 'jdenticon/standalone';
jdenticon.update("[data-jdenticon-value]")
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('user-btn')?.addEventListener("click" , () => {
document.getElementById('user-menu')!.classList.toggle('hidden');
})
document.querySelectorAll('form').forEach((form: HTMLFormElement) => {
form.onsubmit = () => {
form.querySelectorAll('input[type=datetime-local]').forEach((input: HTMLInputElement) => {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'expiredAtUnix'
hiddenInput.value = Math.floor(new Date(input.value).getTime() / 1000).toString();
form.appendChild(hiddenInput);
});
return true;
};
})
const rev = document.querySelector<HTMLElement>('.revision-text');
if (rev) {
const fullRev = rev.innerHTML;
const smallRev = fullRev.substring(0, 7);
rev.innerHTML = smallRev;
rev.onmouseover = () => {
rev.innerHTML = fullRev;
};
rev.onmouseout = () => {
rev.innerHTML = smallRev;
};
}
const colorhash = () => {
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
const lineEl = document.querySelector<HTMLElement>(location.hash);
if (lineEl) {
const nextSibling = lineEl.nextSibling;
if (nextSibling instanceof HTMLElement) {
nextSibling.classList.add('selected');
}
}
};
if (location.hash) {
colorhash();
}
window.onhashchange = colorhash;
document.getElementById('main-menu-button')!.onclick = () => {
document.getElementById('mobile-menu')!.classList.toggle('hidden');
};
const tabs = document.getElementById('gist-tabs');
if (tabs) {
tabs.onchange = (e: Event) => {
const target = e.target as HTMLSelectElement;
window.location.href = target.selectedOptions[0].dataset.url || '';
};
}
const gistmenutoggle = document.getElementById('gist-menu-toggle');
if (gistmenutoggle) {
const gistmenucopy = document.getElementById('gist-menu-copy')!;
const gistmenubuttoncopy = document.getElementById('gist-menu-button-copy')!;
const gistmenuinput = document.getElementById('gist-menu-input') as HTMLInputElement;
const gistmenutitle = document.getElementById('gist-menu-title')!;
gistmenutitle.textContent = gistmenucopy.children[0].firstChild!.textContent;
gistmenuinput.value = (gistmenucopy.children[0] as HTMLElement).dataset.link || '';
gistmenutoggle.onclick = () => {
gistmenucopy.classList.toggle('hidden');
};
for (const item of Array.from(gistmenucopy.children)) {
(item as HTMLElement).onclick = () => {
gistmenutitle.textContent = item.firstChild!.textContent;
gistmenuinput.value = (item as HTMLElement).dataset.link || '';
gistmenucopy.classList.toggle('hidden');
};
}
gistmenubuttoncopy.onclick = () => {
const text = gistmenuinput.value;
navigator.clipboard.writeText(text).catch((err) => {
console.error('Could not copy text: ', err);
});
};
}
const sortgist = document.getElementById('sort-gists-button');
if (sortgist) {
sortgist.onclick = () => {
document.getElementById('sort-gists-dropdown')!.classList.toggle('hidden');
};
}
const searchUserGistsVisibility = document.getElementById('search-user-gists-visibility');
if (searchUserGistsVisibility) {
let dropdown = document.getElementById('search-user-gists-visibility-dropdown');
searchUserGistsVisibility.onclick = () => {
dropdown!.classList.toggle('hidden');
};
let buttons = dropdown.querySelectorAll('button');
buttons.forEach((button) => {
button.onclick = () => {
let value = document.getElementById('visibility-value') as HTMLInputElement;
value.textContent = button.dataset.visibilityStr;
dropdown!.classList.add('hidden');
dropdown.querySelector('input')!.value = button.dataset.visibility || '';
};
});
}
const searchUserGistsLanguage = document.getElementById('search-user-gists-language');
if (searchUserGistsLanguage) {
let dropdown = document.getElementById('search-user-gists-language-dropdown');
searchUserGistsLanguage.onclick = () => {
dropdown!.classList.toggle('hidden');
};
let buttons = dropdown.querySelectorAll('button');
buttons.forEach((button) => {
button.onclick = () => {
let value = document.getElementById('language-value') as HTMLInputElement;
value.textContent = button.dataset.languageStr;
dropdown!.classList.add('hidden');
dropdown.querySelector('input')!.value = button.dataset.language || '';
};
});
}
document.getElementById('language-btn')!.onclick = () => {
document.getElementById('language-list')!.classList.toggle('hidden');
};
document.querySelectorAll('.copy-gist-btn').forEach((e: HTMLElement) => {
e.onclick = () => {
navigator.clipboard.writeText(e.parentNode!.parentNode!.querySelector<HTMLElement>('.gist-content')!.textContent || '').catch((err) => {
console.error('Could not copy text: ', err);
});
};
});
const gistmenuvisibility = document.getElementById('gist-menu-visibility');
if (gistmenuvisibility) {
let submitgistbutton = (document.getElementById('submit-gist') as HTMLInputElement);
document.getElementById('gist-visibility-menu-button')!.onclick = () => {
gistmenuvisibility!.classList.toggle('hidden');
}
const lastVisibility = localStorage.getItem('visibility');
Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => {
const visibility = (el as HTMLElement).dataset.visibility || '0';
(el as HTMLElement).onclick = () => {
submitgistbutton.textContent = (el as HTMLElement).dataset.btntext;
submitgistbutton!.value = visibility;
localStorage.setItem('visibility', visibility);
gistmenuvisibility!.classList.add('hidden');
}
if (lastVisibility === visibility) {
(el as HTMLElement).click();
}
});
}
const searchinput = document.getElementById('search') as HTMLInputElement;
searchinput.addEventListener('focusin', () => {
document.getElementById('search-help').classList.remove('hidden');
})
searchinput.addEventListener('focusout', (e) => {
document.getElementById('search-help').classList.add('hidden');
})
});

View File

@@ -0,0 +1,45 @@
document.addEventListener('DOMContentLoaded', () => {
const noSoftWrapRadio = document.getElementById('no-soft-wrap');
const softWrapRadio = document.getElementById('soft-wrap');
function updateRootClass() {
const table = document.querySelector("table");
if (softWrapRadio.checked) {
table.classList.remove('whitespace-pre');
table.classList.add('whitespace-pre-wrap');
} else {
table.classList.remove('whitespace-pre-wrap');
table.classList.add('whitespace-pre');
}
}
noSoftWrapRadio.addEventListener('change', updateRootClass);
softWrapRadio.addEventListener('change', updateRootClass);
document.getElementById('removedlinecolor').addEventListener('change', function(event) {
const color = hexToRgba(event.target.value, 0.1);
document.documentElement.style.setProperty('--red-diff', color);
});
document.getElementById('addedlinecolor').addEventListener('change', function(event) {
const color = hexToRgba(event.target.value, 0.1);
document.documentElement.style.setProperty('--green-diff', color);
});
document.getElementById('gitlinecolor').addEventListener('change', function(event) {
const color = hexToRgba(event.target.value, 0.38);
document.documentElement.style.setProperty('--git-diff', color);
});
});
function hexToRgba(hex, opacity) {
hex = hex.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}

182
public/ts/webauthn.ts Normal file
View File

@@ -0,0 +1,182 @@
let loginMethod = "login"
function encodeArrayBufferToBase64Url(buffer) {
const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function decodeBase64UrlToArrayBuffer(base64Url) {
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) {
base64 += '=';
}
const binaryString = atob(base64);
const buffer = new ArrayBuffer(binaryString.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < binaryString.length; i++) {
view[i] = binaryString.charCodeAt(i);
}
return buffer;
}
async function bindPasskey() {
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
let waitText = document.getElementById("login-passkey-wait");
try {
this.classList.add('hidden');
waitText.classList.remove('hidden');
let csrf = document.querySelector<HTMLInputElement>('form#webauthn input[name="_csrf"]').value
const beginResponse = await fetch(`${baseUrl}/webauthn/bind`, {
headers: {
'Accept': 'application/json',
},
method: 'POST',
credentials: 'include',
body: new FormData(document.querySelector<HTMLFormElement>('form#webauthn'))
});
const beginData = await beginResponse.json();
beginData.publicKey.challenge = decodeBase64UrlToArrayBuffer(beginData.publicKey.challenge);
beginData.publicKey.user.id = decodeBase64UrlToArrayBuffer(beginData.publicKey.user.id);
for (const cred of beginData.publicKey.excludeCredentials ?? []) {
cred.id = decodeBase64UrlToArrayBuffer(cred.id);
}
const credential = await navigator.credentials.create({
publicKey: beginData.publicKey,
});
if (!credential || !credential.rawId || !credential.response) {
throw new Error('Credential object is missing required properties');
}
const finishResponse = await fetch(`${baseUrl}/webauthn/bind/finish`, {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': csrf
},
body: JSON.stringify({
id: credential.id,
rawId: encodeArrayBufferToBase64Url(credential.rawId),
response: {
attestationObject: encodeArrayBufferToBase64Url(credential.response.attestationObject),
clientDataJSON: encodeArrayBufferToBase64Url(credential.response.clientDataJSON),
},
type: credential.type,
passkeyname: document.querySelector<HTMLInputElement>('form#webauthn input[name="passkeyname"]').value
}),
});
const finishData = await finishResponse.json();
setTimeout(() => {
window.location.reload();
}, 100);
} catch (error) {
console.error('Error during passkey registration:', error);
waitText.classList.add('hidden');
this.classList.remove('hidden');
alert(error);
}
}
async function loginWithPasskey() {
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
let waitText = document.getElementById("login-passkey-wait");
try {
this.classList.add('hidden');
waitText.classList.remove('hidden');
let csrf = document.querySelector<HTMLInputElement>('form#webauthn input[name="_csrf"]').value
const beginResponse = await fetch(`${baseUrl}/webauthn/${loginMethod}`, {
headers: {
'Accept': 'application/json',
},
method: 'POST',
credentials: 'include',
body: new FormData(document.querySelector<HTMLFormElement>('form#webauthn'))
});
const beginData = await beginResponse.json();
beginData.publicKey.challenge = decodeBase64UrlToArrayBuffer(beginData.publicKey.challenge);
if (beginData.publicKey.allowCredentials) {
beginData.publicKey.allowCredentials = beginData.publicKey.allowCredentials.map(cred => ({
...cred,
id: decodeBase64UrlToArrayBuffer(cred.id),
}));
}
const credential = await navigator.credentials.get({
publicKey: beginData.publicKey,
});
if (!credential || !credential.rawId || !credential.response) {
throw new Error('Credential object is missing required properties');
}
const finishResponse = await fetch(`${baseUrl}/webauthn/${loginMethod}/finish`, {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': csrf
},
body: JSON.stringify({
id: credential.id,
rawId: encodeArrayBufferToBase64Url(credential.rawId),
response: {
authenticatorData: encodeArrayBufferToBase64Url(credential.response.authenticatorData),
clientDataJSON: encodeArrayBufferToBase64Url(credential.response.clientDataJSON),
signature: encodeArrayBufferToBase64Url(credential.response.signature),
userHandle: encodeArrayBufferToBase64Url(credential.response.userHandle),
},
type: credential.type,
clientExtensionResults: credential.getClientExtensionResults(),
}),
});
const finishData = await finishResponse.json();
if (!finishResponse.ok) {
throw new Error(finishData.message || 'Unknown error');
}
setTimeout(() => {
window.location.href = `${baseUrl}`;
}, 100);
} catch (error) {
console.error('Login error:', error);
waitText.classList.add('hidden');
this.classList.remove('hidden');
alert(error);
}
}
document.addEventListener('DOMContentLoaded', () => {
const registerButton = document.getElementById('bind-passkey-button');
if (registerButton) {
registerButton.addEventListener('click', bindPasskey);
}
if (document.documentURI.includes('/mfa')) {
loginMethod = "assertion"
}
const loginButton = document.getElementById('login-passkey-button');
if (loginButton) {
loginButton.addEventListener('click', loginWithPasskey);
}
});