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