Skip to content

Commit 1e62977

Browse files
committed
Better TypeScript search
1 parent f6a784c commit 1e62977

4 files changed

Lines changed: 227 additions & 21 deletions

File tree

lib/components_guide/research/sources/typescript.ex

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,24 @@ defmodule ComponentsGuide.Research.Sources.Typescript do
4545
end
4646

4747
defmodule Interface do
48-
defstruct name: nil, line_start: nil, line_end: nil
48+
defstruct name: nil, doc: nil, line_start: nil, line_end: nil
49+
end
50+
51+
defmodule Namespace do
52+
defstruct name: nil, doc: nil, line_start: nil, line_end: nil
4953
end
5054

5155
defmodule GlobalVariable do
52-
defstruct name: nil, line_start: nil, line_end: nil
56+
defstruct name: nil, doc: nil, line_start: nil, line_end: nil
57+
end
58+
59+
defmodule GlobalFunction do
60+
defstruct name: nil, doc: nil, line_start: nil, line_end: nil
5361
end
5462

5563
defmodule Parser do
5664
defmodule State do
57-
defstruct mode: nil, output: []
65+
defstruct mode: nil, prev_doc: nil, output: []
5866
end
5967

6068
def parse(source) when is_binary(source) do
@@ -83,19 +91,104 @@ defmodule ComponentsGuide.Research.Sources.Typescript do
8391
end
8492

8593
defmodule ModeDefinition do
86-
defstruct type: nil, name: nil, line_start: nil, line_end: nil
94+
defstruct type: nil, name: nil, doc: nil, line_start: nil, line_end: nil
95+
end
96+
97+
defmodule JSDoc do
98+
defstruct message: nil, line_start: nil
99+
end
100+
101+
def do_reduce({"/**" <> rest_of_comment, n}, %State{mode: nil} = state) do
102+
{mode, prev_doc} =
103+
case rest_of_comment |> String.replace_suffix("*/", "") do
104+
# <<a:bytes-size(byte_size(rest_of_comment))>> -> {%ModeDefinition{type: :doc_comment, line_start: n}, nil}
105+
s when byte_size(rest_of_comment) == byte_size(s) ->
106+
{%JSDoc{message: String.trim(rest_of_comment), line_start: n}, nil}
107+
108+
message ->
109+
{nil, %JSDoc{message: String.trim(message), line_start: n}}
110+
end
111+
112+
%State{state | mode: mode, prev_doc: prev_doc}
87113
end
88114

89115
def do_reduce({"interface " <> name_and_extends, n}, %State{mode: nil} = state) do
90116
name = extract_name(name_and_extends)
91-
mode = %ModeDefinition{type: :interface, name: name, line_start: n}
92-
%State{state | mode: mode}
117+
118+
{doc, line_start} = read_prev_doc(state, n)
119+
120+
mode = %ModeDefinition{type: :interface, name: name, doc: doc, line_start: line_start}
121+
%State{state | mode: mode, prev_doc: nil}
122+
end
123+
124+
def do_reduce({"declare namespace " <> name_and_extends, n}, %State{mode: nil} = state) do
125+
name = extract_name(name_and_extends)
126+
127+
{doc, line_start} = read_prev_doc(state, n)
128+
129+
mode = %ModeDefinition{type: :namespace, name: name, doc: doc, line_start: line_start}
130+
%State{state | mode: mode, prev_doc: nil}
93131
end
94132

95133
def do_reduce({"declare var " <> name_and_extends, n}, %State{mode: nil} = state) do
96134
name = extract_name(name_and_extends)
97-
mode = %ModeDefinition{type: :global_var, name: name, line_start: n}
98-
%State{state | mode: mode}
135+
136+
case name_and_extends |> String.trim_trailing() |> String.ends_with?(";") do
137+
true ->
138+
{doc, line_start} = read_prev_doc(state, n)
139+
140+
output_item = %GlobalVariable{
141+
name: name,
142+
doc: doc,
143+
line_start: line_start,
144+
line_end: n
145+
}
146+
147+
%State{mode: nil, output: [output_item | state.output], prev_doc: nil}
148+
149+
false ->
150+
mode = %ModeDefinition{type: :global_var, name: name, line_start: n}
151+
%State{state | mode: mode, prev_doc: nil}
152+
end
153+
end
154+
155+
def do_reduce({"declare function " <> name_and_more, n}, %State{mode: nil} = state) do
156+
name = extract_name(name_and_more)
157+
158+
{doc, line_start} = read_prev_doc(state, n)
159+
160+
output_item = %GlobalFunction{
161+
name: name,
162+
doc: doc,
163+
line_start: line_start,
164+
line_end: n
165+
}
166+
167+
%State{mode: nil, output: [output_item | state.output], prev_doc: nil}
168+
end
169+
170+
def do_reduce({line, _n}, %State{mode: %JSDoc{message: message}} = state) do
171+
case line |> String.ends_with?("*/") do
172+
false ->
173+
line =
174+
case line |> String.trim() do
175+
"*" <> rest -> rest |> String.trim_leading()
176+
s -> s
177+
end
178+
179+
message =
180+
case message do
181+
"" -> line
182+
message -> message <> "\n" <> line
183+
end
184+
185+
mode = %JSDoc{state.mode | message: message}
186+
%State{state | mode: mode}
187+
188+
true ->
189+
prev_doc = %JSDoc{message: message, line_start: state.mode.line_start}
190+
%State{state | mode: nil, prev_doc: prev_doc}
191+
end
99192
end
100193

