index.js (view raw)
1const blob = new Blob(['importScripts("https://cdn.jsdelivr.net/npm/lzma@2.3.2/src/lzma_worker.min.js");']);
2const lzma = new LZMA(window.URL.createObjectURL(blob));
3
4let editor = null;
5let select = null;
6let clipboard = null;
7let statsEl = null;
8
9const init = () => {
10 initCodeEditor();
11 initLangSelector();
12 initCode();
13 initClipboard();
14};
15
16const initCodeEditor = () => {
17 CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@5.52.0/mode/%N/%N.js';
18 editor = new CodeMirror(byId('editor'), {
19 lineNumbers: true,
20 theme: 'dracula',
21 readOnly: readOnly,
22 lineWrapping: false,
23 scrollbarStyle: 'simple',
24 });
25 if (readOnly) {
26 document.body.classList.add('readonly');
27 }
28
29 statsEl = byId('stats');
30 editor.on('change', () => {
31 statsEl.innerHTML = `Length: ${editor.getValue().length} | Lines: ${editor['doc'].size}`;
32 hideCopyBar();
33 });
34};
35
36const initLangSelector = () => {
37 select = new SlimSelect({
38 select: '#language',
39 data: CodeMirror.modeInfo.map((e) => ({
40 text: e.name,
41 value: hash(e.name),
42 data: { mime: e.mime, mode: e.mode },
43 })),
44 showContent: 'down',
45 onChange: (e) => {
46 const language = e.data || { mime: null, mode: null };
47 editor.setOption('mode', language.mime);
48 CodeMirror.autoLoadMode(editor, language.mode);
49 },
50 });
51
52 // Retro-compatibility
53 const legacyLang = new URLSearchParams(window.location.search).get('lang');
54 if (legacyLang) {
55 const langObj = CodeMirror.modeInfo.find((e) => slugify(e.name) === legacyLang);
56 if (langObj) {
57 select.set(hash(langObj.name));
58 return;
59 }
60 }
61 // Set lang selector
62 select.set(window.location.hash.charAt(5) === '-' ? window.location.hash.substr(1, 4) : hash('Plain Text'));
63};
64
65const initCode = () => {
66 let base64 = location.pathname.substr(1) || location.hash.substr(1);
67 if (base64.charAt(4) === '-') {
68 base64 = base64.substr(5);
69 }
70 if (base64.length === 0) {
71 return;
72 }
73 decompress(base64, (code, err) => {
74 if (err) {
75 alert('Failed to decompress data: ' + err);
76 return;
77 }
78 editor.setValue(code);
79 });
80};
81
82const initClipboard = () => {
83 clipboard = new ClipboardJS('.clipboard');
84 clipboard.on('success', () => {
85 hideCopyBar(true);
86 });
87};
88
89const generateLink = (mode) => {
90 const data = editor.getValue();
91 compress(data, (base64, err) => {
92 if (err) {
93 alert('Failed to compress data: ' + err);
94 return;
95 }
96 const url = buildUrl(base64, mode);
97 statsEl.innerHTML = `Data length: ${data.length} | Link length: ${
98 url.length
99 } | Compression ratio: ${Math.round((100 * url.length) / data.length)}%`;
100
101 showCopyBar(url);
102 });
103};
104
105// Open the "Copy" bar and select the content
106const showCopyBar = (dataToCopy) => {
107 byId('copy').classList.remove('hidden');
108 const linkInput = byId('copy-link');
109 linkInput.value = dataToCopy;
110 linkInput.focus();
111 linkInput.setSelectionRange(0, dataToCopy.length);
112};
113
114// Close the "Copy" bar
115const hideCopyBar = (success) => {
116 const copyButton = byId('copy-btn');
117 const copyBar = byId('copy');
118 if (!success) {
119 copyBar.classList.add('hidden');
120 return;
121 }
122 copyButton.innerText = 'Copied !';
123 setTimeout(() => {
124 copyBar.classList.add('hidden');
125 copyButton.innerText = 'Copy';
126 }, 800);
127};
128
129const disableLineWrapping = () => {
130 byId('disable-line-wrapping').classList.add('hidden');
131 byId('enable-line-wrapping').classList.remove('hidden');
132 editor.setOption('lineWrapping', false);
133};
134
135const enableLineWrapping = () => {
136 byId('enable-line-wrapping').classList.add('hidden');
137 byId('disable-line-wrapping').classList.remove('hidden');
138 editor.setOption('lineWrapping', true);
139};
140
141const openInNewTab = () => {
142 window.open(location.href.replace('&readonly', ''));
143};
144
145// Build a shareable URL
146const buildUrl = (rawData, mode) => {
147 const base = `${location.protocol}//${location.host}/`;
148 const lang = hash('Plain Text') === select.selected() ? '' : select.selected() + '-';
149 const url = base + '#' + lang + rawData;
150 if (mode === 'markdown') {
151 return `[NoPaste snippet](${url})`;
152 }
153 if (mode === 'iframe') {
154 const height = Math.min(editor['doc'].height + 45, 800);
155 return `<iframe width="100%" height="${height}" frameborder="0" src="${url}"></iframe>`;
156 }
157 return url;
158};
159
160// Transform a compressed base64 string into a plain text string
161const decompress = (base64, cb) => {
162 const progressBar = byId('progress');
163
164 const req = new XMLHttpRequest();
165 req.open('GET', 'data:application/octet;base64,' + base64);
166 req.responseType = 'arraybuffer';
167 req.onload = (e) => {
168 lzma.decompress(
169 new Uint8Array(e.target.response),
170 (result, err) => {
171 progressBar.style.width = '0';
172 cb(result, err);
173 },
174 (progress) => {
175 progressBar.style.width = 100 * progress + '%';
176 }
177 );
178 };
179 req.send();
180};
181
182// Transform a plain text string into a compressed base64 string
183const compress = (str, cb) => {
184 if (str.length === 0) {
185 cb('');
186 return;
187 }
188 const progressBar = byId('progress');
189
190 lzma.compress(
191 str,
192 1,
193 (compressed, err) => {
194 if (err) {
195 progressBar.style.width = '0';
196 cb(compressed, err);
197 return;
198 }
199 const reader = new FileReader();
200 reader.onload = () => {
201 progressBar.style.width = '0';
202 cb(reader.result.substr(reader.result.indexOf(',') + 1));
203 };
204 reader.readAsDataURL(new Blob([new Uint8Array(compressed)]));
205 },
206 (progress) => {
207 progressBar.style.width = 100 * progress + '%';
208 }
209 );
210};
211
212const slugify = (str) =>
213 str
214 .toString()
215 .toLowerCase()
216 .replace(/\s+/g, '-')
217 .replace(/\+/g, '-p')
218 .replace(/#/g, '-sharp')
219 .replace(/[^\w\-]+/g, '');
220
221const byId = (id) => document.getElementById(id);
222
223const hash = function (str, seed = 0) {
224 let h1 = 0xdeadbeef ^ seed;
225 let h2 = 0x41c6ce57 ^ seed;
226 for (let i = 0, ch; i < str.length; i++) {
227 ch = str.charCodeAt(i);
228 h1 = Math.imul(h1 ^ ch, 2654435761);
229 h2 = Math.imul(h2 ^ ch, 1597334677);
230 }
231 h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
232 h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
233 const h = 4294967296 * (2097151 & h2) + (h1 >>> 0);
234 return h.toString(36).substr(0, 4).toUpperCase();
235};
236
237/* Only for tests purposes */
238const testAllModes = () => {
239 for (const [index, language] of Object.entries(CodeMirror.modeInfo)) {
240 setTimeout(() => {
241 console.info(language.name);
242 select.set(slugify(language.name));
243 }, 1000 * index);
244 }
245};
246
247/* Only for tests purposes */
248const testHashCollisions = () => {
249 const hashes = {};
250 console.time('Hash test');
251 for (const lang of CodeMirror.modeInfo) {
252 const hashed = hash(lang.name);
253 if (hashes[hashed]) {
254 console.error(`${lang.name} and ${hashes[hashed]} share the same hash: ${hashed}`);
255 }
256 hashes[hashed] = lang.name;
257 }
258 console.timeEnd('Hash test');
259};
260
261init();