1
0
mirror of https://github.com/alecthomas/chroma.git synced 2025-03-17 20:58:08 +02:00

feat: support sharing in playground

This commit is contained in:
Alec Thomas 2023-01-11 14:43:47 -05:00 committed by Alec Thomas
parent 0e2db44744
commit d330b760dc
13 changed files with 11139 additions and 114 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@
_models/
_examples/
*.min.*

View File

@ -1,6 +1,8 @@
.PHONY: chromad upload all
VERSION ?= $(shell git describe --tags --dirty --always)
export GOOS ?= linux
export GOARCH ?= amd64
all: README.md tokentype_string.go
@ -12,7 +14,9 @@ tokentype_string.go: types.go
chromad:
rm -f chromad
(export CGOENABLED=0 GOOS=linux GOARCH=amd64; cd ./cmd/chromad && go build -ldflags="-X 'main.version=$(VERSION)'" -o ../../chromad .)
esbuild --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
(export CGOENABLED=0 ; cd ./cmd/chromad && go build -ldflags="-X 'main.version=$(VERSION)'" -o ../../chromad .)
upload: chromad
scp chromad root@swapoff.org: && \

1
bin/.esbuild-0.16.16.pkg Symbolic link
View File

@ -0,0 +1 @@
hermit

1
bin/.reflex-0.3.1.pkg Symbolic link
View File

@ -0,0 +1 @@
hermit

1
bin/esbuild Symbolic link
View File

@ -0,0 +1 @@
.esbuild-0.16.16.pkg

1
bin/reflex Symbolic link
View File

@ -0,0 +1 @@
.reflex-0.3.1.pkg

View File