101194
def do_reduce({"}" <> _, n}, %State{mode: %ModeDefinition{}} = state) do
@@ -104,10 +197,28 @@ defmodule ComponentsGuide.Research.Sources.Typescript do
104197
output_item =
105198
case mode.type do
106199
:interface ->
107-
%Interface{name: mode.name, line_start: mode.line_start, line_end: n}
200+
%Interface{
201+
name: mode.name,
202+
doc: mode.doc,
203+
line_start: mode.line_start,
204+
line_end: n
205+
}
206+
207+
:namespace ->
208+
%Namespace{
209+
name: mode.name,
210+
doc: mode.doc,
211+
line_start: mode.line_start,
212+
line_end: n
213+
}
108214

109215
:global_var ->
110-
%GlobalVariable{name: mode.name, line_start: mode.line_start, line_end: n}
216+
%GlobalVariable{
217+
name: mode.name,
218+
doc: mode.doc,
219+
line_start: mode.line_start,
220+
line_end: n
221+
}
111222
end
112223

113224
%State{mode: nil, output: [output_item | state.output]}
@@ -116,10 +227,16 @@ defmodule ComponentsGuide.Research.Sources.Typescript do
116227
def do_reduce(_, state), do: state
117228

118229
defp extract_name(name_and_extends) do
119-
case String.split(name_and_extends, [" ", ":"], parts: 2) do
230+
case String.split(name_and_extends, [" ", ":", "("], parts: 2) do
120231
[name | _] -> name
121232
_ -> nil
122233
end
123234
end
235+
236+
defp read_prev_doc(%State{prev_doc: %JSDoc{message: doc, line_start: doc_n}}, _n) do
237+
{doc, doc_n}
238+
end
239+
240+
defp read_prev_doc(_, n), do: {nil, n}
124241
end
125242
end

lib/components_guide_web/controllers/research_controller.ex

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@ defmodule ComponentsGuideWeb.ResearchController do
99
alias ComponentsGuide.Research.Sources.Typescript
1010

1111
def show(conn, %{"section" => "dom-types"}) do
12+
as_ms = &System.convert_time_unit(&1, :native, :millisecond)
13+
1214
url = "https://cdn.jsdelivr.net/npm/typescript@4.7.4/lib/lib.dom.d.ts"
1315
{:ok, source} = ComponentsGuide.Research.Source.text_at(url)
16+
start = System.monotonic_time()
1417
types = Typescript.Parser.parse(source)
18+
duration = System.monotonic_time() - start
19+
IO.inspect(as_ms.(duration), label: "parse")
1520
types_sources = Typescript.Parser.extract_line_ranges(source, types)
21+
duration = System.monotonic_time() - start
22+
IO.inspect(as_ms.(duration), label: "parse + extract")
23+
24+
IO.inspect(types |> Enum.map(fn %{__struct__: type} -> type end) |> Enum.uniq(),
25+
label: "types"
26+
)
1627

17-
results = Enum.zip_with(types, types_sources, fn type, type_source ->
18-
%{name: type.name, source: type_source}
19-
end)
28+
results =
29+
Enum.zip_with(types, types_sources, fn type, type_source ->
30+
%{name: type.name, doc: type.doc, source: type_source}
31+
end)
2032

2133
conn
2234
|> assign(:page_title, "Search DOM Types")

lib/components_guide_web/templates/research/dom-types.html.heex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<dl>
1010
<%= for result <- @results do %>
1111
<div>
12-
<dt class="font-bold" data-body={result.source}><%= result.name %></dt>
12+
<dt class="font-bold" data-body={result.doc || "" <> " " <> result.source}><%= result.name %></dt>
1313
<dd><pre class="language-ts"><code><%= result.source %></code></pre></dd>
1414
</div>
1515
<% end %>

test/components_guide/research/sources/typescript_test.exs

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
defmodule ComponentsGuide.Research.Sources.TypescriptTest do
22
use ExUnit.Case, async: true
33

