Implement title for pastes
This commit is contained in:
parent
4ae951b21c
commit
21a26daab0
4 changed files with 113 additions and 27 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
</header>
|
||||
|
||||
<div id="settings-container">
|
||||
<input id="title-input" type="text" placeholder="Title (optional)" />
|
||||
<select id="language-select"></select>
|
||||
</div>
|
||||
|
||||
|
@ -35,7 +36,7 @@
|
|||
<div id="length">Waiting...</div>
|
||||
|
||||
<div id="info">
|
||||
v2.0.4 | © Nicd 2022 | <a href="https://gitlab.com/Nicd/t" target="_blank">Source</a> | <a href="./licenses.txt"
|
||||
v2.1.0 | © Nicd 2022 | <a href="https://gitlab.com/Nicd/t" target="_blank">Source</a> | <a href="./licenses.txt"
|
||||
target="_blank">Licenses</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -61,7 +61,8 @@ main {
|
|||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
select,
|
||||
input {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--color);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue