├── .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 | ![The Ink codebase browser running in the browser on a browser window and on an iPhone](https://github.com/thesephist/kin/raw/main/static/img/kin.png) 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 | ![Ink codebase browser with two panes of Ink source code open](https://github.com/thesephist/kin/raw/main/static/img/kin-ink-project.png) 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 | ![Ink codebase browser viewing a rendered Markdown file](https://github.com/thesephist/kin/raw/main/static/img/kin-markdown-project.png) 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 | ![Ink codebase browser viewing an image preview](https://github.com/thesephist/kin/raw/main/static/img/kin-nextjs-project.png) 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 | ![A demo page showing embedded syntax highlighted Ink source files](https://github.com/thesephist/kin/raw/main/static/img/kin-embed-demo.png) 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 | --------------------------------------------------------------------------------