├── dist
└── .gitkeep
├── screenshot.png
├── .gitignore
├── README.md
├── Ports.elm
├── elm-package.json
├── package.json
├── app.js
├── index.html
└── Main.elm
/dist/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fiatjaf/jq-finder/HEAD/screenshot.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules
3 | elm-stuff
4 | *.swo
5 | *.swp
6 | browserify-cache.json
7 | jq.wasm.wasm
8 | jq.wasm.js
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | https://jq.alhur.es/finder/
2 |
3 | 
4 |
5 | A tool inspired by https://chrome.google.com/webstore/detail/json-finder/flhdcaebggmmpnnaljiajhihdfconkbj and written with https://github.com/fiatjaf/jq-web.
6 |
7 |
--------------------------------------------------------------------------------
/Ports.elm:
--------------------------------------------------------------------------------
1 | port module Ports exposing (..)
2 |
3 | port applyfilter : (String, Int, List String) -> Cmd msg
4 | port scrollintopanel : Int -> Cmd msg
5 | port savepanelwidth : (Int, Int) -> Cmd msg
6 |
7 | port gotresult : ((Int, String) -> msg) -> Sub msg
8 | port goterror : ((Int, String) -> msg) -> Sub msg
9 |
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "helpful summary of your project, less than 80 characters",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "."
8 | ],
9 | "exposed-modules": [],
10 | "dependencies": {
11 | "elm-lang/core": "5.1.1 <= v < 6.0.0",
12 | "elm-lang/html": "2.0.0 <= v < 3.0.0",
13 | "elm-lang/http": "1.0.0 <= v < 2.0.0",
14 | "elm-lang/mouse": "1.0.1 <= v < 2.0.0"
15 | },
16 | "elm-version": "0.18.0 <= v < 0.19.0"
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "debounce": "^1.1.0",
4 | "debounce-with-args": "^1.0.1"
5 | },
6 | "devDependencies": {
7 | "browserify": "^11.0.1",
8 | "browserify-incremental": "^3.1.1"
9 | },
10 | "scripts": {
11 | "watch-js": "ls *.js | entr browserifyinc app.js -vd -o dist/bundle.js",
12 | "build-js": "browserify app.js -vd -o dist/bundle.js",
13 | "watch-elm": "ls *.elm | entr fish -c 'elm make Main.elm --output dist/elm.js'",
14 | "build-elm": "elm make --yes Main.elm --output dist/elm.js",
15 | "setup-wasm": "cp node_modules/jq-web/jq.wasm.wasm ./"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | /* global Elm, jq */
2 |
3 | const debounceWithArgs = require('debounce-with-args')
4 |
5 | const target = document.querySelector('main')
6 |
7 | const app = Elm.Main.embed(target, {
8 | input: localStorage.getItem('input') || '',
9 | filters: JSON.parse(localStorage.getItem('filters') || '["."]'),
10 | widths: JSON.parse(localStorage.getItem('widths') || '[250]')
11 | })
12 |
13 | app.ports.scrollintopanel.subscribe(paneln => {
14 | setTimeout(() => {
15 | let el = document.getElementsByClassName('panel')[paneln]
16 | el.scrollIntoView({behavior: 'instant', block: 'end', inline: 'nearest'})
17 | }, 250)
18 | })
19 |
20 | app.ports.savepanelwidth.subscribe(([paneln, width]) => {
21 | var storedwidths = JSON.parse(localStorage.getItem('widths') || '[]')
22 | if (storedwidths.length < paneln + 1) {
23 | storedwidths.push(width)
24 | } else {
25 | storedwidths[paneln] = width
26 | }
27 | if (storedwidths.length === 0) storedwidths = ['.']
28 | localStorage.setItem('widths', JSON.stringify(storedwidths))
29 | })
30 |
31 | app.ports.applyfilter.subscribe(
32 | debounceWithArgs(
33 | applyfilter,
34 | 600,
35 | args => args[0][1]
36 | )
37 | )
38 |
39 | function applyfilter ([raw, i, filters]) {
40 | let filter = filters
41 | .filter(x => x.trim())
42 | .join('|') || '.'
43 |
44 | let prelude = '. as $input | '
45 |
46 | console.log('jq', filters)
47 |
48 | jq.promised.raw(raw, prelude + filter)
49 | .then(res => app.ports.gotresult.send([i, res]))
50 | .catch(e => {
51 | if (typeof e === 'string' && e.slice(0, 5) === 'abort') {
52 | setTimeout(applyfilter, 500, [raw, i, filters])
53 | return
54 | }
55 | console.error(e)
56 | app.ports.goterror.send([i, e.message])
57 | })
58 |
59 | var storedfilters = JSON.parse(localStorage.getItem('filters') || '[]')
60 | for (let i = 0; i < filters.length; i++) {
61 | if (storedfilters.length < i + 1) {
62 | storedfilters.push(filters[i])
63 | } else {
64 | storedfilters[i] = filters[i]
65 | }
66 | }
67 | if (storedfilters.length === 0) storedfilters = ['.']
68 | localStorage.setItem('filters', JSON.stringify(storedfilters))
69 | localStorage.setItem('input', raw)
70 | }
71 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
jq finder
5 |
6 |
123 |
124 |
145 |
146 |
154 |
155 |
156 |
157 |
158 |
174 |
--------------------------------------------------------------------------------
/Main.elm:
--------------------------------------------------------------------------------
1 | import Html exposing (..)
2 | import Html.Attributes exposing (..)
3 | import Html.Events exposing (onClick, onInput, on)
4 | import Http
5 | import Platform.Sub as Sub
6 | import Mouse
7 | import Tuple exposing (second)
8 | import Json.Decode as J exposing
9 | ( string, float, null, bool, dict, oneOf
10 | , Value, Decoder, decodeString, decodeValue
11 | )
12 | import Dict exposing (Dict)
13 | import List exposing (take, any, range, intersperse, concat)
14 | import Array exposing (Array, get, set, push, length)
15 | import Char exposing (toCode)
16 | import String exposing (trim, join, startsWith)
17 |
18 | import Ports exposing (..)
19 |
20 |
21 | main =
22 | Html.programWithFlags
23 | { init = init
24 | , view = view
25 | , update = update
26 | , subscriptions = subscriptions
27 | }
28 |
29 |
30 | -- MODEL
31 |
32 | type alias Model =
33 | { tab : Tab
34 | , input : String
35 | , url : Maybe String
36 | , panels : Array Panel
37 | , drag : Maybe (Int, Int)
38 | }
39 |
40 | type Tab = Input | View
41 |
42 | type alias Panel =
43 | { enabled : Bool
44 | , filter : String
45 | , output : Result String String
46 | , w : Int
47 | }
48 |
49 | getfiltersuntil : Int -> Array Panel -> List String
50 | getfiltersuntil to panels =
51 | panels
52 | |> Array.toList
53 | |> take (to + 1)
54 | |> List.map .filter
55 |
56 | setfilter : Int -> String -> Array Panel -> Array Panel
57 | setfilter index filter panels =
58 | case get index panels of
59 | Nothing -> panels
60 | Just p -> set index { p | filter = filter } panels
61 |
62 | enable : Int -> Array Panel -> Array Panel
63 | enable index panels =
64 | case get index panels of
65 | Nothing -> panels
66 | Just p -> set index { p | enabled = True } panels
67 |
68 | disablefrom : Int -> Array Panel -> Array Panel
69 | disablefrom index panels =
70 | Array.indexedMap (\i p -> if i >= index then { p | enabled = False } else p) panels
71 |
72 | setoutput : Int -> String -> Array Panel -> Array Panel
73 | setoutput index output panels =
74 | case get index panels of
75 | Nothing -> panels
76 | Just p -> set index { p | output = Ok output } panels
77 |
78 | seterror : Int -> String -> Array Panel -> Array Panel
79 | seterror index error panels =
80 | case get index panels of
81 | Nothing -> panels
82 | Just p -> set index { p | output = Err error } panels
83 |
84 | hasNonAsciiLetter : String -> Bool
85 | hasNonAsciiLetter s =
86 | String.toList s
87 | |> any
88 | (\char ->
89 | let
90 | code = toCode char
91 | in
92 | (code < 65) ||
93 | (code > 90 && code < 97) ||
94 | (code > 122)
95 | )
96 |
97 | init : { input : String, filters : List String, widths : List Int } -> (Model, Cmd Msg)
98 | init {input, filters, widths} =
99 | let model =
100 | { tab = View
101 | , input = input
102 | , url = Nothing
103 | , panels = Array.fromList
104 | <| List.map2 (\f w -> Panel True f (Ok "") w) filters widths
105 | , drag = Nothing
106 | }
107 | in
108 | ( model
109 | , Cmd.batch
110 | <| List.map
111 | (\pi -> applyfilter (model.input, pi, model.panels |> getfiltersuntil pi))
112 | <| range 0 (Array.length model.panels)
113 | )
114 |
115 |
116 | -- UPDATE
117 |
118 | type Msg
119 | = SelectTab Tab
120 | | SetInput Bool String
121 | | InputURLResult (Result Http.Error String)
122 | | SetFilter Int String
123 | | SelectDictItem Int String
124 | | SelectListItem Int Int
125 | | GotResult (Int, String)
126 | | GotError (Int, String)
127 | | GoToPanel Int
128 | | DragStart Int Mouse.Position
129 | | DragAt Mouse.Position
130 | | DragEnd Mouse.Position
131 |
132 | update : Msg -> Model -> (Model, Cmd Msg)
133 | update msg model =
134 | case msg of
135 | SelectTab t -> ({ model | tab = t }, Cmd.none)
136 | SetInput manual v ->
137 | if v |> startsWith "http"
138 | then
139 | ( { model | url = Just <| trim v }
140 | , Http.getString (trim v)
141 | |> Http.send InputURLResult
142 | )
143 | else
144 | ( { model | input = v, url = if manual then Nothing else model.url }
145 | , Cmd.batch
146 | <| List.map
147 | (\pi -> applyfilter (model.input, pi, model.panels |> getfiltersuntil pi))
148 | <| range 0 (Array.length model.panels)
149 | )
150 | InputURLResult res ->
151 | case res of
152 | Ok json -> update (SetInput False json) model
153 | Err err -> update (SetInput False (toString err)) model
154 | SetFilter i v ->
155 | let upd = { model | panels = model.panels |> setfilter i v }
156 | in
157 | case v of
158 | "" -> -- erase this output and disable all panels next
159 | ( { upd | panels = upd.panels
160 | |> setoutput i ""
161 | |> disablefrom (i + 1)
162 | }
163 | , Cmd.none
164 | )
165 | _ ->
166 | ( if length upd.panels == i + 1
167 | then -- add a new panel
168 | { upd | panels = upd.panels |> push (Panel True "" (Ok "") 250) }
169 | else -- enable the next panel
170 | { upd | panels = upd.panels |> enable (i + 1) }
171 | , Cmd.batch
172 | [ Cmd.batch
173 | <| List.map
174 | (\pi ->
175 | let filters = (upd.panels |> getfiltersuntil pi)
176 | in applyfilter (model.input, pi, filters)
177 | )
178 | <| range i <| (Array.length upd.panels) - 1
179 | , scrollintopanel (i + 1)
180 | , case get (i + 1) upd.panels of
181 | Nothing -> savepanelwidth ((i + 1), 250)
182 | Just panel -> savepanelwidth ((i + 1), panel.w)
183 | ]
184 | )
185 | SelectDictItem paneln key ->
186 | if hasNonAsciiLetter key
187 | then update (SetFilter (paneln + 1) (".[" ++ toString key ++ "]")) model
188 | else update (SetFilter (paneln + 1) ("." ++ key)) model
189 | SelectListItem paneln index ->
190 | update (SetFilter (paneln + 1) (".[" ++ toString index ++ "]")) model
191 | GotResult (i, v) ->
192 | ( { model | panels = model.panels |> setoutput i v }
193 | , Cmd.none
194 | )
195 | GotError (i, e) ->
196 | ( { model
197 | | panels = model.panels
198 | |> seterror i e
199 | |> disablefrom (i + 1)
200 | }
201 | , Cmd.none
202 | )
203 | GoToPanel paneln ->
204 | ( model
205 | , scrollintopanel paneln
206 | )
207 | DragStart paneln {x} ->
208 | ( case get paneln model.panels of
209 | Just panel ->
210 | { model | drag = Just (paneln, x - panel.w) }
211 | Nothing -> model
212 | , Cmd.none
213 | )
214 | DragAt {x} ->
215 | ( case model.drag of
216 | Just (paneln, initx) ->
217 | case get paneln model.panels of
218 | Just panel ->
219 | { model | panels = model.panels |> set paneln { panel | w = x - initx } }
220 | Nothing -> model
221 | Nothing -> model
222 | , Cmd.none
223 | )
224 | DragEnd _ ->
225 | ( { model | drag = Nothing }
226 | , case model.drag of
227 | Nothing -> Cmd.none
228 | Just (paneln, _) ->
229 | case get paneln model.panels of
230 | Nothing -> Cmd.none
231 | Just panel -> savepanelwidth (paneln, panel.w)
232 | )
233 |
234 |
235 | -- SUBSCRIPTIONS
236 |
237 | subscriptions : Model -> Sub Msg
238 | subscriptions model =
239 | Sub.batch
240 | [ gotresult GotResult
241 | , goterror GotError
242 | , case model.drag of
243 | Nothing -> Sub.none
244 | Just _ ->
245 | Sub.batch [ Mouse.moves DragAt, Mouse.ups DragEnd ]
246 | ]
247 |
248 |
249 | -- VIEW
250 |
251 | view : Model -> Html Msg
252 | view model =
253 | div []
254 | [ div [ class "tabs is-centered is-boxed" ]
255 | [ ul []
256 | [ li [ class <| if model.tab == Input then "is-active" else "" ]
257 | [ a [ onClick (SelectTab Input) ]
258 | [ text "JSON Input"
259 | ]
260 | ]
261 | , li [ class <| if model.tab == View then "is-active" else "" ]
262 | [ a [ onClick (SelectTab View) ]
263 | [ text "View"
264 | ]
265 | ]
266 | ]
267 | ]
268 | , div [ class "container", id "resulting-filter" ]
269 | <| (::) (text ". as $input | ")
270 | <| intersperse (text " | ")
271 | <| List.map
272 | (\(i, f) ->
273 | span
274 | [ class "partial-filter"
275 | , onClick (GoToPanel i)
276 | ] [ text f ]
277 | )
278 | <| List.filter (second >> trim >> (/=) "")
279 | <| List.indexedMap (\i p -> (i, p.filter))
280 | <| List.filter .enabled
281 | <| Array.toList model.panels
282 | , case model.tab of
283 | Input ->
284 | div [ id "input" ]
285 | [ div [ class "container has-text-centered" ] [ text <| Maybe.withDefault "" model.url ]
286 | , textarea
287 | [ class "textarea"
288 | , onInput (SetInput True)
289 | , value model.input
290 | , placeholder "Paste and URL here, or your JSON string directly."
291 | ] [ text model.input ]
292 | ]
293 | View ->
294 | div [ id "panels" ]
295 | <| List.indexedMap viewPanel
296 | <| List.filter .enabled
297 | <| Array.toList model.panels
298 | ]
299 |
300 | viewPanel : Int -> Panel -> Html Msg
301 | viewPanel i {filter, output, w} =
302 | div
303 | [ class "panel"
304 | , style [("width", toString w ++ "px")]
305 | ]
306 | [ input [ class "input", onInput (SetFilter i), value filter ] []
307 | , div [ class "box" ]
308 | [ case output of
309 | Ok json -> viewJSON i json
310 | Err err -> div [ class "error" ] [ text err ]
311 | ]
312 | , div
313 | [ class "resize-handle"
314 | , on "mousedown" (J.map (DragStart i) Mouse.position)
315 | ] []
316 | ]
317 |
318 |
319 | type JRepr
320 | = JScalar JScalarRepr
321 | | JDict (Dict String Value)
322 | | JList (List Value)
323 |
324 | type JScalarRepr
325 | = JNull
326 | | JString String
327 | | JBool Bool
328 | | JNum Float
329 |
330 | multiDecoder : Decoder JRepr
331 | multiDecoder = oneOf
332 | [ J.map JScalar <| J.map JString string
333 | , J.map JScalar <| J.map JBool bool
334 | , J.map JScalar <| J.map JNum float
335 | , J.map JScalar <| null JNull
336 | , J.map JDict (dict J.value)
337 | , J.map JList (J.list J.value)
338 | ]
339 |
340 | viewJSON : Int -> String -> Html Msg
341 | viewJSON paneln json =
342 | case decodeString multiDecoder json of
343 | Ok jrepr -> case jrepr of
344 | JScalar scalar -> scalarView scalar
345 | JDict d -> table [ class "table is-fullwidth is-hoverable" ]
346 | [ tbody []
347 | <| List.map
348 | (\(k, v) ->
349 | tr [ onClick (SelectDictItem paneln k) ]
350 | [ td [] [ text k ]
351 | , td [] [ viewValue v ]
352 | ]
353 | )
354 | <| Dict.toList d
355 | ]
356 | JList l -> table [ class "table is-fullwidth is-hoverable" ]
357 | [ tbody []
358 | <| List.indexedMap
359 | (\i v ->
360 | tr [ onClick (SelectListItem paneln i) ]
361 | [ td [] [ text <| toString i ]
362 | , td [] [ viewValue v ]
363 | ]
364 | )
365 | <| l
366 | ]
367 | Err e -> text json
368 |
369 | viewValue : Value -> Html Msg
370 | viewValue jval =
371 | case decodeValue multiDecoder jval of
372 | Ok jrepr -> case jrepr of
373 | JScalar scalar -> scalarView scalar
374 | JDict d -> div [ class "sub dict" ]
375 | <| concat
376 | [ [ text "{" ]
377 | , intersperse (text ", ")
378 | <| List.map dictSubView
379 | <| take 3
380 | <| Dict.toList d
381 | , if Dict.size d > 3 then [ text ", …" ] else []
382 | , [ text "}" ]
383 | ]
384 | JList l -> div [ class "sub list" ]
385 | <| concat
386 | [ [ text "[" ]
387 | , intersperse (text ", ")
388 | <| List.map arraySubView
389 | <| take 3
390 | <| l
391 | , if List.length l > 3 then [ text ", …" ] else []
392 | , [ text "]" ]
393 | ]
394 | Err e -> text e
395 |
396 | dictSubView : (String, Value) -> Html Msg
397 | dictSubView (k, _) = span [ class "key" ] [ text k ]
398 |
399 | arraySubView : Value -> Html Msg
400 | arraySubView jval =
401 | case decodeValue multiDecoder jval of
402 | Ok jrepr -> case jrepr of
403 | JScalar scalar -> scalarView scalar
404 | JDict _ -> text "{}"
405 | JList _ -> text "[]"
406 | Err e -> text e
407 |
408 | scalarView : JScalarRepr -> Html Msg
409 | scalarView jrepr = case jrepr of
410 | JNull -> span [ class "null" ] [ text "null" ]
411 | JBool b -> span [ class "bool" ] [ text <| if b then "true" else "false" ]
412 | JNum n -> span [ class "num" ] [ text <| toString n ]
413 | JString s -> span [ class "string" ] [ text <| "\"" ++ s ++ "\"" ]
414 |
--------------------------------------------------------------------------------