Skip to content

Commit f6a784c

Browse files
committed
Add search page just for DOM types
1 parent ce0c188 commit f6a784c

9 files changed

Lines changed: 367 additions & 88 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
defmodule ComponentsGuide.Research.Sources.Typescript do
2+
def fetch(_query) do
3+
{:text_url, "https://cdn.jsdelivr.net/npm/typescript@4.7.4/lib/lib.dom.d.ts"}
4+
end
5+
6+
def search(query, {:ok, source}) do
7+
start = System.monotonic_time()
8+
9+
# TODO: add types to a SQLite database?
10+
{:ok, db} = Exqlite.Sqlite3.open(":memory:")
11+
:ok = Exqlite.Sqlite3.execute(db, "CREATE VIRTUAL TABLE files USING FTS5(name, body)")
12+
13+
{:ok, statement} = Exqlite.Sqlite3.prepare(db, "insert into files (name, body) values (?, ?)")
14+
:ok = Exqlite.Sqlite3.bind(db, statement, ["lib.dom.d.ts", source])
15+
:done = Exqlite.Sqlite3.step(db, statement)
16+
:ok = Exqlite.Sqlite3.release(db, statement)
17+
18+
# Prepare a select statement
19+
# {:ok, statement} = Exqlite.Sqlite3.prepare(db, "select highlight(files, 1, '<b>', '</b>') body from files where files match ? order by rank")
20+
{:ok, statement} =
21+
Exqlite.Sqlite3.prepare(
22+
db,
23+
"select snippet(files, 1, '', '', '…', 64) body from files where files match ? order by rank"
24+
)
25+
26+
:ok = Exqlite.Sqlite3.bind(db, statement, [query])
27+
28+
# Get the results
29+
# {:row, row} = Exqlite.Sqlite3.step(db, statement)
30+
# :done = Exqlite.Sqlite3.step(db, statement)
31+
32+
{:ok, rows} = Exqlite.Sqlite3.fetch_all(db, statement)
33+
{:ok, columns} = Exqlite.Sqlite3.columns(db, statement)
34+
:ok = Exqlite.Sqlite3.release(db, statement)
35+
36+
Exqlite.Sqlite3.close(db)
37+
38+
duration = System.monotonic_time() - start
39+
40+
IO.puts(
41+
"SQLite create + text search took #{System.convert_time_unit(duration, :native, :millisecond)}ms"
42+
)
43+
44+
{columns, rows}
45+
end
46+
47+
defmodule Interface do
48+
defstruct name: nil, line_start: nil, line_end: nil
49+
end
50+
51+
defmodule GlobalVariable do
52+
defstruct name: nil, line_start: nil, line_end: nil
53+
end
54+
55+
defmodule Parser do
56+
defmodule State do
57+
defstruct mode: nil, output: []
58+
end
59+
60+
def parse(source) when is_binary(source) do
61+
lines = source |> String.splitter(["\n"]) |> Stream.with_index()
62+
63+
state = lines |> Enum.reduce(%State{}, &do_reduce/2)
64+
Enum.reverse(state.output)
65+
end
66+
67+
def extract_line_ranges(source, line_ranges) when is_binary(source) do
68+
lines = source |> String.splitter(["\n"]) |> Stream.with_index()
69+
lines_map = Map.new(lines, fn {line, n} -> {n, line} end)
70+
71+
for line_range <- line_ranges do
72+
line_range =
73+
case line_range do
74+
%Range{} = range -> range
75+
%{line_start: line_start, line_end: line_end} -> Range.new(line_start, line_end)
76+
end
77+
78+
for line <- line_range do
79+
lines_map[line]
80+
end
81+
|> Enum.join("\n")
82+
end
83+
end
84+
85+
defmodule ModeDefinition do
86+
defstruct type: nil, name: nil, line_start: nil, line_end: nil
87+
end
88+
89+
def do_reduce({"interface " <> name_and_extends, n}, %State{mode: nil} = state) do
90+
name = extract_name(name_and_extends)
91+
mode = %ModeDefinition{type: :interface, name: name, line_start: n}
92+
%State{state | mode: mode}
93+
end
94+
95+
def do_reduce({"declare var " <> name_and_extends, n}, %State{mode: nil} = state) do
96+
name = extract_name(name_and_extends)
97+
mode = %ModeDefinition{type: :global_var, name: name, line_start: n}
98+
%State{state | mode: mode}
99+
end
100+
101+
def do_reduce({"}" <> _, n}, %State{mode: %ModeDefinition{}} = state) do
102+
mode = state.mode
103+
104+
output_item =
105+
case mode.type do
106+
:interface ->
107+
%Interface{name: mode.name, line_start: mode.line_start, line_end: n}
108+
109+
:global_var ->
110+
%GlobalVariable{name: mode.name, line_start: mode.line_start, line_end: n}
111+
end
112+
113+
%State{mode: nil, output: [output_item | state.output]}
114+
end
115+
116+
def do_reduce(_, state), do: state
117+
118+
defp extract_name(name_and_extends) do
119+
case String.split(name_and_extends, [" ", ":"], parts: 2) do
120+
[name | _] -> name
121+
_ -> nil
122+
end
123+
end
124+
end
125+
end