4-
alias ComponentsGuide.Research.Sources.Typescript.{Parser, Interface}
4+
alias ComponentsGuide.Research.Sources.Typescript.{
5+
Parser,
6+
Interface,
7+
Namespace,
8+
GlobalVariable,
9+
GlobalFunction
10+
}
511

612
@typescript_source ~S"""
713
/*! *****************************************************************************
@@ -52,29 +58,100 @@ defmodule ComponentsGuide.Research.Sources.TypescriptTest do
5258
prototype: Blob;
5359
new(blobParts?: BlobPart[], options?: BlobPropertyBag): Blob;
5460
};
61+
62+
/**
63+
* Posts a message to the given window. Messages can be structured objects, e.g. nested objects and arrays, can contain JavaScript values (strings, numbers, Date objects, etc), and can contain certain data objects such as File Blob, FileList, and ArrayBuffer objects.
64+
*
65+
* Objects listed in the transfer member of options are transferred, not just cloned, meaning that they are no longer usable on the sending side.
66+
*
67+
* A target origin can be specified using the targetOrigin member of options. If not provided, it defaults to "/". This default restricts the message to same-origin targets only.
68+
*
69+
* If the origin of the target window doesn't match the given target origin, the message is discarded, to avoid information leakage. To send the message to the target regardless of origin, set the target origin to "*".
70+
*
71+
* Throws a "DataCloneError" DOMException if transfer array contains duplicate objects or if message could not be cloned.
72+
*/
73+
declare function postMessage(message: any, targetOrigin: string, transfer?: Transferable[]): void;
74+
75+
/** Moves the focus to the window's browsing context, if any. */
76+
declare function focus(): void;
77+
78+
declare function requestAnimationFrame(callback: FrameRequestCallback): number;
79+
80+
declare var console: Console;
81+
82+
/** Holds useful CSS-related methods. No object with this interface are implemented: it contains only static methods and therefore is a utilitarian interface. */
83+
declare namespace CSS {
84+
function escape(ident: string): string;
85+
function supports(property: string, value: string): boolean;
86+
function supports(conditionText: string): boolean;
87+
}
5588
"""
5689

5790
test "parse" do
5891
assert Parser.parse(@typescript_source) == [
59-
%ComponentsGuide.Research.Sources.Typescript.Interface{
92+
%Interface{
6093
name: "AddEventListenerOptions",
6194
line_start: 24,
6295
line_end: 28
6396
},
64-
%ComponentsGuide.Research.Sources.Typescript.Interface{
97+
%Interface{
6598
name: "AesCbcParams",
6699
line_start: 30,
67100
line_end: 32
68101
},
69-
%ComponentsGuide.Research.Sources.Typescript.Interface{
102+
%Interface{
70103
name: "Blob",
71-
line_start: 35,
104+
doc:
105+
"A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system.",
106+
line_start: 34,
72107
line_end: 42
73108
},
74-
%ComponentsGuide.Research.Sources.Typescript.GlobalVariable{
109+
%GlobalVariable{
75110
name: "Blob",
76111
line_start: 44,
77112
line_end: 47
113+
},
114+
%GlobalFunction{
115+
name: "postMessage",
116+
doc:
117+
~S"""
118+
Posts a message to the given window. Messages can be structured objects, e.g. nested objects and arrays, can contain JavaScript values (strings, numbers, Date objects, etc), and can contain certain data objects such as File Blob, FileList, and ArrayBuffer objects.
119+
120+
Objects listed in the transfer member of options are transferred, not just cloned, meaning that they are no longer usable on the sending side.
121+
122+
A target origin can be specified using the targetOrigin member of options. If not provided, it defaults to "/". This default restricts the message to same-origin targets only.
123+
124+
If the origin of the target window doesn't match the given target origin, the message is discarded, to avoid information leakage. To send the message to the target regardless of origin, set the target origin to "*".
125+
126+
Throws a "DataCloneError" DOMException if transfer array contains duplicate objects or if message could not be cloned.
127+
"""
128+
|> String.trim_trailing(),
129+
line_start: 49,
130+
line_end: 60
131+
},
132+
%GlobalFunction{
133+
name: "focus",
134+
doc: "Moves the focus to the window's browsing context, if any.",
135+
line_start: 62,
136+
line_end: 63
137+
},
138+
%GlobalFunction{
139+
name: "requestAnimationFrame",
140+
line_start: 65,
141+
line_end: 65
142+
},
143+
%GlobalVariable{
144+
name: "console",
145+
doc: nil,
146+
line_start: 67,
147+
line_end: 67
148+
},
149+
%Namespace{
150+
name: "CSS",
151+
doc:
152+
"Holds useful CSS-related methods. No object with this interface are implemented: it contains only static methods and therefore is a utilitarian interface.",
153+
line_start: 69,
154+
line_end: 74
78155
}
79156
]
80157
end

0 commit comments

Comments
 (0)