From b516742529440779985bab50b6a3a50203f905ed Mon Sep 17 00:00:00 2001 From: skidoodle Date: Mon, 27 Apr 2026 01:16:42 +0200 Subject: [PATCH] overhaul syntax highlighting and line numbering for large pastes --- handler/http.go | 6 -- handler/http_test.go | 25 ------- main.go | 2 +- view/static/favicon.ico | Bin 0 -> 1150 bytes view/static/highlightjs-line-numbers.js | 1 - view/static/script.js | 80 +++++++++++++++++++++ view/static/style.css | 88 ++++++++++++------------ view/templates/base.html | 2 + view/templates/bin.html | 48 +------------ 9 files changed, 128 insertions(+), 124 deletions(-) create mode 100644 view/static/favicon.ico delete mode 100644 view/static/highlightjs-line-numbers.js create mode 100644 view/static/script.js diff --git a/handler/http.go b/handler/http.go index 42e2cfd..2e2cb35 100644 --- a/handler/http.go +++ b/handler/http.go @@ -98,9 +98,6 @@ func (h *HttpHandler) HandleSet(w http.ResponseWriter, r *http.Request) { func (h *HttpHandler) HandleGet(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - if index := strings.LastIndex(id, "."); index > 0 { - id = id[:index] - } paste, exists, err := h.store.Get(id) if err != nil { @@ -131,9 +128,6 @@ func (h *HttpHandler) HandleGet(w http.ResponseWriter, r *http.Request) { func (h *HttpHandler) HandleRaw(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - if index := strings.LastIndex(id, "."); index > 0 { - id = id[:index] - } paste, exists, err := h.store.Get(id) if err != nil { diff --git a/handler/http_test.go b/handler/http_test.go index 8053b9c..ba6caf5 100644 --- a/handler/http_test.go +++ b/handler/http_test.go @@ -207,18 +207,6 @@ func TestHandleGet(t *testing.T) { h.HandleGet(rr, req) assert.Equal(t, http.StatusInternalServerError, rr.Code) }) - - t.Run("With Extension", func(t *testing.T) { - id := "testid" - s.On("Get", id).Return(&store.Paste{Content: "hello", CreatedAt: time.Now()}, true, nil).Once() - req := httptest.NewRequest("GET", "/"+id+".go", nil) - req.SetPathValue("id", id+".go") - rr := httptest.NewRecorder() - - h.HandleGet(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "hello") - }) } type FailingResponseWriter struct { @@ -262,19 +250,6 @@ func TestHandleRaw(t *testing.T) { assert.Equal(t, "text/plain; charset=utf-8", rr.Header().Get("Content-Type")) }) - t.Run("With Extension", func(t *testing.T) { - id := "testid" - content := "raw content" - s.On("Get", id).Return(&store.Paste{Content: content}, true, nil).Once() - req := httptest.NewRequest("GET", "/raw/"+id+".txt", nil) - req.SetPathValue("id", id+".txt") - rr := httptest.NewRecorder() - - h.HandleRaw(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, content, rr.Body.String()) - }) - t.Run("Not Found", func(t *testing.T) { s.On("Get", "missing").Return(nil, false, nil).Once() req := httptest.NewRequest("GET", "/raw/missing", nil) diff --git a/main.go b/main.go index ffcc431..3e78c44 100644 --- a/main.go +++ b/main.go @@ -41,7 +41,7 @@ func securityHeadersMiddleware(next http.Handler) http.Handler { w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") - w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'") + w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; worker-src 'self' blob:") next.ServeHTTP(w, r) }) } diff --git a/view/static/favicon.ico b/view/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d3edef5e5b5c4f3b0d54ff56b2c7bb23ffeb0bef GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x$A1^@s5XBfq!VKABoMrs;h0Oo)4 FF#u2^9!vlL literal 0 HcmV?d00001 diff --git a/view/static/highlightjs-line-numbers.js b/view/static/highlightjs-line-numbers.js deleted file mode 100644 index dd3b041..0000000 --- a/view/static/highlightjs-line-numbers.js +++ /dev/null @@ -1 +0,0 @@ -!function(r,o){"use strict";var e,a="hljs-ln",l="hljs-ln-line",h="hljs-ln-code",s="hljs-ln-numbers",c="hljs-ln-n",m="data-line-number",i=/\r\n|\r|\n/g;function u(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var o=parseInt(t.dataset.lineNumber),i=parseInt(r.dataset.lineNumber);if(o==i)return n;var a,l=t.textContent,s=r.textContent;for(i
{6}',[l,s,c,m,h,o+n.startFrom,0{1}',[a,r])}return e}(e.innerHTML,o)}function v(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r{1}\n',[n,0' + i + ''); + } + ln.innerHTML = nums.join(''); + + const rawText = code.textContent; + const workerCode = ` + self.window = self; + self.document = { + readyState: 'complete', + querySelectorAll: function() { return []; }, + addEventListener: function() {} + }; + importScripts('${window.location.origin}/static/highlight.min.js'); + onmessage = function(e) { + try { + const result = self.hljs.highlightAuto(e.data); + postMessage(result.value); + } catch (err) { + postMessage(e.data); + } + } + `; + const blob = new Blob([workerCode], { type: 'application/javascript' }); + const workerUrl = URL.createObjectURL(blob); + const worker = new Worker(workerUrl); + + worker.onmessage = function (e) { + code.innerHTML = e.data; + URL.revokeObjectURL(workerUrl); + }; + worker.postMessage(rawText); + + function updateActiveLine() { + const active = document.querySelector('.line-numbers a.active'); + if (active) active.classList.remove('active'); + + if (window.location.hash) { + const target = document.getElementById(window.location.hash.substring(1)); + if (target) { + target.scrollIntoView({ block: 'center', behavior: 'smooth' }); + target.classList.add('active'); + } + } + } + + updateActiveLine(); + window.addEventListener('hashchange', updateActiveLine); + + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'a') { + if (e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'INPUT') { + e.preventDefault(); + const range = document.createRange(); + range.selectNodeContents(code); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } + } + }); + } + + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + const form = document.getElementById('paste-form'); + if (form) { + e.preventDefault(); + form.submit(); + } + } + }); +})(); diff --git a/view/static/style.css b/view/static/style.css index 715cb5e..c6dbec3 100644 --- a/view/static/style.css +++ b/view/static/style.css @@ -4,9 +4,7 @@ --text-dim: #8b949e; --border: #30363d; --primary: #238636; - --primary-hover: #2ea043; --secondary: #21262d; - --secondary-hover: #30363d; --accent: #58a6ff; --highlight: rgba(241, 250, 140, 0.1); @@ -138,77 +136,81 @@ textarea { flex-grow: 1; width: 100%; font-family: inherit; - font-size: 0.95rem; + font-size: 0.9rem; line-height: var(--line-height); border: none; outline: none; background-color: transparent !important; color: inherit; resize: none; - padding: 0.5rem; - white-space: pre; + padding: 0; + margin: 0; overflow: auto; } -pre, +pre { + background: transparent !important; + padding: 0 !important; + margin: 0 !important; + font-family: inherit !important; + font-size: 0.9rem !important; + line-height: var(--line-height) !important; + flex-grow: 1; + padding-left: 1rem !important; + overflow: visible; +} + code { background: transparent !important; padding: 0 !important; margin: 0 !important; font-family: inherit !important; - font-size: inherit !important; + font-size: 0.9rem !important; line-height: var(--line-height) !important; -} - -code.with-line-numbers { + white-space: pre !important; + word-wrap: normal !important; display: block; } -.line { - display: grid; - grid-template-columns: calc(var(--digits, 1) * 1ch + 2rem) minmax(0, 1fr); - width: 100%; - min-height: calc(1em * var(--line-height)); - scroll-margin-top: 2rem; - content-visibility: auto; - contain-intrinsic-size: 1px calc(1em * var(--line-height)); - contain: paint; +#paste-content { + display: flex; + flex-direction: row; + overflow: auto; } -.line-number { - grid-column: 1; +.line-numbers { + flex-shrink: 0; text-align: right; padding-right: 1rem; color: var(--text-dim); - opacity: 0.3; border-right: 1px solid var(--border); user-select: none; - text-decoration: none; - cursor: pointer; font-size: 0.9rem; + line-height: var(--line-height); + min-width: calc(var(--digits, 1) * 1ch + 2rem); + display: flex; + flex-direction: column; } -.line-number:hover { +.line-numbers a { + color: inherit; + text-decoration: none; + cursor: pointer; + display: block; + opacity: 0.3; +} + +.line-numbers a:hover { opacity: 0.8; color: var(--accent); } -.line-code { - grid-column: 2; - padding-left: 1rem; - white-space: pre-wrap; - overflow-wrap: anywhere; -} - -.line:target { - background-color: var(--highlight); -} - -.line:target .line-number { +.line-numbers a:target, +.line-numbers a.active { opacity: 1; color: #f1fa8c; - border-right-color: #f1fa8c; - border-right-width: 2px; + border-right: 2px solid #f1fa8c; + margin-right: -1px; } ::selection { @@ -258,15 +260,11 @@ code.with-line-numbers { display: none; } - .line { - grid-template-columns: 1fr; - } - - .line-number { + .line-numbers { display: none; } - .line-code { + pre { padding-left: 0; } } diff --git a/view/templates/base.html b/view/templates/base.html index 7379459..88f89c0 100644 --- a/view/templates/base.html +++ b/view/templates/base.html @@ -5,7 +5,9 @@ + {{ .Title }} + diff --git a/view/templates/bin.html b/view/templates/bin.html index 3e7ee65..6aa086a 100644 --- a/view/templates/bin.html +++ b/view/templates/bin.html @@ -24,46 +24,9 @@
{{ if .IsPreview }}
+
{{ .Content }}
- {{ else }}
{{ if .Error }} @@ -77,14 +40,7 @@ autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">{{ .Content }}
- {{ end }} +
{{ end }}