@ -29,7 +29,29 @@ var (
//go:embed static
staticFiles embed.FS
htmlTemplate = template.Must(template.New("html").Parse(indexTemplate))
htmlTemplate = template.Must(template.New("html").
Funcs(template.FuncMap{
"JS": func(filename string) template.JS {
if version == "devel" {
return template.JS(`import "./static/` + filename + "\";\n")
}
content, err := staticFiles.ReadFile("static/" + strings.TrimSuffix(filename, ".js") + ".min.js")
if err != nil {
panic(err)
}
return template.JS(content)
},
"CSS": func(filename string) template.CSS {
if version == "devel" {
return template.CSS(`@import url("./static/` + filename + "\");")
}
content, err := staticFiles.ReadFile("static/" + strings.TrimSuffix(filename, ".css") + ".min.css")
if err != nil {
panic(err)
}
return template.CSS(content)
},
}).Parse(indexTemplate))
)
type context struct {

2
cmd/chromad/reflex.conf Normal file
View File

@ -0,0 +1,2 @@
-sr '.*' -- \
go run .

View File

@ -0,0 +1,296 @@
/**
* base64.ts
*
* Licensed under the BSD 3-Clause License.
* http://opensource.org/licenses/BSD-3-Clause
*
* References:
* http://en.wikipedia.org/wiki/Base64
*
* @author Dan Kogai (https://github.com/dankogai)
*/
const version = '3.7.4';
/**
* @deprecated use lowercase `version`.
*/
const VERSION = version;
const _hasatob = typeof atob === 'function';
const _hasbtoa = typeof btoa === 'function';
const _hasBuffer = typeof Buffer === 'function';
const _TD = typeof TextDecoder === 'function' ? new TextDecoder() : undefined;
const _TE = typeof TextEncoder === 'function' ? new TextEncoder() : undefined;
const b64ch = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const b64chs = Array.prototype.slice.call(b64ch);
const b64tab = ((a) => {
let tab = {};
a.forEach((c, i) => tab[c] = i);
return tab;
})(b64chs);
const b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
const _fromCC = String.fromCharCode.bind(String);
const _U8Afrom = typeof Uint8Array.from === 'function'
? Uint8Array.from.bind(Uint8Array)
: (it, fn = (x) => x) => new Uint8Array(Array.prototype.slice.call(it, 0).map(fn));
const _mkUriSafe = (src) => src
.replace(/=/g, '').replace(/[+\/]/g, (m0) => m0 == '+' ? '-' : '_');
const _tidyB64 = (s) => s.replace(/[^A-Za-z0-9\+\/]/g, '');
/**
* polyfill version of `btoa`
*/
const btoaPolyfill = (bin) => {
// console.log('polyfilled');
let u32, c0, c1, c2, asc = '';
const pad = bin.length % 3;
for (let i = 0; i < bin.length;) {
if ((c0 = bin.charCodeAt(i++)) > 255 ||
(c1 = bin.charCodeAt(i++)) > 255 ||
(c2 = bin.charCodeAt(i++)) > 255)
throw new TypeError('invalid character found');
u32 = (c0 << 16) | (c1 << 8) | c2;
asc += b64chs[u32 >> 18 & 63]
+ b64chs[u32 >> 12 & 63]
+ b64chs[u32 >> 6 & 63]
+ b64chs[u32 & 63];
}
return pad ? asc.slice(0, pad - 3) + "===".substring(pad) : asc;
};
/**
* does what `window.btoa` of web browsers do.
* @param {String} bin binary string
* @returns {string} Base64-encoded string
*/
const _btoa = _hasbtoa ? (bin) => btoa(bin)
: _hasBuffer ? (bin) => Buffer.from(bin, 'binary').toString('base64')
: btoaPolyfill;
const _fromUint8Array = _hasBuffer
? (u8a) => Buffer.from(u8a).toString('base64')
: (u8a) => {
// cf. https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string/12713326#12713326
const maxargs = 0x1000;
let strs = [];
for (let i = 0, l = u8a.length; i < l; i += maxargs) {
strs.push(_fromCC.apply(null, u8a.subarray(i, i + maxargs)));
}
return _btoa(strs.join(''));
};
/**
* converts a Uint8Array to a Base64 string.
* @param {boolean} [urlsafe] URL-and-filename-safe a la RFC4648 §5
* @returns {string} Base64 string
*/
const fromUint8Array = (u8a, urlsafe = false) => urlsafe ? _mkUriSafe(_fromUint8Array(u8a)) : _fromUint8Array(u8a);
// This trick is found broken https://github.com/dankogai/js-base64/issues/130
// const utob = (src: string) => unescape(encodeURIComponent(src));
// reverting good old fationed regexp
const cb_utob = (c) => {
if (c.length < 2) {
var cc = c.charCodeAt(0);
return cc < 0x80 ? c
: cc < 0x800 ? (_fromCC(0xc0 | (cc >>> 6))
+ _fromCC(0x80 | (cc & 0x3f)))
: (_fromCC(0xe0 | ((cc >>> 12) & 0x0f))
+ _fromCC(0x80 | ((cc >>> 6) & 0x3f))
+ _fromCC(0x80 | (cc & 0x3f)));
}
else {
var cc = 0x10000
+ (c.charCodeAt(0) - 0xD800) * 0x400
+ (c.charCodeAt(1) - 0xDC00);
return (_fromCC(0xf0 | ((cc >>> 18) & 0x07))
+ _fromCC(0x80 | ((cc >>> 12) & 0x3f))
+ _fromCC(0x80 | ((cc >>> 6) & 0x3f))
+ _fromCC(0x80 | (cc & 0x3f)));
}
};
const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
/**
* @deprecated should have been internal use only.
* @param {string} src UTF-8 string
* @returns {string} UTF-16 string
*/
const utob = (u) => u.replace(re_utob, cb_utob);
//
const _encode = _hasBuffer
? (s) => Buffer.from(s, 'utf8').toString('base64')
: _TE
? (s) => _fromUint8Array(_TE.encode(s))
: (s) => _btoa(utob(s));
/**
* converts a UTF-8-encoded string to a Base64 string.
* @param {boolean} [urlsafe] if `true` make the result URL-safe
* @returns {string} Base64 string
*/
const encode = (src, urlsafe = false) => urlsafe
? _mkUriSafe(_encode(src))
: _encode(src);
/**
* converts a UTF-8-encoded string to URL-safe Base64 RFC4648 §5.
* @returns {string} Base64 string
*/
const encodeURI = (src) => encode(src, true);
// This trick is found broken https://github.com/dankogai/js-base64/issues/130
// const btou = (src: string) => decodeURIComponent(escape(src));
// reverting good old fationed regexp
const re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g;
const cb_btou = (cccc) => {
switch (cccc.length) {
case 4:
var cp = ((0x07 & cccc.charCodeAt(0)) << 18)
| ((0x3f & cccc.charCodeAt(1)) << 12)
| ((0x3f & cccc.charCodeAt(2)) << 6)
| (0x3f & cccc.charCodeAt(3)), offset = cp - 0x10000;
return (_fromCC((offset >>> 10) + 0xD800)
+ _fromCC((offset & 0x3FF) + 0xDC00));
case 3:
return _fromCC(((0x0f & cccc.charCodeAt(0)) << 12)
| ((0x3f & cccc.charCodeAt(1)) << 6)
| (0x3f & cccc.charCodeAt(2)));
default:
return _fromCC(((0x1f & cccc.charCodeAt(0)) << 6)
| (0x3f & cccc.charCodeAt(1)));
}
};
/**
* @deprecated should have been internal use only.
* @param {string} src UTF-16 string
* @returns {string} UTF-8 string
*/
const btou = (b) => b.replace(re_btou, cb_btou);
/**
* polyfill version of `atob`
*/
const atobPolyfill = (asc) => {
// console.log('polyfilled');
asc = asc.replace(/\s+/g, '');
if (!b64re.test(asc))
throw new TypeError('malformed base64.');
asc += '=='.slice(2 - (asc.length & 3));
let u24, bin = '', r1, r2;
for (let i = 0; i < asc.length;) {
u24 = b64tab[asc.charAt(i++)] << 18
| b64tab[asc.charAt(i++)] << 12
| (r1 = b64tab[asc.charAt(i++)]) << 6
| (r2 = b64tab[asc.charAt(i++)]);
bin += r1 === 64 ? _fromCC(u24 >> 16 & 255)
: r2 === 64 ? _fromCC(u24 >> 16 & 255, u24 >> 8 & 255)
: _fromCC(u24 >> 16 & 255, u24 >> 8 & 255, u24 & 255);
}
return bin;
};
/**
* does what `window.atob` of web browsers do.
* @param {String} asc Base64-encoded string
* @returns {string} binary string
*/
const _atob = _hasatob ? (asc) => atob(_tidyB64(asc))
: _hasBuffer ? (asc) => Buffer.from(asc, 'base64').toString('binary')
: atobPolyfill;
//
const _toUint8Array = _hasBuffer
? (a) => _U8Afrom(Buffer.from(a, 'base64'))
: (a) => _U8Afrom(_atob(a), c => c.charCodeAt(0));
/**
* converts a Base64 string to a Uint8Array.
*/
const toUint8Array = (a) => _toUint8Array(_unURI(a));
//
const _decode = _hasBuffer
? (a) => Buffer.from(a, 'base64').toString('utf8')
: _TD
? (a) => _TD.decode(_toUint8Array(a))
: (a) => btou(_atob(a));
const _unURI = (a) => _tidyB64(a.replace(/[-_]/g, (m0) => m0 == '-' ? '+' : '/'));
/**
* converts a Base64 string to a UTF-8 string.
* @param {String} src Base64 string. Both normal and URL-safe are supported
* @returns {string} UTF-8 string
*/
const decode = (src) => _decode(_unURI(src));
/**
* check if a value is a valid Base64 string
* @param {String} src a value to check
*/
const isValid = (src) => {
if (typeof src !== 'string')
return false;
const s = src.replace(/\s+/g, '').replace(/={0,2}$/, '');
return !/[^\s0-9a-zA-Z\+/]/.test(s) || !/[^\s0-9a-zA-Z\-_]/.test(s);
};
//
const _noEnum = (v) => {
return {
value: v, enumerable: false, writable: true, configurable: true
};
};
/**
* extend String.prototype with relevant methods
*/
const extendString = function () {
const _add = (name, body) => Object.defineProperty(String.prototype, name, _noEnum(body));
_add('fromBase64', function () { return decode(this); });
_add('toBase64', function (urlsafe) { return encode(this, urlsafe); });
_add('toBase64URI', function () { return encode(this, true); });
_add('toBase64URL', function () { return encode(this, true); });
_add('toUint8Array', function () { return toUint8Array(this); });
};
/**
* extend Uint8Array.prototype with relevant methods
*/
const extendUint8Array = function () {
const _add = (name, body) => Object.defineProperty(Uint8Array.prototype, name, _noEnum(body));
_add('toBase64', function (urlsafe) { return fromUint8Array(this, urlsafe); });
_add('toBase64URI', function () { return fromUint8Array(this, true); });
_add('toBase64URL', function () { return fromUint8Array(this, true); });
};
/**
* extend Builtin prototypes with relevant methods
*/
const extendBuiltins = () => {
extendString();
extendUint8Array();
};
const gBase64 = {
version: version,
VERSION: VERSION,
atob: _atob,
atobPolyfill: atobPolyfill,
btoa: _btoa,
btoaPolyfill: btoaPolyfill,
fromBase64: decode,
toBase64: encode,
encode: encode,
encodeURI: encodeURI,
encodeURL: encodeURI,
utob: utob,
btou: btou,
decode: decode,
isValid: isValid,
fromUint8Array: fromUint8Array,
toUint8Array: toUint8Array,
extendString: extendString,
extendUint8Array: extendUint8Array,
extendBuiltins: extendBuiltins,
};
// makecjs:CUT //
export { version };
export { VERSION };
export { _atob as atob };
export { atobPolyfill };
export { _btoa as btoa };
export { btoaPolyfill };
export { decode as fromBase64 };
export { encode as toBase64 };
export { utob };
export { encode };
export { encodeURI };
export { encodeURI as encodeURL };
export { btou };
export { decode };
export { isValid };
export { fromUint8Array };
export { toUint8Array };
export { extendString };
export { extendUint8Array };
export { extendBuiltins };
// and finally,
export { gBase64 as Base64 };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
@import url("./bulma-0.7.5.css");
textarea {
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
}
#output pre {
padding: 0;
}

