├── 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 | ![screenshot](screenshot.png) 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 | --------------------------------------------------------------------------------