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;
|
||||||
|
}
|
||||||
|
}
|
21
index.html
21
index.html
|
@ -8,21 +8,34 @@
|
||||||
<title>Tahnaroskakori</title>
|
<title>Tahnaroskakori</title>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="./tahnaroskakori.css" />
|
<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"
|
<link rel="icon" type="image/x-icon"
|
||||||
href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAABG1BcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAERERERERERERERERERERERAAAAAAAAAREREREREREREQAAEAAAAAEREREREREREREAAAAQAAAAERERERERERERAAEAAAAAAREREREREREREQAAABEREREREREREREREREAEAAAEBERERERERERERERAAAAAQABABEREREREREREAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" />
|
href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAABG1BcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAERERERERERERERERERERERAAAAAAAAAREREREREREREQAAEAAAAAEREREREREREREAAAAQAAAAERERERERERERAAEAAAAAAREREREREREREQAAABEREREREREREREREREAEAAAEBERERERERERERERAAAAAQABABEREREREREREAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="line-numbers">
|
||||||
<main>
|
<main>
|
||||||
<h1>Tahnaroskakori</h1>
|
<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>
|
<footer>
|
||||||
<div id="length">Waiting...</div>
|
<div id="length">Waiting...</div>
|
||||||
|
|
||||||
<div id="info">
|
<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>
|
href="./licenses.txt" target="_blank">Licenses</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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";
|
@charset "utf-8";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-color: #111;
|
||||||
|
--bg-color-active: #333;
|
||||||
|
--color: #4c4;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -11,9 +17,9 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
#code {
|
#code-edit {
|
||||||
background-color: #111;
|
background-color: var(--bg-color);
|
||||||
color: #4c4;
|
color: var(--color);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
@ -26,24 +32,67 @@ main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
row-gap: 5px;
|
||||||
|
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code {
|
#code-view,
|
||||||
|
#code-edit {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-family: monospace;
|
|
||||||
|
|
||||||
overflow-y: auto;
|
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;
|
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 {
|
#length {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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 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 COMPRESS_WAIT = 500;
|
||||||
const ENCODER = new TextEncoder();
|
const ENCODER = new TextEncoder();
|
||||||
|
@ -10,25 +18,107 @@ const MAX_URL = 2048;
|
||||||
const DECOMPRESS_CHUNK_SIZE = 1000;
|
const DECOMPRESS_CHUNK_SIZE = 1000;
|
||||||
const DECOMPRESS_CHUNK_TIMEOUT = 100;
|
const DECOMPRESS_CHUNK_TIMEOUT = 100;
|
||||||
|
|
||||||
|
let languageDependencies = null;
|
||||||
let compressTimeout = null;
|
let compressTimeout = null;
|
||||||
let rootURLSize = 0;
|
let rootURLSize = 0;
|
||||||
|
|
||||||
let statusEl = null;
|
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.
|
* Render all the language choices in the dropdown.
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
function waitForLoad() {
|
function renderLanguageOptions() {
|
||||||
return new Promise(resolve => {
|
for (const language of LANGUAGE_NAMES) {
|
||||||
// If already loaded, fire immediately
|
const option = document.createElement("option");
|
||||||
if (/complete|interactive|loaded/.test(document.readyState)) {
|
option.value = language;
|
||||||
resolve();
|
option.textContent = language;
|
||||||
|
languageSelectEl.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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`)
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
document.addEventListener('DOMContentLoaded', resolve);
|
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.
|
* Compress the current code in a synchronous way (all at once) and update the status.
|
||||||
* @param {string} data
|
|
||||||
*/
|
*/
|
||||||
async function syncCompress(data) {
|
async function syncCompress() {
|
||||||
const utf8 = ENCODER.encode(data);
|
const utf8 = ENCODER.encode(currentCode);
|
||||||
const compressed = compress(utf8, { quality: QUALITY });
|
const dataWithHeader = dataOptions.serializeTo(utf8);
|
||||||
|
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: ${utf8.length} B -> ${compressed.length} B -> ${encoded.length}/${maxHashLength()} chars`;
|
||||||
|
@ -70,7 +160,9 @@ async function syncCompress(data) {
|
||||||
*/
|
*/
|
||||||
async function streamDecompress(data) {
|
async function streamDecompress(data) {
|
||||||
statusEl.textContent = "Initializing decompress...";
|
statusEl.textContent = "Initializing decompress...";
|
||||||
codeEl.value = "";
|
currentCode = "";
|
||||||
|
|
||||||
|
let decompressedChunks = 0;
|
||||||
|
|
||||||
const inputStream = new ReadableStream({
|
const inputStream = new ReadableStream({
|
||||||
start(controller) {
|
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 textDecoderStream = new TextDecoderStream();
|
||||||
const outputStream = new WritableStream({
|
const outputStream = new WritableStream({
|
||||||
write(chunk) {
|
write(chunk) {
|
||||||
codeEl.value += chunk;
|
currentCode += chunk;
|
||||||
++decompressedChunks;
|
++decompressedChunks;
|
||||||
|
|
||||||
statusEl.textContent = `Decompressing: ${decompressedChunks} chunks...`;
|
statusEl.textContent = `Decompressing: ${decompressedChunks} chunks...`;
|
||||||
|
|
||||||
|
renderCode();
|
||||||
|
|
||||||
// Delay stream between every chunk to avoid zip bombing
|
// Delay stream between every chunk to avoid zip bombing
|
||||||
return new Promise(resolve => setTimeout(resolve, DECOMPRESS_CHUNK_TIMEOUT));
|
return new Promise(resolve => setTimeout(resolve, DECOMPRESS_CHUNK_TIMEOUT));
|
||||||
}
|
}
|
||||||
|
@ -118,40 +227,82 @@ async function streamDecompress(data) {
|
||||||
|
|
||||||
await inputStream
|
await inputStream
|
||||||
.pipeThrough(decompressionRunner)
|
.pipeThrough(decompressionRunner)
|
||||||
|
.pipeThrough(optionsPickerStream)
|
||||||
.pipeThrough(textDecoderStream)
|
.pipeThrough(textDecoderStream)
|
||||||
.pipeTo(outputStream);
|
.pipeTo(outputStream);
|
||||||
await syncCompress(codeEl.value);
|
await syncCompress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for language being changed in language selector.
|
||||||
|
*/
|
||||||
|
async function languageSelected() {
|
||||||
|
codeViewEl.classList.remove(getLanguageClassName(dataOptions.language));
|
||||||
|
codeViewContentEl.classList.remove(getLanguageClassName(dataOptions.language));
|
||||||
|
|
||||||
|
if (LANGUAGES.has(languageSelectEl.value)) {
|
||||||
|
dataOptions.language = languageSelectEl.value;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCode = codeEditEl.value;
|
||||||
|
|
||||||
|
if (currentCode === "") {
|
||||||
|
history.replaceState(null, "", "#");
|
||||||
|
statusEl.textContent = "Waiting...";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
compressTimeout = setTimeout(async () => {
|
||||||
|
await syncCompress();
|
||||||
|
}, COMPRESS_WAIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await brotliInit();
|
codeEditEl = document.getElementById("code-edit");
|
||||||
|
codeViewEl = document.getElementById("code-view");
|
||||||
codeEl = document.getElementById("code");
|
codeViewContentEl = document.getElementById("code-view-content");
|
||||||
statusEl = document.getElementById("length");
|
statusEl = document.getElementById("length");
|
||||||
|
viewModeSwitcherEl = document.getElementById("view-mode-switcher");
|
||||||
|
languageSelectEl = document.getElementById("language-select");
|
||||||
|
|
||||||
codeEl.addEventListener("input", () => {
|
renderLanguageOptions();
|
||||||
if (compressTimeout) {
|
renderView();
|
||||||
clearTimeout(compressTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codeEl.value === "") {
|
[languageDependencies,] = await Promise.all([
|
||||||
history.replaceState(null, "", "#");
|
getDepsData(),
|
||||||
statusEl.textContent = "Waiting...";
|
brotliInit(),
|
||||||
return;
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
compressTimeout = setTimeout(async () => {
|
|
||||||
const content = codeEl.value;
|
codeEditEl.addEventListener("input", codeEdited);
|
||||||
await syncCompress(content);
|
viewModeSwitcherEl.addEventListener("click", switchMode);
|
||||||
}, COMPRESS_WAIT);
|
languageSelectEl.addEventListener("change", languageSelected);
|
||||||
});
|
|
||||||
|
|
||||||
if (window.location.hash.length > 1) {
|
if (window.location.hash.length > 1) {
|
||||||
try {
|
try {
|
||||||
const bytes = BASECODEC.decode(window.location.hash.substring(1));
|
const bytes = BASECODEC.decode(window.location.hash.substring(1));
|
||||||
await streamDecompress(bytes);
|
await streamDecompress(bytes);
|
||||||
} catch (e) {
|
} 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