View File

@ -1,88 +1,165 @@
import * as Base64 from "./base64.js";
document.addEventListener("DOMContentLoaded", function () {
var style = document.createElement('style');
var ref = document.querySelector('script');
ref.parentNode.insertBefore(style, ref);
var style = document.createElement('style');
var ref = document.querySelector('script');
ref.parentNode.insertBefore(style, ref);
var form = document.getElementById('chroma');
var textArea = form.elements["text"];
var styleSelect = form.elements["style"];
var languageSelect = form.elements["language"];
var csrfToken = form.elements["gorilla.csrf.Token"].value;
var output = document.getElementById("output");
var htmlCheckbox = document.getElementById("html");
var form = document.getElementById('chroma');
var textArea = form.elements["text"];
var styleSelect = form.elements["style"];
var languageSelect = form.elements["language"];
var copyButton = form.elements["copy"];
var csrfToken = form.elements["gorilla.csrf.Token"].value;
var output = document.getElementById("output");
var htmlCheckbox = document.getElementById("html");
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
$notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
(document.querySelectorAll('.notification .delete') || []).forEach((el) => {
const notification = el.parentNode;
el.addEventListener('click', () => {
notification.parentNode.removeChild(notification);
});
});
// https://stackoverflow.com/a/37697925/7980
function handleTab(e) {
var after, before, end, lastNewLine, changeLength, re, replace, selection, start, val;
if ((e.charCode === 9 || e.keyCode === 9) && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
start = this.selectionStart;
end = this.selectionEnd;
val = this.value;
before = val.substring(0, start);
after = val.substring(end);
replace = true;
if (start !== end) {
selection = val.substring(start, end);
if (~selection.indexOf('\n')) {
replace = false;
changeLength = 0;
lastNewLine = before.lastIndexOf('\n');
if (!~lastNewLine) {
selection = before + selection;
changeLength = before.length;
before = '';
} else {
selection = before.substring(lastNewLine) + selection;
changeLength = before.length - lastNewLine;
before = before.substring(0, lastNewLine);
}
if (e.shiftKey) {
re = /(\n|^)(\t|[ ]{1,8})/g;
if (selection.match(re)) {
start--;
changeLength--;
}
selection = selection.replace(re, '$1');
} else {
selection = selection.replace(/(\n|^)/g, '$1\t');
start++;
changeLength++;
}
this.value = before + selection + after;
this.selectionStart = start;
this.selectionEnd = start + selection.length - changeLength;
}
}
if (replace && !e.shiftKey) {
this.value = before + '\t' + after;
this.selectionStart = this.selectionEnd = start + 1;
}
}
debouncedEventHandler(e);
}
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this, args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
function getFormJSON() {
return {
"language": languageSelect.value,
"style": styleSelect.value,
"text": textArea.value,
"classes": htmlCheckbox.checked,
}
}
function update(event) {
fetch("api/render", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
redirect: 'follow',
referrer: 'no-referrer',
body: JSON.stringify(getFormJSON()),
}).then(data => {
data.json().then(
value => {
if (value.language) {
languageSelect.value = value.language;
}
style.innerHTML = "#output { " + value.background + "}";
if (htmlCheckbox.checked) {
output.innerText = value.html;
} else {
output.innerHTML = value.html;
}
}
);
}).catch(reason => {
console.log(reason);
});
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this, args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
event.preventDefault();
}
function share(event) {
let data = JSON.stringify(getFormJSON())
data = Base64.encodeURI(data);
location.hash = "#" + data;
try {
navigator.clipboard.writeText(location.href);
} catch (e) {
console.log(e);
}
event.preventDefault();
}
function getFormJSON() {
return {
"language": languageSelect.value,
"style": styleSelect.value,
"text": textArea.value,
"classes": htmlCheckbox.checked,
}
}
if (location.hash) {
let json = Base64.decode(location.hash.substring(1))
json = JSON.parse(json);
textArea.value = json.text;
languageSelect.value = json.language;
styleSelect.value = json.style;
htmlCheckbox.checked = json.classes;
update(new Event('change'));
}
function update(event) {
fetch("api/render", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
redirect: 'follow',
referrer: 'no-referrer',
body: JSON.stringify(getFormJSON()),
}).then(data => {
data.json().then(
value => {
if (value.language) {
languageSelect.value = value.language;
}
style.innerHTML = "#output { " + value.background + "}";
if (htmlCheckbox.checked) {
output.innerText = value.html;
} else {
output.innerHTML = value.html;
}
}
);
}).catch(reason => {
console.log(reason);
});
var eventHandler = (event) => update(event);
var debouncedEventHandler = debounce(eventHandler, 250);
event.preventDefault();
}
languageSelect.addEventListener('change', eventHandler);
styleSelect.addEventListener('change', eventHandler);
htmlCheckbox.addEventListener('change', eventHandler);
copyButton.addEventListener('click', share);
var eventHandler = (event) => update(event);
var debouncedEventHandler = debounce(eventHandler, 250);
languageSelect.addEventListener('change', eventHandler);
styleSelect.addEventListener('change', eventHandler);
htmlCheckbox.addEventListener('change', eventHandler);
textArea.addEventListener('input', debouncedEventHandler);
textArea.addEventListener('change', debouncedEventHandler);
textArea.addEventListener('keydown', handleTab);
textArea.addEventListener('change', debouncedEventHandler);
});

