2022-11-10 18:13:24 +00:00
|
|
|
import brotliInit, {
|
|
|
|
compress,
|
|
|
|
DecompressStream,
|
|
|
|
BrotliStreamResult,
|
|
|
|
} from "./vendor/brotli_wasm.js";
|
2022-10-25 22:10:05 +00:00
|
|
|
import base from "./vendor/base-x.js";
|
2022-10-28 20:18:56 +00:00
|
|
|
import "./vendor/prism/prism.js";
|
|
|
|
import "./vendor/prism/plugins/line-numbers/prism-line-numbers.js";
|
|
|
|
import "./vendor/prism/dependencies.js";
|
|
|
|
|
|
|
|
import { waitForLoad } from "./utils.js";
|
|
|
|
import { DataOptions } from "./dataoptions.js";
|
|
|
|
import { ViewMode } from "./viewmode.js";
|
2022-11-10 18:13:24 +00:00
|
|
|
import {
|
|
|
|
getDependenciesForLanguage,
|
|
|
|
getDepsData,
|
|
|
|
LANGUAGES,
|
|
|
|
LANGUAGE_NAMES,
|
|
|
|
PLAINTEXT,
|
2023-01-03 16:38:31 +00:00
|
|
|
PATCHED_LANGUAGES,
|
2022-11-10 18:13:24 +00:00
|
|
|
} from "./languages.js";
|
2022-10-25 22:10:05 +00:00
|
|
|
|
2022-11-10 18:05:08 +00:00
|
|
|
const PROJECT_NAME = "Tahnaroskakori";
|
2022-10-25 22:10:05 +00:00
|
|
|
const COMPRESS_WAIT = 500;
|
|
|
|
const ENCODER = new TextEncoder();
|
2022-11-10 18:13:24 +00:00
|
|
|
const ALPHABET =
|
|
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?/-._~$&+=";
|
2022-10-27 08:01:59 +00:00
|
|
|
const BASECODEC = base(ALPHABET);
|
2022-10-25 22:10:05 +00:00
|
|
|
const QUALITY = 11;
|
|
|
|
const MAX_URL = 2048;
|
2022-10-26 08:13:32 +00:00
|
|
|
const DECOMPRESS_CHUNK_SIZE = 1000;
|
|
|
|
const DECOMPRESS_CHUNK_TIMEOUT = 100;
|
2022-11-10 18:05:08 +00:00
|
|
|
const TITLE_POSTFIX = ` · ${PROJECT_NAME}`;
|
|
|
|
const TITLE_CODE_LENGTH = 40;
|
2022-10-25 22:10:05 +00:00
|
|
|
|
2022-10-28 20:18:56 +00:00
|
|
|
let languageDependencies = null;
|
2022-10-25 22:10:05 +00:00
|
|
|
let compressTimeout = null;
|
|
|
|
let rootURLSize = 0;
|
2022-10-28 20:18:56 +00:00
|
|
|
|
2022-10-26 08:13:32 +00:00
|
|
|
let statusEl = null;
|
2022-10-28 20:18:56 +00:00
|
|
|
let codeEditEl = null;
|
|
|
|
let codeViewEl = null;
|
|
|
|
let codeViewContentEl = null;
|
|
|
|
let viewModeSwitcherEl = null;
|
|
|
|
let languageSelectEl = null;
|
2022-11-10 18:05:08 +00:00
|
|
|
let titleInputEl = null;
|
2022-10-28 20:18:56 +00:00
|
|
|
|
|
|
|
let currentCode = "";
|
|
|
|
let dataOptions = new DataOptions();
|
|
|
|
let viewMode = ViewMode.VIEW;
|
|
|
|
|
2022-11-10 18:05:08 +00:00
|
|
|
function setTitle() {
|
|
|
|
if (dataOptions.title !== "") {
|
|
|
|
document.title = dataOptions.title + TITLE_POSTFIX;
|
|
|
|
} else if (currentCode !== "") {
|
2022-11-10 18:13:24 +00:00
|
|
|
document.title =
|
|
|
|
currentCode.replace("\n", "").substring(0, TITLE_CODE_LENGTH) +
|
|
|
|
TITLE_POSTFIX;
|
2022-11-10 18:05:08 +00:00
|
|
|
} else {
|
|
|
|
document.title = PROJECT_NAME;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-28 20:18:56 +00:00
|
|
|
function getLanguageClassName(language) {
|
|
|
|
if (language === PLAINTEXT) {
|
|
|
|
return "language-plain";
|
|
|
|
} else {
|
|
|
|
return `language-${language}`;
|
|
|
|
}
|
|
|
|
}
|
2022-10-25 22:10:05 +00:00
|
|
|
|
2022-10-26 08:16:32 +00:00
|
|
|
/**
|
2022-10-28 20:18:56 +00:00
|
|
|
* Render all the language choices in the dropdown.
|
2022-10-26 08:16:32 +00:00
|
|
|
*/
|
2022-10-28 20:18:56 +00:00
|
|
|
function renderLanguageOptions() {
|
2023-01-02 19:29:55 +00:00
|
|
|
const languages = [...LANGUAGE_NAMES];
|
|
|
|
languages.sort();
|
2023-01-02 19:34:01 +00:00
|
|
|
|
|
|
|
// Plain text must be first
|
|
|
|
languages.splice(languages.indexOf(PLAINTEXT), 1);
|
|
|
|
languages.unshift(PLAINTEXT);
|
|
|
|
|
2023-01-02 19:29:55 +00:00
|
|
|
for (const language of languages) {
|
2022-10-28 20:18:56 +00:00
|
|
|
const option = document.createElement("option");
|
|
|
|
option.value = language;
|
|
|
|
option.textContent = language;
|
|
|
|
languageSelectEl.appendChild(option);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use Prism to highlight the current language.
|
2022-11-10 18:13:24 +00:00
|
|
|
*
|
2022-10-28 20:18:56 +00:00
|
|
|
* The language definition will be imported if necessary.
|
|
|
|
*/
|
|
|
|
async function highlightLanguage() {
|
|
|
|
if (viewMode !== ViewMode.VIEW) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
codeViewEl.classList.add(getLanguageClassName(dataOptions.language));
|
|
|
|
|
|
|
|
if (dataOptions.language !== PLAINTEXT) {
|
2022-11-10 18:13:24 +00:00
|
|
|
const deps = getDependenciesForLanguage(
|
|
|
|
languageDependencies,
|
|
|
|
dataOptions.language
|
|
|
|
);
|
2022-10-28 20:18:56 +00:00
|
|
|
for (const dep of [...deps, dataOptions.language]) {
|
2023-01-03 16:38:31 +00:00
|
|
|
const importPath = PATCHED_LANGUAGES.has(dep)
|
|
|
|
? `./vendor/prism-patches/prism-${dep}.js`
|
|
|
|
: `./vendor/prism/components/prism-${dep}.js`;
|
|
|
|
|
|
|
|
await import(importPath);
|
2022-10-25 22:10:05 +00:00
|
|
|
}
|
2022-10-28 20:18:56 +00:00
|
|
|
|
|
|
|
if (viewMode === ViewMode.VIEW) {
|
|
|
|
Prism.highlightElement(codeViewContentEl);
|
2022-10-25 22:10:05 +00:00
|
|
|
}
|
2022-10-28 20:18:56 +00:00
|
|
|
} else {
|
|
|
|
Prism.highlightElement(codeViewContentEl);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render the currently active code on the page.
|
|
|
|
*/
|
|
|
|
function renderCode() {
|
|
|
|
if (viewMode === ViewMode.VIEW) {
|
|
|
|
codeViewContentEl.textContent = currentCode;
|
|
|
|
|
|
|
|
highlightLanguage();
|
|
|
|
} else {
|
|
|
|
codeEditEl.value = currentCode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render the current view, hiding the other.
|
|
|
|
*/
|
|
|
|
function renderView() {
|
|
|
|
if (viewMode === ViewMode.VIEW) {
|
|
|
|
codeViewEl.style.display = "block";
|
|
|
|
codeEditEl.style.display = "none";
|
|
|
|
viewModeSwitcherEl.textContent = "✍️";
|
|
|
|
} else {
|
|
|
|
codeEditEl.style.display = "block";
|
|
|
|
codeViewEl.style.display = "none";
|
|
|
|
viewModeSwitcherEl.textContent = "👁";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Switch the mode between editing and viewing.
|
|
|
|
*/
|
|
|
|
function switchMode() {
|
|
|
|
if (viewMode === ViewMode.VIEW) {
|
|
|
|
viewMode = ViewMode.EDIT;
|
|
|
|
} else {
|
|
|
|
viewMode = ViewMode.VIEW;
|
|
|
|
}
|
|
|
|
|
|
|
|
renderView();
|
|
|
|
renderCode();
|
2022-10-25 22:10:05 +00:00
|
|
|
}
|
|
|
|
|
2022-10-26 08:16:32 +00:00
|
|
|
/**
|
|
|
|
* Calculate the max length of the hash that we can use to avoid overlong URLs.
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
2022-10-25 22:10:05 +00:00
|
|
|
function maxHashLength() {
|
|
|
|
if (rootURLSize === 0) {
|
|
|
|
const rootURL = new URL(window.location.href);
|
|
|
|
rootURL.hash = "";
|
|
|
|
rootURLSize = rootURL.toString().length;
|
|
|
|
}
|
|
|
|
|
|
|
|
return MAX_URL - rootURLSize - 1;
|
|
|
|
}
|
|
|
|
|
2022-11-10 18:05:08 +00:00
|
|
|
/**
|
|
|
|
* Schedule compression to happen after a timeout, avoiding too many compressions.
|
|
|
|
*/
|
|
|
|
function scheduleCompress() {
|
|
|
|
if (compressTimeout) {
|
|
|
|
clearTimeout(compressTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
compressTimeout = setTimeout(async () => {
|
|
|
|
await syncCompress();
|
|
|
|
}, COMPRESS_WAIT);
|
|
|
|
}
|
|
|
|
|
2022-10-26 08:16:32 +00:00
|
|
|
/**
|
2022-10-28 20:18:56 +00:00
|
|
|
* Compress the current code in a synchronous way (all at once) and update the status.
|
2022-10-26 08:16:32 +00:00
|
|
|
*/
|
2022-10-28 20:18:56 +00:00
|
|
|
async function syncCompress() {
|
2022-11-10 18:05:08 +00:00
|
|
|
if (currentCode === "") {
|
|
|
|
history.replaceState(null, "", "#");
|
|
|
|
statusEl.textContent = "Waiting...";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-10-28 20:18:56 +00:00
|
|
|
const utf8 = ENCODER.encode(currentCode);
|
|
|
|
const dataWithHeader = dataOptions.serializeTo(utf8);
|
|
|
|
const compressed = compress(dataWithHeader, { quality: QUALITY });
|
2022-10-27 08:01:59 +00:00
|
|
|
const encoded = BASECODEC.encode(compressed);
|
2022-10-26 08:13:32 +00:00
|
|
|
|
2022-11-10 18:13:24 +00:00
|
|
|
statusEl.textContent = `Length: ${dataWithHeader.length} B -> ${
|
|
|
|
compressed.length
|
|
|
|
} B -> ${encoded.length}/${maxHashLength()} chars`;
|
2022-10-26 08:13:32 +00:00
|
|
|
|
|
|
|
if (encoded.length <= maxHashLength()) {
|
|
|
|
history.replaceState(null, "", `#${encoded}`);
|
|
|
|
} else {
|
|
|
|
statusEl.textContent += " (TOO BIG!)";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-26 08:16:32 +00:00
|
|
|
/**
|
2022-10-28 21:06:19 +00:00
|
|
|
* Decompress the hash data in a streaming way, with pauses between every chunk, to avoid
|
2022-10-26 08:16:32 +00:00
|
|
|
* the user being zip bombed with a short hash that generates gigabytes of output.
|
|
|
|
*/
|
2022-10-28 21:06:19 +00:00
|
|
|
async function streamDecompress() {
|
2022-10-28 21:16:13 +00:00
|
|
|
if (window.location.hash.length <= 1) {
|
|
|
|
currentCode = "";
|
|
|
|
renderCode();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
try {
|
|
|
|
const data = BASECODEC.decode(window.location.hash.substring(1));
|
2022-10-28 20:18:56 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
statusEl.textContent = "Initializing decompress...";
|
|
|
|
currentCode = "";
|
2022-10-26 08:13:32 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
let decompressedChunks = 0;
|
2022-10-26 08:13:32 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
const inputStream = new ReadableStream({
|
|
|
|
start(controller) {
|
|
|
|
controller.enqueue(data);
|
|
|
|
controller.close();
|
2022-11-10 18:13:24 +00:00
|
|
|
},
|
2022-10-28 21:06:19 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const decompressStream = new DecompressStream();
|
|
|
|
const decompressionRunner = new TransformStream({
|
2022-11-10 18:13:24 +00:00
|
|
|
start() {},
|
2022-10-28 21:06:19 +00:00
|
|
|
transform(chunk, controller) {
|
2022-11-10 18:13:24 +00:00
|
|
|
controller.enqueue(
|
|
|
|
decompressStream.decompress(chunk, DECOMPRESS_CHUNK_SIZE)
|
|
|
|
);
|
2022-10-28 21:06:19 +00:00
|
|
|
|
|
|
|
let slice = chunk;
|
|
|
|
|
2022-11-10 18:13:24 +00:00
|
|
|
while (
|
|
|
|
decompressStream.result() === BrotliStreamResult.NeedsMoreOutput
|
|
|
|
) {
|
2022-10-28 21:06:19 +00:00
|
|
|
slice = slice.slice(decompressStream.last_input_offset());
|
2022-11-10 18:13:24 +00:00
|
|
|
controller.enqueue(
|
|
|
|
decompressStream.decompress(slice, DECOMPRESS_CHUNK_SIZE)
|
|
|
|
);
|
2022-10-28 21:06:19 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
flush(controller) {
|
|
|
|
if (decompressStream.result() === BrotliStreamResult.NeedsMoreInput) {
|
2022-11-10 18:13:24 +00:00
|
|
|
controller.enqueue(
|
|
|
|
decompressStream.decompress("", DECOMPRESS_CHUNK_SIZE)
|
|
|
|
);
|
2022-10-28 21:06:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
controller.terminate();
|
2022-11-10 18:13:24 +00:00
|
|
|
},
|
2022-10-28 21:06:19 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const optionsPickerStream = new TransformStream({
|
|
|
|
firstChunk: false,
|
2022-11-10 18:13:24 +00:00
|
|
|
start() {},
|
2022-10-28 21:06:19 +00:00
|
|
|
transform(chunk, controller) {
|
|
|
|
if (!this.firstChunk) {
|
|
|
|
const rest = dataOptions.parseFrom(chunk);
|
|
|
|
|
|
|
|
languageSelectEl.value = dataOptions.language;
|
2022-11-10 18:05:08 +00:00
|
|
|
titleInputEl.value = dataOptions.title;
|
|
|
|
setTitle();
|
2022-10-28 21:06:19 +00:00
|
|
|
|
|
|
|
controller.enqueue(rest);
|
|
|
|
this.firstChunk = true;
|
|
|
|
} else {
|
|
|
|
controller.enqueue(chunk);
|
|
|
|
}
|
2022-11-10 18:13:24 +00:00
|
|
|
},
|
2022-10-28 21:06:19 +00:00
|
|
|
});
|
2022-10-26 08:13:32 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
const textDecoderStream = new TextDecoderStream();
|
|
|
|
const outputStream = new WritableStream({
|
|
|
|
write(chunk) {
|
|
|
|
currentCode += chunk;
|
|
|
|
++decompressedChunks;
|
2022-10-26 08:13:32 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
statusEl.textContent = `Decompressing: ${decompressedChunks} chunks...`;
|
2022-10-26 08:13:32 +00:00
|
|
|
|
2022-11-10 18:05:08 +00:00
|
|
|
setTitle();
|
2022-10-28 21:06:19 +00:00
|
|
|
renderCode();
|
2022-10-28 20:18:56 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
// Delay stream between every chunk to avoid zip bombing
|
2022-11-10 18:13:24 +00:00
|
|
|
return new Promise((resolve) =>
|
|
|
|
setTimeout(resolve, DECOMPRESS_CHUNK_TIMEOUT)
|
|
|
|
);
|
|
|
|
},
|
2022-10-28 21:06:19 +00:00
|
|
|
});
|
2022-10-26 08:13:32 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
await inputStream
|
|
|
|
.pipeThrough(decompressionRunner)
|
|
|
|
.pipeThrough(optionsPickerStream)
|
|
|
|
.pipeThrough(textDecoderStream)
|
|
|
|
.pipeTo(outputStream);
|
|
|
|
await syncCompress();
|
|
|
|
} catch (e) {
|
|
|
|
currentCode = `Unable to open the paste. Perhaps the URL is mistyped?\n\n${e}`;
|
|
|
|
console.error(e);
|
|
|
|
renderCode();
|
|
|
|
}
|
2022-10-26 08:13:32 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 20:18:56 +00:00
|
|
|
/**
|
|
|
|
* Callback for language being changed in language selector.
|
|
|
|
*/
|
|
|
|
async function languageSelected() {
|
|
|
|
codeViewEl.classList.remove(getLanguageClassName(dataOptions.language));
|
2022-11-10 18:13:24 +00:00
|
|
|
codeViewContentEl.classList.remove(
|
|
|
|
getLanguageClassName(dataOptions.language)
|
|
|
|
);
|
2022-10-28 20:18:56 +00:00
|
|
|
|
|
|
|
if (LANGUAGES.has(languageSelectEl.value)) {
|
|
|
|
dataOptions.language = languageSelectEl.value;
|
|
|
|
|
|
|
|
highlightLanguage();
|
|
|
|
} else {
|
|
|
|
dataOptions.language = PLAINTEXT;
|
2022-10-25 22:10:05 +00:00
|
|
|
|
2022-10-28 20:18:56 +00:00
|
|
|
highlightLanguage();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure language info is stored into URL when it's changed.
|
|
|
|
await syncCompress();
|
|
|
|
}
|
|
|
|
|
2022-11-10 18:05:08 +00:00
|
|
|
/**
|
|
|
|
* Callback for title being edited.
|
|
|
|
*/
|
|
|
|
async function titleEdited() {
|
|
|
|
dataOptions.title = titleInputEl.value;
|
|
|
|
|
|
|
|
setTitle();
|
|
|
|
scheduleCompress();
|
|
|
|
}
|
|
|
|
|
2022-10-28 20:18:56 +00:00
|
|
|
/**
|
|
|
|
* Callback for code being edited.
|
|
|
|
*/
|
|
|
|
function codeEdited() {
|
|
|
|
currentCode = codeEditEl.value;
|
|
|
|
|
2022-11-10 18:05:08 +00:00
|
|
|
setTitle();
|
|
|
|
scheduleCompress();
|
2022-10-28 20:18:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function init() {
|
|
|
|
codeEditEl = document.getElementById("code-edit");
|
|
|
|
codeViewEl = document.getElementById("code-view");
|
|
|
|
codeViewContentEl = document.getElementById("code-view-content");
|
2022-10-26 08:13:32 +00:00
|
|
|
statusEl = document.getElementById("length");
|
2022-10-28 20:18:56 +00:00
|
|
|
viewModeSwitcherEl = document.getElementById("view-mode-switcher");
|
|
|
|
languageSelectEl = document.getElementById("language-select");
|
2022-11-10 18:05:08 +00:00
|
|
|
titleInputEl = document.getElementById("title-input");
|
2022-10-25 22:10:05 +00:00
|
|
|
|
2022-10-28 20:48:59 +00:00
|
|
|
if (window.location.hash.length <= 1) {
|
|
|
|
viewMode = ViewMode.EDIT;
|
|
|
|
}
|
|
|
|
|
2022-10-28 20:18:56 +00:00
|
|
|
renderLanguageOptions();
|
|
|
|
renderView();
|
2022-10-25 22:10:05 +00:00
|
|
|
|
2022-11-10 18:13:24 +00:00
|
|
|
[languageDependencies] = await Promise.all([getDepsData(), brotliInit()]);
|
2022-10-28 20:18:56 +00:00
|
|
|
|
|
|
|
codeEditEl.addEventListener("input", codeEdited);
|
|
|
|
viewModeSwitcherEl.addEventListener("click", switchMode);
|
|
|
|
languageSelectEl.addEventListener("change", languageSelected);
|
2022-11-10 18:05:08 +00:00
|
|
|
titleInputEl.addEventListener("input", titleEdited);
|
2022-10-28 21:06:19 +00:00
|
|
|
window.addEventListener("hashchange", streamDecompress);
|
2022-10-25 22:10:05 +00:00
|
|
|
|
2022-10-28 21:06:19 +00:00
|
|
|
await streamDecompress();
|
2022-10-25 22:10:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await waitForLoad();
|
|
|
|
await init();
|