Implement line numbers, code highlighting, protocol header
This commit is contained in:
parent
bee38949e2
commit
149d1053d6
10 changed files with 740 additions and 50 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "vendor/prism"]
|
||||
path = vendor/prism
|
||||
url = https://github.com/PrismJS/prism.git
|
81
dataoptions.js
Normal file
81
dataoptions.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { PLAINTEXT, LANGUAGES } from "./languages.js";
|
||||
|
||||
/**
|
||||
* Data options are stored in a binary header before the payload.
|
||||
*
|
||||
* Header format:
|
||||
*
|
||||
* Byte 1
|
||||
* |xxx----y|
|
||||
* x = amount of extra bytes (other than this byte) in the header (uint)
|
||||
* 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.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* NOTE: If options are set to their default values, the header is minimised
|
||||
* to not include those bytes if possible.
|
||||
*/
|
||||
export class DataOptions {
|
||||
language = PLAINTEXT;
|
||||
|
||||
/**
|
||||
* Parse options from uncompressed bytes.
|
||||
* @param {Uint8Array} data
|
||||
* @returns {Uint8Array} The data without the header.
|
||||
*/
|
||||
parseFrom(data) {
|
||||
const byte1 = data[0];
|
||||
const totalBytes = (byte1 & 0b11100000) >>> 5;
|
||||
|
||||
if (totalBytes >= 1) {
|
||||
const languageIDLowByte = data[1];
|
||||
const languageIDHighBit = (byte1 & 0b00000001);
|
||||
const languageID = (languageIDHighBit << 8) | languageIDLowByte;
|
||||
|
||||
if (LANGUAGES.has(languageID)) {
|
||||
this.language = LANGUAGES.get(languageID);
|
||||
} else {
|
||||
this.language = PLAINTEXT;
|
||||
}
|
||||
} else {
|
||||
this.language = PLAINTEXT;
|
||||
}
|
||||
|
||||
return data.subarray(totalBytes + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize options to uncompressed bytes.
|
||||
* @param {Uint8Array} data
|
||||
* @returns {Uint8Array} Data with the options in a header.
|
||||
*/
|
||||
serializeTo(data) {
|
||||
let byte1LowBit = null;
|
||||
const extra_bytes = [];
|
||||
|
||||
if (this.language !== PLAINTEXT) {
|
||||
const languageID = LANGUAGES.get(this.language);
|
||||
const languageIDLowByte = languageID & 0b011111111;
|
||||
const languageIDHighBit = languageID & 0b100000000;
|
||||
byte1LowBit = languageIDHighBit >>> 8;
|
||||
|
||||
extra_bytes.unshift(languageIDLowByte);
|
||||
}
|
||||
|
||||
let byte1 = (extra_bytes.length & 0b00000111) << 5;
|
||||
if (byte1LowBit !== null) {
|
||||
byte1 |= byte1LowBit;
|
||||
}
|
||||
|
||||
const headerBytes = new Uint8Array([byte1, ...extra_bytes]);
|
||||
|
||||
const combined = new Uint8Array(1 + extra_bytes.length + data.length);
|
||||
combined.set(headerBytes, 0);
|
||||
combined.set(data, headerBytes.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
}
|
19
index.html
19
index.html
|
@ -8,21 +8,34 @@
|
|||
<title>Tahnaroskakori</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="./tahnaroskakori.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./vendor/prism/themes/prism-tomorrow.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./vendor/prism/plugins/line-numbers/prism-line-numbers.css" />
|
||||
<link rel="icon" type="image/x-icon"
|
||||
href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAABG1BcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAERERERERERERERERERERERAAAAAAAAAREREREREREREQAAEAAAAAEREREREREREREAAAAQAAAAERERERERERERAAEAAAAAAREREREREREREQAAABEREREREREREREREREAEAAAEBERERERERERERERAAAAAQABABEREREREREREAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="line-numbers">
|
||||
<main>
|
||||
<header>
|
||||
<h1>Tahnaroskakori</h1>
|
||||
<button id="view-mode-switcher" type="button">✍️</button>
|
||||
</header>
|
||||
|
||||
<textarea id="code"></textarea>
|
||||
<div id="settings-container">
|
||||
<select id="language-select"></select>
|
||||
</div>
|
||||
|
||||
<pre id="code-view"><code id="code-view-content">Loading...</code></pre>
|
||||
|
||||
<noscript>JavaScript is needed to use this website.</noscript>
|
||||
|
||||
<textarea id="code-edit"></textarea>
|
||||
|
||||
<footer>
|
||||
<div id="length">Waiting...</div>
|
||||
|
||||
<div id="info">
|
||||
v1.2.0 | © Nicd 2022 | <a href="https://gitlab.com/Nicd/tahnaroskakori" target="_blank">Source</a> | <a
|
||||
v2.0.0 | © Nicd 2022 | <a href="https://gitlab.com/Nicd/tahnaroskakori" target="_blank">Source</a> | <a
|
||||
href="./licenses.txt" target="_blank">Licenses</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
345
languages.js
Normal file
345
languages.js
Normal file
|
@ -0,0 +1,345 @@
|
|||
/**
|
||||
* Language names from Prism.js files
|
||||
*/
|
||||
export const LANGUAGE_NAMES = [
|
||||
"plaintext",
|
||||
"abap",
|
||||
"abnf",
|
||||
"actionscript",
|
||||
"ada",
|
||||
"agda",
|
||||
"al",
|
||||
"antlr4",
|
||||
"apacheconf",
|
||||
"apex",
|
||||
"apl",
|
||||
"applescript",
|
||||
"aql",
|
||||
"arduino",
|
||||
"arff",
|
||||
"armasm",
|
||||
"arturo",
|
||||
"asciidoc",
|
||||
"asm6502",
|
||||
"asmatmel",
|
||||
"aspnet",
|
||||
"autohotkey",
|
||||
"autoit",
|
||||
"avisynth",
|
||||
"avro-idl",
|
||||
"awk",
|
||||
"bash",
|
||||
"basic",
|
||||
"batch",
|
||||
"bbcode",
|
||||
"bbj",
|
||||
"bicep",
|
||||
"birb",
|
||||
"bison",
|
||||
"bnf",
|
||||
"bqn",
|
||||
"brainfuck",
|
||||
"brightscript",
|
||||
"bro",
|
||||
"bsl",
|
||||
"c",
|
||||
"cfscript",
|
||||
"chaiscript",
|
||||
"cil",
|
||||
"cilkc",
|
||||
"cilkcpp",
|
||||
"clike",
|
||||
"clojure",
|
||||
"cmake",
|
||||
"cobol",
|
||||
"coffeescript",
|
||||
"concurnas",
|
||||
"cooklang",
|
||||
"coq",
|
||||
"cpp",
|
||||
"crystal",
|
||||
"csharp",
|
||||
"cshtml",
|
||||
"csp",
|
||||
// "css-extras",
|
||||
"css",
|
||||
"csv",
|
||||
"cue",
|
||||
"cypher",
|
||||
"d",
|
||||
"dart",
|
||||
"dataweave",
|
||||
"dax",
|
||||
"dhall",
|
||||
"diff",
|
||||
"django",
|
||||
"dns-zone-file",
|
||||
"docker",
|
||||
"dot",
|
||||
"ebnf",
|
||||
"editorconfig",
|
||||
"eiffel",
|
||||
"ejs",
|
||||
"elixir",
|
||||
"elm",
|
||||
"erb",
|
||||
"erlang",
|
||||
"etlua",
|
||||
"excel-formula",
|
||||
"factor",
|
||||
"false",
|
||||
"firestore-security-rules",
|
||||
"flow",
|
||||
"fortran",
|
||||
"fsharp",
|
||||
"ftl",
|
||||
"gap",
|
||||
"gcode",
|
||||
"gdscript",
|
||||
"gedcom",
|
||||
"gettext",
|
||||
"gherkin",
|
||||
"git",
|
||||
"glsl",
|
||||
"gml",
|
||||
"gn",
|
||||
"go-module",
|
||||
"go",
|
||||
"gradle",
|
||||
"graphql",
|
||||
"groovy",
|
||||
"haml",
|
||||
"handlebars",
|
||||
"haskell",
|
||||
"haxe",
|
||||
"hcl",
|
||||
"hlsl",
|
||||
"hoon",
|
||||
"hpkp",
|
||||
"hsts",
|
||||
"http",
|
||||
"ichigojam",
|
||||
"icon",
|
||||
"icu-message-format",
|
||||
"idris",
|
||||
"iecst",
|
||||
"ignore",
|
||||
"inform7",
|
||||
"ini",
|
||||
"io",
|
||||
"j",
|
||||
"java",
|
||||
"javadoc",
|
||||
"javadoclike",
|
||||
"javascript",
|
||||
"javastacktrace",
|
||||
"jexl",
|
||||
"jolie",
|
||||
"jq",
|
||||
"js-extras",
|
||||
"js-templates",
|
||||
"jsdoc",
|
||||
"json",
|
||||
"json5",
|
||||
"jsonp",
|
||||
"jsstacktrace",
|
||||
"jsx",
|
||||
"julia",
|
||||
"keepalived",
|
||||
"keyman",
|
||||
"kotlin",
|
||||
"kumir",
|
||||
"kusto",
|
||||
"latex",
|
||||
"latte",
|
||||
"less",
|
||||
"lilypond",
|
||||
"linker-script",
|
||||
"liquid",
|
||||
"lisp",
|
||||
"livescript",
|
||||
"llvm",
|
||||
"log",
|
||||
"lolcode",
|
||||
"lua",
|
||||
"magma",
|
||||
"makefile",
|
||||
"markdown",
|
||||
"markup-templating",
|
||||
"markup",
|
||||
"mata",
|
||||
"matlab",
|
||||
"maxscript",
|
||||
"mel",
|
||||
"mermaid",
|
||||
"metafont",
|
||||
"mizar",
|
||||
"mongodb",
|
||||
"monkey",
|
||||
"moonscript",
|
||||
"n1ql",
|
||||
"n4js",
|
||||
"nand2tetris-hdl",
|
||||
"naniscript",
|
||||
"nasm",
|
||||
"neon",
|
||||
"nevod",
|
||||
"nginx",
|
||||
"nim",
|
||||
"nix",
|
||||
"nsis",
|
||||
"objectivec",
|
||||
"ocaml",
|
||||
"odin",
|
||||
"opencl",
|
||||
"openqasm",
|
||||
"oz",
|
||||
"parigp",
|
||||
"parser",
|
||||
"pascal",
|
||||
"pascaligo",
|
||||
"pcaxis",
|
||||
"peoplecode",
|
||||
"perl",
|
||||
// "php-extras",
|
||||
"php",
|
||||
"phpdoc",
|
||||
"plant-uml",
|
||||
"plsql",
|
||||
"powerquery",
|
||||
"powershell",
|
||||
"processing",
|
||||
"prolog",
|
||||
"promql",
|
||||
"properties",
|
||||
"protobuf",
|
||||
"psl",
|
||||
"pug",
|
||||
"puppet",
|
||||
"pure",
|
||||
"purebasic",
|
||||
"purescript",
|
||||
"python",
|
||||
"q",
|
||||
"qml",
|
||||
"qore",
|
||||
"qsharp",
|
||||
"r",
|
||||
"racket",
|
||||
"reason",
|
||||
"regex",
|
||||
"rego",
|
||||
"renpy",
|
||||
"rescript",
|
||||
"rest",
|
||||
"rip",
|
||||
"roboconf",
|
||||
"robotframework",
|
||||
"ruby",
|
||||
"rust",
|
||||
"sas",
|
||||
"sass",
|
||||
"scala",
|
||||
"scheme",
|
||||
"scss",
|
||||
"shell-session",
|
||||
"smali",
|
||||
"smalltalk",
|
||||
"smarty",
|
||||
"sml",
|
||||
"solidity",
|
||||
"solution-file",
|
||||
"soy",
|
||||
"sparql",
|
||||
"splunk-spl",
|
||||
"sqf",
|
||||
"sql",
|
||||
"squirrel",
|
||||
"stan",
|
||||
"stata",
|
||||
"stylus",
|
||||
"supercollider",
|
||||
"swift",
|
||||
"systemd",
|
||||
"t4-cs",
|
||||
"t4-templating",
|
||||
"t4-vb",
|
||||
"tap",
|
||||
"tcl",
|
||||
"textile",
|
||||
"toml",
|
||||
"tremor",
|
||||
"tsx",
|
||||
"tt2",
|
||||
"turtle",
|
||||
"twig",
|
||||
"typescript",
|
||||
"typoscript",
|
||||
"unrealscript",
|
||||
"uorazor",
|
||||
"uri",
|
||||
"v",
|
||||
"vala",
|
||||
"vbnet",
|
||||
"velocity",
|
||||
"verilog",
|
||||
"vhdl",
|
||||
"vim",
|
||||
"visual-basic",
|
||||
"warpscript",
|
||||
"wasm",
|
||||
"web-idl",
|
||||
"wgsl",
|
||||
"wiki",
|
||||
"wolfram",
|
||||
"wren",
|
||||
"xeora",
|
||||
"xml-doc",
|
||||
"xojo",
|
||||
"xquery",
|
||||
"yaml",
|
||||
"yang",
|
||||
"zig",
|
||||
];
|
||||
|
||||
export const LANGUAGES = new Map();
|
||||
|
||||
for (let i = 0; i < LANGUAGE_NAMES.length; ++i) {
|
||||
LANGUAGES.set(i, LANGUAGE_NAMES[i]);
|
||||
LANGUAGES.set(LANGUAGE_NAMES[i], i);
|
||||
}
|
||||
|
||||
export const PLAINTEXT = LANGUAGE_NAMES[0];
|
||||
|
||||
export async function getDepsData() {
|
||||
const resp = await fetch("./vendor/prism/components.json");
|
||||
const depsData = await resp.json();
|
||||
const ret = new Map();
|
||||
for (const [lang, data] of Object.entries(depsData.languages)) {
|
||||
if ("require" in data) {
|
||||
const req = Array.isArray(data.require) ? data.require : [data.require];
|
||||
ret.set(lang, req);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getDependenciesForLanguage(depsData, language) {
|
||||
const ret = [];
|
||||
const langsToProcess = [language];
|
||||
|
||||
while (langsToProcess.length > 0) {
|
||||
const currentLang = langsToProcess.shift();
|
||||
const deps = depsData.get(currentLang) ?? [];
|
||||
|
||||
for (const dep of deps) {
|
||||
langsToProcess.push(dep);
|
||||
|
||||
// Deps have to be loaded first
|
||||
ret.unshift(dep);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(new Set(ret));
|
||||
}
|
26
licenses.txt
26
licenses.txt
|
@ -215,3 +215,29 @@ brotli-wasm
|
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
---
|
||||
|
||||
Prism.js
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Copyright (c) 2012 Lea Verou
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
@charset "utf-8";
|
||||
|
||||
:root {
|
||||
--bg-color: #111;
|
||||
--bg-color-active: #333;
|
||||
--color: #4c4;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
|
@ -11,9 +17,9 @@ body {
|
|||
}
|
||||
|
||||
html,
|
||||
#code {
|
||||
background-color: #111;
|
||||
color: #4c4;
|
||||
#code-edit {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
main {
|
||||
|
@ -26,24 +32,67 @@ main {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
row-gap: 5px;
|
||||
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#code {
|
||||
#code-view,
|
||||
#code-edit {
|
||||
flex-grow: 1;
|
||||
|
||||
font-size: 1.2rem;
|
||||
font-family: monospace;
|
||||
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#code-view {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#code-view-content {
|
||||
/* Fix line number alignment bug: https://github.com/PrismJS/prism/issues/1132#issue-225969024 */
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#code-edit {
|
||||
display: none;
|
||||
|
||||
font-family: monospace;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--color);
|
||||
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1.5rem;
|
||||
|
||||
padding: 0.7rem;
|
||||
|
||||
border: 1px solid var(--color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: var(--bg-color-active);
|
||||
}
|
||||
|
||||
#length {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import brotliInit, { compress, decompress, DecompressStream, BrotliStreamResult } from "./vendor/brotli_wasm.js";
|
||||
import brotliInit, { compress, DecompressStream, BrotliStreamResult } from "./vendor/brotli_wasm.js";
|
||||
import base from "./vendor/base-x.js";
|
||||
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";
|
||||
import { getDependenciesForLanguage, getDepsData, LANGUAGES, LANGUAGE_NAMES, PLAINTEXT } from "./languages.js";
|
||||
|
||||
const COMPRESS_WAIT = 500;
|
||||
const ENCODER = new TextEncoder();
|
||||
|
@ -10,25 +18,107 @@ const MAX_URL = 2048;
|
|||
const DECOMPRESS_CHUNK_SIZE = 1000;
|
||||
const DECOMPRESS_CHUNK_TIMEOUT = 100;
|
||||
|
||||
let languageDependencies = null;
|
||||
let compressTimeout = null;
|
||||
let rootURLSize = 0;
|
||||
|
||||
let statusEl = null;
|
||||
let codeEl = null;
|
||||
let codeEditEl = null;
|
||||
let codeViewEl = null;
|
||||
let codeViewContentEl = null;
|
||||
let viewModeSwitcherEl = null;
|
||||
let languageSelectEl = null;
|
||||
|
||||
let currentCode = "";
|
||||
let dataOptions = new DataOptions();
|
||||
let viewMode = ViewMode.VIEW;
|
||||
|
||||
function getLanguageClassName(language) {
|
||||
if (language === PLAINTEXT) {
|
||||
return "language-plain";
|
||||
} else {
|
||||
return `language-${language}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the page DOM has been loaded.
|
||||
* @returns {Promise<void>}
|
||||
* Render all the language choices in the dropdown.
|
||||
*/
|
||||
function waitForLoad() {
|
||||
return new Promise(resolve => {
|
||||
// If already loaded, fire immediately
|
||||
if (/complete|interactive|loaded/.test(document.readyState)) {
|
||||
resolve();
|
||||
function renderLanguageOptions() {
|
||||
for (const language of LANGUAGE_NAMES) {
|
||||
const option = document.createElement("option");
|
||||
option.value = language;
|
||||
option.textContent = language;
|
||||
languageSelectEl.appendChild(option);
|
||||
}
|
||||
else {
|
||||
document.addEventListener('DOMContentLoaded', resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Prism to highlight the current language.
|
||||
*
|
||||
* 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) {
|
||||
const deps = getDependenciesForLanguage(languageDependencies, dataOptions.language);
|
||||
for (const dep of [...deps, dataOptions.language]) {
|
||||
await import(`./vendor/prism/components/prism-${dep}.js`)
|
||||
}
|
||||
|
||||
if (viewMode === ViewMode.VIEW) {
|
||||
Prism.highlightElement(codeViewContentEl);
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,12 +136,12 @@ function maxHashLength() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Compress the given data in a synchronous way (all at once) and update the status.
|
||||
* @param {string} data
|
||||
* Compress the current code in a synchronous way (all at once) and update the status.
|
||||
*/
|
||||
async function syncCompress(data) {
|
||||
const utf8 = ENCODER.encode(data);
|
||||
const compressed = compress(utf8, { quality: QUALITY });
|
||||
async function syncCompress() {
|
||||
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`;
|
||||
|
@ -70,7 +160,9 @@ async function syncCompress(data) {
|
|||
*/
|
||||
async function streamDecompress(data) {
|
||||
statusEl.textContent = "Initializing decompress...";
|
||||
codeEl.value = "";
|
||||
currentCode = "";
|
||||
|
||||
let decompressedChunks = 0;
|
||||
|
||||
const inputStream = new ReadableStream({
|
||||
start(controller) {
|
||||
|
@ -101,16 +193,33 @@ async function streamDecompress(data) {
|
|||
}
|
||||
});
|
||||
|
||||
let decompressedChunks = 0;
|
||||
const optionsPickerStream = new TransformStream({
|
||||
firstChunk: false,
|
||||
start() { },
|
||||
transform(chunk, controller) {
|
||||
if (!this.firstChunk) {
|
||||
const rest = dataOptions.parseFrom(chunk);
|
||||
|
||||
languageSelectEl.value = dataOptions.language;
|
||||
|
||||
controller.enqueue(rest);
|
||||
this.firstChunk = true;
|
||||
} else {
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const textDecoderStream = new TextDecoderStream();
|
||||
const outputStream = new WritableStream({
|
||||
write(chunk) {
|
||||
codeEl.value += chunk;
|
||||
currentCode += chunk;
|
||||
++decompressedChunks;
|
||||
|
||||
statusEl.textContent = `Decompressing: ${decompressedChunks} chunks...`;
|
||||
|
||||
renderCode();
|
||||
|
||||
// Delay stream between every chunk to avoid zip bombing
|
||||
return new Promise(resolve => setTimeout(resolve, DECOMPRESS_CHUNK_TIMEOUT));
|
||||
}
|
||||
|
@ -118,40 +227,82 @@ async function streamDecompress(data) {
|
|||
|
||||
await inputStream
|
||||
.pipeThrough(decompressionRunner)
|
||||
.pipeThrough(optionsPickerStream)
|
||||
.pipeThrough(textDecoderStream)
|
||||
.pipeTo(outputStream);
|
||||
await syncCompress(codeEl.value);
|
||||
await syncCompress();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await brotliInit();
|
||||
/**
|
||||
* Callback for language being changed in language selector.
|
||||
*/
|
||||
async function languageSelected() {
|
||||
codeViewEl.classList.remove(getLanguageClassName(dataOptions.language));
|
||||
codeViewContentEl.classList.remove(getLanguageClassName(dataOptions.language));
|
||||
|
||||
codeEl = document.getElementById("code");
|
||||
statusEl = document.getElementById("length");
|
||||
if (LANGUAGES.has(languageSelectEl.value)) {
|
||||
dataOptions.language = languageSelectEl.value;
|
||||
|
||||
codeEl.addEventListener("input", () => {
|
||||
highlightLanguage();
|
||||
} else {
|
||||
dataOptions.language = PLAINTEXT;
|
||||
|
||||
highlightLanguage();
|
||||
}
|
||||
|
||||
// Ensure language info is stored into URL when it's changed.
|
||||
await syncCompress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for code being edited.
|
||||
*/
|
||||
function codeEdited() {
|
||||
if (compressTimeout) {
|
||||
clearTimeout(compressTimeout);
|
||||
}
|
||||
|
||||
if (codeEl.value === "") {
|
||||
currentCode = codeEditEl.value;
|
||||
|
||||
if (currentCode === "") {
|
||||
history.replaceState(null, "", "#");
|
||||
statusEl.textContent = "Waiting...";
|
||||
return;
|
||||
}
|
||||
|
||||
compressTimeout = setTimeout(async () => {
|
||||
const content = codeEl.value;
|
||||
await syncCompress(content);
|
||||
await syncCompress();
|
||||
}, COMPRESS_WAIT);
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
codeEditEl = document.getElementById("code-edit");
|
||||
codeViewEl = document.getElementById("code-view");
|
||||
codeViewContentEl = document.getElementById("code-view-content");
|
||||
statusEl = document.getElementById("length");
|
||||
viewModeSwitcherEl = document.getElementById("view-mode-switcher");
|
||||
languageSelectEl = document.getElementById("language-select");
|
||||
|
||||
renderLanguageOptions();
|
||||
renderView();
|
||||
|
||||
[languageDependencies,] = await Promise.all([
|
||||
getDepsData(),
|
||||
brotliInit(),
|
||||
]);
|
||||
|
||||
|
||||
codeEditEl.addEventListener("input", codeEdited);
|
||||
viewModeSwitcherEl.addEventListener("click", switchMode);
|
||||
languageSelectEl.addEventListener("change", languageSelected);
|
||||
|
||||
if (window.location.hash.length > 1) {
|
||||
try {
|
||||
const bytes = BASECODEC.decode(window.location.hash.substring(1));
|
||||
await streamDecompress(bytes);
|
||||
} catch (e) {
|
||||
codeEl.textContent = e.stack;
|
||||
currentCode = `Unable to open the paste. Perhaps the URL is mistyped?\n\n${e.stack}`;
|
||||
renderCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
utils.js
Normal file
15
utils.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Returns a promise that resolves when the page DOM has been loaded.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function waitForLoad() {
|
||||
return new Promise(resolve => {
|
||||
// If already loaded, fire immediately
|
||||
if (/complete|interactive|loaded/.test(document.readyState)) {
|
||||
resolve();
|
||||
}
|
||||
else {
|
||||
document.addEventListener('DOMContentLoaded', resolve);
|
||||
}
|
||||
});
|
||||
}
|
1
vendor/prism
vendored
Submodule
1
vendor/prism
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 59e5a3471377057de1f401ba38337aca27b80e03
|
6
viewmode.js
Normal file
6
viewmode.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export class ViewMode {
|
||||
static VIEW = Symbol("view");
|
||||
static EDIT = Symbol("edit");
|
||||
}
|
||||
|
||||
Object.freeze(ViewMode);
|
Loading…
Reference in a new issue