├── .gitignore
├── secrets.sample.ink
├── static
├── img
│ ├── kin.png
│ ├── kin-embed-demo.png
│ ├── kin-ink-project.png
│ ├── kin-mobile-panes.png
│ ├── kin-image-preview.png
│ ├── kin-mobile-sidebar.png
│ ├── kin-nextjs-project.png
│ └── kin-markdown-project.png
├── embed.html
├── index.html
├── css
│ ├── highlight.css
│ └── main.css
├── demo.html
├── js
│ ├── ink.js
│ └── torus.min.js
└── ink
│ └── lib.js
├── test
└── main.ink
├── vendor
├── auth.ink
├── iota.ink
├── mime.ink
├── quicksort.ink
├── suite.ink
├── percent.ink
├── http.ink
├── route.ink
├── str.ink
├── json.ink
├── std.ink
└── tokenize.ink
├── lib
├── stub.ink
├── github.ink
├── cache.ink
└── torus.js.ink
├── kin.service
├── LICENSE
├── Makefile
├── src
├── highlight.ink
├── main.ink
└── app.js.ink
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | secrets.ink
2 |
--------------------------------------------------------------------------------
/secrets.sample.ink:
--------------------------------------------------------------------------------
1 | AccessToken := 'ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
2 |
--------------------------------------------------------------------------------
/static/img/kin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin.png
--------------------------------------------------------------------------------
/static/img/kin-embed-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin-embed-demo.png
--------------------------------------------------------------------------------
/test/main.ink:
--------------------------------------------------------------------------------
1 | s := (load('../vendor/suite').suite)(
2 | 'Kin test suite'
3 | )
4 |
5 | (s.end)()
6 |
7 |
--------------------------------------------------------------------------------
/static/img/kin-ink-project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin-ink-project.png
--------------------------------------------------------------------------------
/static/img/kin-mobile-panes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin-mobile-panes.png
--------------------------------------------------------------------------------
/static/img/kin-image-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin-image-preview.png
--------------------------------------------------------------------------------
/static/img/kin-mobile-sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin-mobile-sidebar.png
--------------------------------------------------------------------------------
/static/img/kin-nextjs-project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin-nextjs-project.png
--------------------------------------------------------------------------------
/static/img/kin-markdown-project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesephist/kin/HEAD/static/img/kin-markdown-project.png
--------------------------------------------------------------------------------
/vendor/auth.ink:
--------------------------------------------------------------------------------
1 | ` auth service for the single-user app `
2 |
3 | ` TODO: improve security `
4 | allow? := req => true
5 |
--------------------------------------------------------------------------------
/lib/stub.ink:
--------------------------------------------------------------------------------
1 | ` Ink stub to paper over JS / Ink environment differences
2 | when loaded into a web environment. `
3 |
4 | load := s => window
5 |
--------------------------------------------------------------------------------
/vendor/iota.ink:
--------------------------------------------------------------------------------
1 | ` generator for consecutive ints, to make clean enums `
2 |
3 | new := () => self := {
4 | i: ~1
5 | next: () => (
6 | self.i := self.i + 1
7 | self.i
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/vendor/mime.ink:
--------------------------------------------------------------------------------
1 | ` mime type `
2 |
3 | str := load('../vendor/str')
4 | split := str.split
5 |
6 | MimeTypes := {
7 | 'html': 'text/html'
8 | 'css': 'text/css'
9 | 'js': 'application/javascript'
10 | 'json': 'application/json'
11 | 'ink': 'text/plain'
12 |
13 | 'jpg': 'image/jpeg'
14 | 'png': 'image/png'
15 | 'svg': 'image/svg+xml'
16 | }
17 |
18 | forPath := path => (
19 | parts := split(path, '.')
20 | ending := parts.(len(parts) - 1)
21 |
22 | guess := MimeTypes.(ending) :: {
23 | () -> 'application/octet-stream'
24 | _ -> guess
25 | }
26 | )
27 |
--------------------------------------------------------------------------------
/kin.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=kin server
3 | ConditionPathExists=/home/kin-user/go/bin/ink
4 | After=network.target
5 |
6 | [Service]
7 | Type=simple
8 | User=kin-user
9 | LimitNOFILE=1024
10 | PermissionsStartOnly=true
11 |
12 | Restart=on-failure
13 | RestartSec=100ms
14 | StartLimitIntervalSec=60
15 |
16 | WorkingDirectory=/home/kin-user/kin
17 | ExecStart=/home/kin-user/go/bin/ink ./src/main.ink
18 |
19 | # make sure log directory exists and owned by syslog
20 | PermissionsStartOnly=true
21 | ExecStartPre=/bin/mkdir -p /var/log/kin
22 | ExecStartPre=/bin/chown syslog:adm /var/log/kin
23 | ExecStartPre=/bin/chmod 755 /var/log/kin
24 | StandardOutput=syslog
25 | StandardError=syslog
26 | SyslogIdentifier=kin
27 |
28 | [Install]
29 | WantedBy=multi-user.target
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Linus Lee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/vendor/quicksort.ink:
--------------------------------------------------------------------------------
1 | ` minimal quicksort implementation
2 | using hoare partition `
3 |
4 | std := load('../vendor/std')
5 |
6 | map := std.map
7 | clone := std.clone
8 |
9 | sortBy := (v, pred) => (
10 | vPred := map(v, pred)
11 | partition := (v, lo, hi) => (
12 | pivot := vPred.(lo)
13 | lsub := i => (vPred.(i) < pivot) :: {
14 | true -> lsub(i + 1)
15 | false -> i
16 | }
17 | rsub := j => (vPred.(j) > pivot) :: {
18 | true -> rsub(j - 1)
19 | false -> j
20 | }
21 | (sub := (i, j) => (
22 | i := lsub(i)
23 | j := rsub(j)
24 | (i < j) :: {
25 | false -> j
26 | true -> (
27 | ` inlined swap! `
28 | tmp := v.(i)
29 | tmpPred := vPred.(i)
30 | v.(i) := v.(j)
31 | v.(j) := tmp
32 | vPred.(i) := vPred.(j)
33 | vPred.(j) := tmpPred
34 |
35 | sub(i + 1, j - 1)
36 | )
37 | }
38 | ))(lo, hi)
39 | )
40 | (quicksort := (v, lo, hi) => len(v) :: {
41 | 0 -> v
42 | _ -> (lo < hi) :: {
43 | false -> v
44 | true -> (
45 | p := partition(v, lo, hi)
46 | quicksort(v, lo, p)
47 | quicksort(v, p + 1, hi)
48 | )
49 | }
50 | })(v, 0, len(v) - 1)
51 | )
52 |
53 | sort! := v => sortBy(v, x => x)
54 |
55 | sort := v => sort!(clone(v))
56 |
--------------------------------------------------------------------------------
/lib/github.ink:
--------------------------------------------------------------------------------
1 | ` GitHub API `
2 |
3 | std := load('../vendor/std')
4 |
5 | log := std.log
6 | f := std.format
7 |
8 | json := load('../vendor/json')
9 | serJSON := json.ser
10 | deJSON := json.de
11 |
12 | cache := load('cache')
13 | secrets := load('../secrets')
14 |
15 | AccessToken := secrets.AccessToken
16 | APIRoot := 'https://api.github.com'
17 | GitHubV3Accept := 'application/vnd.github.v3+json'
18 | UserAgent := 'ink, dotink.co'
19 |
20 | Cache := (cache.new)()
21 | cacheGet := Cache.get
22 |
23 | getAPI := (path, withResp) => (
24 | request := {
25 | url: APIRoot + path
26 | headers: {
27 | 'Accept': GitHubV3Accept
28 | 'User-Agent': UserAgent
29 | 'Authorization': 'token ' + AccessToken
30 | }
31 | }
32 | cacheGet(request, resp => withResp(resp))
33 | )
34 |
35 | ` Get repository JSON with name as user/repo `
36 | getRepo := (name, withRepo) => (
37 | getAPI('/repos/' + name, resp => resp :: {
38 | () -> withRepo(())
39 | _ -> withRepo(deJSON(resp))
40 | })
41 | )
42 |
43 | getContents := (name, path, withContents) => (
44 | getAPI(
45 | f('/repos/{{ 0 }}/contents{{ 1 }}', [name, path])
46 | resp => resp :: {
47 | () -> withContents(())
48 | _ -> withContents(deJSON(resp))
49 | }
50 | )
51 | )
52 |
53 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: run
2 |
3 | # run app server
4 | run:
5 | ink src/main.ink
6 |
7 | # build dependencies
8 | build-libs:
9 | september translate \
10 | lib/stub.ink \
11 | vendor/std.ink \
12 | vendor/str.ink \
13 | vendor/quicksort.ink \
14 | > static/ink/lib.js
15 |
16 | # build september
17 | build-september:
18 | september translate \
19 | ../september/src/iota.ink \
20 | ../september/src/tokenize.ink \
21 | > static/ink/september.js
22 |
23 | # build merlot
24 | build-merlot:
25 | september translate \
26 | ../merlot/lib/reader.ink \
27 | ../merlot/lib/md.ink \
28 | > static/ink/merlot.js
29 |
30 | # build app clients
31 | build:
32 | cat static/js/ink.js \
33 | static/js/torus.min.js \
34 | static/js/highlight.js \
35 | > static/ink/vendor.js
36 | september translate \
37 | src/highlight.ink \
38 | lib/torus.js.ink \
39 | src/app.js.ink \
40 | | tee /dev/stderr > static/ink/common.js
41 | cat \
42 | static/ink/vendor.js \
43 | static/ink/lib.js \
44 | static/ink/september.js \
45 | static/ink/merlot.js \
46 | static/ink/common.js \
47 | > static/ink/bundle.js
48 | b: build
49 |
50 | # run all builds from scratch
51 | build-all: build-libs build-september build-merlot build
52 |
53 | # build whenever Ink sources change
54 | watch:
55 | ls lib/* src/* | entr make build
56 | w: watch
57 |
58 | # run all tests under test/
59 | check:
60 | ink ./test/main.ink
61 | t: check
62 |
63 | fmt:
64 | inkfmt fix lib/*.ink src/*.ink test/*.ink
65 | f: fmt
66 |
67 |
--------------------------------------------------------------------------------
/vendor/suite.ink:
--------------------------------------------------------------------------------
1 | ` ink standard test suite tools `
2 |
3 | std := load('std')
4 |
5 | ` borrow from std `
6 | log := std.log
7 | each := std.each
8 | f := std.format
9 |
10 | ` suite constructor `
11 | suite := label => (
12 | ` suite data store `
13 | s := {
14 | all: 0
15 | passed: 0
16 | msgs: []
17 | }
18 |
19 | ` mark sections of a test suite with human labels `
20 | mark := label => s.msgs.len(s.msgs) := '- ' + label
21 |
22 | ` signal end of test suite, print out results `
23 | end := () => (
24 | log(f('suite: {{ label }}', {label: label}))
25 | each(s.msgs, m => log(' ' + m))
26 | s.passed :: {
27 | s.all -> log(f('ALL {{ passed }} / {{ all }} PASSED', s))
28 | _ -> (
29 | log(f('PARTIAL: {{ passed }} / {{ all }} PASSED', s))
30 | exit(1)
31 | )
32 | }
33 | )
34 |
35 | ` log a passed test `
36 | onSuccess := () => (
37 | s.all := s.all + 1
38 | s.passed := s.passed + 1
39 | )
40 |
41 | ` log a failed test `
42 | onFail := msg => (
43 | s.all := s.all + 1
44 | s.msgs.len(s.msgs) := msg
45 | )
46 |
47 | ` perform a new test case `
48 | indent := ' ' + ' ' + ' ' + ' '
49 | test := (label, result, expected) => result :: {
50 | expected -> onSuccess()
51 | _ -> (
52 | msg := f(' * {{ label }}
53 | {{ indent }}got {{ result }}
54 | {{ indent }}exp {{ expected }}', {
55 | label: label
56 | result: result
57 | expected: expected
58 | indent: indent
59 | })
60 | onFail(msg)
61 | )
62 | }
63 |
64 | ` expose API functions `
65 | {
66 | mark: mark
67 | test: test
68 | end: end
69 | }
70 | )
71 |
--------------------------------------------------------------------------------
/static/embed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ fileName }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
{{ lineNos }}
29 |
{{ prog }}
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ink codebase browser
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/lib/cache.ink:
--------------------------------------------------------------------------------
1 | ` HTTP request LR cache `
2 |
3 | std := load('../vendor/std')
4 |
5 | log := std.log
6 | f := std.format
7 | reduce := std.reduce
8 |
9 | ` Max number of requests that will be cached `
10 | MaxSize := 50
11 | ` Number of seconds after which the request must be re-ferched `
12 | ExpirySecs := 60
13 |
14 | ` Create a new cache `
15 | new := () => (
16 | ` Map `
20 | cache := {}
21 |
22 | cached? := request => cachedResp := cache.(request.url) :: {
23 | () -> false
24 | _ -> cachedResp.timestamp + ExpirySecs < time() :: {
25 | true -> false
26 | _ -> true
27 | }
28 | }
29 |
30 | evictLR := () => (
31 | oldest := reduce(keys(cache), (oldest, k) => entry := cache.(k) :: {
32 | () -> oldest
33 | _ -> entry.timestamp < oldest.timestamp :: {
34 | true -> {
35 | url: k
36 | timestamp: entry.timestamp
37 | }
38 | _ -> oldest
39 | }
40 | }, {url: '//invalid', timestamp: time() + 1})
41 |
42 | log(f('[cache] evict {{ url }}', oldest))
43 | cache.(oldest.url) := ()
44 | )
45 |
46 | get := (request, cb) => cached?(request) :: {
47 | true -> cb(cache.(request.url).data)
48 | _ -> (
49 | withResp := resp => (
50 | cache.(request.url) := {
51 | timestamp: time()
52 | data: resp
53 | }
54 | len(cache) > MaxSize :: {
55 | true -> evictLR()
56 | }
57 | cb(resp)
58 | )
59 |
60 | log(f('[api] GET {{ 0 }}', [request.url]))
61 | req(request, evt => evt.type :: {
62 | 'resp' -> statusCode := evt.data.status :: {
63 | 200 -> withResp(evt.data.body)
64 | _ -> (
65 | log('[err] response status ' + string(statusCode))
66 | withResp(())
67 | )
68 | }
69 | 'error' -> (
70 | log('[err] ' + evt.message)
71 | withResp(())
72 | )
73 | })
74 | )
75 | }
76 |
77 | {
78 | get: get
79 | }
80 | )
81 |
--------------------------------------------------------------------------------
/vendor/percent.ink:
--------------------------------------------------------------------------------
1 | ` percent encoding, also known as URI encoding `
2 |
3 | std := load('../vendor/std')
4 | str := load('../vendor/str')
5 |
6 | log := std.log
7 | reduce := std.reduce
8 | map := std.map
9 | cat := std.cat
10 | hex := std.hex
11 | xeh := std.xeh
12 | digit? := str.digit?
13 | upper? := str.upper?
14 | lower? := str.lower?
15 | upper := str.upper
16 | lower := str.lower
17 |
18 | encodeChar := encodeSlash => c => (
19 | isValidPunct := (encodeSlash :: {
20 | true -> (c = '.') | (c = '_') | (c = '-') | (c = '~')
21 | _ -> (c = '.') | (c = '_') | (c = '-') | (c = '~') | (c = '/')
22 | })
23 | digit?(c) | upper?(c) | lower?(c) | isValidPunct :: {
24 | true -> c
25 | false -> '%' + upper(hex(point(c)))
26 | }
27 | )
28 | encodeKeepSlash := piece => cat(map(piece, encodeChar(false)), '')
29 | encode := piece => cat(map(piece, encodeChar(true)), '')
30 |
31 | checkRange := (lo, hi) => c => lo < point(c) & point(c) < hi
32 | upperAF? := checkRange(point('A') - 1, point('F') + 1)
33 | lowerAF? := checkRange(point('a') - 1, point('f') + 1)
34 | hex? := c => digit?(c) | upperAF?(c) | lowerAF?(c)
35 | decode := str => (
36 | s := {
37 | `
38 | 0 -> default
39 | 1 -> saw %
40 | 2 -> saw 1 hex number
41 | `
42 | stage: 0
43 | buf: ()
44 | }
45 | reduce(str, (decoded, curr) => s.stage :: {
46 | 0 -> curr :: {
47 | '+' -> (
48 | decoded + ' '
49 | )
50 | '%' -> (
51 | s.stage := 1
52 | decoded
53 | )
54 | _ -> decoded + curr
55 | }
56 | 1 -> hex?(curr) :: {
57 | false -> (
58 | s.stage := 0
59 | decoded + '%' + curr
60 | )
61 | _ -> (
62 | s.stage := 2
63 | s.buf := curr
64 | decoded
65 | )
66 | }
67 | _ -> (
68 | last := s.buf
69 | s.stage := 0
70 | s.buf := ()
71 | hex?(curr) :: {
72 | false -> decoded + '%' + last + curr
73 | _ -> decoded + char(xeh(lower(last + curr)))
74 | }
75 | )
76 | }, '')
77 | )
78 |
--------------------------------------------------------------------------------
/vendor/http.ink:
--------------------------------------------------------------------------------
1 | ` http server abstraction `
2 |
3 | std := load('../vendor/std')
4 |
5 | log := std.log
6 | f := std.format
7 | slice := std.slice
8 | each := std.each
9 |
10 | auth := load('auth')
11 | allow? := auth.allow?
12 |
13 | route := load('route')
14 |
15 | new := () => (
16 | router := (route.new)()
17 |
18 | ` routes added to router here `
19 |
20 | start := port => listen('0.0.0.0:' + string(port), evt => (
21 | (route.catch)(router, params => (req, end) => end({
22 | status: 404
23 | body: 'service not found'
24 | }))
25 |
26 | evt.type :: {
27 | 'error' -> log('server start error: ' + evt.message)
28 | 'req' -> (
29 | log(f('{{ method }}: {{ url }}', evt.data))
30 |
31 | handleWithHeaders := evt => (
32 | handler := (route.match)(router, evt.data.url)
33 | handler(evt.data, resp => (
34 | resp.headers := hdr(resp.headers :: {
35 | () -> {}
36 | _ -> resp.headers
37 | })
38 | (evt.end)(resp)
39 | ))
40 | )
41 | [allow?(evt.data), evt.data.method] :: {
42 | [true, 'GET'] -> handleWithHeaders(evt)
43 | [true, 'POST'] -> handleWithHeaders(evt)
44 | [true, 'PUT'] -> handleWithHeaders(evt)
45 | [true, 'DELETE'] -> handleWithHeaders(evt)
46 | _ -> (evt.end)({
47 | status: 405
48 | headers: hdr({})
49 | body: 'method not allowed'
50 | })
51 | }
52 | )
53 | }
54 | ))
55 |
56 | {
57 | addRoute: (url, handler) => (route.add)(router, url, handler)
58 | start: start
59 | }
60 | )
61 |
62 | ` prepare standard header `
63 | hdr := attrs => (
64 | base := {
65 | 'X-Served-By': 'ink-serve'
66 | 'Content-Type': 'text/plain'
67 | }
68 | each(keys(attrs), k => base.(k) := attrs.(k))
69 | base
70 | )
71 |
72 | ` trim query parameters `
73 | trimQP := path => (
74 | max := len(path)
75 | (sub := (idx, acc) => idx :: {
76 | max -> path
77 | _ -> path.(idx) :: {
78 | '?' -> acc
79 | _ -> sub(idx + 1, acc + path.(idx))
80 | }
81 | })(0, '')
82 | )
83 |
--------------------------------------------------------------------------------
/lib/torus.js.ink:
--------------------------------------------------------------------------------
1 | ` Torus : Ink API adapter
2 |
3 | renderer.ink provides an Ink interface for writing web user interfaces
4 | against the DOM with idiomatic Ink. The renderer operates on a single
5 | global render tree that renders against a single root node on the page, and
6 | uses a light Torus-backed virtual DOM to efficiently dispatch DOM edits.
7 |
8 | Initialize a render tree with the Renderer constructor:
9 |
10 | rootEl := bind(document, 'querySelector')('#root')
11 | r := Renderer(rootEl)
12 | update := r.update
13 |
14 | then call update() every time the app needs to update with the new render
15 | tree. The renderer comes with a few helper functions, h(), ha(), and hae(),
16 | for making render trees ergonomic to construct with Ink.
17 |
18 | It is conventional to create a single object called App that instantiates a
19 | renderer and closes over the update function with its own Redux-style
20 | global state management logic that it exposes to its child elements through
21 | a more restricted API. `
22 |
23 | ` text nodes passed to Torus.render can't be normal Ink strings, because typeof
24 | != 'string'. We wrap Ink strings in str() here when passing to
25 | Torus to render strings correctly. `
26 | str := s => bind(s, 'valueOf')(s)
27 |
28 | ` To quickly convert object-like Ink maps to arrays `
29 | arr := bind(Object, 'values')
30 |
31 | ` Torus jdom declaration helpers `
32 |
33 | hae := (tag, classList, attrs, events, children) => {
34 | tag: str(tag)
35 | attrs: attrs.('class') := arr(map(classList, str))
36 | events: events
37 | children: arr(map(children, child => type(child) :: {
38 | 'string' -> str(child)
39 | _ -> child
40 | }))
41 | }
42 | ha := (tag, classList, attrs, children) => hae(tag, classList, attrs, {}, children)
43 | h := (tag, classList, children) => hae(tag, classList, {}, {}, children)
44 |
45 | ` generic abstraction for a view that can be updated asynchronously `
46 |
47 | Renderer := root => (
48 | render := window.Torus.render
49 |
50 | InitialDom := h('div', [], [])
51 |
52 | node := render((), (), InitialDom)
53 | bind(root, 'appendChild')(node)
54 |
55 | self := {
56 | node: node
57 | prev: InitialDom
58 | update: jdom => (
59 | self.node := render(self.node, self.prev, jdom)
60 | self.prev := jdom
61 | self.node
62 | )
63 | }
64 | )
65 |
66 |
--------------------------------------------------------------------------------
/vendor/route.ink:
--------------------------------------------------------------------------------
1 | ` url router `
2 |
3 | std := load('../vendor/std')
4 | str := load('../vendor/str')
5 |
6 | log := std.log
7 | slice := std.slice
8 | each := std.each
9 | map := std.map
10 | cat := std.cat
11 | filter := std.filter
12 | split := str.split
13 |
14 | percent := load('percent')
15 | pctDecode := percent.decode
16 |
17 | new := () => []
18 |
19 | add := (router, pattern, handler) => router.len(router) := [pattern, handler]
20 | catch := (router, handler) => add(router, '', handler)
21 |
22 | splitPath := url => filter(split(url, '/'), s => ~(s = ''))
23 |
24 | ` if path matches pattern, return a hash of matched params.
25 | else, return () `
26 | matchPath := (pattern, path) => (
27 | params := {}
28 |
29 | ` process query parameters `
30 | pathParts := split(path, '?')
31 | path := pathParts.0
32 | pathParts.1 :: {
33 | () -> ()
34 | '' -> ()
35 | _ -> (
36 | queries := map(split(pathParts.1, '&'), pair => split(pair, '='))
37 | each(queries, pair => params.(pair.0) := pctDecode(pair.1))
38 | )
39 | }
40 |
41 | desired := splitPath(pattern)
42 | actual := splitPath(path)
43 |
44 | max := len(desired)
45 | findMatchingParams := (sub := i => i :: {
46 | max -> params
47 | _ -> (
48 | desiredPart := (desired.(i) :: {
49 | () -> ''
50 | _ -> desired.(i)
51 | })
52 | actualPart := (actual.(i) :: {
53 | () -> ''
54 | _ -> actual.(i)
55 | })
56 |
57 | desiredPart.0 :: {
58 | ':' -> (
59 | params.(slice(desiredPart, 1, len(desiredPart))) := actualPart
60 | sub(i + 1)
61 | )
62 | '*' -> (
63 | params.(slice(desiredPart, 1, len(desiredPart))) := cat(slice(actual, i, len(actual)), '/')
64 | params
65 | )
66 | _ -> desiredPart :: {
67 | actualPart -> sub(i + 1)
68 | _ -> ()
69 | }
70 | }
71 | )
72 | })
73 |
74 | [len(desired) < len(actual) | len(desired) = len(actual), pattern] :: {
75 | ` '' is used as a catch-all pattern `
76 | [_, ''] -> params
77 | [true, _] -> findMatchingParams(0)
78 | _ -> ()
79 | }
80 | )
81 |
82 | ` returns the proper handler curried with url params `
83 | match := (router, path) => (sub := i => i :: {
84 | len(router) -> req => (req.end)({
85 | status: 200
86 | headers: {}
87 | body: 'dropped route. you should never see this in production.'
88 | })
89 | _ -> (
90 | result := matchPath(router.(i).0, path)
91 | result :: {
92 | () -> sub(i + 1)
93 | _ -> (router.(i).1)(result)
94 | }
95 | )
96 | })(0)
97 |
--------------------------------------------------------------------------------
/src/highlight.ink:
--------------------------------------------------------------------------------
1 | ` september syntax highlighter command, forked to highlight HTML elements `
2 |
3 | std := load('../vendor/std')
4 | str := load('../vendor/str')
5 |
6 | log := std.log
7 | f := std.format
8 | map := std.map
9 | each := std.each
10 | slice := std.slice
11 | cat := std.cat
12 | replace := str.replace
13 |
14 | Tokenize := load('../vendor/tokenize')
15 | Tok := Tokenize.Tok
16 | tokenize := Tokenize.tokenizeWithComments
17 |
18 | ` associating token types with their highlight colors `
19 | classForTok := tok => tok.type :: {
20 | Tok.Separator -> 'hljs-separator'
21 |
22 | Tok.Comment -> 'hljs-comment'
23 |
24 | Tok.Ident -> ''
25 | Tok.EmptyIdent -> ''
26 |
27 | Tok.NumberLiteral -> 'hljs-number'
28 | Tok.StringLiteral -> 'hljs-string'
29 | Tok.TrueLiteral -> 'hljs-literal'
30 | Tok.FalseLiteral -> 'hljs-literal'
31 |
32 | Tok.AccessorOp -> 'hljs-operator'
33 | Tok.EqOp -> 'hljs-operator'
34 |
35 | Tok.FunctionArrow -> 'hljs-operator'
36 |
37 | ` operators are all red `
38 | Tok.KeyValueSeparator -> 'hljs-operator'
39 | Tok.DefineOp -> 'hljs-operator'
40 | Tok.MatchColon -> 'hljs-operator'
41 | Tok.CaseArrow -> 'hljs-operator'
42 | Tok.SubOp -> 'hljs-operator'
43 | Tok.NegOp -> 'hljs-operator'
44 | Tok.AddOp -> 'hljs-operator'
45 | Tok.MulOp -> 'hljs-operator'
46 | Tok.DivOp -> 'hljs-operator'
47 | Tok.ModOp -> 'hljs-operator'
48 | Tok.GtOp -> 'hljs-operator'
49 | Tok.LtOp -> 'hljs-operator'
50 | Tok.AndOp -> 'hljs-operator'
51 | Tok.OrOp -> 'hljs-operator'
52 | Tok.XorOp -> 'hljs-operator'
53 |
54 | Tok.LParen -> 'hljs-punctuation'
55 | Tok.RParen -> 'hljs-punctuation'
56 | Tok.LBracket -> 'hljs-punctuation'
57 | Tok.RBracket -> 'hljs-punctuation'
58 | Tok.LBrace -> 'hljs-punctuation'
59 | Tok.RBrace -> 'hljs-punctuation'
60 |
61 | _ -> () `` should error, unreachable
62 | }
63 |
64 | escapeHTML := s => replace(replace(s, '&', '&'), '<', '<')
65 |
66 | highlightInkProg := prog => (
67 | tokens := tokenize(prog)
68 | spans := map(tokens, (tok, i) => {
69 | class: [tok.type, tokens.(i + 1)] :: {
70 | ` direct function calls are marked green
71 | on a best-effort basis `
72 | [
73 | Tok.Ident
74 | {type: Tok.LParen, val: _, line: _, col: _, i: _}
75 | ] -> 'hljs-title function_'
76 | _ -> classForTok(tok)
77 | }
78 | start: tok.i
79 | end: tokens.(i + 1) :: {
80 | () -> len(prog)
81 | _ -> tokens.(i + 1).i
82 | }
83 | })
84 | pcs := map(
85 | spans
86 | span => '' +
87 | escapeHTML(slice(prog, span.start, span.end)) + ' '
88 | )
89 | cat(pcs, '')
90 | )
91 |
--------------------------------------------------------------------------------
/static/css/highlight.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Theme: GitHub
3 | Description: Light theme as seen on github.com
4 | Author: github.com
5 | Maintainer: @Hirse
6 | Updated: 2021-05-15
7 |
8 | Outdated base version: https://github.com/primer/github-syntax-light
9 | Current colors taken from GitHub's CSS
10 | */
11 |
12 | .hljs {
13 | color: #24292e;
14 | background: #ffffff;
15 | }
16 |
17 | .hljs-doctag,
18 | .hljs-keyword,
19 | .hljs-meta .hljs-keyword,
20 | .hljs-template-tag,
21 | .hljs-template-variable,
22 | .hljs-type,
23 | .hljs-variable.language_ {
24 | /* prettylights-syntax-keyword */
25 | color: #d73a49;
26 | }
27 |
28 | .hljs-title,
29 | .hljs-title.class_,
30 | .hljs-title.class_.inherited__,
31 | .hljs-title.function_ {
32 | /* prettylights-syntax-entity */
33 | color: #6f42c1;
34 | }
35 |
36 | .hljs-attr,
37 | .hljs-attribute,
38 | .hljs-literal,
39 | .hljs-meta,
40 | .hljs-number,
41 | .hljs-operator,
42 | .hljs-variable,
43 | .hljs-selector-attr,
44 | .hljs-selector-class,
45 | .hljs-selector-id {
46 | /* prettylights-syntax-constant */
47 | color: #005cc5;
48 | }
49 |
50 | .hljs-regexp,
51 | .hljs-string,
52 | .hljs-meta .hljs-string {
53 | /* prettylights-syntax-string */
54 | color: #032f62;
55 | }
56 |
57 | .hljs-built_in,
58 | .hljs-symbol {
59 | /* prettylights-syntax-variable */
60 | color: #e36209;
61 | }
62 |
63 | .hljs-comment,
64 | .hljs-code,
65 | .hljs-formula {
66 | /* prettylights-syntax-comment */
67 | color: #6a737d;
68 | }
69 |
70 | .hljs-name,
71 | .hljs-quote,
72 | .hljs-selector-tag,
73 | .hljs-selector-pseudo {
74 | /* prettylights-syntax-entity-tag */
75 | color: #22863a;
76 | }
77 |
78 | .hljs-subst {
79 | /* prettylights-syntax-storage-modifier-import */
80 | color: #24292e;
81 | }
82 |
83 | .hljs-section {
84 | /* prettylights-syntax-markup-heading */
85 | color: #005cc5;
86 | font-weight: bold;
87 | }
88 |
89 | .hljs-bullet {
90 | /* prettylights-syntax-markup-list */
91 | color: #735c0f;
92 | }
93 |
94 | .hljs-emphasis {
95 | /* prettylights-syntax-markup-italic */
96 | color: #24292e;
97 | font-style: italic;
98 | }
99 |
100 | .hljs-strong {
101 | /* prettylights-syntax-markup-bold */
102 | color: #24292e;
103 | font-weight: bold;
104 | }
105 |
106 | .hljs-addition {
107 | /* prettylights-syntax-markup-inserted */
108 | color: #22863a;
109 | background-color: #f0fff4;
110 | }
111 |
112 | .hljs-deletion {
113 | /* prettylights-syntax-markup-deleted */
114 | color: #b31d28;
115 | background-color: #ffeef0;
116 | }
117 |
118 | .hljs-char.escape_,
119 | .hljs-link,
120 | .hljs-params,
121 | .hljs-property,
122 | .hljs-punctuation,
123 | .hljs-tag {
124 | /* purposely ignored */
125 | }
126 |
--------------------------------------------------------------------------------
/static/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ink syntax highlighting proxy demo | Ink codebase browser
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
36 |
37 |
38 |
39 |
Ink syntax highlighting proxy demo
40 |
41 |
42 | In addition to a full web application for exploring GitHub repositories, Kin
43 | uses much of the same code to provide a second useful service: Ink syntax
44 | highlighting as a service .
45 |
46 |
47 |
48 | To make the service available, Kin acts as an HTTP proxy for public source
49 | files on GitHub. Kin URLs of the format https://code.dotink.co/embed/{{
50 | GitHub Download URL }} are parsed to the right files. In each case, the
51 | file is downloaded from GitHub, and if it's an Ink source file, it's
52 | additionally highlighted and annotated with line numbers before being sent back
53 | to the client. This means embedding a syntax-highlighted, line-numbered Ink
54 | source file in any webpage is as simple as writing
55 |
56 |
57 |
<iframe
58 | src="/embed/https://raw.githubusercontent.com/thesephist/ink/master/samples/std.ink"
59 | frameborder="0">
60 | </iframe>
61 |
62 |
63 | For example, here's a syntax-highlighted version of the Ink standard library .
65 |
66 |
67 |
68 |
69 |
70 | This file implements the sorting algorithm quicksort , used as the
72 | sorting algorithm in most Ink programs today.
73 |
74 |
75 |
76 |
77 |
78 | More complex Ink source files can also be highlighted this way. For example,
79 | the following source file is the semantic analysis algorithm in the September compiler , an
81 | Ink-to-JavaScript compiler written in Ink itself.
82 |
83 |
84 |
85 |
86 |
87 | Embedding source files that aren't Ink programs will simply return
88 | line-numbered previews -- no syntax highlighting occurs. For example, the
89 | following is the JavaScript runtime used by the September compiler.
90 |
91 |
92 |
93 |
94 |
95 | Syntax highlighting as a service is a feature of the Ink codebase browser .
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/main.ink:
--------------------------------------------------------------------------------
1 | std := load('../vendor/std')
2 | str := load('../vendor/str')
3 | json := load('../vendor/json')
4 |
5 | log := std.log
6 | f := std.format
7 | range := std.range
8 | cat := std.cat
9 | map := std.map
10 | each := std.each
11 | readFile := std.readFile
12 | split := str.split
13 | replace := str.replace
14 | hasSuffix? := str.hasSuffix?
15 | serJSON := json.ser
16 | deJSON := json.de
17 |
18 | http := load('../vendor/http')
19 | mimeForPath := load('../vendor/mime').forPath
20 |
21 | github := load('../lib/github')
22 | getRepo := github.getRepo
23 | getContents := github.getContents
24 |
25 | highlight := load('highlight')
26 | highlightInkProg := highlight.highlightInkProg
27 |
28 | Port := 9870
29 | Newline := char(10)
30 | MaxProxyFile := 500000
31 |
32 | server := (http.new)()
33 | NotFound := {status: 404, body: 'file not found'}
34 | MethodNotAllowed := {status: 405, body: 'method not allowed'}
35 |
36 | serveStatic := path => (req, end) => req.method :: {
37 | 'GET' -> readFile('static/' + path, file => file :: {
38 | () -> end(NotFound)
39 | _ -> end({
40 | status: 200
41 | headers: {'Content-Type': mimeForPath(path)}
42 | body: file
43 | })
44 | })
45 | _ -> end(MethodNotAllowed)
46 | }
47 |
48 | addRoute := server.addRoute
49 |
50 | translateFileFromAPI := fileFromAPI => {
51 | name: fileFromAPI.name
52 | path: fileFromAPI.path
53 | type: fileFromAPI.type
54 | size: fileFromAPI.size
55 | download: fileFromAPI.'download_url'
56 | content: ()
57 | children: ()
58 | }
59 |
60 | ` directory traversal paths `
61 | addRoute('/repo/:userName/:repoName/files/*pathName', params => (req, end) => req.method :: {
62 | 'GET' -> getContents(
63 | params.userName + '/' + params.repoName
64 | '/' + params.pathName
65 | contents => contents :: {
66 | () -> end(NotFound)
67 | _ -> end({
68 | status: 200
69 | headers: {'Content-Type': 'text/plain'}
70 | body: serJSON(map(contents, translateFileFromAPI))
71 | })
72 | }
73 | )
74 | _ -> end(MethodNotAllowed)
75 | })
76 | addRoute('/repo/:userName/:repoName/files', params => (req, end) => req.method :: {
77 | 'GET' -> getContents(
78 | params.userName + '/' + params.repoName
79 | '/'
80 | contents => contents :: {
81 | () -> end(NotFound)
82 | _ -> (
83 | end({
84 | status: 200
85 | headers: {'Content-Type': 'text/plain'}
86 | body: serJSON(map(contents, translateFileFromAPI))
87 | })
88 | )
89 | }
90 | )
91 | _ -> end(MethodNotAllowed)
92 | })
93 |
94 | ` repo read paths `
95 | addRoute('/repo/:userName/:repoName', params => (req, end) => req.method :: {
96 | 'GET' -> getRepo(params.userName + '/' + params.repoName, repo => repo :: {
97 | () -> end(NotFound)
98 | _ -> end({
99 | status: 200
100 | headers: {'Content-Type': 'application/json'}
101 | body: serJSON({
102 | owner: {
103 | username: repo.owner.login
104 | avatar: repo.owner.'avatar_url'
105 | url: repo.owner.'html_url'
106 | }
107 | description: repo.description
108 | homepage: repo.homepage
109 | language: repo.language
110 | branch: repo.'default_branch'
111 | })
112 | })
113 | })
114 | _ -> end(MethodNotAllowed)
115 | })
116 |
117 | addRoute('/embed/*githubURL', params => (request, end) => request.method :: {
118 | 'GET' -> req(
119 | {
120 | ` URL processing seems to trim double-slashes `
121 | url: replace(params.githubURL, 'https:/', 'https://')
122 | }
123 | evt => evt.type :: {
124 | 'resp' -> readFile('./static/embed.html', file => file :: {
125 | () -> end({
126 | status: 500
127 | headers: {'Content-Type': 'text/plain'}
128 | body: evt.message
129 | })
130 | _ -> (
131 | fileToRender := (len(evt.data.body) > MaxProxyFile :: {
132 | true -> 'file too big to render'
133 | _ -> evt.data.body
134 | })
135 | end({
136 | status: 200
137 | headers: {'Content-Type': 'text/html'}
138 | body: f(file, {
139 | fileName: params.githubURL
140 | lineNos: cat(map(
141 | range(1, len(split(fileToRender, Newline)) + 1, 1)
142 | string
143 | ), Newline)
144 | prog: hasSuffix?(params.githubURL, '.ink') :: {
145 | true -> highlightInkProg(fileToRender)
146 | _ -> fileToRender
147 | }
148 | })
149 | })
150 | )
151 | })
152 | 'error' -> end({
153 | status: 500
154 | headers: {'Content-Type': 'text/plain'}
155 | body: evt.message
156 | })
157 | }
158 | )
159 | _ -> end(MethodNotAllowed)
160 | })
161 |
162 | ` static paths `
163 | addRoute('/static/*staticPath', params => serveStatic(params.staticPath))
164 | addRoute('/favicon.ico', params => serveStatic('favicon.ico'))
165 | addRoute('/demo', params => serveStatic('demo.html'))
166 | addRoute('/', params => serveStatic('index.html'))
167 |
168 | start := () => (
169 | end := (server.start)(Port)
170 | log(f('Kin started, listening on 0.0.0.0:{{0}}', [Port]))
171 | )
172 |
173 | start()
174 |
175 |
--------------------------------------------------------------------------------
/vendor/str.ink:
--------------------------------------------------------------------------------
1 | ` standard string library `
2 |
3 | std := load('std')
4 |
5 | map := std.map
6 | slice := std.slice
7 | reduce := std.reduce
8 | reduceBack := std.reduceBack
9 |
10 | ` checking if a given character is of a type `
11 | checkRange := (lo, hi) => c => (
12 | p := point(c)
13 | lo < p & p < hi
14 | )
15 | upper? := checkRange(point('A') - 1, point('Z') + 1)
16 | lower? := checkRange(point('a') - 1, point('z') + 1)
17 | digit? := checkRange(point('0') - 1, point('9') + 1)
18 | letter? := c => upper?(c) | lower?(c)
19 |
20 | ` is the char a whitespace? `
21 | ws? := c => point(c) :: {
22 | ` space `
23 | 32 -> true
24 | ` newline `
25 | 10 -> true
26 | ` hard tab `
27 | 9 -> true
28 | ` carriage return `
29 | 13 -> true
30 | _ -> false
31 | }
32 |
33 | ` hasPrefix? checks if a string begins with the given prefix substring `
34 | hasPrefix? := (s, prefix) => reduce(prefix, (acc, c, i) => acc & (s.(i) = c), true)
35 |
36 | ` hasSuffix? checks if a string ends with the given suffix substring `
37 | hasSuffix? := (s, suffix) => (
38 | diff := len(s) - len(suffix)
39 | reduce(suffix, (acc, c, i) => acc & (s.(i + diff) = c), true)
40 | )
41 |
42 | ` mostly used for internal bookkeeping, matchesAt? reports if a string contains
43 | the given substring at the given index idx. `
44 | matchesAt? := (s, substring, idx) => (
45 | max := len(substring)
46 | (sub := i => i :: {
47 | max -> true
48 | _ -> s.(idx + i) :: {
49 | (substring.(i)) -> sub(i + 1)
50 | _ -> false
51 | }
52 | })(0)
53 | )
54 |
55 | ` index is indexOf() for ink strings `
56 | index := (s, substring) => (
57 | max := len(s) - 1
58 | (sub := i => matchesAt?(s, substring, i) :: {
59 | true -> i
60 | false -> i < max :: {
61 | true -> sub(i + 1)
62 | false -> ~1
63 | }
64 | })(0)
65 | )
66 |
67 | ` contains? checks if a string contains the given substring `
68 | contains? := (s, substring) => index(s, substring) > ~1
69 |
70 | ` transforms given string to lowercase `
71 | lower := s => reduce(s, (acc, c, i) => upper?(c) :: {
72 | true -> acc.(i) := char(point(c) + 32)
73 | false -> acc.(i) := c
74 | }, '')
75 |
76 | ` transforms given string to uppercase`
77 | upper := s => reduce(s, (acc, c, i) => lower?(c) :: {
78 | true -> acc.(i) := char(point(c) - 32)
79 | false -> acc.(i) := c
80 | }, '')
81 |
82 | ` primitive "title-case" transformation, uppercases first letter
83 | and lowercases the rest. `
84 | title := s => (
85 | lowered := lower(s)
86 | lowered.0 := upper(lowered.0)
87 | )
88 |
89 | replaceNonEmpty := (s, old, new) => (
90 | lold := len(old)
91 | lnew := len(new)
92 | (sub := (acc, i) => matchesAt?(acc, old, i) :: {
93 | true -> sub(
94 | slice(acc, 0, i) + new + slice(acc, i + lold, len(acc))
95 | i + lnew
96 | )
97 | false -> i < len(acc) :: {
98 | true -> sub(acc, i + 1)
99 | false -> acc
100 | }
101 | })(s, 0)
102 | )
103 |
104 | ` replace all occurrences of old substring with new substring in a string `
105 | replace := (s, old, new) => old :: {
106 | '' -> s
107 | _ -> replaceNonEmpty(s, old, new)
108 | }
109 |
110 | splitNonEmpty := (s, delim) => (
111 | coll := []
112 | ldelim := len(delim)
113 | (sub := (acc, i, last) => matchesAt?(acc, delim, i) :: {
114 | true -> (
115 | coll.len(coll) := slice(acc, last, i)
116 | sub(acc, i + ldelim, i + ldelim)
117 | )
118 | false -> i < len(acc) :: {
119 | true -> sub(acc, i + 1, last)
120 | false -> coll.len(coll) := slice(acc, last, len(acc))
121 | }
122 | })(s, 0, 0)
123 | )
124 |
125 | ` split given string into a list of substrings, splitting by the delimiter `
126 | split := (s, delim) => delim :: {
127 | '' -> map(s, c => c)
128 | _ -> splitNonEmpty(s, delim)
129 | }
130 |
131 | trimPrefixNonEmpty := (s, prefix) => (
132 | max := len(s)
133 | lpref := len(prefix)
134 | idx := (sub := i => i < max :: {
135 | true -> matchesAt?(s, prefix, i) :: {
136 | true -> sub(i + lpref)
137 | false -> i
138 | }
139 | false -> i
140 | })(0)
141 | slice(s, idx, len(s))
142 | )
143 |
144 | ` trim string from start until it does not begin with prefix.
145 | trimPrefix is more efficient than repeated application of
146 | hasPrefix? because it minimizes copying. `
147 | trimPrefix := (s, prefix) => prefix :: {
148 | '' -> s
149 | _ -> trimPrefixNonEmpty(s, prefix)
150 | }
151 |
152 | trimSuffixNonEmpty := (s, suffix) => (
153 | lsuf := len(suffix)
154 | idx := (sub := i => i > ~1 :: {
155 | true -> matchesAt?(s, suffix, i - lsuf) :: {
156 | true -> sub(i - lsuf)
157 | false -> i
158 | }
159 | false -> i
160 | })(len(s))
161 | slice(s, 0, idx)
162 | )
163 |
164 | ` trim string from end until it does not end with suffix.
165 | trimSuffix is more efficient than repeated application of
166 | hasSuffix? because it minimizes copying. `
167 | trimSuffix := (s, suffix) => suffix :: {
168 | '' -> s
169 | _ -> trimSuffixNonEmpty(s, suffix)
170 | }
171 |
172 | ` trim string from both start and end with substring ss `
173 | trim := (s, ss) => trimPrefix(trimSuffix(s, ss), ss)
174 |
--------------------------------------------------------------------------------
/vendor/json.ink:
--------------------------------------------------------------------------------
1 | ` JSON serde `
2 |
3 | std := load('std')
4 | str := load('str')
5 |
6 | map := std.map
7 | cat := std.cat
8 |
9 | ws? := str.ws?
10 | digit? := str.digit?
11 |
12 | ` string escape '"' `
13 | esc := c => point(c) :: {
14 | 9 -> '\\t'
15 | 10 -> '\\n'
16 | 13 -> '\\r'
17 | 34 -> '\\"'
18 | 92 -> '\\\\'
19 | _ -> c
20 | }
21 | escape := s => (
22 | max := len(s)
23 | (sub := (i, acc) => i :: {
24 | max -> acc
25 | _ -> sub(i + 1, acc + esc(s.(i)))
26 | })(0, '')
27 | )
28 |
29 | ` composite to JSON string `
30 | ser := c => type(c) :: {
31 | '()' -> 'null'
32 | 'string' -> '"' + escape(c) + '"'
33 | 'number' -> string(c)
34 | 'boolean' -> string(c)
35 | ` do not serialize functions `
36 | 'function' -> 'null'
37 | 'composite' -> '{' + cat(map(keys(c), k => '"' + k + '":' + ser(c.(k))), ',') + '}'
38 | }
39 |
40 | ` is this character a numeral digit or .? `
41 | num? := c => c :: {
42 | '' -> false
43 | '.' -> true
44 | _ -> digit?(c)
45 | }
46 |
47 | ` reader implementation with internal state for deserialization `
48 | reader := s => (
49 | state := {
50 | idx: 0
51 | ` has there been a parse error? `
52 | err?: false
53 | }
54 |
55 | next := () => (
56 | state.idx := state.idx + 1
57 | c := s.(state.idx - 1) :: {
58 | () -> ''
59 | _ -> c
60 | }
61 | )
62 |
63 | peek := () => c := s.(state.idx) :: {
64 | () -> ''
65 | _ -> c
66 | }
67 |
68 | {
69 | next: next
70 | peek: peek
71 | ` fast-forward through whitespace `
72 | ff: () => (sub := () => ws?(peek()) :: {
73 | true -> (
74 | state.idx := state.idx + 1
75 | sub()
76 | )
77 | })()
78 | done?: () => ~(state.idx < len(s))
79 | err: () => state.err? := true
80 | err?: () => state.err?
81 | }
82 | )
83 |
84 | ` deserialize null `
85 | deNull := r => (
86 | n := r.next
87 | n() + n() + n() + n() :: {
88 | 'null' -> ()
89 | _ -> (r.err)()
90 | }
91 | )
92 |
93 | ` deserialize string `
94 | deString := r => (
95 | n := r.next
96 | p := r.peek
97 |
98 | ` known to be a '"' `
99 | n()
100 |
101 | (sub := acc => p() :: {
102 | '' -> (
103 | (r.err)()
104 | ()
105 | )
106 | '\\' -> (
107 | ` eat backslash `
108 | n()
109 | sub(acc + (c := n() :: {
110 | 't' -> char(9)
111 | 'n' -> char(10)
112 | 'r' -> char(13)
113 | '"' -> '"'
114 | _ -> c
115 | }))
116 | )
117 | '"' -> (
118 | n()
119 | acc
120 | )
121 | _ -> sub(acc + n())
122 | })('')
123 | )
124 |
125 | ` deserialize number `
126 | deNumber := r => (
127 | n := r.next
128 | p := r.peek
129 | state := {
130 | ` have we seen a '.' yet? `
131 | negate?: false
132 | decimal?: false
133 | }
134 |
135 | p() :: {
136 | '-' -> (
137 | n()
138 | state.negate? := true
139 | )
140 | }
141 |
142 | result := (sub := acc => num?(p()) :: {
143 | true -> p() :: {
144 | '.' -> state.decimal? :: {
145 | true -> (r.err)()
146 | false -> (
147 | state.decimal? := true
148 | sub(acc + n())
149 | )
150 | }
151 | _ -> sub(acc + n())
152 | }
153 | false -> acc
154 | })('')
155 |
156 | state.negate? :: {
157 | false -> number(result)
158 | true -> ~number(result)
159 | }
160 | )
161 |
162 | ` deserialize boolean `
163 | deTrue := r => (
164 | n := r.next
165 | n() + n() + n() + n() :: {
166 | 'true' -> true
167 | _ -> (r.err)()
168 | }
169 | )
170 | deFalse := r => (
171 | n := r.next
172 | n() + n() + n() + n() + n() :: {
173 | 'false' -> false
174 | _ -> (r.err)()
175 | }
176 | )
177 |
178 | ` deserialize list `
179 | deList := r => (
180 | n := r.next
181 | p := r.peek
182 | ff := r.ff
183 | state := {
184 | idx: 0
185 | }
186 |
187 | ` known to be a '[' `
188 | n()
189 | ff()
190 |
191 | (sub := acc => (r.err?)() :: {
192 | true -> ()
193 | false -> p() :: {
194 | '' -> (
195 | (r.err)()
196 | ()
197 | )
198 | ']' -> (
199 | n()
200 | acc
201 | )
202 | _ -> (
203 | acc.(state.idx) := der(r)
204 | state.idx := state.idx + 1
205 |
206 | ff()
207 | p() :: {
208 | ',' -> n()
209 | }
210 |
211 | ff()
212 | sub(acc)
213 | )
214 | }
215 | })([])
216 | )
217 |
218 | ` deserialize composite `
219 | deComp := r => (
220 | n := r.next
221 | p := r.peek
222 | ff := r.ff
223 |
224 | ` known to be a '{' `
225 | n()
226 | ff()
227 |
228 | (sub := acc => (r.err?)() :: {
229 | true -> ()
230 | false -> p() :: {
231 | '' -> (r.err)()
232 | '}' -> (
233 | n()
234 | acc
235 | )
236 | _ -> (
237 | key := deString(r)
238 |
239 | (r.err?)() :: {
240 | false -> (
241 | ff()
242 | p() :: {
243 | ':' -> n()
244 | }
245 |
246 | ff()
247 | val := der(r)
248 |
249 | (r.err?)() :: {
250 | false -> (
251 | ff()
252 | p() :: {
253 | ',' -> n()
254 | }
255 |
256 | ff()
257 | acc.(key) := val
258 | sub(acc)
259 | )
260 | }
261 | )
262 | }
263 | )
264 | }
265 | })({})
266 | )
267 |
268 | ` JSON string in reader to composite `
269 | der := r => (
270 | ` trim preceding whitespace `
271 | (r.ff)()
272 |
273 | result := ((r.peek)() :: {
274 | 'n' -> deNull(r)
275 | '"' -> deString(r)
276 | 't' -> deTrue(r)
277 | 'f' -> deFalse(r)
278 | '[' -> deList(r)
279 | '{' -> deComp(r)
280 | _ -> deNumber(r)
281 | })
282 |
283 | ` if there was a parse error, just return null result `
284 | (r.err?)() :: {
285 | true -> ()
286 | false -> result
287 | }
288 | )
289 |
290 | ` JSON string to composite `
291 | de := s => der(reader(s))
292 |
--------------------------------------------------------------------------------
/vendor/std.ink:
--------------------------------------------------------------------------------
1 | ` the ink standard library `
2 |
3 | log := val => out(string(val) + '
4 | ')
5 |
6 | scan := cb => (
7 | acc := ['']
8 | in(evt => evt.type :: {
9 | 'end' -> cb(acc.0)
10 | 'data' -> (
11 | acc.0 := acc.0 + slice(evt.data, 0, len(evt.data) - 1)
12 | false
13 | )
14 | })
15 | )
16 |
17 | ` hexadecimal conversion utility functions `
18 | hToN := {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15}
19 | nToH := '0123456789abcdef'
20 |
21 | ` take number, return hex string `
22 | hex := n => (sub := (p, acc) => p < 16 :: {
23 | true -> nToH.(p) + acc
24 | false -> sub(floor(p / 16), nToH.(p % 16) + acc)
25 | })(floor(n), '')
26 |
27 | ` take hex string, return number `
28 | xeh := s => (
29 | ` i is the num of places from the left, 0-indexed `
30 | max := len(s)
31 | (sub := (i, acc) => i :: {
32 | max -> acc
33 | _ -> sub(i + 1, acc * 16 + hToN.(s.(i)))
34 | })(0, 0)
35 | )
36 |
37 | ` find minimum in list `
38 | min := numbers => reduce(numbers, (acc, n) => n < acc :: {
39 | true -> n
40 | false -> acc
41 | }, numbers.0)
42 |
43 | ` find maximum in list `
44 | max := numbers => reduce(numbers, (acc, n) => n > acc :: {
45 | true -> n
46 | false -> acc
47 | }, numbers.0)
48 |
49 | ` like Python's range(), but no optional arguments `
50 | range := (start, end, step) => (
51 | span := end - start
52 | sub := (i, v, acc) => (v - start) / span < 1 :: {
53 | true -> (
54 | acc.(i) := v
55 | sub(i + 1, v + step, acc)
56 | )
57 | false -> acc
58 | }
59 |
60 | ` preempt potential infinite loops `
61 | (end - start) / step > 0 :: {
62 | true -> sub(0, start, [])
63 | false -> []
64 | }
65 | )
66 |
67 | ` clamp start and end numbers to ranges, such that
68 | start < end. Utility used in slice `
69 | clamp := (start, end, min, max) => (
70 | start := (start < min :: {
71 | true -> min
72 | false -> start
73 | })
74 | end := (end < min :: {
75 | true -> min
76 | false -> end
77 | })
78 | end := (end > max :: {
79 | true -> max
80 | false -> end
81 | })
82 | start := (start > end :: {
83 | true -> end
84 | false -> start
85 | })
86 |
87 | {
88 | start: start
89 | end: end
90 | }
91 | )
92 |
93 | ` get a substring of a given string, or sublist of a given list `
94 | slice := (s, start, end) => (
95 | ` bounds checks `
96 | x := clamp(start, end, 0, len(s))
97 | start := x.start
98 | max := x.end - start
99 |
100 | (sub := (i, acc) => i :: {
101 | max -> acc
102 | _ -> sub(i + 1, acc.(i) := s.(start + i))
103 | })(0, type(s) :: {
104 | 'string' -> ''
105 | 'composite' -> []
106 | })
107 | )
108 |
109 | ` join one list to the end of another, return the original first list `
110 | append := (base, child) => (
111 | baseLength := len(base)
112 | childLength := len(child)
113 | (sub := i => i :: {
114 | childLength -> base
115 | _ -> (
116 | base.(baseLength + i) := child.(i)
117 | sub(i + 1)
118 | )
119 | })(0)
120 | )
121 |
122 | ` join one list to the end of another, return the third list `
123 | join := (base, child) => append(clone(base), child)
124 |
125 | ` clone a composite value `
126 | clone := x => type(x) :: {
127 | 'string' -> '' + x
128 | 'composite' -> reduce(keys(x), (acc, k) => acc.(k) := x.(k), {})
129 | _ -> x
130 | }
131 |
132 | ` tail recursive numeric list -> string converter `
133 | stringList := list => '[' + cat(map(list, string), ', ') + ']'
134 |
135 | ` tail recursive reversing a list `
136 | reverse := list => (sub := (acc, i, j) => j :: {
137 | 0 -> acc.(i) := list.0
138 | _ -> sub(acc.(i) := list.(j), i + 1, j - 1)
139 | })([], 0, len(list) - 1)
140 |
141 | ` tail recursive map `
142 | map := (list, f) => reduce(list, (l, item, i) => l.(i) := f(item, i), {})
143 |
144 | ` tail recursive filter `
145 | filter := (list, f) => reduce(list, (l, item, i) => f(item, i) :: {
146 | true -> l.len(l) := item
147 | _ -> l
148 | }, [])
149 |
150 | ` tail recursive reduce `
151 | reduce := (list, f, acc) => (
152 | max := len(list)
153 | (sub := (i, acc) => i :: {
154 | max -> acc
155 | _ -> sub(i + 1, f(acc, list.(i), i))
156 | })(0, acc)
157 | )
158 |
159 | ` tail recursive reduce from list end `
160 | reduceBack := (list, f, acc) => (sub := (i, acc) => i :: {
161 | ~1 -> acc
162 | _ -> sub(i - 1, f(acc, list.(i), i))
163 | })(len(list) - 1, acc)
164 |
165 | ` flatten by depth 1 `
166 | flatten := list => reduce(list, append, [])
167 |
168 | ` true iff some items in list are true `
169 | some := list => reduce(list, (acc, x) => acc | x, false)
170 |
171 | ` true iff every item in list is true `
172 | every := list => reduce(list, (acc, x) => acc & x, true)
173 |
174 | ` concatenate (join) a list of strings into a string `
175 | cat := (list, joiner) => max := len(list) :: {
176 | 0 -> ''
177 | _ -> (sub := (i, acc) => i :: {
178 | max -> acc
179 | _ -> sub(i + 1, acc.len(acc) := joiner + list.(i))
180 | })(1, clone(list.0))
181 | }
182 |
183 | ` for-each loop over a list `
184 | each := (list, f) => (
185 | max := len(list)
186 | (sub := i => i :: {
187 | max -> ()
188 | _ -> (
189 | f(list.(i), i)
190 | sub(i + 1)
191 | )
192 | })(0)
193 | )
194 |
195 | ` encode string buffer into a number list `
196 | encode := str => (
197 | max := len(str)
198 | (sub := (i, acc) => i :: {
199 | max -> acc
200 | _ -> sub(i + 1, acc.(i) := point(str.(i)))
201 | })(0, [])
202 | )
203 |
204 | ` decode number list into an ascii string `
205 | decode := data => reduce(data, (acc, cp) => acc.len(acc) := char(cp), '')
206 |
207 | ` utility for reading an entire file `
208 | readFile := (path, cb) => (
209 | BufSize := 4096 ` bytes `
210 | (sub := (offset, acc) => read(path, offset, BufSize, evt => evt.type :: {
211 | 'error' -> cb(())
212 | 'data' -> (
213 | dataLen := len(evt.data)
214 | dataLen = BufSize :: {
215 | true -> sub(offset + dataLen, acc.len(acc) := evt.data)
216 | false -> cb(acc.len(acc) := evt.data)
217 | }
218 | )
219 | }))(0, '')
220 | )
221 |
222 | ` utility for writing an entire file
223 | it's not buffered, because it's simpler, but may cause jank later
224 | we'll address that if/when it becomes a performance issue `
225 | writeFile := (path, data, cb) => delete(path, evt => evt.type :: {
226 | ` write() by itself will not truncate files that are too long,
227 | so we delete the file and re-write. Not efficient, but writeFile
228 | is not meant for large files `
229 | 'end' -> write(path, 0, data, evt => evt.type :: {
230 | 'error' -> cb(())
231 | 'end' -> cb(true)
232 | })
233 | _ -> cb(())
234 | })
235 |
236 | ` template formatting with {{ key }} constructs `
237 | format := (raw, values) => (
238 | ` parser state `
239 | state := {
240 | ` current position in raw `
241 | idx: 0
242 | ` parser internal state:
243 | 0 -> normal
244 | 1 -> seen one {
245 | 2 -> seen two {
246 | 3 -> seen a valid } `
247 | which: 0
248 | ` buffer for currently reading key `
249 | key: ''
250 | ` result build-up buffer `
251 | buf: ''
252 | }
253 |
254 | ` helper function for appending to state.buf `
255 | append := c => state.buf := state.buf + c
256 |
257 | ` read next token, update state `
258 | readNext := () => (
259 | c := raw.(state.idx)
260 |
261 | state.which :: {
262 | 0 -> c :: {
263 | '{' -> state.which := 1
264 | _ -> append(c)
265 | }
266 | 1 -> c :: {
267 | '{' -> state.which := 2
268 | ` if it turns out that earlier brace was not
269 | a part of a format expansion, just backtrack `
270 | _ -> (
271 | append('{' + c)
272 | state.which := 0
273 | )
274 | }
275 | 2 -> c :: {
276 | '}' -> (
277 | ` insert key value `
278 | state.buf := state.buf + string(values.(state.key))
279 | state.key := ''
280 | state.which := 3
281 | )
282 | ` ignore spaces in keys -- not allowed `
283 | ' ' -> ()
284 | _ -> state.key := state.key + c
285 | }
286 | 3 -> c :: {
287 | '}' -> state.which := 0
288 | ` ignore invalid inputs -- treat them as nonexistent `
289 | _ -> ()
290 | }
291 | }
292 |
293 | state.idx := state.idx + 1
294 | )
295 |
296 | ` main recursive sub-loop `
297 | max := len(raw)
298 | (sub := () => state.idx < max :: {
299 | true -> (
300 | readNext()
301 | sub()
302 | )
303 | false -> state.buf
304 | })()
305 | )
306 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ink codebase browser, _"Kin"_ 🔍
2 |
3 | The **Ink codebase browser** is a tool to explore open-source code on GitHub, especially my side projects written in the [Ink](https://dotink.co) programming language. It offers multi-pane, multi-tab interfaces for looking at source code from projects on GitHub, and generates Markdown previews and syntax highlighting for many languages including Ink.
4 |
5 | 
6 |
7 | The codebase browser is a full-stack Ink application. It's written in pure Ink except for the syntax highlighting library used to highlight non-Ink source code ([highlight.js](https://highlightjs.org/)) and [Torus](https://github.com/thesephist/torus) used as the UI rendering backend. In addition to these libraries, the project leans on [September](https://github.com/thesephist/september) for building the frontend and for syntax highlighting Ink source code; and it depends on [Merlot](https://github.com/thesephist/merlot) for rendering Markdown file previews.
8 |
9 | **"Kin"** was my codename for the project before I gave it its final name of the Ink codebase browser — I liked the name because it's an anagram of "ink", and it's short.
10 | ## Features
11 |
12 | There are three main problems I wanted to solve with the codebase browser.
13 |
14 | 1. Most of my side projects written in Ink are open-source on GitHub, but GitHub doesn't (yet) **syntax highlight Ink source code**. This makes Ink code harder to read on GitHub than it has to be.
15 | 2. GitHub's **file browser interface** isn't great for just exploring the file hierarchy of a project. Compared to modern code editors, with a file tree sidebar, multi-pane and multi-tab layouts,
16 | 3. Frequently, I find myself needing to reference Ink source files on my GitHub from elsewhere on the Internet like a blog, and I'm disappointed that I can't simply link to a line-numbered, syntax-highlighted file preview. I wanted some way to **embed Ink source files** from other webpages.
17 |
18 | The codebase browser solves all of these problems through the main interface and the "proxy" service described below. In addition, to improve the experience of looking around a project the app supports some extra features initially unplanned. Some of these are described below.
19 | ### Ink syntax highlighting
20 |
21 | The codebase browser uses [September](https://github.com/thesephist/september)'s Ink syntax highlighting algorithm to syntax highlight Ink programs. The algorithm is compiled to JavaScript so it can run fully in the browser.
22 |
23 | 
24 |
25 | ### Rich file previews
26 |
27 | The codebase browser recognizes four different types of files
28 |
29 | 1. Binary "blob" files, which are ignored for the purpose of the file preview
30 | 2. Image files which are displayed as images in the preview
31 | 3. Text files, which are displayed with line numbers and syntax highlighting, if the language can be detected
32 | 4. Markdown files, which are rendered as their compiled versions with [Merlot](https://github.com/thesephist/merlot)'s Markdown engine (note that Merlot isn't fully compliant with any common Markdown spec -- it mostly just has the subset that I use, though it's being improved.)
33 |
34 | 
35 |
36 | You can see an example Markdown file preview above, next to an Ink source file that's been syntax highlighted. Below, we have a TypeScript React file next to an image preview.
37 |
38 | 
39 |
40 | File previews require the full file to be fetched from GitHub and potentially parsed locally, so files larger than 1MB are not rendered.
41 |
42 | ### File tree and multi-pane multi-tab layout
43 |
44 | As we can see above, Ink codebase browser lets us browse files in a GitHub repository as if we have a text editor or IDE open to the project on a filesystem. Compared to GitHub's browsing interface, where each page can only show one folder and file at a time, this interface with multiple panes and tabs makes it much easier to explore and read source code.
45 |
46 | >⚠ _Tabs and panes can't be drag-and-dropped to rearrange them. I'm not sure if I'll bother adding that functionality, to be honest. But for now, all files initially open in the leftmost/topmost pane, and can be moved to other panes with the right arrow (→) button._
47 |
48 | ## Syntax highlighting as a service
49 |
50 | The Ink codebase browser has a less obvious second feature, which is what I call _syntax highlighting as a service_.
51 |
52 | I frequently want to show some bit of Ink program online, on one of my technical blogs or linked from a comment or tweet, but simply linking to a file or a GitHub preview is suboptimal because Ink has no syntax highlighting support in most places. Given that this project already had a way to syntax highlight Ink code for the web, I chose to repurpose this code to make a service that acts as an HTTP proxy that highlights any Ink code passing through it.
53 |
54 | 
55 |
56 | Using this "proxy" service, I can embed links to syntax-highlighted versions of Ink source files that live on my GitHub repositories. For example, above is a sample document that takes advantage of this. You can see a live demo at [code.dotink.co/demo](https://code.dotink.co/demo).
57 |
58 | ## Architecture
59 |
60 | The Ink codebase browser is a full-stack Ink application. The backend is written in Ink as a single HTTP monolithic server, serving a few different API routes to interface with GitHub's API and act as the syntax highlighting proxy. The frontend is an Ink application that renders to the DOM using a [Torus](https://github.com/thesephist/torus) compatibility layer. I've used this approach a few times now, with other projects like [Merlot](https://github.com/thesephist/merlot) and the [Ink playground](https://github.com/thesephist/maverick).
61 |
62 | The backend is a simple HTTP server. In addition to serving static files, it provides a few internal API endpoints that map 1:1 to specific GitHub API endpoints but translate and filter API responses to the specific data needed by the client-side UI.
63 |
64 | There are a few different pieces of [isomorphic](https://dotink.co/posts/eliza/) Ink code being used in the codebase browser. Much of the standard library is, obviously, cross-compiled to JavaScript. But the syntax highlighting library from the September compiler toolchain is both compiled to JavaScript and run natively on the backend, and the Markdown engine from Merlot is also compiled down for the browser.
65 |
66 | ## Development
67 |
68 | Kin uses a Makefile for development tasks. To develop Kin, you'll need to install [Ink](https://dotink.co/) and the [September](https://github.com/thesephist/september) compiler. [inkfmt](https://github.com/thesephist/inkfmt) is optional, and used for code formatting.
69 |
70 | - `make run` starts the web server in the same way as the production environment. To run the server, you'll need to replace the dummy token in `secrets.sample.ink` with your GitHub access token and copy it to `secrets.ink`.
71 | - `make watch` runs `make build` every time a relevant Ink source file in `./src` or `./lib` changes.
72 | - `make check` or `make t` runs unit tests, which lives in `test/`.
73 | - `make fmt` runs the [inkfmt](https://github.com/thesephist/inkfmt) code formatter over all Ink source code in the project.
74 |
75 | ### Build tasks
76 |
77 | Because of the number of dependencies in this project, Kin has a multi-part build process. We first build four different components (three of which change rarely), and bundle them into one production bundle at the end.
78 |
79 | - `make build-libs`, `make build-september` and `make build-merlot` perform builds of different dependencies of Kin -- standard library dependencies, and dependencies from September and Merlot (syntax highlighter and Markdown engine).
80 | - `make build` builds the frontend JavaScript bundle from `./src/app.js.ink`.
81 | - `make build-all` performs a fresh build, assuming all sibling dependencies (September and Merlot) are present next to this project's root directory on your system.
82 |
83 | September and Merlot are development-time dependencies for this project, but these libraries are compiled to JavaScript vendored into the project, so for normal builds using just `make build`, they are not necessary to be on your machine.
84 |
--------------------------------------------------------------------------------
/static/js/ink.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Ink/JavaScript runtime/interop layer
3 | * implements Ink system interfaces for web and Node JS runtimes
4 | */
5 |
6 | const __NODE = typeof process === 'object';
7 |
8 | /* Ink builtins */
9 |
10 | function args() {
11 | return process.argv;
12 | }
13 |
14 | function __ink_ident_in(cb) {
15 | // TODO
16 | }
17 |
18 | function out(s) {
19 | s = __as_ink_string(s);
20 | if (__NODE) {
21 | process.stdout.write(string(s).valueOf());
22 | } else {
23 | console.log(string(s).valueOf());
24 | }
25 | return null;
26 | }
27 |
28 | function dir(path, cb) {
29 | // TODO
30 | }
31 |
32 | function make(path, cb) {
33 | // TODO
34 | }
35 |
36 | function stat(path, cb) {
37 | // TODO
38 | }
39 |
40 | function read(path, offset, length, cb) {
41 | // TODO
42 | }
43 |
44 | function write(path, offset, data, cb) {
45 | // TODO
46 | }
47 |
48 | function __ink_ident_delete(path, cb) {
49 | // TODO
50 | }
51 |
52 | function listen(host, handler) {
53 | // TODO
54 | }
55 |
56 | function req(data, callback) {
57 | // TODO
58 | }
59 |
60 | function rand() {
61 | return Math.random();
62 | }
63 |
64 | function urand(length) {
65 | // TODO
66 | }
67 |
68 | function time() {
69 | return Date.now() / 1000;
70 | }
71 |
72 | function wait(duration, cb) {
73 | setTimeout(cb, duration * 1000);
74 | return null;
75 | }
76 |
77 | function exec(path, args, stdin, stdoutFn) {
78 | // TODO
79 | }
80 |
81 | function exit(code) {
82 | if (__NODE) {
83 | process.exit(code);
84 | } else {
85 | // TODO
86 | }
87 | return null;
88 | }
89 |
90 | function sin(n) {
91 | return Math.sin(n);
92 | }
93 |
94 | function cos(n) {
95 | return Math.cos(n);
96 | }
97 |
98 | function asin(n) {
99 | return Math.asin(n);
100 | }
101 |
102 | function acos(n) {
103 | return Math.acos(n);
104 | }
105 |
106 | function pow(b, n) {
107 | return Math.pow(b, n);
108 | }
109 |
110 | function ln(n) {
111 | return Math.log(n);
112 | }
113 |
114 | function floor(n) {
115 | return Math.floor(n);
116 | }
117 |
118 | function load(path) {
119 | if (__NODE) {
120 | return require(string(path).valueOf());
121 | } else {
122 | throw new Error('load() not implemented!');
123 | }
124 | }
125 |
126 | function __is_ink_string(x) {
127 | if (x == null) {
128 | return false;
129 | }
130 | return x.__mark_ink_string;
131 | }
132 |
133 | // both JS native strings and __Ink_Strings are valid in the runtime
134 | // semantics but we want to coerce values to __Ink_Strings
135 | // within runtime builtins; this utility fn is useful for this.
136 | function __as_ink_string(x) {
137 | if (typeof x === 'string') {
138 | return __Ink_String(x);
139 | }
140 | return x;
141 | }
142 |
143 | function string(x) {
144 | x = __as_ink_string(x);
145 | if (x === null) {
146 | return '()';
147 | } else if (typeof x === 'number') {
148 | const sign = x > 0 ? 1 : -1;
149 | x = sign * x;
150 | const whole = Math.floor(x);
151 | const frac = x - whole;
152 | const wholeStr = (sign * whole).toString();
153 | if (frac == 0) {
154 | return wholeStr;
155 | } else {
156 | const fracStr = frac.toString().substr(0, 10).padEnd(10, '0').substr(1);
157 | return wholeStr + fracStr;
158 | }
159 | } else if (__is_ink_string(x)) {
160 | return x;
161 | } else if (typeof x === 'boolean') {
162 | return x.toString();
163 | } else if (typeof x === 'function') {
164 | return x.toString(); // implementation-dependent, not specified
165 | } else if (Array.isArray(x) || typeof x === 'object') {
166 | const entries = [];
167 | for (const key of keys(x)) {
168 | entries.push(`${key}: ${__is_ink_string(x[key]) ? `'${x[key].valueOf().replace('\\', '\\\\').replace('\'', '\\\'')}'` : string(x[key])}`);
169 | }
170 | return '{' + entries.join(', ') + '}';
171 | } else if (x === undefined) {
172 | return 'undefined'; // undefined behavior
173 | }
174 | throw new Error('string() called on unknown type ' + x);
175 | }
176 |
177 | function number(x) {
178 | x = __as_ink_string(x);
179 | if (x === null) {
180 | return 0;
181 | } else if (typeof x === 'number') {
182 | return x;
183 | } else if (__is_ink_string(x)) {
184 | const n = parseFloat(x);
185 | return isNaN(n) ? null : n;
186 | } else if (typeof x === 'boolean') {
187 | return x ? 1 : 0;
188 | }
189 | return 0;
190 | }
191 |
192 | function point(c) {
193 | c = __as_ink_string(c);
194 | return c.valueOf().charCodeAt(0);
195 | }
196 |
197 | function char(n) {
198 | return String.fromCharCode(n);
199 | }
200 |
201 | function type(x) {
202 | x = __as_ink_string(x);
203 | if (x === null) {
204 | return '()';
205 | } else if (typeof x === 'number') {
206 | return 'number';
207 | } else if (__is_ink_string(x)) {
208 | return 'string';
209 | } else if (typeof x === 'boolean') {
210 | return 'boolean'
211 | } else if (typeof x === 'function') {
212 | return 'function';
213 | } else if (Array.isArray(x) || typeof x === 'object') {
214 | return 'composite';
215 | }
216 | throw new Error('type() called on unknown type ' + x);
217 | }
218 |
219 | function len(x) {
220 | x = __as_ink_string(x);
221 | switch (type(x)) {
222 | case 'string':
223 | return x.valueOf().length;
224 | case 'composite':
225 | if (Array.isArray(x)) {
226 | // -1 for .length
227 | return Object.getOwnPropertyNames(x).length - 1;
228 | } else {
229 | return Object.getOwnPropertyNames(x).length;
230 | }
231 | default:
232 | throw new Error('len() takes a string or composite value, but got ' + string(x));
233 | }
234 | }
235 |
236 | function keys(x) {
237 | if (type(x).valueOf() === 'composite') {
238 | if (Array.isArray(x)) {
239 | return Object.getOwnPropertyNames(x).filter(name => name !== 'length');
240 | } else {
241 | return Object.getOwnPropertyNames(x);
242 | }
243 | }
244 | throw new Error('keys() takes a composite value, but got ' + string(x).valueOf());
245 | }
246 |
247 | /* Ink semantics polyfill */
248 |
249 | function __ink_negate(x) {
250 | if (x === true) {
251 | return false;
252 | }
253 | if (x === false) {
254 | return true;
255 | }
256 |
257 | return -x;
258 | }
259 |
260 | function __ink_eq(a, b) {
261 | a = __as_ink_string(a);
262 | b = __as_ink_string(b);
263 | if (a === __Ink_Empty || b === __Ink_Empty) {
264 | return true;
265 | }
266 |
267 | if (a === null && b === null) {
268 | return true;
269 | }
270 | if (a === null || b === null) {
271 | return false;
272 | }
273 |
274 | if (typeof a !== typeof b) {
275 | return false;
276 | }
277 | if (__is_ink_string(a) && __is_ink_string(b)) {
278 | return a.valueOf() === b.valueOf();
279 | }
280 | if (typeof a === 'number' || typeof a === 'boolean' || typeof a === 'function') {
281 | return a === b;
282 | }
283 |
284 | // deep equality check for composite types
285 | if (typeof a !== 'object') {
286 | return false;
287 | }
288 | if (len(a) !== len(b)) {
289 | return false;
290 | }
291 | for (const key of keys(a)) {
292 | if (!__ink_eq(a[key], b[key])) {
293 | return false;
294 | }
295 | }
296 | return true;
297 | }
298 |
299 | function __ink_and(a, b) {
300 | if (typeof a === 'boolean' && typeof b === 'boolean') {
301 | return a && b;
302 | }
303 |
304 | if (__is_ink_string(a) && __is_ink_string(b)) {
305 | const max = Math.max(a.length, b.length);
306 | const get = (s, i) => s.valueOf().charCodeAt(i) || 0;
307 |
308 | let res = '';
309 | for (let i = 0; i < max; i ++) {
310 | res += String.fromCharCode(get(a, i) & get(b, i));
311 | }
312 | return res;
313 | }
314 |
315 | return a & b;
316 | }
317 |
318 | function __ink_or(a, b) {
319 | if (typeof a === 'boolean' && typeof b === 'boolean') {
320 | return a || b;
321 | }
322 |
323 | if (__is_ink_string(a) && __is_ink_string(b)) {
324 | const max = Math.max(a.length, b.length);
325 | const get = (s, i) => s.valueOf().charCodeAt(i) || 0;
326 |
327 | let res = '';
328 | for (let i = 0; i < max; i ++) {
329 | res += String.fromCharCode(get(a, i) | get(b, i));
330 | }
331 | return res;
332 | }
333 |
334 | return a | b;
335 | }
336 |
337 | function __ink_xor(a, b) {
338 | if (typeof a === 'boolean' && typeof b === 'boolean') {
339 | return (a && !b) || (!a && b);
340 | }
341 |
342 | if (__is_ink_string(a) && __is_ink_string(b)) {
343 | const max = Math.max(a.length, b.length);
344 | const get = (s, i) => s.valueOf().charCodeAt(i) || 0;
345 |
346 | let res = '';
347 | for (let i = 0; i < max; i ++) {
348 | res += String.fromCharCode(get(a, i) ^ get(b, i));
349 | }
350 | return res;
351 | }
352 |
353 | return a ^ b;
354 | }
355 |
356 | function __ink_match(cond, clauses) {
357 | for (const [target, expr] of clauses) {
358 | if (__ink_eq(cond, target())) {
359 | return expr();
360 | }
361 | }
362 | return null;
363 | }
364 |
365 | /* Ink types */
366 |
367 | const __Ink_Empty = Symbol('__Ink_Empty');
368 |
369 | const __Ink_String = s => {
370 | if (__is_ink_string(s)) return s;
371 |
372 | return {
373 | __mark_ink_string: true,
374 | assign(i, slice) {
375 | if (i === s.length) {
376 | return s += slice;
377 | }
378 |
379 | return s = s.substr(0, i) + slice + s.substr(i + slice.length);
380 | },
381 | toString() {
382 | return s;
383 | },
384 | valueOf() {
385 | return s;
386 | },
387 | get length() {
388 | return s.length;
389 | },
390 | }
391 | }
392 |
393 | /* TCE trampoline helpers */
394 |
395 | function __ink_resolve_trampoline(fn, ...args) {
396 | let rv = fn(...args);
397 | while (rv && rv.__is_ink_trampoline) {
398 | rv = rv.fn(...rv.args);
399 | }
400 | return rv;
401 | }
402 |
403 | function __ink_trampoline(fn, ...args) {
404 | return {
405 | __is_ink_trampoline: true,
406 | fn: fn,
407 | args: args,
408 | }
409 | }
410 |
411 | /* Ink -> JavaScript interop helpers */
412 |
413 | const bind = (target, fn) => target[fn].bind(target);
414 |
415 | function jsnew(Constructor, args) {
416 | return new Constructor(...args);
417 | }
418 |
--------------------------------------------------------------------------------
/vendor/tokenize.ink:
--------------------------------------------------------------------------------
1 | std := load('../vendor/std')
2 |
3 | log := std.log
4 | f := std.format
5 | slice := std.slice
6 | map := std.map
7 | reduce := std.reduce
8 | every := std.every
9 |
10 | str := load('../vendor/str')
11 |
12 | digit? := str.digit?
13 | hasPrefix? := str.hasPrefix?
14 | index := str.index
15 |
16 | mkiota := load('iota').new
17 |
18 | Newline := char(10)
19 | Tab := char(9)
20 |
21 | iota := mkiota().next
22 | Tok := {
23 | Separator: iota()
24 |
25 | Comment: iota()
26 |
27 | Ident: iota()
28 | EmptyIdent: iota()
29 |
30 | NumberLiteral: iota()
31 | StringLiteral: iota()
32 |
33 | TrueLiteral: iota()
34 | FalseLiteral: iota()
35 |
36 | AccessorOp: iota()
37 |
38 | EqOp: iota()
39 | FunctionArrow: iota()
40 |
41 | KeyValueSeparator: iota()
42 | DefineOp: iota()
43 | MatchColon: iota()
44 |
45 | CaseArrow: iota()
46 | SubOp: iota()
47 |
48 | NegOp: iota()
49 | AddOp: iota()
50 | MulOp: iota()
51 | DivOp: iota()
52 | ModOp: iota()
53 | GtOp: iota()
54 | LtOp: iota()
55 |
56 | AndOp: iota()
57 | OrOp: iota()
58 | XorOp: iota()
59 |
60 | LParen: iota()
61 | RParen: iota()
62 | LBracket: iota()
63 | RBracket: iota()
64 | LBrace: iota()
65 | RBrace: iota()
66 | }
67 |
68 | typeName := type => reduce(keys(Tok), (acc, k) => Tok.(k) :: {
69 | type -> k
70 | _ -> acc
71 | }, '(unknown token)')
72 |
73 | tkString := tok => f('{{ 0 }}({{ 1 }}) @ {{2}}:{{3}}'
74 | [typeName(tok.type), tok.val, tok.line, tok.col])
75 |
76 | token := (type, val, line, col, i) => {
77 | type: type
78 | val: val
79 | line: line
80 | col: col
81 | i: i
82 | }
83 |
84 | tokenizeWithOpt := (s, lexComments) => (
85 | S := {
86 | i: ~1
87 | buf: ''
88 | strbuf: ''
89 | strbufLine: 0
90 | strbufCol: 0
91 |
92 | lastType: Tok.Separator
93 | line: 1
94 | col: 0
95 |
96 | inStringLiteral: false
97 | }
98 | tokens := []
99 |
100 | simpleCommit := tok => (
101 | S.lastType := tok.type
102 | tokens.len(tokens) := tok
103 | )
104 | simpleCommitChar := type => simpleCommit(token(
105 | type
106 | ()
107 | S.line
108 | S.col
109 | type :: {
110 | Tok.TrueLiteral -> S.i - 4
111 | Tok.FalseLiteral -> S.i - 5
112 | _ -> S.i - 1
113 | }
114 | ))
115 | commitClear := () => S.buf :: {
116 | '' -> _
117 | _ -> (
118 | cbuf := S.buf
119 | S.buf := ''
120 | cbuf :: {
121 | 'true' -> simpleCommitChar(Tok.TrueLiteral)
122 | 'false' -> simpleCommitChar(Tok.FalseLiteral)
123 | _ -> digit?(cbuf) :: {
124 | true -> simpleCommit(token(
125 | Tok.NumberLiteral
126 | number(cbuf)
127 | S.line
128 | S.col - len(cbuf)
129 | S.i - len(cbuf)
130 | ))
131 | false -> simpleCommit(token(
132 | Tok.Ident
133 | cbuf
134 | S.line
135 | S.col - len(cbuf)
136 | S.i - len(cbuf)
137 | ))
138 | }
139 | }
140 | )
141 | }
142 | commit := tok => (
143 | commitClear()
144 | simpleCommit(tok)
145 | )
146 | commitChar := type => commit(token(type, (), S.line, S.col, S.i))
147 | ensureSeparator := () => (
148 | commitClear()
149 | S.lastType :: {
150 | Tok.Separator -> ()
151 | Tok.LParen -> ()
152 | Tok.LBracket -> ()
153 | Tok.LBrace -> ()
154 | Tok.AddOp -> ()
155 | Tok.SubOp -> ()
156 | Tok.MulOp -> ()
157 | Tok.DivOp -> ()
158 | Tok.ModOp -> ()
159 | Tok.NegOp -> ()
160 | Tok.GtOp -> ()
161 | Tok.LtOp -> ()
162 | Tok.EqOp -> ()
163 | Tok.DefineOp -> ()
164 | Tok.AccessorOp -> ()
165 | Tok.KeyValueSeparator -> ()
166 | Tok.FunctionArrow -> ()
167 | Tok.MatchColon -> ()
168 | Tok.CaseArrow -> ()
169 | _ -> commitChar(Tok.Separator)
170 | }
171 | )
172 | finalize := () => (
173 | ensureSeparator()
174 | tokens
175 | )
176 |
177 | hasPrefix?(s, '#!') :: {
178 | true -> (
179 | S.i := index(s, Newline)
180 | S.line := S.line + 1
181 | )
182 | }
183 |
184 | (sub := () => (
185 | S.i := S.i + 1
186 | S.col := S.col + 1
187 | c := s.(S.i)
188 | [c, S.inStringLiteral] :: {
189 | [(), _] -> finalize()
190 | ['\'', _] -> S.inStringLiteral :: {
191 | true -> (
192 | commit(token(
193 | Tok.StringLiteral
194 | S.strbuf
195 | S.strbufLine
196 | S.strbufCol
197 | S.i - len(S.strbuf) - 1
198 | ))
199 | S.inStringLiteral := false
200 | sub()
201 | )
202 | false -> (
203 | S.strbuf := ''
204 | S.strbufLine := S.line
205 | S.strbufCol := S.col
206 | S.inStringLiteral := true
207 | sub()
208 | )
209 | }
210 | [_, true] -> c :: {
211 | Newline -> (
212 | S.line := S.line + 1
213 | S.col := 0
214 | S.strbuf := S.strbuf + c
215 | sub()
216 | )
217 | '\\' -> (
218 | S.i := S.i + 1
219 | S.strbuf := S.strbuf + s.(S.i)
220 | S.col := S.col + 1
221 | sub()
222 | )
223 | _ -> (
224 | S.strbuf := S.strbuf + c
225 | sub()
226 | )
227 | }
228 | _ -> c :: {
229 | '`' -> s.(S.i + 1) :: {
230 | ` line comment `
231 | '`' -> advance := index(slice(s, S.i, len(s)), Newline) :: {
232 | ~1 -> (
233 | lexComments :: {
234 | true -> commit(token(
235 | Tok.Comment
236 | slice(s, S.i, len(s))
237 | S.line
238 | S.col
239 | S.i
240 | ))
241 | }
242 | finalize()
243 | )
244 | _ -> (
245 | line := S.line
246 | col := S.col
247 | i := S.i
248 |
249 | S.i := S.i + advance
250 | lexComments :: {
251 | true -> commit(token(
252 | Tok.Comment
253 | slice(s, i, S.i)
254 | line
255 | col
256 | i
257 | ))
258 | }
259 | ensureSeparator()
260 | S.line := S.line + 1
261 | S.col := 0
262 | sub()
263 | )
264 | }
265 | _ -> (
266 | ` block comment, keep taking until end of block `
267 | line := S.line
268 | col := S.col
269 | i := S.i
270 |
271 | S.i := S.i + 1
272 | (sub := () => s.(S.i) :: {
273 | '`' -> S.col := S.col + 1
274 | Newline -> (
275 | S.i := S.i + 1
276 | S.line := S.line + 1
277 | S.col := 0
278 | sub()
279 | )
280 | ` comments that don't end should be ignored `
281 | () -> ()
282 | _ -> (
283 | S.i := S.i + 1
284 | S.col := S.col + 1
285 | sub()
286 | )
287 | })()
288 | lexComments :: {
289 | true -> commit(token(
290 | Tok.Comment
291 | slice(s, i, S.i + 1)
292 | line
293 | col
294 | i
295 | ))
296 | }
297 | sub()
298 | )
299 | }
300 | Newline -> (
301 | ensureSeparator()
302 | S.line := S.line + 1
303 | S.col := 0
304 | sub()
305 | )
306 | Tab -> (
307 | commitClear()
308 | sub()
309 | )
310 | ' ' -> (
311 | commitClear()
312 | sub()
313 | )
314 | '_' -> (
315 | commitChar(Tok.EmptyIdent)
316 | sub()
317 | )
318 | '~' -> (
319 | commitChar(Tok.NegOp)
320 | sub()
321 | )
322 | '+' -> (
323 | commitChar(Tok.AddOp)
324 | sub()
325 | )
326 | '*' -> (
327 | commitChar(Tok.MulOp)
328 | sub()
329 | )
330 | '/' -> (
331 | commitChar(Tok.DivOp)
332 | sub()
333 | )
334 | '%' -> (
335 | commitChar(Tok.ModOp)
336 | sub()
337 | )
338 | '&' -> (
339 | commitChar(Tok.AndOp)
340 | sub()
341 | )
342 | '|' -> (
343 | commitChar(Tok.OrOp)
344 | sub()
345 | )
346 | '^' -> (
347 | commitChar(Tok.XorOp)
348 | sub()
349 | )
350 | '<' -> (
351 | commitChar(Tok.LtOp)
352 | sub()
353 | )
354 | '>' -> (
355 | commitChar(Tok.GtOp)
356 | sub()
357 | )
358 | ',' -> (
359 | ensureSeparator()
360 | sub()
361 | )
362 | '.' -> [S.buf, every(map(S.buf, digit?))] :: {
363 | ['', _] -> (
364 | commitChar(Tok.AccessorOp)
365 | sub()
366 | )
367 | [_, true] -> (
368 | S.buf := S.buf + '.'
369 | sub()
370 | )
371 | _ -> (
372 | commitChar(Tok.AccessorOp)
373 | sub()
374 | )
375 | }
376 | ':' -> s.(S.i + 1) :: {
377 | '=' -> (
378 | commitChar(Tok.DefineOp)
379 | S.i := S.i + 1
380 | sub()
381 | )
382 | ':' -> (
383 | commitChar(Tok.MatchColon)
384 | S.i := S.i + 1
385 | sub()
386 | )
387 | _ -> (
388 | ensureSeparator()
389 | commitChar(Tok.KeyValueSeparator)
390 | sub()
391 | )
392 | }
393 | '=' -> s.(S.i + 1) :: {
394 | '>' -> (
395 | commitChar(Tok.FunctionArrow)
396 | S.i := S.i + 1
397 | sub()
398 | )
399 | _ -> (
400 | commitChar(Tok.EqOp)
401 | sub()
402 | )
403 | }
404 | '-' -> s.(S.i + 1) :: {
405 | '>' -> (
406 | commitChar(Tok.CaseArrow)
407 | S.i := S.i + 1
408 | sub()
409 | )
410 | _ -> (
411 | commitChar(Tok.SubOp)
412 | sub()
413 | )
414 | }
415 | '(' -> (
416 | commitChar(Tok.LParen)
417 | sub()
418 | )
419 | ')' -> (
420 | ensureSeparator()
421 | commitChar(Tok.RParen)
422 | sub()
423 | )
424 | '[' -> (
425 | commitChar(Tok.LBracket)
426 | sub()
427 | )
428 | ']' -> (
429 | ensureSeparator()
430 | commitChar(Tok.RBracket)
431 | sub()
432 | )
433 | '{' -> (
434 | commitChar(Tok.LBrace)
435 | sub()
436 | )
437 | '}' -> (
438 | ensureSeparator()
439 | commitChar(Tok.RBrace)
440 | sub()
441 | )
442 | _ -> (
443 | ` strange hack required for mutating a nested string.
444 | might be an Ink interpreter bug... `
445 | S.buf := S.buf + c
446 | sub()
447 | )
448 | }
449 | }
450 | ))()
451 | )
452 |
453 | tokenize := s => tokenizeWithOpt(s, false)
454 |
455 | tokenizeWithComments := s => tokenizeWithOpt(s, true)
456 |
--------------------------------------------------------------------------------
/static/js/torus.min.js:
--------------------------------------------------------------------------------
1 | !function(t){var e={};function s(r){if(e[r])return e[r].exports;var n=e[r]={i:r,l:!1,exports:{}};return t[r].call(n.exports,n,n.exports,s),n.l=!0,n.exports}s.m=t,s.c=e,s.d=function(t,e,r){s.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},s.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},s.t=function(t,e){if(1&e&&(t=s(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(s.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)s.d(r,n,function(e){return t[e]}.bind(null,n));return r},s.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return s.d(e,"a",e),e},s.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},s.p="",s(s.s=0)}([function(t,e,s){const{render:r,Component:n,Styled:o,StyledComponent:i,List:c,ListOf:l,Record:a,Store:d,StoreOf:u,Router:h}=s(1),{jdom:f,css:m}=s(2);t.exports={render:r,Component:n,Styled:o,StyledComponent:i,List:c,ListOf:l,Record:a,Store:d,StoreOf:u,Router:h,jdom:f,css:m}},function(t,e,s){let r=0;const n=t=>null!==t&&"object"==typeof t,o=t=>{void 0===t.attrs&&(t.attrs={}),void 0===t.events&&(t.events={}),void 0===t.children&&(t.children=[])},i=t=>Array.isArray(t)?t:[t],c=()=>document.createComment("");let l=[];const a={replaceChild:()=>{}};const d=(t,e,s)=>{for(const r of Object.keys(t)){const n=i(t[r]),o=i(e[r]||[]);for(const t of n)o.includes(t)||"function"!=typeof t||s(r,t)}},u=(t,e,s)=>{const i=e=>{t&&t!==e&&l.push([2,t,e]),t=e};if(r++,e!==s)if(null===s)i(c());else if("string"==typeof s||"number"==typeof s)"string"==typeof e||"number"==typeof e?t.data=s:i(document.createTextNode(s));else if(void 0!==s.appendChild)i(s);else{(void 0===t||!n(e)||e&&void 0!==e.appendChild||e.tag!==s.tag)&&(e={tag:null},i(document.createElement(s.tag))),o(e),o(s);for(const r of Object.keys(s.attrs)){const n=e.attrs[r],o=s.attrs[r];if("class"===r){const e=o;Array.isArray(e)?t.className=e.join(" "):t.className=e}else if("style"===r){const e=n||{},s=o;for(const r of Object.keys(s))s[r]!==e[r]&&(t.style[r]=s[r]);for(const r of Object.keys(e))void 0===s[r]&&(t.style[r]="")}else r in t?(t[r]!==o||void 0===n&&n!==o)&&(t[r]=o):n!==o&&t.setAttribute(r,o)}for(const r of Object.keys(e.attrs))void 0===s.attrs[r]&&(r in t?t[r]=null:t.removeAttribute(r));d(s.events,e.events,(e,s)=>{t.addEventListener(e,s)}),d(e.events,s.events,(e,s)=>{t.removeEventListener(e,s)});const r=e.children,c=s.children,a=r.length,h=c.length;if(h+a>0){const n=e._nodes||[],o=a{}},this.init(...t),void 0===this.node&&this.render()}static from(t){return class extends h{init(...t){this.args=t}compose(){return t(...this.args)}}}init(){}get record(){return this.event.source}bind(t,e){if(this.unbind(),!(t instanceof j))throw new Error(`cannot bind to ${t}, which is not an instance of Evented.`);this.event={source:t,handler:e},t.addHandler(e)}unbind(){this.record&&this.record.removeHandler(this.event.handler),this.event={source:null,handler:()=>{}}}remove(){this.unbind()}compose(){return null}preprocess(t){return t}render(t){t=t||this.record&&this.record.summarize();const e=this.preprocess(this.compose(t),t);if(void 0===e)throw new Error(this.constructor.name+".compose() returned undefined.");try{this.node=u(this.node,this.jdom,e)}catch(t){console.error("rendering error.",t)}return this.jdom=e}}const f=new Set;let m;const p=new WeakMap,v=(t,e)=>t+"{"+e+"}",b=(t,e)=>{let s=[],r="";for(const n of Object.keys(e)){const o=e[n];if("@"===n[0])n.startsWith("@media")?s.push(v(n,b(t,o).join(""))):s.push(v(n,b("",o).join("")));else if("object"==typeof o){const e=n.split(",");for(const r of e)if(r.includes("&")){const e=r.replace(/&/g,t);s=s.concat(b(e,o))}else s=s.concat(b(t+" "+r,o))}else r+=n+":"+o+";"}return r&&s.unshift(v(t,r)),s},g=t=>{const e=(t=>{if(!p.has(t)){const e=JSON.stringify(t);let s=e.length,r=1989;for(;s;)r=13*r^e.charCodeAt(--s);p.set(t,"_torus"+(r>>>0))}return p.get(t)})(t);let s=0;if(!f.has(e)){m||(()=>{const t=document.createElement("style");t.setAttribute("data-torus",""),document.head.appendChild(t),m=t.sheet})();const r=b("."+e,t);for(const t of r)m.insertRule(t,s++);f.add(e)}return e},y=t=>class extends t{styles(){return{}}preprocess(t,e){return n(t)&&(t.attrs=t.attrs||{},t.attrs.class=i(t.attrs.class||[]),t.attrs.class.push(g(this.styles(e)))),t}};class x extends h{get itemClass(){return h}init(t,...e){this.store=t,this.items=new Map,this.filterFn=null,this.itemData=e,this.bind(this.store,()=>this.itemsChanged())}itemsChanged(){const t=this.store.summarize(),e=this.items;for(const s of e.keys())t.includes(s)||(e.get(s).remove(),e.delete(s));for(const s of t)e.has(s)||e.set(s,new this.itemClass(s,()=>this.store.remove(s),...this.itemData));let s=[...e.entries()];null!==this.filterFn&&(s=s.filter(t=>this.filterFn(t[0]))),s.sort((e,s)=>t.indexOf(e[0])-t.indexOf(s[0])),this.items=new Map(s),this.render()}filter(t){this.filterFn=t,this.itemsChanged()}unfilter(){this.filterFn=null,this.itemsChanged()}get components(){return[...this]}get nodes(){return this.components.map(t=>t.node)}[Symbol.iterator](){return this.items.values()}remove(){super.remove();for(const t of this.items.values())t.remove()}compose(){return{tag:"ul",children:this.nodes}}}class j{constructor(){this.handlers=new Set}summarize(){}emitEvent(){const t=this.summarize();for(const e of this.handlers)e(t)}addHandler(t){this.handlers.add(t),t(this.summarize())}removeHandler(t){this.handlers.delete(t)}}class w extends j{constructor(t,e={}){super(),n(t)&&(e=t,t=null),this.id=t,this.data=e}update(t){Object.assign(this.data,t),this.emitEvent()}get(t){return this.data[t]}summarize(){return Object.assign({id:this.id},this.data)}serialize(){return this.summarize()}}class O extends j{constructor(t=[]){super(),this.reset(t)}get recordClass(){return w}get comparator(){return null}create(t,e){return this.add(new this.recordClass(t,e))}add(t){return this.records.add(t),this.emitEvent(),t}remove(t){return this.records.delete(t),this.emitEvent(),t}[Symbol.iterator](){return this.records.values()}find(t){for(const e of this.records)if(e.id===t)return e;return null}reset(t){this.records=new Set(t),this.emitEvent()}summarize(){return[...this.records].map(t=>[this.comparator?this.comparator(t):null,t]).sort((t,e)=>t[0]e[0]?1:0).map(t=>t[1])}serialize(){return this.summarize().map(t=>t.serialize())}}const C=t=>{let e;const s=[];for(;null!==e;)if(e=/:\w+/.exec(t),e){const r=e[0];s.push(r.substr(1)),t=t.replace(r,"(.+)")}return[new RegExp(t),s]};const S={render:u,Component:h,Styled:y,StyledComponent:y(h),List:x,ListOf:t=>class extends x{get itemClass(){return t}},Record:w,Store:O,StoreOf:t=>class extends O{get recordClass(){return t}},Router:class extends j{constructor(t){super(),this.routes=Object.entries(t).map(([t,e])=>[t,...C(e)]),this.lastMatch=["",null],this._cb=()=>this.route(location.pathname),window.addEventListener("popstate",this._cb),this._cb()}summarize(){return this.lastMatch}go(t,{replace:e=!1}={}){window.location.pathname!==t&&(e?history.replaceState(null,document.title,t):history.pushState(null,document.title,t),this.route(t))}route(t){for(const[e,s,r]of this.routes){const n=s.exec(t);if(null!==n){const t={},s=n.slice(1);r.forEach((e,r)=>t[e]=s[r]),this.lastMatch=[e,t];break}}this.emitEvent()}remove(){window.removeEventListener("popstate",this._cb)}}};"object"==typeof window&&(window.Torus=S),t.exports&&(t.exports=S)},function(t,e,s){const r=t=>null!==t&&"object"==typeof t,n=(t,e)=>t.substr(0,t.length-e.length),o=(t,e)=>{let s=t[0];for(let r=1,n=e.length;r<=n;r++)s+=e[r-1]+t[r];return s};class i{constructor(t){this.idx=0,this.content=t,this.len=t.length}next(){const t=this.content[this.idx++];return void 0===t&&(this.idx=this.len),t}back(){this.idx--}readUpto(t){const e=this.content.substr(this.idx).indexOf(t);return this.toNext(e)}readUntil(t){const e=this.content.substr(this.idx).indexOf(t)+t.length;return this.toNext(e)}toNext(t){const e=this.content.substr(this.idx);if(-1===t)return this.idx=this.len,e;{const s=e.substr(0,t);return this.idx+=t,s}}clipEnd(t){return!!this.content.endsWith(t)&&(this.content=n(this.content,t),!0)}}const c=t=>{let e="";for(let s=0,r=t.length;s{if("!"===(t=t.trim())[0])return{jdom:null,selfClosing:!0};if(!t.includes(" ")){const e=t.endsWith("/");return{jdom:{tag:e?n(t,"/"):t,attrs:{},events:{}},selfClosing:e}}const e=new i(t),s=e.clipEnd("/");let r="",o=!1,l=!1;const a=[];let d=0;const u=t=>{r=r.trim(),(""!==r||t)&&(a.push({type:d,value:r}),o=!1,r="")};for(let t=e.next();void 0!==t;t=e.next())switch(t){case"=":l?r+=t:(u(),o=!0,d=1);break;case" ":l?r+=t:o||(u(),d=0);break;case"\\":l&&(t=e.next(),r+=t);break;case'"':l?(l=!1,u(!0),d=0):1===d&&(l=!0);break;default:r+=t,o=!1}u();let h="";const f={},m={};h=a.shift().value;let p=null,v=a.shift();const b=()=>{p=v,v=a.shift()};for(;void 0!==v;){if(1===v.type){const t=p.value;let e=v.value.trim();if(t.startsWith("on"))m[t.substr(2)]=[e];else if("class"===t)""!==e&&(f[t]=e.split(" "));else if("style"===t){e.endsWith(";")&&(e=e.substr(0,e.length-1));const s={};for(const t of e.split(";")){const e=t.indexOf(":"),r=t.substr(0,e),n=t.substr(e+1);s[c(r.trim())]=n.trim()}f[t]=s}else f[t]=e;b()}else p&&(f[p.value]=!0);b()}return p&&0===p.type&&(f[p.value]=!0),{jdom:{tag:h,attrs:f,events:m},selfClosing:s}},a=t=>{const e=[];let s=null,r=!1;const n=()=>{r&&""===s.trim()||s&&e.push(s),s=null,r=!1},o=t=>{!1===r&&(n(),r=!0,s=""),s+=t};for(let e=t.next();void 0!==e;e=t.next())if("<"===e){if(n(),"/"===t.next()){t.readUntil(">");break}{t.back();const e=l(t.readUpto(">"));t.next(),s=e&&e.jdom,e.selfClosing||null===s||(s.children=a(t))}}else o("&"===e?(i=e+t.readUntil(";"),String.fromCodePoint(+/(\w+);/.exec(i)[1])):e);var i;return n(),e},d=new Map,u=/jdom_tpl_obj_\[(\d+)\]/,h=(t,e)=>{if((t=>"string"==typeof t&&t.includes("jdom_tpl_"))(t)){const s=u.exec(t),r=t.split(s[0]),n=s[1],o=h(r[1],e);let i=[];return""!==r[0]&&i.push(r[0]),Array.isArray(e[n])?i=i.concat(e[n]):i.push(e[n]),0!==o.length&&(i=i.concat(o)),i}return""!==t?[t]:[]},f=(t,e)=>{const s=[];for(const n of t)for(const t of h(n,e))r(t)&&v(t,e),s.push(t);const n=s[0],o=s[s.length-1];return"string"==typeof n&&""===n.trim()&&s.shift(),"string"==typeof o&&""===o.trim()&&s.pop(),s},m=(t,e)=>{if(t.length<14)return t;{const s=u.exec(t);if(null===s)return t;if(t.trim()===s[0])return e[s[1]];{const r=t.split(s[0]);return r[0]+e[s[1]]+m(r[1],e)}}},p=(t,e)=>{for(let s=0,r=t.length;s{for(const s of Object.keys(t)){const n=t[s];"string"==typeof n?t[s]=m(n,e):Array.isArray(n)?"children"===s?t.children=f(n,e):p(n,e):r(n)&&v(n,e)}},b=t=>{const e={};let s=0,r=["",""];const n=()=>{"string"==typeof r[1]?e[r[0].trim()]=r[1].trim():e[r[0].trim()]=r[1],r=["",""]};t.readUntil("{");for(let e=t.next();void 0!==e&&"}"!==e;e=t.next()){const o=r[0];switch(e){case'"':case"'":for(r[s]+=e+t.readUntil(e);r[s].endsWith("\\"+e);)r[s]+=t.readUntil(e);break;case":":""===o.trim()||o.includes("&")||o.includes("@")||o.includes(":")?r[s]+=e:s=1;break;case";":s=0,n();break;case"{":t.back(),r[1]=b(t),n();break;default:r[s]+=e}}return""!==r[0].trim()&&n(),e},g=new Map,y={jdom:(t,...e)=>{const s=t.join("jdom_tpl_joiner");try{if(!d.has(s)){const r=e.map((t,e)=>`jdom_tpl_obj_[${e}]`),n=new i(o(t.map(t=>t.replace(/\s+/g," ")),r)),c=a(n)[0],l=typeof c,u=JSON.stringify(c);d.set(s,t=>{if("string"===l)return m(c,t);if("object"===l){const e={},s=JSON.parse(u);return v(Object.assign(e,s),t),e}return null})}return d.get(s)(e)}catch(s){return console.error(`jdom parse error.\ncheck for mismatched brackets, tags, quotes.\n${o(t,e)}\n${s.stack||s}`),""}},css:(t,...e)=>{const s=o(t,e).trim();return g.has(s)||g.set(s,b(new i("{"+s+"}"))),g.get(s)}};"object"==typeof window&&Object.assign(window,y),t.exports&&(t.exports=y)}]);
--------------------------------------------------------------------------------
/static/css/main.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | /* themes */
8 |
9 | body {
10 | background: var(--primary-bg);
11 | font-family: var(--sans);
12 |
13 | --sans: 'IBM Plex Sans', sans-serif;
14 | --mono: 'IBM Plex Mono', monospace;
15 |
16 | /* color variables taken from Merlot */
17 | --primary-bg: #f9fafb;
18 | --primary-text: #111111;
19 | --secondary-bg: #f3f4f6;
20 | --secondary-text: #9b9b9b;
21 | --hover-bg: #eaebec;
22 | --active-bg: #dcdfe4;
23 | --translucent: rgba(249, 250, 251, .8);
24 | --transparent: rgba(249, 250, 251, 0);
25 | }
26 |
27 | .dark {
28 | --primary-bg: #2f3437;
29 | --primary-text: #ebebeb;
30 | --secondary-bg: #373c3f;
31 | --secondary-text: #a4a7a9;
32 | --hover-bg: #474c50;
33 | --active-bg: #626569;
34 | --translucent: rgba(47, 52, 55, .8);
35 | --transparent: rgba(47, 52, 55, 0);
36 | }
37 |
38 | /* resets */
39 |
40 | a {
41 | color: var(--primary-text);
42 | text-decoration: underline;
43 | }
44 |
45 | button {
46 | padding: 0;
47 | font-size: 1em;
48 | }
49 |
50 | button,
51 | button:hover,
52 | button:active {
53 | color: var(--primary-text);
54 | }
55 |
56 | pre,
57 | code {
58 | font-size: 1em;
59 | font-family: var(--mono);
60 | }
61 |
62 | input,
63 | select,
64 | option,
65 | textarea {
66 | color: var(--primary-text);
67 | font-size: 1em;
68 | font-family: var(--sans);
69 | }
70 |
71 | /* highlight.js overrides */
72 |
73 | .hljs {
74 | color: var(--primary-text);
75 | background: var(--primary-bg);
76 | }
77 |
78 | .hljs-comment {
79 | font-style: italic;
80 | color: var(--secondary-text);
81 | }
82 | .hljs-number,
83 | .hljs-literal {
84 | color: #af57d2;
85 | }
86 | .hljs-title.function_ { color: #3563cd; }
87 | .hljs-operator { color: #e0281a; }
88 | .hljs-separator {
89 | /* ink-specific token type */
90 | color: #a6a6a6;
91 | }
92 | .hljs-string { color: #13915e; }
93 |
94 | /* loading animation */
95 |
96 | .loading {
97 | width: 100%;
98 | flex-grow: 1;
99 | margin: 0;
100 | height: 3px;
101 | position: relative;
102 | background: var(--hover-bg);
103 | overflow: hidden;
104 | }
105 |
106 | @keyframes slider {
107 | 0% {
108 | transform: translateX(-100%);
109 | }
110 | 100% {
111 | transform: translateX(100%);
112 | }
113 | }
114 |
115 | .loading::after {
116 | content: '';
117 | display: block;
118 | height: 100%;
119 | width: 60%;
120 | padding-right: 40%;
121 | background-color: var(--primary-text);
122 | position: absolute;
123 | top: 0;
124 | left: 0;
125 | animation: slider 1s linear infinite;
126 | }
127 |
128 | /* application styles */
129 |
130 | #root,
131 | .app {
132 | width: 100%;
133 | height: 100vh;
134 | overflow: hidden;
135 | }
136 |
137 | .app {
138 | display: flex;
139 | flex-direction: row;
140 | justify-content: space-between;
141 | }
142 |
143 | .sidebar {
144 | width: 300px;
145 | flex-grow: 0;
146 | flex-shrink: 0;
147 | display: flex;
148 | flex-direction: column;
149 | background: var(--secondary-bg);
150 | padding: 12px;
151 | box-sizing: border-box;
152 | }
153 |
154 | .sidebar-show-button,
155 | .sidebar-hide-button {
156 | cursor: pointer;
157 | border: 0;
158 | background: transparent;
159 | color: var(--primary-text);
160 | height: 28px;
161 | width: 28px;
162 | font-size: 20px;
163 | line-height: 28px;
164 | border-radius: 4px;
165 | transition: background .2s;
166 | }
167 |
168 | .sidebar-show-button:hover,
169 | .sidebar-hide-button:hover {
170 | background: var(--active-bg);
171 | }
172 |
173 | .sidebar-show-button {
174 | position: absolute;
175 | top: 4px;
176 | left: 4px;
177 | }
178 |
179 | .sidebar-hide-button {
180 | margin-right: 8px;
181 | }
182 |
183 | nav {
184 | margin-top: 8px;
185 | margin-bottom: 16px;
186 | display: flex;
187 | flex-direction: row;
188 | align-items: center;
189 | }
190 |
191 | nav a.home-link {
192 | text-decoration: none;
193 | font-weight: bold;
194 | font-size: 1.125em;
195 | transition: opacity .2s;
196 | }
197 |
198 | nav a.home-link:hover {
199 | opacity: .8;
200 | }
201 |
202 | .sidebar-credits {
203 | margin: 0;
204 | margin-top: 8px;
205 | }
206 |
207 | .sidebar-credits a {
208 | color: var(--secondary-text);
209 | font-size: 14px;
210 | }
211 |
212 | .repo-panel .repo-panel-header {
213 | display: flex;
214 | flex-direction: row;
215 | align-items: center;
216 | justify-content: space-between;
217 | margin-bottom: 8px;
218 | }
219 |
220 | .repo-panel .repo-header-link {
221 | width: 0;
222 | flex-grow: 1;
223 | }
224 |
225 | .repo-panel .repo-toggle-input {
226 | flex-grow: 0;
227 | flex-shrink: 0;
228 | border: 0;
229 | background: transparent;
230 | color: var(--secondary-text);
231 | cursor: pointer;
232 | }
233 |
234 | .repo-panel .repo-toggle-input:hover {
235 | text-decoration: underline;
236 | }
237 |
238 | .repo-input-panel {
239 | display: flex;
240 | flex-direction: row;
241 | margin-bottom: 8px;
242 | }
243 |
244 | .repo-input-panel input,
245 | .repo-input-panel button {
246 | border-radius: 4px;
247 | padding: 6px 8px;
248 | border: 0;
249 | box-sizing: border-box;
250 | }
251 |
252 | .repo-input-panel input {
253 | border: 0;
254 | background: var(--hover-bg);
255 | width: 0;
256 | flex-grow: 1;
257 | margin-right: 6px;
258 | }
259 |
260 | .repo-input-panel input:focus {
261 | outline: 0;
262 | background: var(--active-bg);
263 | }
264 |
265 | .repo-input-panel .repo-input-submit {
266 | color: var(--primary-bg);
267 | background: var(--primary-text);
268 | cursor: pointer;
269 | flex-grow: 0;
270 | flex-shrink: 0;
271 | }
272 |
273 | .repo-input-panel .repo-input-submit:focus,
274 | .repo-input-panel .repo-input-submit:hover {
275 | opacity: .7;
276 | }
277 |
278 | .repo-info-panel {
279 | display: flex;
280 | flex-direction: column;
281 | align-items: flex-start;
282 | }
283 |
284 | .repo-info-panel.loading {
285 | width: 72%;
286 | margin: 2em auto 0 auto;
287 | }
288 |
289 | .repo-panel .repo-header-link a,
290 | .repo-info-panel .repo-info-homepage a {
291 | text-decoration: none;
292 | }
293 |
294 | .repo-panel .repo-header-link a:hover,
295 | .repo-info-panel .repo-info-homepage a:hover {
296 | text-decoration: underline;
297 | }
298 |
299 | .repo-info-panel .repo-info-description {
300 | line-height: 1.5em;
301 | margin-bottom: 8px;
302 | word-break: break-word;
303 | }
304 |
305 | .repo-info-panel .repo-info-homepage,
306 | .repo-info-panel .repo-info-language {
307 | font-size: .875em;
308 | }
309 |
310 | .repo-info-panel .repo-info-homepage {
311 | margin-bottom: 12px;
312 | }
313 |
314 | .repo-info-panel .repo-info-homepage a {
315 | color: var(--secondary-text);
316 | }
317 |
318 | .repo-info-panel .repo-info-language {
319 | border-radius: 6px;
320 | color: var(--primary-bg);
321 | background: var(--secondary-text);
322 | padding: 2px 7px;
323 | }
324 |
325 | .file-tree-list-container {
326 | height: 0;
327 | flex-grow: 1;
328 | overflow-y: auto;
329 | margin-top: 18px;
330 | }
331 |
332 | .file-tree-branch-label {
333 | color: var(--secondary-text);
334 | }
335 |
336 | .file-tree-branch-label:hover::after {
337 | content: ' (git branch)';
338 | }
339 |
340 | .file-tree-branch-icon {
341 | display: inline-block;
342 | width: 24px;
343 | }
344 |
345 | ul.file-tree-list {
346 | margin: 0;
347 | padding-left: 0;
348 | list-style: none;
349 | }
350 |
351 | ul.file-tree-list ul.file-tree-list {
352 | padding-left: 12px;
353 | }
354 |
355 | li.file-tree-list-item {
356 | margin: 1px 0;
357 | }
358 |
359 | .file-tree-node-row {
360 | display: flex;
361 | flex-direction: row;
362 | align-items: center;
363 | cursor: pointer;
364 | border-radius: 4px;
365 | padding: 2px 0;
366 | }
367 |
368 | .file-tree-node-row:hover {
369 | background: var(--hover-bg);
370 | }
371 |
372 | .file-tree-node-row:active,
373 | .file-tree-node-row.in-workspace {
374 | background: var(--active-bg);
375 | }
376 |
377 | .file-tree-node-toggle {
378 | flex-grow: 0;
379 | flex-shrink: 0;
380 | height: 1rem;
381 | width: 1rem;
382 | font-size: 12px;
383 | display: inline-block;
384 | color: var(--secondary-text);
385 | box-sizing: content-box;
386 | padding: 2px;
387 | margin: 2px;
388 | transform: rotate(-90deg);
389 | background: 0;
390 | border: transparent;
391 | cursor: pointer;
392 | border-radius: 4px;
393 | transition: transform .2s;
394 | }
395 |
396 | .file-tree-node-toggle:hover {
397 | background: var(--active-bg);
398 | }
399 |
400 | .file-tree-node-toggle.open {
401 | transform: rotate(0deg);
402 | }
403 |
404 | .file-tree-node-name {
405 | border: 0;
406 | background: transparent;
407 | cursor: pointer;
408 | margin-left: calc(1em + 8px);
409 | width: 0;
410 | flex-grow: 1;
411 | overflow: hidden;
412 | white-space: nowrap;
413 | text-overflow: ellipsis;
414 | text-align: left;
415 | line-height: 1.4em;
416 | }
417 |
418 | .file-tree-node-toggle + .file-tree-node-name {
419 | margin: 0;
420 | }
421 |
422 | .file-tree-node-loading.loading {
423 | width: calc(100% - 20px);
424 | margin: 1em 0 1em auto;
425 | }
426 |
427 | .file-panes {
428 | width: 0;
429 | flex-grow: 1;
430 | display: flex;
431 | flex-direction: row;
432 | align-items: center;
433 | justify-content: center;
434 | height: 100%;
435 | }
436 |
437 | .file-panes:empty::after {
438 | content: 'Open a file from the sidebar to explore';
439 | display: block;
440 | width: 80%;
441 | text-align: center;
442 | max-width: 300px;
443 | font-style: italic;
444 | color: var(--secondary-text);
445 | }
446 |
447 | .file-pane {
448 | background: var(--hover-bg);
449 | border-right: 1px solid var(--secondary-text);
450 | height: 100%;
451 | width: 0;
452 | flex-grow: 1;
453 | display: flex;
454 | flex-direction: column;
455 | }
456 |
457 | .file-pane:last-child {
458 | border-right: 0;
459 | border-bottom: 0;
460 | }
461 |
462 | .file-pane .file-pane-header {
463 | flex-grow: 0;
464 | flex-shrink: 0;
465 | display: flex;
466 | flex-direction: row;
467 | align-items: center;
468 | justify-content: flex-start;
469 | background: var(--hover-bg);
470 | overflow-x: auto;
471 | }
472 |
473 | .sidebar-show-button + .file-panes .file-pane:first-child .file-pane-header {
474 | margin-left: 36px;
475 | }
476 |
477 | .file-pane .file-pane-header-path {
478 | color: var(--secondary-text);
479 | }
480 |
481 | /*
482 | * TODO: Make each header like a pill icon, except the active one which is
483 | * attached to the current page. Active tab should be --primary-bg to seem
484 | * "attached" to the code preview; others can be hover-bg or something to seem
485 | * less attached.
486 | */
487 |
488 | .file-pane .file-pane-header-tab {
489 | padding: 8px 12px;
490 | background: var(--hover-bg);
491 | display: flex;
492 | flex-direction: row;
493 | align-items: center;
494 | }
495 |
496 | .file-pane .file-pane-header-tab.active {
497 | background: var(--primary-bg);
498 | }
499 |
500 | .file-pane .file-pane-header-info {
501 | border: 0;
502 | background: transparent;
503 | cursor: pointer;
504 | white-space: nowrap;
505 | }
506 |
507 | .file-pane .file-pane-split,
508 | .file-pane .file-pane-close {
509 | border: 0;
510 | background: transparent;
511 | display: inline-block;
512 | margin-left: 8px;
513 | opacity: 0;
514 | height: 1em;
515 | width: 1em;
516 | cursor: pointer;
517 | border-radius: 4px;
518 | padding: 2px;
519 | box-sizing: content-box;
520 | transition: opacity .2s, background .2s;
521 | }
522 |
523 | .file-pane .file-pane-split:hover,
524 | .file-pane .file-pane-close:hover {
525 | background: var(--active-bg);
526 | }
527 |
528 | .file-pane .file-pane-split {
529 | /* system fonts seem to be better at this glyph */
530 | font-family: system-ui, sans-serif;
531 | }
532 |
533 | .file-pane .file-pane-header-tab:hover .file-pane-split,
534 | .file-pane .file-pane-split:focus,
535 | .file-pane .file-pane-header-tab:hover .file-pane-close,
536 | .file-pane .file-pane-close:focus,
537 | .file-pane .file-pane-header-tab.active .file-pane-split,
538 | .file-pane .file-pane-split.active,
539 | .file-pane .file-pane-header-tab.active .file-pane-close,
540 | .file-pane .file-pane-close.active {
541 | opacity: 1;
542 | }
543 |
544 | .file-preview {
545 | height: 0;
546 | width: 100%;
547 | flex-grow: 1;
548 | margin: 0;
549 | color: var(--primary-text);
550 | background: var(--primary-bg);
551 | }
552 |
553 | .file-preview.file-preview-image {
554 | padding: 12px;
555 | box-sizing: border-box;
556 | background: var(--active-bg);
557 | }
558 |
559 | .file-preview .file-preview-image-content {
560 | width: 100%;
561 | height: 100%;
562 | object-fit: scale-down;
563 | }
564 |
565 | .file-preview .file-preview-loading {
566 | width: 80%;
567 | margin: 0 auto;
568 | max-width: 500px;
569 | margin-top: 5em;
570 | }
571 |
572 | .file-preview.file-preview-text {
573 | tab-size: 4;
574 | overflow-x: hidden;
575 | overflow-y: auto;
576 | -webkit-overflow-scrolling: auto;
577 | line-height: 1.25em;
578 | height: 0;
579 | flex-grow: 1;
580 | }
581 |
582 | .file-preview .file-preview-text-scroller {
583 | display: flex;
584 | flex-direction: row;
585 | margin: 8px 0;
586 | }
587 |
588 | .file-preview-text .file-preview-line-nos,
589 | .file-preview-text .file-preview-line-texts {
590 | margin: 0;
591 | }
592 |
593 | .embed-preview {
594 | tab-size: 4;
595 | display: flex;
596 | flex-direction: row;
597 | }
598 |
599 | .embed-preview pre {
600 | margin: 4px 0;
601 | }
602 |
603 | .embed-preview-line-nos,
604 | .file-preview-text .file-preview-line-nos {
605 | color: var(--secondary-text);
606 | padding-right: 12px;
607 | text-align: right;
608 | width: 3em;
609 | user-select: none;
610 | -moz-user-select: none;
611 | -webkit-user-select: none;
612 | flex-shrink: 0;
613 | flex-grow: 0;
614 | }
615 |
616 | .embed-preview-texts,
617 | .file-preview-text .file-preview-line-texts {
618 | width: 0;
619 | flex-grow: 1;
620 | height: 100%;
621 | overflow-x: auto;
622 | overflow-y: hidden;
623 | }
624 |
625 | .file-preview-markdown .file-preview-markdown-container {
626 | overflow-y: auto;
627 | -webkit-overflow-scrolling: auto;
628 | height: 100%;
629 | width: calc(100% - 32px);
630 | max-width: 760px;
631 | margin: 0 auto;
632 | }
633 |
634 | .file-preview-markdown .file-preview-markdown-container > :first-child {
635 | margin-top: 3rem;
636 | }
637 |
638 | .file-preview-markdown .file-preview-markdown-container > :last-child {
639 | margin-bottom: 6rem;
640 | }
641 |
642 | /* Markdown styling, taken from thesephist/merlot */
643 |
644 | .file-preview-markdown-container p,
645 | .file-preview-markdown-container li {
646 | line-height: 1.5em;
647 | }
648 |
649 | .file-preview-markdown-container strike {
650 | color: var(--secondary-text);
651 | }
652 |
653 | .file-preview-markdown-container img {
654 | max-width: 100%;
655 | }
656 |
657 | .file-preview-markdown-container a {
658 | color: var(--primary-text);
659 | text-decoration: underline;
660 | }
661 |
662 | .file-preview-markdown-container pre,
663 | .file-preview-markdown-container code {
664 | background: var(--hover-bg);
665 | font-size: 1em;
666 | font-family: 'IBM Plex Mono', 'Menlo', 'Monaco', monospace;
667 | }
668 |
669 | .file-preview-markdown-container pre {
670 | border-radius: 4px;
671 | box-sizing: border-box;
672 | padding: 12px 8px;
673 | overflow-x: auto;
674 | }
675 |
676 | .file-preview-markdown-container code {
677 | padding: 1px 5px;
678 | border-radius: 4px;
679 | }
680 |
681 | .file-preview-markdown-container pre code {
682 | padding: 0;
683 | }
684 |
685 | .file-preview-markdown-container blockquote {
686 | margin: 0;
687 | border-left: 4px solid var(--active-bg);
688 | padding-left: 1em;
689 | display: block;
690 | }
691 |
692 | .file-preview-markdown-container input[type="checkbox"] {
693 | pointer-events: none;
694 | }
695 |
696 | .alerts {
697 | position: fixed;
698 | top: 0;
699 | right: 0;
700 | width: 100%;
701 | max-width: 360px;
702 | max-height: 100vh;
703 | overflow-y: auto;
704 | }
705 |
706 | @keyframes slide-in {
707 | 0% {
708 | transform: translateX(calc(100% + 12px));
709 | }
710 | 100% {
711 | transform: translateX(0);
712 | }
713 | }
714 |
715 | .alert {
716 | background: #e6525e;
717 | padding: 8px 12px;
718 | box-sizing: border-box;
719 | border-radius: 4px;
720 | box-shadow: 0 3px 6px rgba(0, 0, 0, .3);
721 | color: var(--primary-bg);
722 | margin: 8px;
723 | line-height: 1.5em;
724 | animation: slide-in .5s cubic-bezier(.14,.43,.31,1.03);
725 | }
726 |
727 | /* mobile */
728 |
729 | @media only screen and (max-width: 700px) {
730 | .sidebar {
731 | position: fixed;
732 | top: 0;
733 | left: 0;
734 | bottom: 0;
735 | box-shadow: 0 0 0 100vw rgba(0, 0, 0, .35);
736 | }
737 | .file-panes {
738 | flex-direction: column;
739 | }
740 | .file-pane {
741 | border-right: 0;
742 | border-bottom: 1px solid var(--secondary-text);
743 | width: 100%;
744 | height: 0;
745 | }
746 | .embed-preview pre,
747 | .file-pane pre {
748 | font-size: .875em;
749 | }
750 | }
751 |
752 |
--------------------------------------------------------------------------------
/src/app.js.ink:
--------------------------------------------------------------------------------
1 | ` Main application UI `
2 |
3 | std := load('std')
4 | log := std.log
5 | f := std.format
6 |
7 | ` constants `
8 |
9 | Newline := char(10)
10 | MaxPathChars := 16
11 | MaxFilePreviewSize := 1000000
12 |
13 | FileType := {
14 | ` cannot display `
15 | Blob: ~1
16 | ` code `
17 | Text: 0
18 | ` preview `
19 | Image: 1
20 |
21 | ` markdown `
22 | Markdown: 2
23 | }
24 |
25 | ` utilities `
26 |
27 | mobile? := () => ~(window.innerWidth > 700)
28 |
29 | fetchAPI := (url, data, description, withRespJSON) => (
30 | resp := fetch(url, data)
31 | bind(resp, 'then')(resp => resp.status :: {
32 | 200 -> (
33 | json := bind(resp, 'json')()
34 | bind(json, 'then')(data => withRespJSON(data))
35 | )
36 | _ -> (
37 | showAlert(f('Couldn\'t get {{ 0 }}. Double-check your URLs and try again.', [description]))
38 | withRespJSON(())
39 | )
40 | })
41 | )
42 |
43 | fetchRepo := (userName, repoName, withRepo) => fetchAPI(
44 | f('/repo/{{ 0 }}/{{ 1 }}', [userName, repoName])
45 | {}
46 | 'repository metadata'
47 | data => withRepo(data)
48 | )
49 |
50 | fetchContents := (userName, repoName, path, withContents) => fetchAPI(
51 | f('/repo/{{ 0 }}/{{ 1 }}/files{{ 2 }}', [userName, repoName, path])
52 | {}
53 | 'repository files'
54 | data => withContents(data)
55 | )
56 |
57 | fileInPane? := (pane, file) => pane :: {
58 | () -> false
59 | _ -> (
60 | openFiles := pane.files
61 | (sub := i => i :: {
62 | len(openFiles) -> false
63 | _ -> openFiles.(i) :: {
64 | file -> true
65 | _ -> sub(i + 1)
66 | }
67 | })(0)
68 | )
69 | }
70 |
71 | fileInWorkspace? := file => (
72 | openFiles := flatten(map(State.panes, pane => pane.files))
73 | (sub := i => i :: {
74 | len(openFiles) -> false
75 | _ -> openFiles.(i) :: {
76 | file -> true
77 | _ -> sub(i + 1)
78 | }
79 | })(0)
80 | )
81 |
82 | fileTypeFromPath := path => true :: {
83 | hasSuffix?(path, '.jpg') -> FileType.Image
84 | hasSuffix?(path, '.png') -> FileType.Image
85 | hasSuffix?(path, '.gif') -> FileType.Image
86 | hasSuffix?(path, '.bmp') -> FileType.Image
87 | hasSuffix?(path, '.svg') -> FileType.Image
88 |
89 | hasSuffix?(path, '.md') -> FileType.Markdown
90 | hasSuffix?(path, '.markdown') -> FileType.Markdown
91 |
92 | hasSuffix?(path, '.sqlite') -> FileType.Blob
93 |
94 | _ -> FileType.Text
95 | }
96 |
97 | ` highlight.js interop `
98 |
99 | getLanguage := hljs.getLanguage
100 | languageList := (hljs.listLanguages)()
101 | Languages := (
102 | langs := {}
103 | each(keys(languageList), i => (
104 | langKey := languageList.(i)
105 | langName := getLanguage(langKey).name
106 | langs.(langName) := langKey
107 | ))
108 | langs
109 | )
110 |
111 | highlightProg := (fileName, content) => (
112 | ` langCode must be a JS string because highlight.js's API expects only JS
113 | strings, and passing an Ink-compatible string will error. `
114 | langCode := str(fileName :: {
115 | 'Dockerfile' -> 'dockerfile'
116 | 'Makefile' -> 'makefile'
117 | _ -> dotParts := split(fileName, '.') :: {
118 | ` Klisp is a lisp flavor written in Ink. It's syntactically closest
119 | to scheme, which is in our language package. `
120 | [_, 'klisp'] -> 'scheme'
121 | [_, 'cljs'] -> 'clj'
122 | [_, 'edn'] -> 'clj'
123 | [_] -> 'unknown'
124 | _ -> dotParts.(len(dotParts) - 1)
125 | }
126 | })
127 |
128 | ` a bit of a trick to convert undefined (which highlight.js's API returns
129 | here sometimes) to a null, which is the only empty value Ink understands. `
130 | langName := (eval(str('getLanguage(langCode) || null')) :: {
131 | () -> langCode
132 | _ -> getLanguage(langCode).name
133 | })
134 | ` highlight.js sometimes returns this value as e.g. "html, xml" `
135 | language := (langKey := Languages.(langName) :: {
136 | () -> langName
137 | _ -> langKey
138 | })
139 |
140 | filter(languageList, lg => lg = language) :: {
141 | [_] -> (
142 | result := (hljs.highlight)(content, {
143 | language: langCode
144 | })
145 | result.value
146 | )
147 | _ -> langCode :: {
148 | 'ink' -> highlightInkProg(content)
149 | _ -> escapeHTML(content)
150 | }
151 | }
152 | )
153 |
154 | ` initial state `
155 |
156 | State := {
157 | theme: 'light'
158 | userName: 'thesephist'
159 | repoName: 'september'
160 | sidebar?: true
161 | alerts: []
162 | ` {
163 | owner: {
164 | username: string
165 | avatar: string
166 | url: string
167 | }
168 | description: string
169 | homepage: string
170 | language: string
171 | } `
172 | repo: ()
173 | ` List | ()
181 | }> `
182 | files: []
183 | ` List
186 | }> `
187 | panes: []
188 | }
189 |
190 | ` UI components `
191 |
192 | Link := (name, href) => ha('a', [], {href: href, target: '_blank'}, [name])
193 |
194 | RepoPanel := (
195 | state := {
196 | userName: State.userName
197 | repoName: State.repoName
198 | editing?: false
199 | }
200 |
201 | submit := () => (
202 | state.editing? := false
203 | render()
204 |
205 | goTo('/' + state.userName + '/' + state.repoName)
206 | )
207 |
208 | handleKeydown := evt => evt.key :: {
209 | 'Escape' -> render(state.editing? := false)
210 | 'Enter' -> submit()
211 | }
212 |
213 | () => h('div', ['repo-panel'], [
214 | state.editing? :: {
215 | false -> h('div', ['repo-panel-header'], [
216 | h('div', ['repo-header-link'], [
217 | Link(
218 | f('{{ userName }}/{{ repoName }}', State)
219 | f('https://github.com/{{ userName }}/{{ repoName }}', State)
220 | )
221 | ])
222 | hae('button', ['repo-toggle-input'], {}, {
223 | click: evt => (
224 | state.editing? := ~(state.editing?)
225 | state.editing? :: {
226 | true -> (
227 | state.userName := State.userName
228 | state.repoName := State.repoName
229 | )
230 | }
231 | render()
232 | )
233 | }, ['edit'])
234 | ])
235 | true -> h('div', ['repo-input-panel'], [
236 | hae(
237 | 'input', ['repo-input-username']
238 | {
239 | value: state.userName
240 | placeholder: 'username'
241 | }
242 | {
243 | input: evt => (
244 | state.userName := evt.target.value
245 | render()
246 | )
247 | keydown: handleKeydown
248 | }
249 | []
250 | )
251 | hae(
252 | 'input', ['repo-input-reponame']
253 | {
254 | value: state.repoName
255 | placeholder: 'repo name'
256 | }
257 | {
258 | input: evt => (
259 | state.repoName := evt.target.value
260 | render()
261 | )
262 | keydown: handleKeydown
263 | }
264 | []
265 | )
266 | hae('button', ['repo-input-submit'], {}, {click: submit}, ['Go'])
267 | ])
268 | }
269 | repo := State.repo :: {
270 | () -> h('div', ['repo-info-panel', 'loading'], [])
271 | _ -> h('div', ['repo-info-panel'], [
272 | repo.description :: {
273 | () -> ()
274 | _ -> h('div', ['repo-info-description'], [repo.description])
275 | }
276 | repo.homepage :: {
277 | () -> ()
278 | _ -> h('div', ['repo-info-homepage'], [Link(repo.homepage, repo.homepage)])
279 | }
280 | repo.language :: {
281 | () -> ()
282 | _ -> h('div', ['repo-info-language'], [repo.language])
283 | }
284 | ])
285 | }
286 | ])
287 | )
288 |
289 | FileTreeNode := file => h('div', ['file-tree-node'], [
290 | h(
291 | 'div'
292 | [
293 | 'file-tree-node-row'
294 | fileInWorkspace?(file) :: {
295 | true -> 'in-workspace'
296 | _ -> ''
297 | }
298 | ]
299 | [
300 | file.type :: {
301 | 'dir' -> hae(
302 | 'button'
303 | [
304 | 'file-tree-node-toggle'
305 | file.open? :: {
306 | true -> 'open'
307 | _ -> 'closed'
308 | }
309 | ]
310 | {}
311 | {
312 | click: () => (
313 | file.open? := ~(file.open?)
314 | fetchFileChildren(file, render)
315 | render()
316 | )
317 | }
318 | ['▼']
319 | )
320 | _ -> ()
321 | }
322 | hae('button', ['file-tree-node-name'], {}, {
323 | click: () => file.type :: {
324 | 'file' -> openFileInPane(State.panes.0, file)
325 | 'dir' -> (
326 | file.open? := ~(file.open?)
327 | fetchFileChildren(file, render)
328 | render()
329 | )
330 | }
331 | }, [file.name])
332 | ]
333 | )
334 | file.open? :: {
335 | false -> ()
336 | _ -> file.children :: {
337 | () -> h('div', ['file-tree-node-loading', 'loading'], [])
338 | _ -> FileTreeList(file.children)
339 | }
340 | }
341 | ])
342 |
343 | FileTreeList := files => h('ul', ['file-tree-list'], (
344 | sortedFiles := sortBy(files, file => file.name)
345 | map(sortedFiles, file => h('li', ['file-tree-list-item'], [
346 | FileTreeNode(file)
347 | ]))
348 | ))
349 |
350 | Sidebar := () => State.sidebar? :: {
351 | false -> hae('button', ['sidebar-show-button'], {}, {
352 | click: () => render(State.sidebar? := true)
353 | }, ['»'])
354 | _ -> h('div', ['sidebar'], [
355 | h('nav', [], [
356 | hae('button', ['sidebar-hide-button'], {}, {
357 | click: () => render(State.sidebar? := false)
358 | }, ['«'])
359 | hae('a', ['home-link'], {href: '/'}, {
360 | click: evt => (
361 | bind(evt, 'preventDefault')()
362 | goTo('/')
363 | )
364 | }, ['Ink codebase browser'])
365 | ])
366 | RepoPanel()
367 | h('div', ['file-tree-list-container'], [
368 | repo := State.repo :: {
369 | () -> ()
370 | _ -> h('div', ['file-tree-branch-label'], [
371 | h('span', ['file-tree-branch-icon'], ['⎇ '])
372 | State.repo.branch
373 | ])
374 | }
375 | FileTreeList(State.files)
376 | ])
377 | h('p', ['sidebar-credits'], [
378 | hae('a', [], {href: '/thesephist/kin'}, {
379 | click: evt => (
380 | bind(evt, 'preventDefault')()
381 | goTo('/thesephist/kin')
382 | )
383 | }, ['About this project...'])
384 | ])
385 | ])
386 | }
387 |
388 | FilePreview := (
389 | ` mutating the DOM to render Markdown from an HTML string (as Merlot's
390 | Markdown engine does) is expensive, so we cache fully rendered HTML
391 | elements here. There is a cache per pane to allow one Markdown document to
392 | be displayed in multiple panes. `
393 | mdRenderCaches := []
394 |
395 | (file, paneIndex) => fileTypeFromPath(file.path) :: {
396 | FileType.Blob -> h('div', ['file-preview', 'file-preview-blob'], [
397 | 'Can\'t preview this type of file'
398 | ])
399 | FileType.Image -> h(
400 | 'div'
401 | ['file-preview', 'file-preview-image']
402 | [ha('img', ['file-preview-image-content'], {src: file.download}, [])]
403 | )
404 | FileType.Text -> content := file.content :: {
405 | () -> h('div', ['file-preview', 'file-preview-text'], [
406 | h('div', ['file-preview-loading', 'loading'], [])
407 | ])
408 | _ -> h(
409 | 'div'
410 | ['file-preview', 'file-preview-text']
411 | [(
412 | ` for performance reasons, we shell out to a JS call here. The
413 | Ink stdlib's str.split takes up to 100s of ms, which is
414 | unacceptable on renders. `
415 | lineCount := len(bind(str(content), 'split')(Newline)) - 1
416 |
417 | ha(
418 | 'div'
419 | ['file-preview-text-scroller']
420 | {
421 | style: {
422 | height: string(lineCount * 1.25 + 5) + 'em'
423 | }
424 | }
425 | [
426 | h('pre', ['file-preview-line-nos']
427 | [cat(map(range(1, lineCount + 2, 1), string), Newline)])
428 | (
429 | el := bind(document, 'createElement')('pre')
430 | el.className := 'file-preview-line-texts'
431 | el.innerHTML := content
432 | )
433 | ]
434 | )
435 | )]
436 | )
437 | }
438 | FileType.Markdown -> content := file.content :: {
439 | () -> h('div', ['file-preview', 'file-preview-markdown'], [
440 | h('div', ['file-preview-loading', 'loading'], [])
441 | ])
442 | _ -> h(
443 | 'div'
444 | ['file-preview', 'file-preview-markdown']
445 | [(
446 | cacheEntry := (entry := mdRenderCaches.(paneIndex) :: {
447 | () -> (
448 | entry := {content: '', el: 9}
449 | mdRenderCaches.(paneIndex) := entry
450 | entry
451 | )
452 | _ -> entry
453 | })
454 |
455 | cacheEntry.content :: {
456 | content -> cacheEntry.el
457 | _ -> (
458 | previewEl := bind(document, 'createElement')('div')
459 | previewEl.className := 'file-preview-markdown-container'
460 | previewEl.innerHTML := content
461 | cacheEntry.content := content
462 | cacheEntry.el := previewEl
463 | previewEl
464 | )
465 | }
466 | )]
467 | )
468 | }
469 | }
470 | )
471 |
472 | FilePane := (pane, paneIndex) => h('div', ['file-pane'], [
473 | h('div', ['file-pane-header'], map(pane.files, file => h('div', ['file-pane-header-tab-container'], [
474 | h(
475 | 'div'
476 | [
477 | 'file-pane-header-tab'
478 | pane.active :: {
479 | file -> 'active'
480 | _ -> ''
481 | }
482 | ]
483 | [
484 | hae(
485 | 'button'
486 | ['file-pane-header-info']
487 | {title: file.path}
488 | {
489 | click: () => render(pane.active := file)
490 | }
491 | [
492 | h('span', ['file-pane-header-path'], [(
493 | path := trimSuffix(file.path, file.name)
494 | len(path) < MaxPathChars :: {
495 | true -> path
496 | _ -> '...' + slice(path, len(path) - MaxPathChars, len(path))
497 | }
498 | )])
499 | h('span', ['file-pane-header-name'], [file.name])
500 | ]
501 | )
502 | pane.active :: {
503 | file -> hae('button', ['file-pane-split'], {}, {
504 | click: () => openFileInPane(State.panes.(paneIndex + 1), pane.active)
505 | }, ['→'])
506 | }
507 | hae('button', ['file-pane-close'], {}, {
508 | click: () => (
509 | pane.files := filter(pane.files, f => ~(f = file))
510 | pane.files :: {
511 | ` if pane is empty, remove pane from panes `
512 | [] -> State.panes := filter(State.panes, p => ~(p = pane))
513 | ` otherwise, set active pane file to something else `
514 | _ -> pane.active :: {
515 | ` if current file was active, choose a different active file `
516 | file -> pane.active := pane.files.0
517 | }
518 | }
519 | render()
520 | )
521 | }, ['×'])
522 | ]
523 | )
524 | ])))
525 | FilePreview(pane.active, paneIndex)
526 | ])
527 |
528 | FilePanes := () => h(
529 | 'div'
530 | ['file-panes']
531 | map(State.panes, (pane, i) => FilePane(pane, i))
532 | )
533 |
534 | Alert := message => h('div', ['alert'], [
535 | message
536 | ])
537 |
538 | Alerts := () => h('div', ['alerts'], map(State.alerts, Alert))
539 |
540 | ` globals and callbacks `
541 |
542 | root := bind(document, 'querySelector')('#root')
543 | r := Renderer(root)
544 | update := r.update
545 |
546 | refreshRepo := initialFilePath => (
547 | State.repo := ()
548 | State.files := []
549 | State.panes := []
550 | render()
551 |
552 | fetchRepo(State.userName, State.repoName, repo => (
553 | State.repo := repo
554 | render()
555 | ))
556 | fetchContents(State.userName, State.repoName, '/', contents => contents :: {
557 | () -> ()
558 | _ -> (
559 | State.files := map(contents, file => file.open? := false)
560 |
561 | ` open a default file `
562 | path := '/' + trimPrefix(initialFilePath, '/')
563 | pathParts := split(path, '/')
564 | dirParts := slice(pathParts, 0, len(pathParts) - 1)
565 | dirPath := '/' + cat(dirParts, '/')
566 | fileName := pathParts.len(dirParts)
567 |
568 | doesNotExistAlert := () =>
569 | showAlert(f('Could not open {{ 0 }} — the file doesn\'t exist.', [initialFilePath]))
570 |
571 | dirPath :: {
572 | '/' -> initialFile := filter(State.files, file => lower(file.name) = lower(fileName)).0 :: {
573 | () -> doesNotExistAlert()
574 | _ -> openFileInPane(State.panes.0, initialFile)
575 | }
576 | _ -> fetchContents(State.userName, State.repoName, dirPath, contents => contents :: {
577 | () -> doesNotExistAlert()
578 | _ -> initialFile := filter(contents, file => lower(file.name) = lower(fileName)).0 :: {
579 | () -> doesNotExistAlert()
580 | _ -> openFileInPane(State.panes.0, initialFile)
581 | }
582 | })
583 | }
584 |
585 | render()
586 | )
587 | })
588 | )
589 |
590 | openFileInPane := (pane, file) => fileInPane?(pane, file) :: {
591 | true -> render(pane.active := file)
592 | _ -> (
593 | pane :: {
594 | () -> State.panes.len(State.panes) := {
595 | active: file
596 | files: [file]
597 | }
598 | _ -> (
599 | pane.files.len(pane.files) := file
600 | pane.active := file
601 | )
602 | }
603 | fetchFileContent(file, render)
604 |
605 | mobile?() :: {
606 | true -> State.sidebar? := false
607 | }
608 |
609 | render()
610 | )
611 | }
612 |
613 | fetchFileChildren := (file, cb) => file.children :: {
614 | () -> (
615 | ` must mutate file in-place, return value does not matter `
616 | fetchContents(State.userName, State.repoName, '/' + file.path, contents => (
617 | file.children := map(contents, file => file.open? := false)
618 | cb()
619 | ))
620 | )
621 | _ -> cb()
622 | }
623 |
624 | fetchFileContent := (file, cb) => file.content :: {
625 | () -> file.size > MaxFilePreviewSize :: {
626 | true -> (
627 | file.content := 'file too big for preview'
628 | cb()
629 | )
630 | _ -> (
631 | resp := fetch(file.download)
632 | text := bind(resp, 'then')(resp => bind(resp, 'text')())
633 | bind(text, 'then')(text => (
634 | file.content := (fileTypeFromPath(file.path) :: {
635 | FileType.Text -> highlightProg(file.name, text)
636 | FileType.Markdown -> transform(text)
637 | _ -> text
638 | })
639 | cb()
640 | ))
641 | )
642 | }
643 | _ -> cb()
644 | }
645 |
646 | showAlert := msg => (
647 | render(State.alerts.len(State.alerts) := msg)
648 | wait(4, () => render(
649 | State.alerts := slice(State.alerts, 1, len(State.alerts))
650 | ))
651 | )
652 |
653 | render := () => update(h(
654 | 'div'
655 | ['app']
656 | [
657 | Sidebar()
658 | FilePanes()
659 | Alerts()
660 | ]
661 | ))
662 |
663 | ` router with Torus.Router `
664 |
665 | router := jsnew(Torus.Router, [{
666 | githubSlash: str('/https://github.com/:userName/:repoName/')
667 | github: str('/https://github.com/:userName/:repoName')
668 | repoSlash: str('/:userName/:repoName/')
669 | repo: str('/:userName/:repoName')
670 | home: str('/')
671 | }])
672 |
673 | goTo := bind(router, 'go')
674 |
675 | bind(router, 'addHandler')(
676 | routeInfo => (
677 | name := routeInfo.0
678 | params := routeInfo.1
679 |
680 | name :: {
681 | 'repo' -> (
682 | State.userName := params.userName
683 | State.repoName := params.repoName
684 |
685 | ` the user may specify a first "default" file to open after the
686 | hash in the URL. It defaults to the repository's README. `
687 | initialFilePath := (givenPath := slice(location.hash, 1, len(location.hash)) :: {
688 | '' -> 'README.md'
689 | _ -> givenPath
690 | })
691 | refreshRepo(initialFilePath)
692 |
693 | document.title := f('{{ userName }}/{{ repoName }} | Ink Codebase Browser', State)
694 | render()
695 | )
696 | 'repoSlash' -> goTo('/' + params.userName + '/' + params.repoName)
697 | 'github' -> goTo('/' + params.userName + '/' + params.repoName)
698 | 'githubSlash' -> goTo('/' + params.userName + '/' + params.repoName)
699 | _ -> goTo('/thesephist/kin')
700 | }
701 | )
702 | )
703 |
704 |
--------------------------------------------------------------------------------
/static/ink/lib.js:
--------------------------------------------------------------------------------
1 | load = s => window
2 |
3 | log = val => out(__as_ink_string(string(val) + __Ink_String(`
4 | `)));
5 | scan = cb => (() => { let acc; acc = [__Ink_String(``)]; return __ink_ident_in(evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`end`)), () => (cb((() => {let __ink_acc_trgt = __as_ink_string(acc); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})()))], [() => (__Ink_String(`data`)), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(0, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(acc); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})() + slice((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(), 0, (len((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) - 1)))) : (__ink_assgn_trgt[0]) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(acc); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})() + slice((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(), 0, (len((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) - 1))); return __ink_assgn_trgt})(); return false })())]])) })();
6 | hToN = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, [__Ink_String(`a`)]: 10, [__Ink_String(`b`)]: 11, [__Ink_String(`c`)]: 12, [__Ink_String(`d`)]: 13, [__Ink_String(`e`)]: 14, [__Ink_String(`f`)]: 15};
7 | nToH = __Ink_String(`0123456789abcdef`);
8 | hex = n => (() => { let __ink_trampolined_sub; let sub; return sub = (p, acc) => (() => { __ink_trampolined_sub = (p, acc) => __ink_match((p < 16), [[() => (true), () => (__as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(nToH); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return p })()] || null : (__ink_acc_trgt[(() => { return p })()] !== undefined ? __ink_acc_trgt[(() => { return p })()] : null)})() + acc))], [() => (false), () => (__ink_trampoline(__ink_trampolined_sub, floor((p / 16)), __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(nToH); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (p % 16) })()] || null : (__ink_acc_trgt[(() => { return (p % 16) })()] !== undefined ? __ink_acc_trgt[(() => { return (p % 16) })()] : null)})() + acc)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, p, acc) })() })()(floor(n), __Ink_String(``));
9 | xeh = s => (() => { let max; max = len(s); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), __as_ink_string((acc * 16) + (() => {let __ink_acc_trgt = __as_ink_string(hToN); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() })()] : null)})())))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, 0) })();
10 | min = numbers => reduce(numbers, (acc, n) => __ink_match((n < acc), [[() => (true), () => (n)], [() => (false), () => (acc)]]), (() => {let __ink_acc_trgt = __as_ink_string(numbers); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})());
11 | max = numbers => reduce(numbers, (acc, n) => __ink_match((n > acc), [[() => (true), () => (n)], [() => (false), () => (acc)]]), (() => {let __ink_acc_trgt = __as_ink_string(numbers); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})());
12 | range = (start, end, step) => (() => { let __ink_trampolined_sub; let span; let sub; span = (end - start); sub = (i, v, acc) => (() => { __ink_trampolined_sub = (i, v, acc) => __ink_match((((() => { return (v - start) })() / span) < 1), [[() => (true), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), v) : (__ink_assgn_trgt[(() => { return i })()]) = v; return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), __as_ink_string(v + step), acc) })())], [() => (false), () => (acc)]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, v, acc) })(); return __ink_match((((() => { return (end - start) })() / step) > 0), [[() => (true), () => (sub(0, start, []))], [() => (false), () => ([])]]) })();
13 | clamp = (start, end, min, max) => (() => { start = (() => { return __ink_match((start < min), [[() => (true), () => (min)], [() => (false), () => (start)]]) })(); end = (() => { return __ink_match((end < min), [[() => (true), () => (min)], [() => (false), () => (end)]]) })(); end = (() => { return __ink_match((end > max), [[() => (true), () => (max)], [() => (false), () => (end)]]) })(); start = (() => { return __ink_match((start > end), [[() => (true), () => (end)], [() => (false), () => (start)]]) })(); return {start: start, end: end} })();
14 | slice = (s, start, end) => (() => { let max; let x; x = clamp(start, end, 0, len(s)); start = (() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[start] || null : (__ink_acc_trgt.start !== undefined ? __ink_acc_trgt.start : null)})(); max = ((() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[end] || null : (__ink_acc_trgt.end !== undefined ? __ink_acc_trgt.end : null)})() - start); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(start + i) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(start + i) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(start + i) })()] : null)})(); return __ink_assgn_trgt})()))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, __ink_match(type(s), [[() => (__Ink_String(`string`)), () => (__Ink_String(``))], [() => (__Ink_String(`composite`)), () => ([])]])) })();
15 | append = (base, child) => (() => { let baseLength; let childLength; baseLength = len(base); childLength = len(child); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(i, [[() => (childLength), () => (base)], [() => (__Ink_Empty), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(base); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return __as_ink_string(baseLength + i) })(), (() => {let __ink_acc_trgt = __as_ink_string(child); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()) : (__ink_assgn_trgt[(() => { return __as_ink_string(baseLength + i) })()]) = (() => {let __ink_acc_trgt = __as_ink_string(child); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(); return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)) })())]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })();
16 | join = (base, child) => append(clone(base), child);
17 | clone = x => __ink_match(type(x), [[() => (__Ink_String(`string`)), () => (__as_ink_string(__Ink_String(``) + x))], [() => (__Ink_String(`composite`)), () => (reduce(keys(x), (acc, k) => (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return k })(), (() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return k })()] || null : (__ink_acc_trgt[(() => { return k })()] !== undefined ? __ink_acc_trgt[(() => { return k })()] : null)})()) : (__ink_assgn_trgt[(() => { return k })()]) = (() => {let __ink_acc_trgt = __as_ink_string(x); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return k })()] || null : (__ink_acc_trgt[(() => { return k })()] !== undefined ? __ink_acc_trgt[(() => { return k })()] : null)})(); return __ink_assgn_trgt})(), {}))], [() => (__Ink_Empty), () => (x)]]);
18 | stringList = list => __as_ink_string(__as_ink_string(__Ink_String(`[`) + cat(map(list, string), __Ink_String(`, `))) + __Ink_String(`]`));
19 | reverse = list => (() => { let __ink_trampolined_sub; let sub; return sub = (acc, i, j) => (() => { __ink_trampolined_sub = (acc, i, j) => __ink_match(j, [[() => (0), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})(); return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})(); return __ink_assgn_trgt})(), __as_ink_string(i + 1), (j - 1)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, acc, i, j) })() })()([], 0, (len(list) - 1));
20 | map = (list, f) => reduce(list, (l, item, i) => (() => {let __ink_assgn_trgt = __as_ink_string(l); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), f(item, i)) : (__ink_assgn_trgt[(() => { return i })()]) = f(item, i); return __ink_assgn_trgt})(), {});
21 | filter = (list, f) => reduce(list, (l, item, i) => __ink_match(f(item, i), [[() => (true), () => ((() => {let __ink_assgn_trgt = __as_ink_string(l); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(l), item) : (__ink_assgn_trgt[len(l)]) = item; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => (l)]]), []);
22 | reduce = (list, f, acc) => (() => { let max; max = len(list); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), f(acc, (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), i)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, acc) })();
23 | reduceBack = (list, f, acc) => (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (__ink_negate(1)), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, (i - 1), f(acc, (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), i)))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()((len(list) - 1), acc);
24 | flatten = list => reduce(list, append, []);
25 | some = list => reduce(list, (acc, x) => __ink_or(acc, x), false);
26 | every = list => reduce(list, (acc, x) => __ink_and(acc, x), true);
27 | cat = (list, joiner) => (() => { let max; return __ink_match(max = len(list), [[() => (0), () => (__Ink_String(``))], [() => (__Ink_Empty), () => ((() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), __as_ink_string(joiner + (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})())) : (__ink_assgn_trgt[len(acc)]) = __as_ink_string(joiner + (() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()); return __ink_assgn_trgt})()))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(1, clone((() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})())))]]) })();
28 | each = (list, f) => (() => { let max; max = len(list); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(i, [[() => (max), () => (null)], [() => (__Ink_Empty), () => ((() => { f((() => {let __ink_acc_trgt = __as_ink_string(list); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), i); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)) })())]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })();
29 | encode = str => (() => { let max; max = len(str); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, acc) => (() => { __ink_trampolined_sub = (i, acc) => __ink_match(i, [[() => (max), () => (acc)], [() => (__Ink_Empty), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), point((() => {let __ink_acc_trgt = __as_ink_string(str); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})())) : (__ink_assgn_trgt[(() => { return i })()]) = point((() => {let __ink_acc_trgt = __as_ink_string(str); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})()); return __ink_assgn_trgt})()))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i, acc) })() })()(0, []) })();
30 | decode = data => reduce(data, (acc, cp) => (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), char(cp)) : (__ink_assgn_trgt[len(acc)]) = char(cp); return __ink_assgn_trgt})(), __Ink_String(``));
31 | readFile = (path, cb) => (() => { let BufSize; BufSize = 4096; return (() => { let sub; return sub = (offset, acc) => read(path, offset, BufSize, evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`error`)), () => (cb(null))], [() => (__Ink_String(`data`)), () => ((() => { let dataLen; dataLen = len((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()); return __ink_match(__ink_eq(dataLen, BufSize), [[() => (true), () => (sub(__as_ink_string(offset + dataLen), (() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) : (__ink_assgn_trgt[len(acc)]) = (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(); return __ink_assgn_trgt})()))], [() => (false), () => (cb((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(acc), (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})()) : (__ink_assgn_trgt[len(acc)]) = (() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[data] || null : (__ink_acc_trgt.data !== undefined ? __ink_acc_trgt.data : null)})(); return __ink_assgn_trgt})()))]]) })())]])) })()(0, __Ink_String(``)) })();
32 | writeFile = (path, data, cb) => __ink_ident_delete(path, evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`end`)), () => (write(path, 0, data, evt => __ink_match((() => {let __ink_acc_trgt = __as_ink_string(evt); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[type] || null : (__ink_acc_trgt.type !== undefined ? __ink_acc_trgt.type : null)})(), [[() => (__Ink_String(`error`)), () => (cb(null))], [() => (__Ink_String(`end`)), () => (cb(true))]])))], [() => (__Ink_Empty), () => (cb(null))]]));
33 | format = (raw, values) => (() => { let append; let max; let readNext; let state; state = {idx: 0, which: 0, key: __Ink_String(``), buf: __Ink_String(``)}; append = c => (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(buf, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + c)) : (__ink_assgn_trgt.buf) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + c); return __ink_assgn_trgt})(); readNext = () => (() => { let c; c = (() => {let __ink_acc_trgt = __as_ink_string(raw); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() })()] : null)})(); __ink_match((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[which] || null : (__ink_acc_trgt.which !== undefined ? __ink_acc_trgt.which : null)})(), [[() => (0), () => (__ink_match(c, [[() => (__Ink_String(`{`)), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 1) : (__ink_assgn_trgt.which) = 1; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => (append(c))]]))], [() => (1), () => (__ink_match(c, [[() => (__Ink_String(`{`)), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 2) : (__ink_assgn_trgt.which) = 2; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => ((() => { append(__as_ink_string(__Ink_String(`{`) + c)); return (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 0) : (__ink_assgn_trgt.which) = 0; return __ink_assgn_trgt})() })())]]))], [() => (2), () => (__ink_match(c, [[() => (__Ink_String(`}`)), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(buf, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + string((() => {let __ink_acc_trgt = __as_ink_string(values); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] : null)})()))) : (__ink_assgn_trgt.buf) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})() + string((() => {let __ink_acc_trgt = __as_ink_string(values); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] || null : (__ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] !== undefined ? __ink_acc_trgt[(() => { return (() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() })()] : null)})())); return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(key, __Ink_String(``)) : (__ink_assgn_trgt.key) = __Ink_String(``); return __ink_assgn_trgt})(); return (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 3) : (__ink_assgn_trgt.which) = 3; return __ink_assgn_trgt})() })())], [() => (__Ink_String(` `)), () => (null)], [() => (__Ink_Empty), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(key, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() + c)) : (__ink_assgn_trgt.key) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[key] || null : (__ink_acc_trgt.key !== undefined ? __ink_acc_trgt.key : null)})() + c); return __ink_assgn_trgt})())]]))], [() => (3), () => (__ink_match(c, [[() => (__Ink_String(`}`)), () => ((() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(which, 0) : (__ink_assgn_trgt.which) = 0; return __ink_assgn_trgt})())], [() => (__Ink_Empty), () => (null)]]))]]); return (() => {let __ink_assgn_trgt = __as_ink_string(state); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(idx, __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() + 1)) : (__ink_assgn_trgt.idx) = __as_ink_string((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() + 1); return __ink_assgn_trgt})() })(); max = len(raw); return (() => { let __ink_trampolined_sub; let sub; return sub = () => (() => { __ink_trampolined_sub = () => __ink_match(((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[idx] || null : (__ink_acc_trgt.idx !== undefined ? __ink_acc_trgt.idx : null)})() < max), [[() => (true), () => ((() => { readNext(); return __ink_trampoline(__ink_trampolined_sub) })())], [() => (false), () => ((() => {let __ink_acc_trgt = __as_ink_string(state); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[buf] || null : (__ink_acc_trgt.buf !== undefined ? __ink_acc_trgt.buf : null)})())]]); return __ink_resolve_trampoline(__ink_trampolined_sub) })() })()() })()
34 |
35 | std = load(__Ink_String(`std`));
36 | map = (() => {let __ink_acc_trgt = __as_ink_string(std); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[map] || null : (__ink_acc_trgt.map !== undefined ? __ink_acc_trgt.map : null)})();
37 | slice = (() => {let __ink_acc_trgt = __as_ink_string(std); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[slice] || null : (__ink_acc_trgt.slice !== undefined ? __ink_acc_trgt.slice : null)})();
38 | reduce = (() => {let __ink_acc_trgt = __as_ink_string(std); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[reduce] || null : (__ink_acc_trgt.reduce !== undefined ? __ink_acc_trgt.reduce : null)})();
39 | reduceBack = (() => {let __ink_acc_trgt = __as_ink_string(std); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[reduceBack] || null : (__ink_acc_trgt.reduceBack !== undefined ? __ink_acc_trgt.reduceBack : null)})();
40 | checkRange = (lo, hi) => c => (() => { let p; p = point(c); return __ink_and((lo < p), (p < hi)) })();
41 | upper__ink_qm__ = checkRange((point(__Ink_String(`A`)) - 1), __as_ink_string(point(__Ink_String(`Z`)) + 1));
42 | lower__ink_qm__ = checkRange((point(__Ink_String(`a`)) - 1), __as_ink_string(point(__Ink_String(`z`)) + 1));
43 | digit__ink_qm__ = checkRange((point(__Ink_String(`0`)) - 1), __as_ink_string(point(__Ink_String(`9`)) + 1));
44 | letter__ink_qm__ = c => __ink_or(upper__ink_qm__(c), lower__ink_qm__(c));
45 | ws__ink_qm__ = c => __ink_match(point(c), [[() => (32), () => (true)], [() => (10), () => (true)], [() => (9), () => (true)], [() => (13), () => (true)], [() => (__Ink_Empty), () => (false)]]);
46 | hasPrefix__ink_qm__ = (s, prefix) => reduce(prefix, (acc, c, i) => __ink_and(acc, (() => { return __ink_eq((() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(), c) })()), true);
47 | hasSuffix__ink_qm__ = (s, suffix) => (() => { let diff; diff = (len(s) - len(suffix)); return reduce(suffix, (acc, c, i) => __ink_and(acc, (() => { return __ink_eq((() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(i + diff) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(i + diff) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(i + diff) })()] : null)})(), c) })()), true) })();
48 | matchesAt__ink_qm__ = (s, substring, idx) => (() => { let max; max = len(substring); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(i, [[() => (max), () => (true)], [() => (__Ink_Empty), () => (__ink_match((() => {let __ink_acc_trgt = __as_ink_string(s); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return __as_ink_string(idx + i) })()] || null : (__ink_acc_trgt[(() => { return __as_ink_string(idx + i) })()] !== undefined ? __ink_acc_trgt[(() => { return __as_ink_string(idx + i) })()] : null)})(), [[() => ((() => { return (() => {let __ink_acc_trgt = __as_ink_string(substring); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() })()), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)))], [() => (__Ink_Empty), () => (false)]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })();
49 | index = (s, substring) => (() => { let max; max = (len(s) - 1); return (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match(matchesAt__ink_qm__(s, substring, i), [[() => (true), () => (i)], [() => (false), () => (__ink_match((i < max), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1)))], [() => (false), () => (__ink_negate(1))]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0) })();
50 | contains__ink_qm__ = (s, substring) => (index(s, substring) > __ink_negate(1));
51 | lower = s => reduce(s, (acc, c, i) => __ink_match(upper__ink_qm__(c), [[() => (true), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), char(__as_ink_string(point(c) + 32))) : (__ink_assgn_trgt[(() => { return i })()]) = char(__as_ink_string(point(c) + 32)); return __ink_assgn_trgt})())], [() => (false), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), c) : (__ink_assgn_trgt[(() => { return i })()]) = c; return __ink_assgn_trgt})())]]), __Ink_String(``));
52 | upper = s => reduce(s, (acc, c, i) => __ink_match(lower__ink_qm__(c), [[() => (true), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), char((point(c) - 32))) : (__ink_assgn_trgt[(() => { return i })()]) = char((point(c) - 32)); return __ink_assgn_trgt})())], [() => (false), () => ((() => {let __ink_assgn_trgt = __as_ink_string(acc); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), c) : (__ink_assgn_trgt[(() => { return i })()]) = c; return __ink_assgn_trgt})())]]), __Ink_String(``));
53 | title = s => (() => { let lowered; lowered = lower(s); return (() => {let __ink_assgn_trgt = __as_ink_string(lowered); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(0, upper((() => {let __ink_acc_trgt = __as_ink_string(lowered); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})())) : (__ink_assgn_trgt[0]) = upper((() => {let __ink_acc_trgt = __as_ink_string(lowered); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[0] || null : (__ink_acc_trgt[0] !== undefined ? __ink_acc_trgt[0] : null)})()); return __ink_assgn_trgt})() })();
54 | replaceNonEmpty = (s, old, __ink_ident_new) => (() => { let lnew; let lold; lold = len(old); lnew = len(__ink_ident_new); return (() => { let __ink_trampolined_sub; let sub; return sub = (acc, i) => (() => { __ink_trampolined_sub = (acc, i) => __ink_match(matchesAt__ink_qm__(acc, old, i), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(__as_ink_string(slice(acc, 0, i) + __ink_ident_new) + slice(acc, __as_ink_string(i + lold), len(acc))), __as_ink_string(i + lnew)))], [() => (false), () => (__ink_match((i < len(acc)), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, acc, __as_ink_string(i + 1)))], [() => (false), () => (acc)]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, acc, i) })() })()(s, 0) })();
55 | replace = (s, old, __ink_ident_new) => __ink_match(old, [[() => (__Ink_String(``)), () => (s)], [() => (__Ink_Empty), () => (replaceNonEmpty(s, old, __ink_ident_new))]]);
56 | splitNonEmpty = (s, delim) => (() => { let coll; let ldelim; coll = []; ldelim = len(delim); return (() => { let __ink_trampolined_sub; let sub; return sub = (acc, i, last) => (() => { __ink_trampolined_sub = (acc, i, last) => __ink_match(matchesAt__ink_qm__(acc, delim, i), [[() => (true), () => ((() => { (() => {let __ink_assgn_trgt = __as_ink_string(coll); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(coll), slice(acc, last, i)) : (__ink_assgn_trgt[len(coll)]) = slice(acc, last, i); return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, acc, __as_ink_string(i + ldelim), __as_ink_string(i + ldelim)) })())], [() => (false), () => (__ink_match((i < len(acc)), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, acc, __as_ink_string(i + 1), last))], [() => (false), () => ((() => {let __ink_assgn_trgt = __as_ink_string(coll); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign(len(coll), slice(acc, last, len(acc))) : (__ink_assgn_trgt[len(coll)]) = slice(acc, last, len(acc)); return __ink_assgn_trgt})())]]))]]); return __ink_resolve_trampoline(__ink_trampolined_sub, acc, i, last) })() })()(s, 0, 0) })();
57 | split = (s, delim) => __ink_match(delim, [[() => (__Ink_String(``)), () => (map(s, c => c))], [() => (__Ink_Empty), () => (splitNonEmpty(s, delim))]]);
58 | trimPrefixNonEmpty = (s, prefix) => (() => { let idx; let lpref; let max; max = len(s); lpref = len(prefix); idx = (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match((i < max), [[() => (true), () => (__ink_match(matchesAt__ink_qm__(s, prefix, i), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + lpref)))], [() => (false), () => (i)]]))], [() => (false), () => (i)]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(0); return slice(s, idx, len(s)) })();
59 | trimPrefix = (s, prefix) => __ink_match(prefix, [[() => (__Ink_String(``)), () => (s)], [() => (__Ink_Empty), () => (trimPrefixNonEmpty(s, prefix))]]);
60 | trimSuffixNonEmpty = (s, suffix) => (() => { let idx; let lsuf; lsuf = len(suffix); idx = (() => { let __ink_trampolined_sub; let sub; return sub = i => (() => { __ink_trampolined_sub = i => __ink_match((i > __ink_negate(1)), [[() => (true), () => (__ink_match(matchesAt__ink_qm__(s, suffix, (i - lsuf)), [[() => (true), () => (__ink_trampoline(__ink_trampolined_sub, (i - lsuf)))], [() => (false), () => (i)]]))], [() => (false), () => (i)]]); return __ink_resolve_trampoline(__ink_trampolined_sub, i) })() })()(len(s)); return slice(s, 0, idx) })();
61 | trimSuffix = (s, suffix) => __ink_match(suffix, [[() => (__Ink_String(``)), () => (s)], [() => (__Ink_Empty), () => (trimSuffixNonEmpty(s, suffix))]]);
62 | trim = (s, ss) => trimPrefix(trimSuffix(s, ss), ss)
63 |
64 | std = load(__Ink_String(`../vendor/std`));
65 | map = (() => {let __ink_acc_trgt = __as_ink_string(std); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[map] || null : (__ink_acc_trgt.map !== undefined ? __ink_acc_trgt.map : null)})();
66 | clone = (() => {let __ink_acc_trgt = __as_ink_string(std); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[clone] || null : (__ink_acc_trgt.clone !== undefined ? __ink_acc_trgt.clone : null)})();
67 | sortBy = (v, pred) => (() => { let partition; let vPred; vPred = map(v, pred); partition = (v, lo, hi) => (() => { let __ink_trampolined_lsub; let __ink_trampolined_rsub; let lsub; let pivot; let rsub; pivot = (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return lo })()] || null : (__ink_acc_trgt[(() => { return lo })()] !== undefined ? __ink_acc_trgt[(() => { return lo })()] : null)})(); lsub = i => (() => { __ink_trampolined_lsub = i => __ink_match((() => { return ((() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})() < pivot) })(), [[() => (true), () => (__ink_trampoline(__ink_trampolined_lsub, __as_ink_string(i + 1)))], [() => (false), () => (i)]]); return __ink_resolve_trampoline(__ink_trampolined_lsub, i) })(); rsub = j => (() => { __ink_trampolined_rsub = j => __ink_match((() => { return ((() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})() > pivot) })(), [[() => (true), () => (__ink_trampoline(__ink_trampolined_rsub, (j - 1)))], [() => (false), () => (j)]]); return __ink_resolve_trampoline(__ink_trampolined_rsub, j) })(); return (() => { let __ink_trampolined_sub; let sub; return sub = (i, j) => (() => { __ink_trampolined_sub = (i, j) => (() => { i = lsub(i); j = rsub(j); return __ink_match((() => { return (i < j) })(), [[() => (false), () => (j)], [() => (true), () => ((() => { let tmp; let tmpPred; tmp = (() => {let __ink_acc_trgt = __as_ink_string(v); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(); tmpPred = (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return i })()] || null : (__ink_acc_trgt[(() => { return i })()] !== undefined ? __ink_acc_trgt[(() => { return i })()] : null)})(); (() => {let __ink_assgn_trgt = __as_ink_string(v); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(v); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(v); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})(); return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(v); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return j })(), tmp) : (__ink_assgn_trgt[(() => { return j })()]) = tmp; return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(vPred); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return i })(), (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})()) : (__ink_assgn_trgt[(() => { return i })()]) = (() => {let __ink_acc_trgt = __as_ink_string(vPred); return __is_ink_string(__ink_acc_trgt) ? __ink_acc_trgt.valueOf()[(() => { return j })()] || null : (__ink_acc_trgt[(() => { return j })()] !== undefined ? __ink_acc_trgt[(() => { return j })()] : null)})(); return __ink_assgn_trgt})(); (() => {let __ink_assgn_trgt = __as_ink_string(vPred); __is_ink_string(__ink_assgn_trgt) ? __ink_assgn_trgt.assign((() => { return j })(), tmpPred) : (__ink_assgn_trgt[(() => { return j })()]) = tmpPred; return __ink_assgn_trgt})(); return __ink_trampoline(__ink_trampolined_sub, __as_ink_string(i + 1), (j - 1)) })())]]) })(); return __ink_resolve_trampoline(__ink_trampolined_sub, i, j) })() })()(lo, hi) })(); return (() => { let __ink_trampolined_quicksort; let quicksort; return quicksort = (v, lo, hi) => (() => { __ink_trampolined_quicksort = (v, lo, hi) => __ink_match(len(v), [[() => (0), () => (v)], [() => (__Ink_Empty), () => (__ink_match((() => { return (lo < hi) })(), [[() => (false), () => (v)], [() => (true), () => ((() => { let p; p = partition(v, lo, hi); quicksort(v, lo, p); return __ink_trampoline(__ink_trampolined_quicksort, v, __as_ink_string(p + 1), hi) })())]]))]]); return __ink_resolve_trampoline(__ink_trampolined_quicksort, v, lo, hi) })() })()(v, 0, (len(v) - 1)) })();
68 | sort__ink_em__ = v => sortBy(v, x => x);
69 | sort = v => sort__ink_em__(clone(v))
70 |
71 |
--------------------------------------------------------------------------------