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";
|
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.
|
* 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
|
* Byte 1
|
||||||
* |xxx----y|
|
* |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)
|
* 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
|
* Byte 2 (+ lowest bit of byte 1) is language ID (uint), i.e. index of the
|
||||||
* language in the LANGUAGE_NAMES list.
|
* 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
|
* 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 {
|
export class DataOptions {
|
||||||
language = PLAINTEXT;
|
language = PLAINTEXT;
|
||||||
|
title = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse options from uncompressed bytes.
|
* Parse options from uncompressed bytes.
|
||||||
|
@ -28,9 +47,9 @@ export class DataOptions {
|
||||||
*/
|
*/
|
||||||
parseFrom(data) {
|
parseFrom(data) {
|
||||||
const byte1 = data[0];
|
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 languageIDLowByte = data[1];
|
||||||
const languageIDHighBit = (byte1 & 0b00000001);
|
const languageIDHighBit = (byte1 & 0b00000001);
|
||||||
const languageID = (languageIDHighBit << 8) | languageIDLowByte;
|
const languageID = (languageIDHighBit << 8) | languageIDLowByte;
|
||||||
|
@ -44,7 +63,25 @@ export class DataOptions {
|
||||||
this.language = PLAINTEXT;
|
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) {
|
serializeTo(data) {
|
||||||
let byte1LowBit = null;
|
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 languageID = LANGUAGES.get(this.language);
|
||||||
const languageIDLowByte = languageID & 0b011111111;
|
const languageIDLowByte = languageID & 0b011111111;
|
||||||
const languageIDHighBit = languageID & 0b100000000;
|
const languageIDHighBit = languageID & 0b100000000;
|
||||||
byte1LowBit = languageIDHighBit >>> 8;
|
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) {
|
if (byte1LowBit !== null) {
|
||||||
byte1 |= byte1LowBit;
|
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(headerBytes, 0);
|
||||||
combined.set(data, headerBytes.length);
|
combined.set(data, headerBytes.length);
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="settings-container">
|
<div id="settings-container">
|
||||||
|
<input id="title-input" type="text" placeholder="Title (optional)" />
|
||||||
<select id="language-select"></select>
|
<select id="language-select"></select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@
|
||||||
<div id="length">Waiting...</div>
|
<div id="length">Waiting...</div>
|
||||||
|
|
||||||
<div id="info">
|
<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>
|
target="_blank">Licenses</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -61,7 +61,8 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
select {
|
select,
|
||||||
|
input {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { DataOptions } from "./dataoptions.js";
|
||||||
import { ViewMode } from "./viewmode.js";
|
import { ViewMode } from "./viewmode.js";
|
||||||
import { getDependenciesForLanguage, getDepsData, LANGUAGES, LANGUAGE_NAMES, PLAINTEXT } from "./languages.js";
|
import { getDependenciesForLanguage, getDepsData, LANGUAGES, LANGUAGE_NAMES, PLAINTEXT } from "./languages.js";
|
||||||
|
|
||||||
|
const PROJECT_NAME = "Tahnaroskakori";
|
||||||
const COMPRESS_WAIT = 500;
|
const COMPRESS_WAIT = 500;
|
||||||
const ENCODER = new TextEncoder();
|
const ENCODER = new TextEncoder();
|
||||||
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?/-._~$&+=";
|
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?/-._~$&+=";
|
||||||
|
@ -17,6 +18,8 @@ const QUALITY = 11;
|
||||||
const MAX_URL = 2048;
|
const MAX_URL = 2048;
|
||||||
const DECOMPRESS_CHUNK_SIZE = 1000;
|
const DECOMPRESS_CHUNK_SIZE = 1000;
|
||||||
const DECOMPRESS_CHUNK_TIMEOUT = 100;
|
const DECOMPRESS_CHUNK_TIMEOUT = 100;
|
||||||
|
const TITLE_POSTFIX = ` · ${PROJECT_NAME}`;
|
||||||
|
const TITLE_CODE_LENGTH = 40;
|
||||||
|
|
||||||
let languageDependencies = null;
|
let languageDependencies = null;
|
||||||
let compressTimeout = null;
|
let compressTimeout = null;
|
||||||
|
@ -28,11 +31,22 @@ let codeViewEl = null;
|
||||||
let codeViewContentEl = null;
|
let codeViewContentEl = null;
|
||||||
let viewModeSwitcherEl = null;
|
let viewModeSwitcherEl = null;
|
||||||
let languageSelectEl = null;
|
let languageSelectEl = null;
|
||||||
|
let titleInputEl = null;
|
||||||
|
|
||||||
let currentCode = "";
|
let currentCode = "";
|
||||||
let dataOptions = new DataOptions();
|
let dataOptions = new DataOptions();
|
||||||
let viewMode = ViewMode.VIEW;
|
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) {
|
function getLanguageClassName(language) {
|
||||||
if (language === PLAINTEXT) {
|
if (language === PLAINTEXT) {
|
||||||
return "language-plain";
|
return "language-plain";
|
||||||
|
@ -135,16 +149,35 @@ function maxHashLength() {
|
||||||
return MAX_URL - rootURLSize - 1;
|
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.
|
* Compress the current code in a synchronous way (all at once) and update the status.
|
||||||
*/
|
*/
|
||||||
async function syncCompress() {
|
async function syncCompress() {
|
||||||
|
if (currentCode === "") {
|
||||||
|
history.replaceState(null, "", "#");
|
||||||
|
statusEl.textContent = "Waiting...";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const utf8 = ENCODER.encode(currentCode);
|
const utf8 = ENCODER.encode(currentCode);
|
||||||
const dataWithHeader = dataOptions.serializeTo(utf8);
|
const dataWithHeader = dataOptions.serializeTo(utf8);
|
||||||
const compressed = compress(dataWithHeader, { quality: QUALITY });
|
const compressed = compress(dataWithHeader, { quality: QUALITY });
|
||||||
const encoded = BASECODEC.encode(compressed);
|
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()) {
|
if (encoded.length <= maxHashLength()) {
|
||||||
history.replaceState(null, "", `#${encoded}`);
|
history.replaceState(null, "", `#${encoded}`);
|
||||||
|
@ -209,6 +242,8 @@ async function streamDecompress() {
|
||||||
const rest = dataOptions.parseFrom(chunk);
|
const rest = dataOptions.parseFrom(chunk);
|
||||||
|
|
||||||
languageSelectEl.value = dataOptions.language;
|
languageSelectEl.value = dataOptions.language;
|
||||||
|
titleInputEl.value = dataOptions.title;
|
||||||
|
setTitle();
|
||||||
|
|
||||||
controller.enqueue(rest);
|
controller.enqueue(rest);
|
||||||
this.firstChunk = true;
|
this.firstChunk = true;
|
||||||
|
@ -226,6 +261,7 @@ async function streamDecompress() {
|
||||||
|
|
||||||
statusEl.textContent = `Decompressing: ${decompressedChunks} chunks...`;
|
statusEl.textContent = `Decompressing: ${decompressedChunks} chunks...`;
|
||||||
|
|
||||||
|
setTitle();
|
||||||
renderCode();
|
renderCode();
|
||||||
|
|
||||||
// Delay stream between every chunk to avoid zip bombing
|
// Delay stream between every chunk to avoid zip bombing
|
||||||
|
@ -267,25 +303,24 @@ async function languageSelected() {
|
||||||
await syncCompress();
|
await syncCompress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for title being edited.
|
||||||
|
*/
|
||||||
|
async function titleEdited() {
|
||||||
|
dataOptions.title = titleInputEl.value;
|
||||||
|
|
||||||
|
setTitle();
|
||||||
|
scheduleCompress();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for code being edited.
|
* Callback for code being edited.
|
||||||
*/
|
*/
|
||||||
function codeEdited() {
|
function codeEdited() {
|
||||||
if (compressTimeout) {
|
|
||||||
clearTimeout(compressTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCode = codeEditEl.value;
|
currentCode = codeEditEl.value;
|
||||||
|
|
||||||
if (currentCode === "") {
|
setTitle();
|
||||||
history.replaceState(null, "", "#");
|
scheduleCompress();
|
||||||
statusEl.textContent = "Waiting...";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
compressTimeout = setTimeout(async () => {
|
|
||||||
await syncCompress();
|
|
||||||
}, COMPRESS_WAIT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
@ -295,6 +330,7 @@ async function init() {
|
||||||
statusEl = document.getElementById("length");
|
statusEl = document.getElementById("length");
|
||||||
viewModeSwitcherEl = document.getElementById("view-mode-switcher");
|
viewModeSwitcherEl = document.getElementById("view-mode-switcher");
|
||||||
languageSelectEl = document.getElementById("language-select");
|
languageSelectEl = document.getElementById("language-select");
|
||||||
|
titleInputEl = document.getElementById("title-input");
|
||||||
|
|
||||||
if (window.location.hash.length <= 1) {
|
if (window.location.hash.length <= 1) {
|
||||||
viewMode = ViewMode.EDIT;
|
viewMode = ViewMode.EDIT;
|
||||||
|
@ -312,6 +348,7 @@ async function init() {
|
||||||
codeEditEl.addEventListener("input", codeEdited);
|
codeEditEl.addEventListener("input", codeEdited);
|
||||||
viewModeSwitcherEl.addEventListener("click", switchMode);
|
viewModeSwitcherEl.addEventListener("click", switchMode);
|
||||||
languageSelectEl.addEventListener("change", languageSelected);
|
languageSelectEl.addEventListener("change", languageSelected);
|
||||||
|
titleInputEl.addEventListener("input", titleEdited);
|
||||||
window.addEventListener("hashchange", streamDecompress);
|
window.addEventListener("hashchange", streamDecompress);
|
||||||
|
|
||||||
await streamDecompress();
|
await streamDecompress();
|
||||||
|
|
Loading…
Reference in a new issue