@@ -7,6 +7,114 @@ window.ProbeRender = (function () {
77 var EXPECT_BG = '#444c56' ;
88 var pillCss = 'text-align:center;padding:2px 4px;font-size:11px;font-weight:600;color:#fff;border-radius:3px;min-width:28px;display:inline-block;line-height:18px;' ;
99
10+ // ── Scrollbar styling (injected once) ──────────────────────────
11+ var scrollStyleInjected = false ;
12+ function injectScrollStyle ( ) {
13+ if ( scrollStyleInjected ) return ;
14+ scrollStyleInjected = true ;
15+ var isDark = document . documentElement . classList . contains ( 'dark' ) ;
16+ var trackBg = isDark ? '#2a2f38' : '#e5e7eb' ;
17+ var thumbBg = isDark ? '#4b5563' : '#94a3b8' ;
18+ var thumbHover = isDark ? '#6b7280' : '#64748b' ;
19+ var css = '.probe-scroll{overflow-x:auto}'
20+ + '.probe-scroll::-webkit-scrollbar{height:8px}'
21+ + '.probe-scroll::-webkit-scrollbar-track{background:' + trackBg + ';border-radius:4px}'
22+ + '.probe-scroll::-webkit-scrollbar-thumb{background:' + thumbBg + ';border-radius:4px}'
23+ + '.probe-scroll::-webkit-scrollbar-thumb:hover{background:' + thumbHover + '}'
24+ + '.probe-scroll{scrollbar-width:thin;scrollbar-color:' + thumbBg + ' ' + trackBg + '}' ;
25+ var style = document . createElement ( 'style' ) ;
26+ style . textContent = css ;
27+ document . head . appendChild ( style ) ;
28+ }
29+
30+ // ── Test ID → doc page URL mapping ─────────────────────────────
31+ var TEST_URLS = {
32+ 'COMP-ABSOLUTE-FORM' : '/Http11Probe/docs/request-line/absolute-form/' ,
33+ 'COMP-ASTERISK-WITH-GET' : '/Http11Probe/docs/request-line/asterisk-with-get/' ,
34+ 'COMP-BASELINE' : '' ,
35+ 'COMP-CONNECT-EMPTY-PORT' : '/Http11Probe/docs/request-line/connect-empty-port/' ,
36+ 'COMP-DUPLICATE-HOST-SAME' : '/Http11Probe/docs/host-header/duplicate-host-same/' ,
37+ 'COMP-HOST-WITH-PATH' : '/Http11Probe/docs/host-header/host-with-path/' ,
38+ 'COMP-HOST-WITH-USERINFO' : '/Http11Probe/docs/host-header/host-with-userinfo/' ,
39+ 'COMP-LEADING-CRLF' : '/Http11Probe/docs/line-endings/leading-crlf/' ,
40+ 'COMP-METHOD-CASE' : '/Http11Probe/docs/request-line/method-case/' ,
41+ 'COMP-OPTIONS-STAR' : '/Http11Probe/docs/request-line/options-star/' ,
42+ 'COMP-UNKNOWN-TE-501' : '/Http11Probe/docs/request-line/unknown-te-501/' ,
43+ 'COMP-WHITESPACE-BEFORE-HEADERS' : '/Http11Probe/docs/headers/whitespace-before-headers/' ,
44+ 'MAL-BINARY-GARBAGE' : '/Http11Probe/docs/malformed-input/binary-garbage/' ,
45+ 'MAL-CHUNK-EXTENSION-LONG' : '/Http11Probe/docs/malformed-input/chunk-extension-long/' ,
46+ 'MAL-CHUNK-SIZE-OVERFLOW' : '/Http11Probe/docs/malformed-input/chunk-size-overflow/' ,
47+ 'MAL-CL-OVERFLOW' : '/Http11Probe/docs/malformed-input/cl-overflow/' ,
48+ 'MAL-CONTROL-CHARS-HEADER' : '/Http11Probe/docs/malformed-input/control-chars-header/' ,
49+ 'MAL-EMPTY-REQUEST' : '/Http11Probe/docs/malformed-input/empty-request/' ,
50+ 'MAL-H2-PREFACE' : '/Http11Probe/docs/malformed-input/h2-preface/' ,
51+ 'MAL-INCOMPLETE-REQUEST' : '/Http11Probe/docs/malformed-input/incomplete-request/' ,
52+ 'MAL-LONG-HEADER-NAME' : '/Http11Probe/docs/malformed-input/long-header-name/' ,
53+ 'MAL-LONG-HEADER-VALUE' : '/Http11Probe/docs/malformed-input/long-header-value/' ,
54+ 'MAL-LONG-METHOD' : '/Http11Probe/docs/malformed-input/long-method/' ,
55+ 'MAL-LONG-URL' : '/Http11Probe/docs/malformed-input/long-url/' ,
56+ 'MAL-MANY-HEADERS' : '/Http11Probe/docs/malformed-input/many-headers/' ,
57+ 'MAL-NON-ASCII-HEADER-NAME' : '/Http11Probe/docs/malformed-input/non-ascii-header-name/' ,
58+ 'MAL-NON-ASCII-URL' : '/Http11Probe/docs/malformed-input/non-ascii-url/' ,
59+ 'MAL-NUL-IN-HEADER-VALUE' : '/Http11Probe/docs/malformed-input/nul-in-header-value/' ,
60+ 'MAL-NUL-IN-URL' : '/Http11Probe/docs/malformed-input/nul-in-url/' ,
61+ 'MAL-WHITESPACE-ONLY-LINE' : '/Http11Probe/docs/malformed-input/whitespace-only-line/' ,
62+ 'RFC9110-5.4-DUPLICATE-HOST' : '/Http11Probe/docs/host-header/duplicate-host/' ,
63+ 'RFC9110-5.6.2-SP-BEFORE-COLON' : '/Http11Probe/docs/headers/sp-before-colon/' ,
64+ 'RFC9110-8.6-DUPLICATE-CL' : '/Http11Probe/docs/smuggling/duplicate-cl/' ,
65+ 'RFC9112-2.2-BARE-LF-HEADER' : '/Http11Probe/docs/line-endings/bare-lf-header/' ,
66+ 'RFC9112-2.2-BARE-LF-REQUEST-LINE' : '/Http11Probe/docs/line-endings/bare-lf-request-line/' ,
67+ 'RFC9112-2.3-HTTP09-REQUEST' : '/Http11Probe/docs/request-line/http09-request/' ,
68+ 'RFC9112-2.3-INVALID-VERSION' : '/Http11Probe/docs/request-line/invalid-version/' ,
69+ 'RFC9112-3-CR-ONLY-LINE-ENDING' : '/Http11Probe/docs/line-endings/cr-only-line-ending/' ,
70+ 'RFC9112-3-MISSING-TARGET' : '/Http11Probe/docs/request-line/missing-target/' ,
71+ 'RFC9112-3-MULTI-SP-REQUEST-LINE' : '/Http11Probe/docs/request-line/multi-sp-request-line/' ,
72+ 'RFC9112-3.2-FRAGMENT-IN-TARGET' : '/Http11Probe/docs/request-line/fragment-in-target/' ,
73+ 'RFC9112-5-EMPTY-HEADER-NAME' : '/Http11Probe/docs/headers/empty-header-name/' ,
74+ 'RFC9112-5-HEADER-NO-COLON' : '/Http11Probe/docs/headers/header-no-colon/' ,
75+ 'RFC9112-5-INVALID-HEADER-NAME' : '/Http11Probe/docs/headers/invalid-header-name/' ,
76+ 'RFC9112-5.1-OBS-FOLD' : '/Http11Probe/docs/headers/obs-fold/' ,
77+ 'RFC9112-6.1-CL-LEADING-ZEROS' : '/Http11Probe/docs/smuggling/cl-leading-zeros/' ,
78+ 'RFC9112-6.1-CL-NEGATIVE' : '/Http11Probe/docs/smuggling/cl-negative/' ,
79+ 'RFC9112-6.1-CL-NON-NUMERIC' : '/Http11Probe/docs/content-length/cl-non-numeric/' ,
80+ 'RFC9112-6.1-CL-PLUS-SIGN' : '/Http11Probe/docs/content-length/cl-plus-sign/' ,
81+ 'RFC9112-6.1-CL-TE-BOTH' : '/Http11Probe/docs/smuggling/cl-te-both/' ,
82+ 'RFC9112-7.1-MISSING-HOST' : '/Http11Probe/docs/host-header/missing-host/' ,
83+ 'SMUG-BARE-CR-HEADER-VALUE' : '/Http11Probe/docs/smuggling/bare-cr-header-value/' ,
84+ 'SMUG-CHUNK-BARE-SEMICOLON' : '/Http11Probe/docs/smuggling/chunk-bare-semicolon/' ,
85+ 'SMUG-CHUNK-HEX-PREFIX' : '/Http11Probe/docs/smuggling/chunk-hex-prefix/' ,
86+ 'SMUG-CHUNK-LEADING-SP' : '/Http11Probe/docs/smuggling/chunk-leading-sp/' ,
87+ 'SMUG-CHUNK-MISSING-TRAILING-CRLF' : '/Http11Probe/docs/smuggling/chunk-missing-trailing-crlf/' ,
88+ 'SMUG-CHUNK-UNDERSCORE' : '/Http11Probe/docs/smuggling/chunk-underscore/' ,
89+ 'SMUG-CHUNKED-WITH-PARAMS' : '/Http11Probe/docs/smuggling/chunked-with-params/' ,
90+ 'SMUG-CL-COMMA-DIFFERENT' : '/Http11Probe/docs/smuggling/cl-comma-different/' ,
91+ 'SMUG-CL-COMMA-SAME' : '/Http11Probe/docs/smuggling/cl-comma-same/' ,
92+ 'SMUG-CL-EXTRA-LEADING-SP' : '/Http11Probe/docs/smuggling/cl-extra-leading-sp/' ,
93+ 'SMUG-CL-HEX-PREFIX' : '/Http11Probe/docs/smuggling/cl-hex-prefix/' ,
94+ 'SMUG-CL-INTERNAL-SPACE' : '/Http11Probe/docs/smuggling/cl-internal-space/' ,
95+ 'SMUG-CL-OCTAL' : '/Http11Probe/docs/smuggling/cl-octal/' ,
96+ 'SMUG-CL-TRAILING-SPACE' : '/Http11Probe/docs/smuggling/cl-trailing-space/' ,
97+ 'SMUG-CLTE-PIPELINE' : '/Http11Probe/docs/smuggling/clte-pipeline/' ,
98+ 'SMUG-EXPECT-100-CL' : '/Http11Probe/docs/smuggling/expect-100-cl/' ,
99+ 'SMUG-HEADER-INJECTION' : '/Http11Probe/docs/smuggling/header-injection/' ,
100+ 'SMUG-TE-CASE-MISMATCH' : '/Http11Probe/docs/smuggling/te-case-mismatch/' ,
101+ 'SMUG-TE-DOUBLE-CHUNKED' : '/Http11Probe/docs/smuggling/te-double-chunked/' ,
102+ 'SMUG-TE-DUPLICATE-HEADERS' : '/Http11Probe/docs/smuggling/te-duplicate-headers/' ,
103+ 'SMUG-TE-EMPTY-VALUE' : '/Http11Probe/docs/smuggling/te-empty-value/' ,
104+ 'SMUG-TE-HTTP10' : '/Http11Probe/docs/smuggling/te-http10/' ,
105+ 'SMUG-TE-LEADING-COMMA' : '/Http11Probe/docs/smuggling/te-leading-comma/' ,
106+ 'SMUG-TE-NOT-FINAL-CHUNKED' : '/Http11Probe/docs/smuggling/te-not-final-chunked/' ,
107+ 'SMUG-TE-SP-BEFORE-COLON' : '/Http11Probe/docs/smuggling/te-sp-before-colon/' ,
108+ 'SMUG-TE-TRAILING-SPACE' : '/Http11Probe/docs/smuggling/te-trailing-space/' ,
109+ 'SMUG-TE-XCHUNKED' : '/Http11Probe/docs/smuggling/te-xchunked/' ,
110+ 'SMUG-TECL-PIPELINE' : '/Http11Probe/docs/smuggling/tecl-pipeline/' ,
111+ 'SMUG-TRANSFER_ENCODING' : '/Http11Probe/docs/smuggling/transfer-encoding-underscore/'
112+ } ;
113+
114+ function testUrl ( tid ) {
115+ return TEST_URLS [ tid ] || '' ;
116+ }
117+
10118 function pill ( bg , label ) {
11119 return '<span style="' + pillCss + 'background:' + bg + ';">' + label + '</span>' ;
12120 }
@@ -73,6 +181,7 @@ window.ProbeRender = (function () {
73181 }
74182
75183 function renderTable ( targetId , categoryKey , ctx ) {
184+ injectScrollStyle ( ) ;
76185 var el = document . getElementById ( targetId ) ;
77186 if ( ! el ) return ;
78187 var names = ctx . names , lookup = ctx . lookup , testIds = ctx . testIds ;
@@ -93,7 +202,7 @@ window.ProbeRender = (function () {
93202 return tid . replace ( / ^ ( R F C \d + - [ \d . ] + - | C O M P - | S M U G - | M A L - ) / , '' ) ;
94203 } ) ;
95204
96- var t = '<div style="overflow-x:auto; "><table style="border-collapse:collapse;font-size:12px;white-space:nowrap;">' ;
205+ var t = '<div class="probe-scroll "><table style="border-collapse:collapse;font-size:12px;white-space:nowrap;">' ;
97206
98207 // Column header row (diagonal labels)
99208 t += '<thead><tr>' ;
@@ -102,11 +211,17 @@ window.ProbeRender = (function () {
102211 var first = lookup [ names [ 0 ] ] [ tid ] ;
103212 var isUnscored = first . scored === false ;
104213 var opacity = isUnscored ? 'opacity:0.55;' : '' ;
214+ var url = testUrl ( tid ) ;
105215 t += '<th style="padding:0;height:110px;width:30px;vertical-align:bottom;' + opacity + '">' ;
106216 t += '<div style="width:30px;height:110px;position:relative;">' ;
107- t += '<a href="/Http11Probe/glossary/#test-' + tid + '" style="font-size:10px;font-weight:500;color:inherit;text-decoration:none;position:absolute;bottom:6px;left:50%;transform-origin:bottom left;transform:rotate(-55deg);white-space:nowrap;" title="' + first . description + '">' + shortLabels [ i ] ;
217+ if ( url ) {
218+ t += '<a href="' + url + '" style="font-size:10px;font-weight:500;color:inherit;text-decoration:none;position:absolute;bottom:6px;left:50%;transform-origin:bottom left;transform:rotate(-55deg);white-space:nowrap;" title="' + first . description + '">' + shortLabels [ i ] ;
219+ } else {
220+ t += '<span style="font-size:10px;font-weight:500;color:inherit;position:absolute;bottom:6px;left:50%;transform-origin:bottom left;transform:rotate(-55deg);white-space:nowrap;" title="' + first . description + '">' + shortLabels [ i ] ;
221+ }
108222 if ( isUnscored ) t += '*' ;
109- t += '</a></div></th>' ;
223+ t += url ? '</a>' : '</span>' ;
224+ t += '</div></th>' ;
110225 } ) ;
111226 t += '</tr></thead><tbody>' ;
112227
@@ -145,12 +260,73 @@ window.ProbeRender = (function () {
145260 el . innerHTML = t ;
146261 }
147262
263+ // ── Language filter ────────────────────────────────────────────
264+ function renderLanguageFilter ( targetId , data , onChange ) {
265+ var el = document . getElementById ( targetId ) ;
266+ if ( ! el || ! data . servers || data . servers . length === 0 ) return ;
267+
268+ var langs = { } ;
269+ data . servers . forEach ( function ( sv ) {
270+ if ( sv . language ) langs [ sv . language ] = true ;
271+ } ) ;
272+ var langList = Object . keys ( langs ) . sort ( ) ;
273+ if ( langList . length === 0 ) return ;
274+
275+ var isDark = document . documentElement . classList . contains ( 'dark' ) ;
276+ var baseBg = isDark ? '#21262d' : '#f6f8fa' ;
277+ var baseFg = isDark ? '#c9d1d9' : '#24292f' ;
278+ var baseBorder = isDark ? '#30363d' : '#d0d7de' ;
279+ var activeBg = isDark ? '#1f6feb' : '#0969da' ;
280+
281+ var btnStyle = 'display:inline-block;padding:4px 12px;font-size:12px;font-weight:600;'
282+ + 'border-radius:20px;cursor:pointer;border:1px solid ' + baseBorder + ';'
283+ + 'margin-right:6px;margin-bottom:6px;transition:all 0.15s;' ;
284+
285+ var html = '<div style="margin-bottom:12px;">' ;
286+ html += '<button class="probe-lang-btn" data-lang="" style="' + btnStyle
287+ + 'background:' + activeBg + ';color:#fff;border-color:' + activeBg + ';">All</button>' ;
288+ langList . forEach ( function ( lang ) {
289+ html += '<button class="probe-lang-btn" data-lang="' + lang + '" style="' + btnStyle
290+ + 'background:' + baseBg + ';color:' + baseFg + ';">' + lang + '</button>' ;
291+ } ) ;
292+ html += '</div>' ;
293+ el . innerHTML = html ;
294+
295+ var buttons = el . querySelectorAll ( '.probe-lang-btn' ) ;
296+ buttons . forEach ( function ( btn ) {
297+ btn . addEventListener ( 'click' , function ( ) {
298+ var lang = btn . getAttribute ( 'data-lang' ) ;
299+ buttons . forEach ( function ( b ) {
300+ if ( b === btn ) {
301+ b . style . background = activeBg ;
302+ b . style . color = '#fff' ;
303+ b . style . borderColor = activeBg ;
304+ } else {
305+ b . style . background = baseBg ;
306+ b . style . color = baseFg ;
307+ b . style . borderColor = baseBorder ;
308+ }
309+ } ) ;
310+ if ( ! lang ) {
311+ onChange ( data ) ;
312+ } else {
313+ var filtered = {
314+ commit : data . commit ,
315+ servers : data . servers . filter ( function ( sv ) { return sv . language === lang ; } )
316+ } ;
317+ onChange ( filtered ) ;
318+ }
319+ } ) ;
320+ } ) ;
321+ }
322+
148323 return {
149324 pill : pill ,
150325 verdictBg : verdictBg ,
151326 buildLookups : buildLookups ,
152327 renderSummary : renderSummary ,
153328 renderTable : renderTable ,
329+ renderLanguageFilter : renderLanguageFilter ,
154330 EXPECT_BG : EXPECT_BG
155331 } ;
156332} ) ( ) ;
0 commit comments