11defmodule ComponentsGuideWeb.ViewSourceLive do
22 use ComponentsGuideWeb ,
3- { :live_view , container: { :div , class: "max-w-xl mx-auto text-lg text-white pb-24" } }
3+ { :live_view , container: { :div , class: "max-w-6xl mx-auto text-lg text-white pb-24" } }
44
55 alias ComponentsGuide.Fetch
66
77 defmodule State do
88 defstruct url_string: "" ,
9+ request: nil ,
910 response: nil
1011
1112 def default ( ) do
@@ -14,10 +15,15 @@ defmodule ComponentsGuideWeb.ViewSourceLive do
1415 }
1516 end
1617
17- def add_response ( % __MODULE__ { } = state , response ) do
18+ def add_response (
19+ % __MODULE__ { } = state ,
20+ request = % Fetch.Request { } ,
21+ response = % Fetch.Response { }
22+ ) do
1823 % __MODULE__ {
1924 state
20- | response: response ,
25+ | request: request ,
26+ response: response ,
2127 url_string: response . url
2228 }
2329 end
@@ -26,42 +32,89 @@ defmodule ComponentsGuideWeb.ViewSourceLive do
2632 @ impl true
2733 def render ( assigns ) do
2834 ~H"""
29- < h1 class = "text-4xl font-bold pt-8 pb-4 " > <%= "Head requests" %> </ h1 >
3035 < . form
3136 let = { f }
3237 for = { :editor }
38+ id = "view_source_form "
3339 phx-submit = "submitted "
34- class = "space-y-4 "
40+ class = "max-w-2xl mx-auto space-y-2 "
3541 >
3642
3743 < fieldset y-y y-stretch class = "gap-1 " >
38- < label for = "url " > Perform HEAD request for URL </ label >
44+ < label for = "url " > Enter URL to request: </ label >
3945 < input id = "url " type = "url " name = "url_string " value = { @ state . url_string } class = "text-black " >
4046 </ fieldset >
4147
42- < button type = " submit " class = "px-3 py-1 text-blue-100 bg-blue-600 rounded " > Load </ button >
43-
44- < output class = " block pt-2 ">
45- <%= if @ state . response do % >
46- < p > HEAD <%= @ state . response . url %> </ p >
47- < p > Received <%= @ state . response . status %> </ p >
48- < p > Loaded in <%= System . convert_time_unit ( @ state . response . timings . duration , :native , :millisecond ) %> ms </ p >
49- < dl class = " font-mono ">
50- <%= for { name , value } <- @ state . response . headers do % >
51- < dt class = " font-bold " > <%= name %> </ dt >
52- < dd class = " pl-8 " > <%= value %> </ dd >
53- <% end % >
54- </ dl >
55- <% end % >
56- </ output >
57-
48+ < div class = "flex " >
49+ < fieldset class = " flex items-center gap-2 " >
50+ < label for = " head-radio ">
51+ < input id = " head-radio " type = " radio " name = " method " value = " HEAD " checked = { @ state . request == nil || match? ( % { method: "HEAD" } , @ state . request ) } / >
52+ HEAD
53+ </ label >
54+
55+ < label for = " get-radio ">
56+ < input id = " get-radio " type = " radio " name = " method " value = " GET " checked = { match? ( % { method: "GET" } , @ state . request ) } / >
57+ GET
58+ </ label >
59+ </ fieldset >
60+
61+ < span class = " mx-auto " > </ span >
62+ < button type = " submit " class = " px-3 py-1 text-blue-100 bg-blue-600 rounded " > Load </ button >
63+ </ div >
5864 </ .form >
65+
66+ < script type = "module " >
67+ window.customElements.define('view-source-filter', class extends HTMLElement {
68+ connectedCallback ( ) {
69+ this . aborter = new AbortController ( ) ;
70+ const signal = this . aborter . signal ;
71+ this . addEventListener ( 'input' , ( ) => {
72+ const listItems = this . parentNode . querySelectorAll ( 'dl dt' ) ;
73+ const values = new FormData ( this . querySelector ( 'form' ) ) ;
74+ const q = values . get ( 'q' ) . trim ( ) . toLowerCase ( ) ;
75+ for ( const li of Array . from ( listItems ) ) {
76+ const matches = q === '' ? true : li . textContent . toLowerCase ( ) . includes ( q ) ;
77+ li . hidden = ! matches ;
78+ }
79+ } , { signal } ) ;
80+
81+ this . querySelector ( 'input' ) . focus ( ) ;
82+ }
83+
84+ disconnectedCallback ( ) {
85+ this . aborter . abort ( ) ;
86+ }
87+ } )
88+ </ script >
89+
90+ < output form = "view_source_form " class = "prose prose-invert block pt-4 max-w-none text-center " >
91+ <%= if @ state . response do %>
92+ < pre > <%= @ state . request . method %> <%= @ state . response . url %> </ pre >
93+ < p >
94+ Received < span class = "px-2 py-1 bg-green-400 text-green-900 rounded " > <%= @ state . response . status %> </ span >
95+ in <%= System . convert_time_unit ( @ state . response . timings . duration , :native , :millisecond ) %> ms
96+ </ p >
97+ < view-source-filter >
98+ < form role = "search " id = "filter-results " >
99+ < input name = "q " type = "search " placeholder = "Filter results… " class = "text-white bg-gray-800 border-gray-700 rounded " >
100+ </ form >
101+ </ view-source-filter >
102+ < . headers_preview headers = { @ state . response . headers } >
103+ </ . headers_preview >
104+ <%= if ( @ state . response . body || "" ) != "" do %>
105+ < . html_preview html = { @ state . response . body } >
106+ </ . html_preview >
107+ <% end %>
108+ <% end %>
109+ </ output >
59110 < style >
60111 :root {
61112 -- fetch - html - color : green ;
62113 }
63114
64- fieldset label + label { margin - left : 1 rem ; }
115+ dt[hidden] + dd {
116+ display : none ;
117+ }
65118 </ style >
66119 """
67120 end
@@ -88,13 +141,14 @@ defmodule ComponentsGuideWeb.ViewSourceLive do
88141 def handle_event ( "submitted" , form_values , socket ) do
89142 # state = State.from(form_values)
90143 IO . inspect ( form_values )
144+ method = Map . get ( form_values , "method" , "HEAD" )
91145
92- case Fetch.Request . new ( form_values [ "url_string" ] , method: "HEAD" ) do
146+ case Fetch.Request . new ( form_values [ "url_string" ] , method: method ) do
93147 { :ok , request } ->
94148 response = Fetch . load! ( request )
95149 IO . inspect ( response . headers )
96150
97- state = socket . assigns . state |> State . add_response ( response )
151+ state = socket . assigns . state |> State . add_response ( request , response )
98152
99153 socket = socket |> assign_state ( state )
100154 { :noreply , socket }
@@ -108,4 +162,79 @@ defmodule ComponentsGuideWeb.ViewSourceLive do
108162 def handle_info ( :update , socket ) do
109163 { :noreply , socket }
110164 end
165+
166+ def headers_preview ( assigns ) do
167+ ~H"""
168+ < h2 > Response Headers</ h2 >
169+ < dl class = "grid grid-cols-2 gap-y-1 font-mono break-words " >
170+ <%= for { name , value } <- @ headers do %>
171+ < dt class = "text-right font-bold " > <%= name %> </ dt >
172+ < dd class = "text-left pl-8 " > <%= value %> </ dd >
173+ <% end %>
174+ </ dl >
175+ """
176+ end
177+
178+ def html_preview ( assigns ) do
179+ ~H"""
180+ <%= for { kind , values } <- list_html_features ( @ html ) do %>
181+ <%= if kind == :link_values do %>
182+ < h2 > Links</ h2 >
183+ < dl class = "grid grid-cols-2 gap-y-1 font-mono break-words " >
184+ <%= for { name , value } <- values do %>
185+ < dt class = "text-right font-bold " > <%= name %> </ dt >
186+ < dd class = "text-left pl-8 " > <%= value %> </ dd >
187+ <% end %>
188+ </ dl >
189+ <% end %>
190+ <%= if kind == :meta_values do %>
191+ < h2 > Meta</ h2 >
192+ < dl class = "grid grid-cols-2 gap-y-1 font-mono break-words " >
193+ <%= for { name , value } <- values do %>
194+ < dt class = "text-right font-bold " > <%= name %> </ dt >
195+ < dd class = "text-left pl-8 " > <%= value %> </ dd >
196+ <% end %>
197+ </ dl >
198+ <% end %>
199+ <% end %>
200+ """
201+ end
202+
203+ def list_html_features ( html ) do
204+ with { :ok , document } <- Floki . parse_document ( html ) do
205+ meta_values =
206+ for { "meta" , attrs , _ } <- Floki . find ( document , "head meta" ) ,
207+ key_value <- extract_meta_key_values ( Map . new ( attrs ) ) do
208+ key_value
209+ end
210+
211+ link_values =
212+ for { "link" , attrs , _ } <- Floki . find ( document , "head link" ) ,
213+ key_value <- extract_link_key_values ( Map . new ( attrs ) ) do
214+ key_value
215+ end
216+
217+ [ meta_values: meta_values , link_values: link_values ]
218+ else
219+ _ -> [ ]
220+ end
221+ end
222+
223+ def extract_link_key_values ( % { "rel" => rel , "href" => href } ) do
224+ [ { rel , href } ]
225+ end
226+
227+ def extract_link_key_values ( _ ) do
228+ [ ]
229+ end
230+
231+ def extract_meta_key_values ( % { "name" => name , "content" => content } ) do
232+ [ { name , content } ]
233+ end
234+
235+ def extract_meta_key_values ( % { "property" => property , "content" => content } ) do
236+ [ { property , content } ]
237+ end
238+
239+ def extract_meta_key_values ( _ ) , do: [ ]
111240end
0 commit comments