lib/components_guide_web/controllers/research_controller.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ defmodule ComponentsGuideWeb.ResearchController do
66
alias ComponentsGuide.Research.Static
77
alias ComponentsGuideWeb.ResearchView, as: View
88
alias ComponentsGuideWeb.ResearchView.Section, as: Section
9+
alias ComponentsGuide.Research.Sources.Typescript
10+
11+
def show(conn, %{"section" => "dom-types"}) do
12+
url = "https://cdn.jsdelivr.net/npm/typescript@4.7.4/lib/lib.dom.d.ts"
13+
{:ok, source} = ComponentsGuide.Research.Source.text_at(url)
14+
types = Typescript.Parser.parse(source)
15+
types_sources = Typescript.Parser.extract_line_ranges(source, types)
16+
17+
results = Enum.zip_with(types, types_sources, fn type, type_source ->
18+
%{name: type.name, source: type_source}
19+
end)
20+
21+
conn
22+
|> assign(:page_title, "Search DOM Types")
23+
|> render("dom-types.html", results: results)
24+
end
925

1026
# TODO: add Tailwind search e.g. "ml-4" or "ml-[5rem]" and see what is produced
1127

lib/components_guide_web/live/view_source.ex

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,6 @@ defmodule ComponentsGuideWeb.ViewSourceLive do
108108
<% end %>
109109
</output>
110110
<style>
111-
:root {
112-
--fetch-html-color: green;
113-
}
114-
115111
dt[hidden] + dd {
116112
display: none;
117113
}

lib/components_guide_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule ComponentsGuideWeb.Router do
2525
resources "/react-playground", ReactEditorController, only: [:index, :show]
2626

2727
get("/research", ResearchController, :index)
28+
get("/research/:section", ResearchController, :show)
2829

2930
get("/concepts", ConceptsController, :index)
3031

