Skip to content

Commit 09d752f

Browse files
committed
Allow view source to GET and show <meta> & <link>
1 parent 6c925dd commit 09d752f

1 file changed

Lines changed: 154 additions & 25 deletions

File tree

lib/components_guide_web/live/view_source.ex

Lines changed: 154 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
defmodule 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: 1rem; }
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: []
111240
end

0 commit comments

Comments
 (0)