From 21a26daab0f829c4bb426e45a6d3ab3ceecbeebe Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Thu, 10 Nov 2022 20:05:08 +0200 Subject: [PATCH] Implement title for pastes --- dataoptions.js | 69 ++++++++++++++++++++++++++++++++++++++-------- index.html | 3 +- tahnaroskakori.css | 3 +- tahnaroskakori.js | 65 +++++++++++++++++++++++++++++++++---------- 4 files changed, 113 insertions(+), 27 deletions(-) diff --git a/dataoptions.js b/dataoptions.js index aa067a6..9e9cf07 100644 --- a/dataoptions.js +++ b/dataoptions.js @@ -1,18 +1,36 @@ import { PLAINTEXT, LANGUAGES } from "./languages.js"; +const UTF8_ENCODER = new TextEncoder(); +const UTF8_DECODER = new TextDecoder(); + +class DataOptionsError extends Error { } +class DecodeError extends DataOptionsError { } + /** * Data options are stored in a binary header before the payload. * - * Header format: + * The header consists of a fixed section, followed by a dynamic section. + * The fixed section consists of bytes with a fixed meaning and positioning. + * The dynamic section is based on the contents of the fixed section and + * contains data related to the fixed section. + * + * Fixed section: * * Byte 1 * |xxx----y| - * x = amount of extra bytes (other than this byte) in the header (uint) + * x = amount of extra bytes (other than this byte) in the header's fixed + * section (uid) * y = highest bit of language ID (the rest in byte 2) * * Byte 2 (+ lowest bit of byte 1) is language ID (uint), i.e. index of the * language in the LANGUAGE_NAMES list. * + * Byte 3 is number of bytes reserved for the page title. + * + * Dynamic section parts: + * + * 1. Bytes of page title (UTF-8). + * * --- * * NOTE: If options are set to their default values, the header is minimised @@ -20,6 +38,7 @@ import { PLAINTEXT, LANGUAGES } from "./languages.js"; */ export class DataOptions { language = PLAINTEXT; + title = ""; /** * Parse options from uncompressed bytes. @@ -28,9 +47,9 @@ export class DataOptions { */ parseFrom(data) { const byte1 = data[0]; - const totalBytes = (byte1 & 0b11100000) >>> 5; + const fixedHeaderBytes = ((byte1 & 0b11100000) >>> 5) + 1; - if (totalBytes >= 1) { + if (fixedHeaderBytes >= 2) { const languageIDLowByte = data[1]; const languageIDHighBit = (byte1 & 0b00000001); const languageID = (languageIDHighBit << 8) | languageIDLowByte; @@ -44,7 +63,25 @@ export class DataOptions { this.language = PLAINTEXT; } - return data.subarray(totalBytes + 1); + let titleLen = 0; + + if (fixedHeaderBytes >= 3) { + titleLen = data[2]; + + if (titleLen > data) { + throw new DecodeError( + `Title length ${titleLen} was bigger than chunk size ${data.length}.` + ); + } + + this.title = UTF8_DECODER.decode( + data.subarray(fixedHeaderBytes, fixedHeaderBytes + titleLen) + ); + } else { + this.title = ""; + } + + return data.subarray(fixedHeaderBytes + titleLen); } /** @@ -54,25 +91,35 @@ export class DataOptions { */ serializeTo(data) { let byte1LowBit = null; - const extra_bytes = []; + const fixedBytes = []; + const dynamicBytes = []; - if (this.language !== PLAINTEXT) { + const hasTitleBytes = this.title !== ""; + const hasLanguageBytes = this.language !== PLAINTEXT || hasTitleBytes; + + if (hasLanguageBytes) { const languageID = LANGUAGES.get(this.language); const languageIDLowByte = languageID & 0b011111111; const languageIDHighBit = languageID & 0b100000000; byte1LowBit = languageIDHighBit >>> 8; - extra_bytes.unshift(languageIDLowByte); + fixedBytes.push(languageIDLowByte); } - let byte1 = (extra_bytes.length & 0b00000111) << 5; + if (hasTitleBytes) { + const languageBytes = UTF8_ENCODER.encode(this.title); + fixedBytes.push(languageBytes.length); + dynamicBytes.push(...languageBytes); + } + + let byte1 = (fixedBytes.length & 0b00000111) << 5; if (byte1LowBit !== null) { byte1 |= byte1LowBit; } - const headerBytes = new Uint8Array([byte1, ...extra_bytes]); + const headerBytes = new Uint8Array([byte1, ...fixedBytes, ...dynamicBytes]); - const combined = new Uint8Array(1 + extra_bytes.length + data.length); + const combined = new Uint8Array(headerBytes.length + data.length); combined.set(headerBytes, 0); combined.set(data, headerBytes.length); diff --git a/index.html b/index.html index 1a6e155..e7081c5 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,7 @@
+
@@ -35,7 +36,7 @@
Waiting...
- v2.0.4 | © Nicd 2022 | Source | Source | Licenses
diff --git a/tahnaroskakori.css b/tahnaroskakori.css index 589736a..10153ef 100644 --- a/tahnaroskakori.css +++ b/tahnaroskakori.css @@ -61,7 +61,8 @@ main { } button, -select { +select, +input { background-color: var(--bg-color); color: var(--color); diff --git a/tahnaroskakori.js b/tahnaroskakori.js index abac08f..cec823c 100644 --- a/tahnaroskakori.js +++ b/tahnaroskakori.js @@ -9,6 +9,7 @@ import { DataOptions } from "./dataoptions.js"; import { ViewMode } from "./viewmode.js"; import { getDependenciesForLanguage, getDepsData, LANGUAGES, LANGUAGE_NAMES, PLAINTEXT } from "./languages.js"; +const PROJECT_NAME = "Tahnaroskakori"; const COMPRESS_WAIT = 500; const ENCODER = new TextEncoder(); const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?/-._~$&+="; @@ -17,6 +18,8 @@ const QUALITY = 11; const MAX_URL = 2048; const DECOMPRESS_CHUNK_SIZE = 1000; const DECOMPRESS_CHUNK_TIMEOUT = 100; +const TITLE_POSTFIX = ` · ${PROJECT_NAME}`; +const TITLE_CODE_LENGTH = 40; let languageDependencies = null; let compressTimeout = null; @@ -28,11 +31,22 @@ let codeViewEl = null; let codeViewContentEl = null; let viewModeSwitcherEl = null; let languageSelectEl = null; +let titleInputEl = null; let currentCode = ""; let dataOptions = new DataOptions(); let viewMode = ViewMode.VIEW; +function setTitle() { + if (dataOptions.title !== "") { + document.title = dataOptions.title + TITLE_POSTFIX; + } else if (currentCode !== "") { + document.title = currentCode.replace("\n", "").substring(0, TITLE_CODE_LENGTH) + TITLE_POSTFIX; + } else { + document.title = PROJECT_NAME; + } +} + function getLanguageClassName(language) { if (language === PLAINTEXT) { return "language-plain"; @@ -135,16 +149,35 @@ function maxHashLength() { return MAX_URL - rootURLSize - 1; } +/** + * Schedule compression to happen after a timeout, avoiding too many compressions. + */ +function scheduleCompress() { + if (compressTimeout) { + clearTimeout(compressTimeout); + } + + compressTimeout = setTimeout(async () => { + await syncCompress(); + }, COMPRESS_WAIT); +} + /** * Compress the current code in a synchronous way (all at once) and update the status. */ async function syncCompress() { + if (currentCode === "") { + history.replaceState(null, "", "#"); + statusEl.textContent = "Waiting..."; + return; + } + const utf8 = ENCODER.encode(currentCode); const dataWithHeader = dataOptions.serializeTo(utf8); const compressed = compress(dataWithHeader, { quality: QUALITY }); const encoded = BASECODEC.encode(compressed); - statusEl.textContent = `Length: ${utf8.length} B -> ${compressed.length} B -> ${encoded.length}/${maxHashLength()} chars`; + statusEl.textContent = `Length: ${dataWithHeader.length} B -> ${compressed.length} B -> ${encoded.length}/${maxHashLength()} chars`; if (encoded.length <= maxHashLength()) { history.replaceState(null, "", `#${encoded}`); @@ -209,6 +242,8 @@ async function streamDecompress() { const rest = dataOptions.parseFrom(chunk); languageSelectEl.value = dataOptions.language; + titleInputEl.value = dataOptions.title; + setTitle(); controller.enqueue(rest); this.firstChunk = true; @@ -226,6 +261,7 @@ async function streamDecompress() { statusEl.textContent = `Decompressing: ${decompressedChunks} chunks...`; + setTitle(); renderCode(); // Delay stream between every chunk to avoid zip bombing @@ -267,25 +303,24 @@ async function languageSelected() { await syncCompress(); } +/** + * Callback for title being edited. + */ +async function titleEdited() { + dataOptions.title = titleInputEl.value; + + setTitle(); + scheduleCompress(); +} + /** * Callback for code being edited. */ function codeEdited() { - if (compressTimeout) { - clearTimeout(compressTimeout); - } - currentCode = codeEditEl.value; - if (currentCode === "") { - history.replaceState(null, "", "#"); - statusEl.textContent = "Waiting..."; - return; - } - - compressTimeout = setTimeout(async () => { - await syncCompress(); - }, COMPRESS_WAIT); + setTitle(); + scheduleCompress(); } async function init() { @@ -295,6 +330,7 @@ async function init() { statusEl = document.getElementById("length"); viewModeSwitcherEl = document.getElementById("view-mode-switcher"); languageSelectEl = document.getElementById("language-select"); + titleInputEl = document.getElementById("title-input"); if (window.location.hash.length <= 1) { viewMode = ViewMode.EDIT; @@ -312,6 +348,7 @@ async function init() { codeEditEl.addEventListener("input", codeEdited); viewModeSwitcherEl.addEventListener("click", switchMode); languageSelectEl.addEventListener("change", languageSelected); + titleInputEl.addEventListener("input", titleEdited); window.addEventListener("hashchange", streamDecompress); await streamDecompress();