View File

@ -2,21 +2,12 @@
<html>
<head>
<title>Chroma Playground ({{.Version}})</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css"/>
<style>
textarea {
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
}
{{CSS "index.css"}}
#output {
{{.Background}};
}
#output pre {
padding: 0;
}
</style>
<script src="static/index.js?{{.Version}}"></script>
</head>
<body>
<div class="container">
@ -24,6 +15,7 @@
<h1 class="title">Chroma Playground ({{.Version}})</h1>
<div class="notification">
<button class="delete"></button>
<a href="https://github.com/alecthomas/chroma">Chroma</a> is a general purpose syntax highlighter in pure Go.
It takes source code and other structured text and converts it into syntax highlighted HTML, ANSI-coloured text,
etc. Chroma is based heavily on Pygments, and includes translators for Pygments lexers and styles.
@ -31,40 +23,54 @@
<form id="chroma" method="post">
{{ .CSRFField }}
<div class="columns">
<div class="column field">
<label class="label">Language</label>
<div class="control">
<div class="select">
<select name="language" id="language">
<option value="" disabled{{if eq "" $.SelectedLanguage}} selected{{end}}>Language</option>
{{- range .Languages}}
<option value="{{.}}"{{if eq . $.SelectedLanguage}} selected{{end}}>{{.}}</option>
{{- end}}
</select>
<nav class="level">
<div class="level-left">
<div class="level-item">
<div class="label">Code</div>
</div>
<div class="level-item">
<div class="control">
<div class="select">
<select name="language" id="language">
<option value="" disabled{{if eq "" $.SelectedLanguage}} selected{{end}}>Language</option>
{{- range .Languages}}
<option value="{{.}}"{{if eq . $.SelectedLanguage}} selected{{end}}>{{.}}</option>
{{- end}}
</select>
</div>
</div>
</div>
<div class="level-item">
<div class="control">
<div class="select">
<select name="style" id="style">
<option value="" disabled{{if eq "" $.SelectedStyle}} selected{{end}}>Style</option>
{{- range .Styles}}
<option value="{{.}}"{{if eq . $.SelectedStyle}} selected{{end}}>{{.}}</option>
{{- end}}
</select>
</div>
</div>
</div>
</div>
<div class="column field">
<label class="label">Style</label>
<div class="control">
<div class="select">
<select name="style" id="style">
<option value="" disabled{{if eq "" $.SelectedStyle}} selected{{end}}>Style</option>
{{- range .Styles}}
<option value="{{.}}"{{if eq . $.SelectedStyle}} selected{{end}}>{{.}}</option>
{{- end}}
</select>
</div>
<div class="level-right">
<div class="level-item">
<button name="copy" id="copy" class="button">
<span class="icon is-small">
<ion-icon name="copy-outline"></ion-icon>
</span>
</button>
</div>
</div>
</div>
</nav>
<div class="field">
<label class="label">Code</label>
<div class="control">
<textarea autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="textarea" id="text" name="text" rows="25" cols="80"></textarea>
<textarea autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="textarea" id="text" name="text" rows="15" cols="80"></textarea>
</div>
</div>
@ -82,5 +88,10 @@
<div class="field box" id="output"></div>
</form>
</div>
<script type="module">
{{JS "index.js"}}
</script>
<script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>
</body>
</html>