lib/components_guide_web/templates/research/_header.html.eex

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<%
2+
heading = "Search packages, features & specs"
3+
search_url = fn (q) -> "?" <> URI.encode_query(q: q) end
4+
search_link = fn (q) -> link(q, to: search_url.(q)) end
5+
%>
6+
7+
<header class="text-white">
8+
<h2 class="pb-4 mx-auto max-w-4xl text-xl text-center font-bold">
9+
<%= heading %>
10+
</h2>
11+
<section class="container px-6">
12+
<form role="search" action="/research" class="flex h-full items-center">
13+
<input type="text" name="q" placeholder="Search" class="w-full py-1 px-6 text-3xl bg-white text-black border rounded-full" value={@query}>
14+
</form>
15+
<details open={assigns[:show_help]}>
16+
<summary class="pt-2 pl-6 text-sm text-indigo-300"><%= "Expand for example searches. Search across NPM, Can I Use, BundlePhobia, RFCs & specifications." %></summary>
17+
<dl
18+
class="text-xl grid gap-2 items-center pt-4 px-6"
19+
data-links="underline-on-hover"
20+
style="
21+
grid-template-columns: auto 1fr;
22+
--link-color: #76a9fa;
23+
--hover\:link-decoration: underline;"
24+
>
25+
<dt>HTML & Roles</dt>
26+
<dd>
27+
<ul class="list-none flex flex-wrap italic" data-links="p-3">
28+
<li><%= link("form", to: "?q=form") %></li>
29+
<li><%= link("button", to: "?q=button") %></li>
30+
<li><%= link("contentinfo", to: "?q=contentinfo") %></li>
31+
<li><%= link("dialog", to: "?q=dialog") %></li>
32+
<li><%= link("menu", to: "?q=menu") %></li>
33+
</ul>
34+
</dd>
35+
<dt>Libraries</dt>
36+
<dd>
37+
<ul class="list-none flex flex-wrap italic" data-links="p-3">
38+
<li><%= link("react-dom", to: "?q=react-dom") %></li>
39+
<li><%= link("preact", to: "?q=preact") %></li>
40+
<li><%= link("vue", to: "?q=vue") %></li>
41+
<li><%= link("lit-html", to: "?q=lit-html") %></li>
42+
<li><%= link("xstate", to: "?q=xstate") %></li>
43+
<li><%= link("mobx", to: "?q=mobx") %></li>
44+
<li><%= link("lodash", to: "?q=lodash") %></li>
45+
<li><%= link("wonka", to: "?q=wonka") %></li>
46+
</ul>
47+
</dd>
48+
<dt>Features</dt>
49+
<dd>
50+
<ul class="list-none flex flex-wrap italic" data-links="p-3">
51+
<li><%= search_link.("CSS Variables") %></li>
52+
<li><%= search_link.("Generators") %></li>
53+
<li><%= search_link.("Template Literals") %></li>
54+
<li><%= search_link.("Proxy") %></li>
55+
<li><%= search_link.("CSS Grid") %></li>
56+
<li><%= search_link.("Dynamic Import") %></li>
57+
<li><%= search_link.("BigInt") %></li>
58+
</ul>
59+
</dd>
60+
<dt>Specs</dt>
61+
<dd>
62+
<ul class="list-none flex flex-wrap italic" data-links="p-3">
63+
<li><%= link("JSON", to: "?q=JSON") %></li>
64+
<li><%= link("CSV", to: "?q=CSV") %></li>
65+
<li><%= link("ISO 8601", to: "?q=ISO%208601") %></li>
66+
</ul>
67+
</dd>
68+
</dl>
69+
</details>
70+
</section>
71+
</header>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<article class="max-w-5xl mx-auto text-xl text-white">
2+
3+
<view-source-filter>
4+
<form role="search" id="filter-results" class="mb-8">
5+
<input name="q" type="search" placeholder="Filter types" class="text-white bg-gray-800 border-gray-700 rounded">
6+
</form>
7+
</view-source-filter>
8+
9+
<dl>
10+
<%= for result <- @results do %>
11+
<div>
12+
<dt class="font-bold" data-body={result.source}><%= result.name %></dt>
13+
<dd><pre class="language-ts"><code><%= result.source %></code></pre></dd>
14+
</div>
15+
<% end %>
16+
</dl>
17+
18+
</article>
19+
20+
<style>
21+
dt[hidden] + dd {
22+
display: none;
23+
}
24+
</style>
25+
26+
<script type="module">
27+
window.customElements.define('view-source-filter', class extends HTMLElement {
28+
connectedCallback() {
29+
this.aborter = new AbortController();
30+
const signal = this.aborter.signal;
31+
this.addEventListener('input', () => {
32+
const items = this.closest('article').querySelectorAll('dl dt');
33+
const values = new FormData(this.querySelector('form'));
34+
const q = values.get('q').trim().toLowerCase();
35+
for (const item of Array.from(items)) {
36+
let matches = q === '';
37+
matches ||= item.textContent.toLowerCase().includes(q);
38+
matches ||= item.dataset.body.toLowerCase().includes(q);
39+
item.hidden = !matches;
40+
}
41+
}, { signal });
42+
43+
this.querySelector('input').focus();
44+
}
45+
46+
disconnectedCallback() {
47+
this.aborter.abort();
48+
}
49+
})
50+
</script>
Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1 @@
1-
<header class="text-white">
2-
<section class="container px-6 pt-12 pb-12">
3-
<h2 class="mx-auto max-w-4xl text-5xl text-center font-bold leading-tight text-shadow">
4-
<%= "Research 🔍 specs and implementations" %>
5-
</h2>
6-
</section>
7-
</header>
8-
9-
<div class="bg-white py-16">
10-
<div class="container px-6">
11-
<h1>No query</h1>
12-
</div>
13-
</div>
1+
<%= render view_module(@conn), "_header.html", query: "", show_help: true %>

0 commit comments

Comments
 (0)