├── .gitattributes
├── .github
└── workflows
│ └── ink.yml
├── .gitignore
├── .ink-version
├── LICENSE
├── Makefile
├── README.md
├── examples.ink
├── examples
├── control-flow.ink
├── execing-processes.ink
├── files.ink
├── functions.ink
├── hello-world.ink
├── http.ink
├── io.ink
├── lists.ink
├── loops.ink
├── maps.ink
├── random.ink
├── sorting.ink
└── values.ink
├── netlify.toml
├── src
├── build.ink
├── highlight.ink
├── ink-darwin-1.9
├── ink-linux-1.9
└── test.ink
├── static
├── 404.html
├── favicon.ico
├── preview.png
└── site.css
├── templates
├── example.html
└── index.html
└── vendor
├── iota.ink
├── quicksort.ink
├── std.ink
├── str.ink
└── tokenize.ink
/.gitattributes:
--------------------------------------------------------------------------------
1 | public/* linguist-generated=true
2 | vendor/* linguist-vendored=true
3 |
4 |
--------------------------------------------------------------------------------
/.github/workflows/ink.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the main branch
8 | push:
9 | branches: [ main ]
10 | pull_request:
11 | branches: [ main ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | # Steps represent a sequence of tasks that will be executed as part of the job
24 | steps:
25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26 | - uses: actions/checkout@v2
27 |
28 | # Runs a single command using the runners shell
29 | - name: Run examples
30 | run: make test-linux
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .idea
3 | .vscode
4 | tmp/
5 | public/
--------------------------------------------------------------------------------
/.ink-version:
--------------------------------------------------------------------------------
1 | v0.1.9
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Andrew Healey
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build-linux:
2 | cd ./src && chmod +x ./ink-linux-1.9 && ./ink-linux-1.9 ./build.ink
3 |
4 | build-mac:
5 | cd ./src && chmod +x ./ink-darwin-1.9 && ./ink-darwin-1.9 ./build.ink
6 |
7 | test-linux:
8 | cd ./src && chmod +x ./ink-linux-1.9 && ./ink-linux-1.9 ./build.ink && ./ink-linux-1.9 test.ink
9 |
10 | test-mac:
11 | cd ./src && chmod +x ./ink-darwin-1.9 && ./ink-darwin-1.9 ./build.ink && ./ink-darwin-1.9 test.ink
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/healeycodes/inkbyexample/actions/workflows/ink.yml)
2 |
3 | # Ink by Example
4 |
5 | Content and build toolchain for [Ink by Example](https://inkbyexample.com), a site that teaches [Ink](https://dotink.co/) via annotated example programs.
6 |
7 | All tooling is written in Ink and the programs are evaluated as part of the build process.
8 |
9 | Syntax highlighting powered by [September](https://github.com/thesephist/september).
10 |
11 |
12 |
13 | [](https://inkbyexample.com/)
14 |
15 |
16 |
17 | ## Add an Annotated Program
18 |
19 | - Add the title to `examples.ink`
20 | - Create a matching Ink program in `/examples` that exports `intro`, `rows`, and `end`.
21 | - A title of `Foo Bar` needs a program named `foo-bar.ink`
22 |
23 | ```
24 | intro := 'The introduction precedes the code sections. It should not be left empty.'
25 |
26 | rows := [
27 | {
28 | docs: 'A that function adds two numbers together. This text will appear on the left of the code.'
29 | code: 'add := (x, y) => x + y'
30 | },
31 | {
32 | docs: 'A function multiplies two numbers together.'
33 | code: 'multiply := (x, y) => x * y'
34 | }
35 | ]
36 |
37 | end := 'The end section is left of the terminal output. It can be left empty.'
38 | ```
39 |
40 |
41 |
42 | ## Contributing
43 |
44 | Raise an issue before starting a PR. **I'm happy to help out!** You can also DM me on Twitter [@healeycodes](https://twitter.com/healeycodes)
45 |
46 |
47 |
48 | ## Build
49 |
50 | `make build-linux` or `make build-mac`
51 |
52 | The website is built to `/public`.
53 |
54 |
55 |
56 | ## Test
57 |
58 | `make test-linux` or `make test-mac`
59 |
60 | Check stdout for runtime errors.
61 |
62 |
63 |
64 | ## Deploy
65 |
66 | This repository uses Netlify for CI/CD and there's a `netlify.toml` with the configuration required.
67 |
68 | Otherwise, serve the `/public` directory.
69 |
70 |
71 |
72 | ## Thanks to
73 |
74 | [@thesephist](https://github.com/thesephist) for creating the Ink programming language, writing the `SPEC.md` and amazing documentation at https://dotink.co/ which served as a jumping off point.
75 |
76 | Mark McGranaghan for creating the original [Go by Example](https://github.com/mmcgrana/gobyexample) which provided styling and structure for this project.
77 |
78 |
79 |
80 | ## License
81 |
82 | Go by Example is copyright Mark McGranaghan and licensed under a
83 | [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/).
84 |
85 | Everything else is licensed under MIT.
86 |
--------------------------------------------------------------------------------
/examples.ink:
--------------------------------------------------------------------------------
1 | all := [
2 | 'Hello World',
3 | 'Values',
4 | 'IO',
5 | 'Loops',
6 | 'Control Flow',
7 | 'Lists',
8 | 'Maps',
9 | 'Functions',
10 | 'Files',
11 | 'HTTP',
12 | 'Random',
13 | 'Sorting',
14 | 'Execing Processes'
15 | ]
16 |
--------------------------------------------------------------------------------
/examples/control-flow.ink:
--------------------------------------------------------------------------------
1 | intro := 'Instead of if
/else
branching, match expressions are used for control flow.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | filter := std.filter
8 | log := std.log
9 |
10 |
11 | '
12 | },
13 | {
14 | docs: 'Like the switch
expression from other programming languages, the case clauses are checked from top to bottom. The checking stops when there\'s a match.'
15 | code: 'error := true
16 | error :: {
17 | false -> log(\'No problem!\')
18 | true -> log(\'There is a problem..\')
19 | }
20 |
21 | '
22 | },
23 | {
24 | docs: 'The default, or else
, branch is signified with the underscore character. This will match anything.'
25 | code: 'device := \'windows\'
26 | device :: {
27 | \'linux\' -> log(\'Linux!\')
28 | \'macOS\' -> log(\'Mac!\')
29 | _ -> log(\'Neither Linux or Mac.\')
30 | }
31 |
32 | '
33 | },
34 | {
35 | docs: 'A match target can be an expression.'
36 | code: 'greeting := \'Hello!\'
37 | greeting :: {
38 | \'Hello\' -> log(\'They greeted us.\')
39 | \'Hello\' + \'!\' -> log(\'They greeted us loudly.\')
40 | }
41 |
42 | '
43 | },
44 | {
45 | docs: 'Composite values are deep compared so the underscore can be used to catch complex objects that, while different, have a similar characteristic.'
46 | code: 'first := {
47 | code: 2,
48 | user: \'alice@google\'
49 | }
50 | second := {
51 | code: 2,
52 | user: \'claire@amazon\'
53 | }
54 |
55 | checkEvent := event => event :: {
56 | {code: 2, user: _} -> log(\'All good!\')
57 | _ -> log(\'Bad event..\')
58 | }
59 | checkEvent(first)
60 | checkEvent(second)
61 |
62 | '
63 | },
64 | {
65 | docs: 'std.filter
can be passed a match expression. Here we use it to filter out odd numbers.'
66 | code: 'numbers := [1, 2, 3, 4, 5, 6]
67 | onlyEven := num => num % 2 :: {
68 | 0 -> true
69 | _ -> false
70 | }
71 | even := filter(numbers, onlyEven)
72 | log(even)
73 |
74 | '
75 | }
76 | ]
77 |
78 | end := ''
79 |
--------------------------------------------------------------------------------
/examples/execing-processes.ink:
--------------------------------------------------------------------------------
1 | intro := 'Ink can spawn child processes. The stdout data is passed to a callback when the process ends.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | each := std.each
8 | log := std.log
9 |
10 |
11 | '
12 | },
13 | {
14 | docs: 'Let\'s take a look at the files in the directory relative to the executing file (in this instance, this website\'s build script).'
15 | code: 'path := \'ls\' `` path to executable
16 | arguments := []
17 | stdin := \'\'
18 | stdoutFn := out => log(out)
19 | exec(path, arguments, stdin, stdoutFn)
20 |
21 | '
22 | },
23 | {
24 | docs: 'args
gives us the command-line arguments for the current context. This can be useful if you\'re chaining Ink programs.'
25 | code: '
26 | log(\'Arguments:\')
27 | each(args(), arg => log(\' \' + arg))
28 | '
29 | }
30 | ]
31 |
32 | end := ''
33 |
--------------------------------------------------------------------------------
/examples/files.ink:
--------------------------------------------------------------------------------
1 | intro := 'You will usually interact with files through two standard library APIs — std.readFile
and std.writeFile
. The runtimes and builtins documentation lists the operating system interfaces.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | writeFile := std.writeFile
8 | readFile := std.readFile
9 | slice := std.slice
10 | log := std.log
11 |
12 |
13 | '
14 | },
15 | {
16 | docs: 'Let\'s read in the README for this project.'
17 | code: '
18 | readFile(\'../README.md\', data => (
19 | log(\'Read:\')
20 | log(
21 | ` data is the entire file content `
22 | slice(data, 0, 16))
23 | ))
24 |
25 | '
26 | },
27 | {
28 | docs: 'If the read fails, null is passed to the callback. The read can fail if you try to access a file that doesn\'t exist. So we should handle that.'
29 | code: '
30 | readFile(\'../doesnt-exist.md\', data => data :: {
31 | () -> log(\'The read failed.\')
32 | _ -> log(\'The read succeeded.\')
33 | })
34 |
35 | '
36 | },
37 | {
38 | docs: 'Here we write content to a file. If the file exists, it\'s truncated — otherwise the file is created. The path is relative to the executing file.'
39 | code: '
40 | path := \'../tmp/_writeExample.txt\'
41 | data := \'Some data. \'
42 | writeFile(path, data, cb => cb :: {
43 | ` either true or null `
44 | true -> log(\'The write succeeded.\')
45 | _ -> log(\'The write failed.\')
46 | })
47 |
48 | '
49 | },
50 | {
51 | docs: 'make
can be used to recursively create directories.'
52 | code: '
53 | make(\'../tmp/_makeExample/\', cb => cb.type :: {
54 | \'error\' -> (
55 | log(\'Make:\')
56 | log(\' \' + string(cb.message))
57 | )
58 | _ -> log(\'Make succeeded.\')
59 | })
60 |
61 | '
62 | },
63 | {
64 | docs: 'We can get directory information with dir
.'
65 | code: '
66 | dir(\'../src/\', cb => cb.type :: {
67 | \'error\' -> log(\'Dir failed.\')
68 | ` cb.data is a list of files and directories`
69 | _ -> (
70 | log(\'Dir:\')
71 | log(\' \' + string(cb.data.0))
72 | )
73 | })
74 |
75 | '
76 | },
77 | {
78 | docs: 'Similarly, stat
returns metadata about an object in the file tree.'
79 | code: '
80 |
81 | stat(\'../README.md\', cb => cb.type :: {
82 | \'error\' -> log(\'Stat failed.\')
83 | _ -> (
84 | log(\'Stat:\')
85 | log(\' \' + string(cb))
86 | )
87 | })
88 |
89 | '
90 | }
91 | ]
92 |
93 | end := 'We\'re not using ordered callbacks so the order of the logs will be dependant on the system calls when this website was last built and the code was evaluated. This is an example of Ink\'s event loop!'
94 |
--------------------------------------------------------------------------------
/examples/functions.ink:
--------------------------------------------------------------------------------
1 | intro := 'Functions are the building blocks of Ink and enable us to write terse functional code.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | log := std.log
8 |
9 |
10 | '
11 | },
12 | {
13 | docs: 'Functions are created with the arrow =>
symbol.'
14 | code: '
15 | shout := (word) => word + \'!\'
16 | log(shout(\'Hello\'))
17 |
18 | multiply := (x, y) => x * y
19 | log(multiply(2, 8))
20 |
21 | '
22 | },
23 | {
24 | docs: 'When a group of expressions follow the arrow symbol, the result of the final expression is the return value.'
25 | code: '` single argument parentheses are optional`
26 | sayAndMultiply := num => (
27 | log(\'About to multiply \' + string(num) + \':\')
28 |
29 | ` the return value `
30 | num * num
31 | )
32 | log(sayAndMultiply(8))
33 |
34 | '
35 | },
36 | {
37 | docs: 'Function invocation ()
takes precedence over property-access .
— instead of obj.func(argument)
, we use (obj.func)(argument)
.'
38 | code: 'math := {
39 | swapSign: n => 0 - n
40 | }
41 | weight := (math.swapSign)(100)
42 | log(weight)
43 |
44 | '
45 | },
46 | {
47 | docs: 'Here\'s an example of tail recursion.'
48 | code: 'factorial := n => n :: {
49 | 0 -> 1
50 | _ -> n * factorial(n-1)
51 | }
52 | log(factorial(5))
53 |
54 | '
55 | },
56 | {
57 | docs: 'It\'s natural to use closures — functions that have access to the parent scope after the parent function has closed.'
58 | code: 'makeMultiplier := x => (
59 | y := y => x * y
60 | )
61 | multiplySeven := makeMultiplier(7)
62 | multiplyTwenty := makeMultiplier(20)
63 | log(multiplySeven(2))
64 | log(multiplyTwenty(4))
65 |
66 | '
67 | },
68 | {
69 | docs: 'Functions that immediately evalute and return another function is called currying in functional programming land.'
70 | code: 'greeting := a => b => a + \' \' + b
71 | message := greeting(\'Hello there,\')(\'General Kenobi\')
72 | log(message)
73 | '
74 | }
75 | ]
76 |
77 | end := ''
78 |
--------------------------------------------------------------------------------
/examples/hello-world.ink:
--------------------------------------------------------------------------------
1 | intro := 'Our first program will print the classic "hello world" message. Here\'s the full source code.'
2 |
3 | rows := [
4 | {
5 | docs: 'Ink programs are imported by calling load
and passing the relative path without .ink
. Top level values are brought in as a map.'
6 | code: 'std := load(\'../vendor/std\')
7 | log := std.log
8 |
9 |
10 | log(\'hello world\')
11 | '
12 | },
13 | {
14 | docs: 'We can add comments using the grave accent (U+0060).'
15 | code: '` a single line comment `
16 |
17 | `
18 | a multi-line comment
19 | `
20 |
21 | '
22 | },
23 | {
24 | docs: 'A double accent comments the rest of the line.'
25 | code: '`` log(\'This should not run!\')
26 | '
27 | }
28 | ]
29 |
30 | end := 'To run the program, put the code in hello-world.ink
and run it with the interpreter.'
31 |
--------------------------------------------------------------------------------
/examples/http.ink:
--------------------------------------------------------------------------------
1 | intro := 'Ink can both send and receive HTTP requests.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | log := std.log
8 |
9 |
10 | '
11 | },
12 | {
13 | docs: 'We can initiate an HTTP request with req
. It returns a function that aborts the request. Let\'s also time this request.'
14 | code: '
15 | t := time()
16 | data := {
17 | method: \'GET\'
18 | url: \'https://example.org\'
19 | headers: {}
20 | body: \'\'
21 | }
22 |
23 | close := req(data, evt => evt.type :: {
24 | ` this matches during a request fail
25 | or if the request is aborted `
26 | \'error\' -> log(\'Error: \' + evt.message)
27 | _ -> (
28 | log(\'Req: \')
29 | elasped := string(time() - t)
30 | log(\' elapsed time: \' + elasped)
31 | log(\' \' + string(keys(evt.data)))
32 | )
33 | })
34 | log(\'This code runs while we request.\')
35 | `` close() would abort the request
36 |
37 |
38 | '
39 | },
40 | {
41 | docs: 'A HTTP web server can be started with listen
. Similar to req
, it returns a function that stops the server.'
42 | code: '
43 | resp := {
44 | status: 200
45 | headers: {\'Content-Type\': \'text/plain\'}
46 | body: \'Hello from Ink land!\'
47 | }
48 | close := listen(\'0.0.0.0:80\', evt => evt.type :: {
49 | \'error\' -> log(\'Error: \' + evt.message)
50 | \'req\' -> (evt.end)(resp)
51 | })
52 | log(\'This code runs while we listen.\')
53 |
54 | ` stop the server `
55 | close()
56 | '
57 | }
58 | ]
59 |
60 | end := 'listen
errors in this project\'s deploy pipeline due to permissions but will work locally.'
61 |
--------------------------------------------------------------------------------
/examples/io.ink:
--------------------------------------------------------------------------------
1 | intro := 'Ink has access to standard input (stdin) and standard output (stdout). You can pipe input to an Ink program in the form echo "Hello, World!" | ink program.ink
.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | scan := std.scan
8 | log := std.log
9 | f := std.format
10 |
11 |
12 | '
13 | },
14 | {
15 | docs: 'To send data to standard output we can use the builtin out
, which accepts a string, or the helper std.log
which accepts strings and non-strings and adds a linebreak.'
16 | code: 'out(\'Hello, with a manual linebreak.
17 | \')
18 | log(7) `` print a number
19 | log([1, 2]) `` a composite
20 | log(() => ()) `` a function
21 | log(()) `` or null
22 | '
23 | },
24 | {
25 | docs: 'For reading in data from standard input, there is the builtin in
as well as the helper std.scan
which reads in a line.'
26 | code: 'prompt := (msg, cb) => (
27 | log(msg)
28 | scan(cb)
29 | )
30 | ` usage:
31 | prompt(\'Are you sure?\', msg => (
32 | log(msg)
33 | ))`
34 |
35 | '
36 | },
37 | {
38 | docs: 'std.format
allows us to build template strings and pass in a map of values.'
39 | code: 'log(f(\'A message without any passed in values.\', {}))
40 | log(f(\'The time is: {{ time }}.\', {time: time()}))
41 | '
42 | }
43 | ]
44 |
45 | end := ''
46 |
--------------------------------------------------------------------------------
/examples/lists.ink:
--------------------------------------------------------------------------------
1 | intro := 'Lists and maps share the same data structure — the composite value. At runtime, lists are hashmaps with string keys representing indices.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | log := std.log
8 | reverse := std.reverse
9 | slice := std.slice
10 | range := std.range
11 | each := std.each
12 | join := std.join
13 |
14 |
15 | '
16 | },
17 | {
18 | docs: 'Lists can be declared with array syntax.'
19 | code: 'names := [\'Alice\', \'Andrew\']
20 | log(names)
21 |
22 | '
23 | },
24 | {
25 | docs: 'To access or mutate an element we use the dot syntax.'
26 | code: 'log(names.0) `` first element
27 | log(names.(len(names) - 1)) `` last element
28 |
29 | names.0 := \'Madeline\'
30 | log(names)
31 |
32 | '
33 | },
34 | {
35 | docs: 'Like strings, we can append to the end of a list by mutating the next index value.'
36 | code: 'names.len(names) := \'Francisca\'
37 | log(names)
38 |
39 | '
40 | },
41 | {
42 | docs: ''
43 | code: '
44 | indexer := (name, index) => (
45 | log(index)
46 | log(name)
47 | )
48 | each(names, indexer)
49 |
50 | '
51 | },
52 | {
53 | docs: 'std.range
works similar to Python\'s range
. It takes start, end, and step values and returns a list.'
54 | code: '
55 | numbers := range(0, 6, 1)
56 | log(numbers)
57 |
58 | '
59 | },
60 | {
61 | docs: 'We can get a sublist with std.slice
, and join two lists to create a third with std.join
.'
62 | code: '
63 | half := slice(numbers, 0, 3)
64 | log(half)
65 |
66 | first := [98, 99]
67 | second := [100, 101]
68 | third := join(first, second)
69 | log(third)
70 |
71 | '
72 | },
73 | {
74 | docs: 'To reverse, use std.reverse
. For other list utilities, refer to the standard library .'
75 | code: '
76 | log(reverse([\'one\', \'two\', \'three\', ]))
77 | '
78 | }
79 | ]
80 |
81 | end := ''
82 |
--------------------------------------------------------------------------------
/examples/loops.ink:
--------------------------------------------------------------------------------
1 | intro := 'Instead of a for
loop construct, we use tail recursion .
2 | The standard library contains utility functions — like std.each
, std.map
, and std.reduce
— that can be used in place of a for
loop.'
3 |
4 | rows := [
5 | {
6 | docs: ''
7 | code: 'std := load(\'../vendor/std\')
8 | log := std.log
9 | reduce := std.reduce
10 | each := std.each
11 | map := std.map
12 |
13 |
14 | '
15 | },
16 | {
17 | docs: ''
18 | code: 'numbers := [0, 1, 2, 3, 4, 5, 6]
19 |
20 | '
21 | },
22 | {
23 | docs: 'We can loop over a list with std.each
, which accepts a list and a function as arguments.'
24 | code: 'logger := num => log(num)
25 | each(numbers, logger)
26 |
27 | '
28 | },
29 | {
30 | docs: 'Let\'s create a new list with the squares of these numbers with std.map
.'
31 | code: 'squares := map(numbers, num => num * num)
32 | log(squares)
33 |
34 | '
35 | },
36 | {
37 | docs: 'We can get the sum of these squares by using std.reduce
.'
38 | code: 'sum := reduce(squares, (acc, num) => acc + num, 0)
39 | log(sum)'
40 | }
41 | ]
42 |
43 | end := ''
44 |
--------------------------------------------------------------------------------
/examples/maps.ink:
--------------------------------------------------------------------------------
1 | intro := 'Like lists, maps use the composite value — Ink\'s single built-in data structure. At runtime, maps are hashmaps with string keys.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | clone := std.clone
8 | each := std.each
9 | log := std.log
10 |
11 |
12 | '
13 | },
14 | {
15 | docs: ''
16 | code: 'observation := {
17 | weather: \'Sunny\'
18 | \'observedAt\': {
19 | time: time()
20 | }
21 | }
22 | log(observation)
23 |
24 | '
25 | },
26 | {
27 | docs: 'Like lists, we use the dot syntax to access and mutate.'
28 | code: 'observation.(\'weather\') := \'Raining\'
29 |
30 | '
31 | },
32 | {
33 | docs: 'If we know the key before runtime, we can use a shortcut and just write it out.'
34 | code: 'observation.weather := \'Raining\'
35 | log(observation)
36 |
37 | '
38 | },
39 | {
40 | docs: 'std.clone
can be used to create a copy of a map.'
41 | code: 'cloned := clone(observation)
42 |
43 | '
44 | },
45 | {
46 | docs: 'We can iterate through the keys by calling keys
.'
47 | code: 'each(keys(observation), (key) => (
48 | log(observation.(key))
49 | ))
50 | '
51 | }
52 | ]
53 |
54 | end := ''
55 |
--------------------------------------------------------------------------------
/examples/random.ink:
--------------------------------------------------------------------------------
1 | intro := 'We can access randomness through two builtins — rand
and urand
. On Linux they typically map to /dev/random
and /dev/urandom
.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | range := std.range
8 | each := std.each
9 | log := std.log
10 |
11 |
12 | '
13 | },
14 | {
15 | docs: 'For a random number in the range [0, 1) — including zero but excluding one — we use rand
.'
16 | code: 'choices := [rand(), rand(), rand()]
17 | log(choices)
18 |
19 | '
20 | },
21 | {
22 | docs: 'We can use this to shuffle a list using the Fisher-Yates algorithm.'
23 | code: '
24 | ` an ordered list `
25 | numbers := range(0, 10, 1)
26 |
27 | shuffle := list => (
28 | each(list, (val, i) => (
29 | pick := floor(rand() * (len(list) - i - 1))
30 | current := len(list) - 1 - i
31 | tmp := list.(current)
32 | list.(current) := list.(pick)
33 | list.(pick) := tmp
34 | ))
35 | list
36 | )
37 | log(shuffle(numbers))
38 |
39 | '
40 | },
41 | {
42 | docs: 'For cryptographically secure randomness, use urand
. This uses the most secure random number generation interface your system provides to generate a string of random data.'
43 | code: '
44 | length := 10
45 | log(urand(10))
46 |
47 | '
48 | }
49 | ]
50 |
51 | end := ''
52 |
--------------------------------------------------------------------------------
/examples/sorting.ink:
--------------------------------------------------------------------------------
1 | intro := 'The quicksort
library provides a minimal Quicksort implementation that uses Hoare\'s partitioning.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | log := std.log
8 |
9 | qs := load(\'../vendor/quicksort\')
10 | sortBy := qs.sortBy
11 | sort! := qs.sort!
12 | sort := qs.sort
13 |
14 |
15 | '
16 | },
17 | {
18 | docs: 'We can sort a list in ascending order with quicksort.sort
.'
19 | code: 'numbers := [921, 14, 0, 0.002]
20 | log(sort(numbers))
21 |
22 | '
23 | },
24 | {
25 | docs: 'Function definitions that end in !
mutate their arguments — like quicksort.sort!
.'
26 | code: 'temperatures := [33, 1, 5, 22]
27 | sort!(temperatures)
28 |
29 | ` the existing list has changed `
30 | log(temperatures)
31 |
32 | '
33 | },
34 | {
35 | docs: 'quicksort.sortBy
allows us to pass a predicate — a function that each item should be measured with.'
36 | code: 'menu := {
37 | apples: 1.50
38 | oranges: 2.00
39 | coconut: 3.50
40 | grapes: 1.75
41 | }
42 | items := [\'apples\', \'oranges\', \'coconut\', \'grapes\']
43 |
44 | ` sort items by their menu price `
45 | log(sortBy(items, item => menu.(item)))
46 |
47 | '
48 | }
49 | ]
50 |
51 | end := ''
52 |
--------------------------------------------------------------------------------
/examples/values.ink:
--------------------------------------------------------------------------------
1 | intro := 'Ink has number, string, boolean, null, and composite values. Here are a few basic examples.'
2 |
3 | rows := [
4 | {
5 | docs: ''
6 | code: 'std := load(\'../vendor/std\')
7 | log := std.log
8 |
9 |
10 | '
11 | },
12 | {
13 | docs: 'Numbers, which support arithmetic operations, and more advanced functions using built-in native functions.'
14 | code: 'length := (0.5 + 2 * 3 / 4 - 5) + ~6 % 7
15 | log(length)
16 |
17 | ` a negative literal `
18 | temperature := ~5
19 | log(temperature)
20 |
21 | year := pow(10, 3) * 2 + 20
22 | log(year)
23 |
24 | '
25 | },
26 | {
27 | docs: 'Strings, which can be added together with +
, and appended to by mutating the next index value.'
28 | code: 'name := \'ada\' + \' \' + \'lovelace\'
29 | log(name)
30 | name.0 := \'A\'
31 | name.len(name) := \'!\'
32 | log(name)
33 |
34 | '
35 | },
36 | {
37 | docs: 'Boolean.'
38 | code: 'authed := true
39 | log(authed)
40 | log(~authed)
41 | log(true | false)
42 | log(true & false)
43 |
44 | '
45 | },
46 | {
47 | docs: 'Null.'
48 | code: 'exists := ()
49 | log(exists)
50 |
51 | '
52 | },
53 | {
54 | docs: 'Composite values, which are used for lists and maps.'
55 | code: 'primes := [2, 3, 5, 7]
56 | log(primes)
57 |
58 | menu := {
59 | apples: 1.00
60 | oranges: 1.50
61 | }
62 | log(menu)
63 |
64 | '
65 | },
66 | {
67 | docs: 'We can get the value of a type with type
.'
68 | code: 'log(type(0))
69 |
70 | '
71 | }
72 | ]
73 |
74 | end := ''
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish = "public/"
3 | command = "make build-linux"
4 |
--------------------------------------------------------------------------------
/src/build.ink:
--------------------------------------------------------------------------------
1 | std := load('../vendor/std')
2 | writeFile := std.writeFile
3 | readFile := std.readFile
4 | append := std.append
5 | reduce := std.reduce
6 | each := std.each
7 | f := std.format
8 | map := std.map
9 | cat := std.cat
10 | log := std.log
11 |
12 | str := load('../vendor/str')
13 | replace := str.replace
14 | lower := str.lower
15 | split := str.split
16 |
17 | highlight := load('highlight').main
18 |
19 | ` in order to execute the code examples, they need to be written to disk `
20 | evaluate := (fileName, source, cb) => (
21 | writeFile('../tmp/' + fileName + '.ink', source, result => result :: {
22 | () -> log('error creating source code file for: ' + example)
23 | _ -> (
24 | exec(args().0, ['../tmp/' + fileName + '.ink'], '', output => output :: {
25 | () -> log('error executing program for: ' + example)
26 | _ -> cb(output)
27 | })
28 | )
29 | })
30 | )
31 |
32 | renderIndexPage := (template, examples, inkVersion) => (
33 | ` render the index page with the list of examples and links `
34 | exampleList := reduce(examples, (acc, item) => (
35 | acc := acc + f('
36 | {{ name }}
37 | ', {
38 | name: item
39 | path: replace(lower(item), ' ', '-') + '.html'
40 | })
41 | ), '')
42 |
43 | page := f(template, {
44 | firstExample: replace(lower(examples.0), ' ', '-') + '.html'
45 | exampleList: exampleList
46 | inkVersion: inkVersion
47 | })
48 | writeFile('../public/index.html', page, result => result :: {
49 | () -> log('error writing index.html')
50 | })
51 | )
52 |
53 | renderExamplePages := (template, examples) => (
54 | each(examples, (example, i) => (
55 | fileName := replace(lower(example), ' ', '-')
56 | data := load('../examples/' + fileName)
57 | source := reduce(data.rows, (acc, item) => acc := acc + '
58 | ' + item.code, '')
59 |
60 | evaluate(fileName, source, result => (
61 | ` pages link to the next example if one exists `
62 | len(examples) :: {
63 | (i + 1) -> next := ''
64 | _ -> next := f('
65 | Next example: {{ example }} .
66 |
',
67 | {
68 | path: replace(lower(examples.(i + 1)), ' ', '-') + '.html'
69 | example: examples.(i + 1)
70 | })
71 | }
72 |
73 | page := f(template, {
74 | name: example
75 | rows: reduce(data.rows, (acc, item) => (
76 | class := (len(item.code) :: {
77 | 0 -> 'empty'
78 | _ -> ''
79 | })
80 | acc := acc + f('
81 | {{ docs }}
82 |
83 | {{ code }}
84 |
85 | ',
86 | {
87 | docs: item.docs
88 | code: highlight(item.code)
89 | class: class
90 | })
91 | ), f('
92 | {{ intro }}
93 |
94 | ',
95 | {
96 | intro: data.intro
97 | })
98 | )
99 | program: f('
100 |
101 | {{ end }}
102 |
103 | $ ink {{ fileName }}
104 | {{ result }}
105 |
106 |
107 |
',
108 | {
109 | fileName: fileName + '.ink'
110 | result: result.data
111 | end: data.end
112 | })
113 | next: next
114 | })
115 |
116 | writeFile('../public/' + fileName + '.html', page, result => result :: {
117 | () -> log('error creating example: ' + example)
118 | })
119 | ))
120 | ))
121 | )
122 |
123 | ` move over static content `
124 | moveStatic := () => dir('../static', item => (
125 | each(item.data, file => (
126 | readFile('../static/' + file.name, data => (
127 | writeFile('../public/' + file.name, data, result => result :: {
128 | () -> log('error creating static file: ' + file.name)
129 | })
130 | ))
131 | ))
132 | ))
133 |
134 | ` setup folders `
135 | createPublic := cb => make('../public', result => result :: {
136 | () -> log('error creating public folder')
137 | _ -> (
138 | moveStatic()
139 | cb()
140 | )
141 | })
142 | createTmp := () => make('../tmp', result => result :: {
143 | () -> log('error creating tmp folder')
144 | })
145 |
146 | examples := load('../examples').all
147 | createPublic(() => (
148 |
149 | ` grab the ink version `
150 | readFile('../.ink-version', inkVersion => inkVersion :: {
151 | () -> log('error reading .ink-version')
152 | _ -> (
153 |
154 | ` render index page `
155 | readFile('../templates/index.html', template => template :: {
156 | () -> log('error reading index template')
157 | _ -> renderIndexPage(template, examples, inkVersion)
158 | })
159 | )
160 | })
161 |
162 | ` render example pages `
163 | readFile('../templates/example.html', template => template :: {
164 | () -> log('error reading example template')
165 | _ -> renderExamplePages(template, examples)
166 | })
167 | ))
168 | createTmp()
169 |
--------------------------------------------------------------------------------
/src/highlight.ink:
--------------------------------------------------------------------------------
1 | ` adapted from https://github.com/thesephist/september to enabled HTML syntax highlighting `
2 |
3 | std := load('../vendor/std')
4 |
5 | log := std.log
6 | map := std.map
7 | each := std.each
8 | slice := std.slice
9 | cat := std.cat
10 |
11 | Tokenize := load('../vendor/tokenize')
12 | Tok := Tokenize.Tok
13 | tokenize := Tokenize.tokenizeWithComments
14 |
15 | ` associating token types with their highlight colors `
16 | colorFn := tok => tok.type :: {
17 | (Tok.Separator) -> s => s
18 |
19 | (Tok.Comment) -> s => '' + s + ' '
20 |
21 | (Tok.Ident) -> s => s
22 | (Tok.EmptyIdent) -> s => s
23 |
24 | (Tok.NumberLiteral) -> s => '' + s + ' '
25 | (Tok.StringLiteral) -> s => '' + s + ' '
26 | (Tok.TrueLiteral) -> s => '' + s + ' '
27 | (Tok.FalseLiteral) -> s => '' + s + ' '
28 |
29 | (Tok.AccessorOp) -> s => '' + s + ' '
30 | (Tok.EqOp) -> s => '' + s + ' '
31 |
32 | (Tok.FunctionArrow) -> s => '' + s + ' '
33 |
34 | ` operators are all Name.Function color `
35 | (Tok.KeyValueSeparator) -> s => '' + s + ' '
36 | (Tok.DefineOp) -> s => '' + s + ' '
37 | (Tok.MatchColon) -> s => '' + s + ' '
38 | (Tok.CaseArrow) -> s => '' + s + ' '
39 | (Tok.SubOp) -> s => '' + s + ' '
40 | (Tok.NegOp) -> s => '' + s + ' '
41 | (Tok.AddOp) -> s => '' + s + ' '
42 | (Tok.MulOp) -> s => '' + s + ' '
43 | (Tok.DivOp) -> s => '' + s + ' '
44 | (Tok.ModOp) -> s => '' + s + ' '
45 | (Tok.GtOp) -> s => '' + s + ' '
46 | (Tok.LtOp) -> s => '' + s + ' '
47 | (Tok.AndOp) -> s => '' + s + ' '
48 | (Tok.OrOp) -> s => '' + s + ' '
49 | (Tok.XorOp) -> s => '' + s + ' '
50 |
51 | (Tok.LParen) -> s => '' + s + ' '
52 | (Tok.RParen) -> s => '' + s + ' '
53 | (Tok.LBracket) -> s => '' + s + ' '
54 | (Tok.RBracket) -> s => '' + s + ' '
55 | (Tok.LBrace) -> s => '' + s + ' '
56 | (Tok.RBrace) -> s => '' + s + ' '
57 |
58 | _ -> () `` should error, unreachable
59 | }
60 |
61 | main := prog => (
62 | tokens := tokenize(prog)
63 | spans := map(tokens, (tok, i) => {
64 | colorFn: [tok.type, tokens.(i + 1)] :: {
65 | ` direct function calls are marked Name.Function color
66 | on a best-effort basis `
67 | [
68 | Tok.Ident
69 | {type: Tok.LParen, val: _, line: _, col: _, i: _}
70 | ] -> s => '' + s + ' '
71 | _ -> colorFn(tok)
72 | }
73 | start: tok.i
74 | end: tokens.(i + 1) :: {
75 | () -> len(prog)
76 | _ -> tokens.(i + 1).i
77 | }
78 | })
79 | pcs := map(
80 | spans
81 | span => (span.colorFn)(slice(prog, span.start, span.end))
82 | )
83 | cat(pcs, '')
84 | )
85 |
--------------------------------------------------------------------------------
/src/ink-darwin-1.9:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/src/ink-darwin-1.9
--------------------------------------------------------------------------------
/src/ink-linux-1.9:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/src/ink-linux-1.9
--------------------------------------------------------------------------------
/src/test.ink:
--------------------------------------------------------------------------------
1 | std := load('../vendor/std')
2 | each := std.each
3 | log := std.log
4 |
5 | str := load('../vendor/str')
6 | split := str.split
7 |
8 |
9 | dir('../examples/', files => each(files.data, file => file.dir :: {
10 | false -> (
11 | example := split(file.name, '.ink').0
12 | load('../tmp/' + example)
13 | )
14 | _ -> 'skip directory'
15 | }))
16 |
--------------------------------------------------------------------------------
/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ink by Example: Not Found
6 |
7 |
8 |
9 |
10 |
13 |
Sorry, we couldn't find that! Check out the home page ?
14 |
Ping @healeycodes and I'll fix this ASAP!
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/static/favicon.ico
--------------------------------------------------------------------------------
/static/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/healeycodes/inkbyexample/661fa148161b488b8dba2b63ea8b669a6c2580f2/static/preview.png
--------------------------------------------------------------------------------
/static/site.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');
2 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap');
3 |
4 | /* CSS reset: http://meyerweb.com/eric/tools/css/reset/ */
5 | html, body, div, span, applet, object, iframe,
6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
7 | a, abbr, acronym, address, big, cite, code,
8 | del, dfn, em, img, ins, kbd, q, s, samp,
9 | small, strike, strong, sub, sup, tt, var,
10 | b, u, i, center,
11 | dl, dt, dd, ol, ul, li,
12 | fieldset, form, label, legend,
13 | table, caption, tbody, tfoot, thead, tr, th, td,
14 | article, aside, canvas, details, embed,
15 | figure, figcaption, footer, header, hgroup,
16 | menu, nav, output, ruby, section, summary,
17 | time, mark, audio, video {
18 | margin: 0;
19 | padding: 0;
20 | border: 0;
21 | font-size: 100%;
22 | font: inherit;
23 | vertical-align: baseline;
24 | }
25 | article, aside, details, figcaption, figure,
26 | footer, header, hgroup, menu, nav, section {
27 | display: block;
28 | }
29 | body {
30 | line-height: 1;
31 | }
32 | ol, ul {
33 | list-style: none;
34 | }
35 | blockquote, q {
36 | quotes: none;
37 | }
38 | blockquote:before, blockquote:after,
39 | q:before, q:after {
40 | content: '';
41 | content: none;
42 | }
43 | table {
44 | border-collapse: collapse;
45 | border-spacing: 0;
46 | }
47 |
48 | /* Layout and typography */
49 | body {
50 | font-family: 'Inter', sans-serif;
51 | font-size: 16px;
52 | line-height: 20px;
53 | color: #252519;
54 | }
55 | em {
56 | font-style: italic;
57 | }
58 | a, a:visited {
59 | color: #261a3b;
60 | }
61 | h2 {
62 | font-size: 32px;
63 | line-height: 40px;
64 | margin-top: 40px;
65 | }
66 | h2 a {
67 | text-decoration: none;
68 | }
69 | div.example {
70 | width: 900px;
71 | min-width: 900px;
72 | max-width: 900px;
73 | margin-left: auto;
74 | margin-right: auto;
75 | margin-bottom: 120px;
76 | }
77 | div.example table {
78 | margin-top: 15px;
79 | margin-bottom: 20px;
80 | }
81 | p.next {
82 | margin-bottom: 20px;
83 | }
84 | p.footer {
85 | color: grey;
86 | }
87 | p.footer a, p.footer a:visited {
88 | color: grey;
89 | }
90 | div#intro {
91 | max-width: 420px;
92 | margin-left: auto;
93 | margin-right: auto;
94 | margin-bottom: 120px;
95 | padding: 40px;
96 | }
97 | div#intro p {
98 | padding-top: 20px;
99 | }
100 | div#intro ul {
101 | padding-top: 20px;
102 | }
103 | table td {
104 | border: 0;
105 | outline: 0;
106 | }
107 | td.docs {
108 | width: 420px;
109 | max-width: 420px;
110 | min-width: 420px;
111 | min-height: 5px;
112 | vertical-align: top;
113 | text-align: left;
114 | }
115 | td.docs p {
116 | padding-right: 5px;
117 | padding-top: 5px;
118 | padding-bottom: 15px;
119 | }
120 | td.code {
121 | width: 480px;
122 | max-width: 480px;
123 | min-width: 480px;
124 | padding-top: 5px;
125 | padding-right: 5px;
126 | padding-left: 5px;
127 | padding-bottom: 5px;
128 | vertical-align: top;
129 | background: #f0f0f0;
130 | }
131 | td.code.leading {
132 | padding-bottom: 11px;
133 | }
134 | td.code.empty {
135 | background: #ffffff;
136 | }
137 | pre {
138 | white-space: pre-wrap;
139 | }
140 | pre, code {
141 | font-size: 15px;
142 | line-height: 18px;
143 | font-family: 'Roboto Mono', 'Menlo', 'Monaco', 'Consolas', 'Lucida Console', monospace;
144 | }
145 | code {
146 | background: #f0f0f0;
147 | }
148 | img.copy, img.run {
149 | height: 16px;
150 | width: 16px;
151 | float: right
152 | }
153 | img.copy, img.run {
154 | cursor: pointer;
155 | }
156 | img.copy {
157 | margin-right: 4px;
158 | }
159 |
160 | /* Syntax highlighting */
161 | body .hll { background-color: #ffffcc }
162 | body .err { border: 1px solid #FF0000 } /* Error */
163 | body .c { color: #408080; font-style: italic } /* Comment */
164 | body .k { color: #954121 } /* Keyword */
165 | body .o { color: #666666 } /* Operator */
166 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
167 | body .cp { color: #BC7A00 } /* Comment.Preproc */
168 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */
169 | body .cs { color: #408080; font-style: italic } /* Comment.Special */
170 | body .gd { color: #A00000 } /* Generic.Deleted */
171 | body .ge { font-style: italic } /* Generic.Emph */
172 | body .gr { color: #FF0000 } /* Generic.Error */
173 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */
174 | body .gi { color: #00A000 } /* Generic.Inserted */
175 | body .go { color: #808080 } /* Generic.Output */
176 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
177 | body .gs { font-weight: bold } /* Generic.Strong */
178 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
179 | body .gt { color: #0040D0 } /* Generic.Traceback */
180 | body .kc { color: #954121 } /* Keyword.Constant */
181 | body .kd { color: #954121 } /* Keyword.Declaration */
182 | body .kn { color: #954121 } /* Keyword.Namespace */
183 | body .kp { color: #954121 } /* Keyword.Pseudo */
184 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */
185 | body .kt { color: #B00040 } /* Keyword.Type */
186 | body .m { color: #666666 } /* Literal.Number */
187 | body .s { color: #219161 } /* Literal.String */
188 | body .na { color: #7D9029 } /* Name.Attribute */
189 | body .nb { color: #954121 } /* Name.Builtin */
190 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */
191 | body .no { color: #880000 } /* Name.Constant */
192 | body .nd { color: #AA22FF } /* Name.Decorator */
193 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */
194 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
195 | body .nf { color: #0000FF } /* Name.Function */
196 | body .nl { color: #A0A000 } /* Name.Label */
197 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
198 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */
199 | body .nv { color: #19469D } /* Name.Variable */
200 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
201 | body .w { color: #bbbbbb } /* Text.Whitespace */
202 | body .mf { color: #666666 } /* Literal.Number.Float */
203 | body .mh { color: #666666 } /* Literal.Number.Hex */
204 | body .mi { color: #666666 } /* Literal.Number.Integer */
205 | body .mo { color: #666666 } /* Literal.Number.Oct */
206 | body .sb { color: #219161 } /* Literal.String.Backtick */
207 | body .sc { color: #219161 } /* Literal.String.Char */
208 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */
209 | body .s2 { color: #219161 } /* Literal.String.Double */
210 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
211 | body .sh { color: #219161 } /* Literal.String.Heredoc */
212 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
213 | body .sx { color: #954121 } /* Literal.String.Other */
214 | body .sr { color: #BB6688 } /* Literal.String.Regex */
215 | body .s1 { color: #219161 } /* Literal.String.Single */
216 | body .ss { color: #19469D } /* Literal.String.Symbol */
217 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */
218 | body .vc { color: #19469D } /* Name.Variable.Class */
219 | body .vg { color: #19469D } /* Name.Variable.Global */
220 | body .vi { color: #19469D } /* Name.Variable.Instance */
221 | body .il { color: #666666 } /* Literal.Number.Integer.Long */
222 |
--------------------------------------------------------------------------------
/templates/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 | Ink by Example — {{ name }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
33 |
34 |
35 | {{ rows }}
36 | {{ program }}
37 |
38 |
39 | {{ next }}
40 |
41 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Ink by Example
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 | Ink is a minimal programming language inspired by modern JavaScript and Go, with a functional style.
37 |
38 |
39 |
40 | Ink by Example is a hands-on introduction
41 | to Ink using annotated example programs. Check out
42 | the first example or
43 | browse the full list below.
44 |
45 |
46 |
47 | {{ exampleList }}
48 |
49 |
50 |
51 | This website's toolchain is powered by Ink {{ inkVersion }}
52 |
53 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/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/quicksort.ink:
--------------------------------------------------------------------------------
1 | ` minimal quicksort implementation
2 | using hoare partition
3 | https://github.com/thesephist/ink/blob/master/samples/quicksort.ink `
4 |
5 | std := load('../vendor/std')
6 |
7 | map := std.map
8 | clone := std.clone
9 |
10 | sortBy := (v, pred) => (
11 | vPred := map(v, pred)
12 | partition := (v, lo, hi) => (
13 | pivot := vPred.(lo)
14 | lsub := i => vPred.(i) < pivot :: {
15 | true -> lsub(i + 1)
16 | false -> i
17 | }
18 | rsub := j => vPred.(j) > pivot :: {
19 | true -> rsub(j - 1)
20 | false -> j
21 | }
22 | (sub := (i, j) => (
23 | i := lsub(i)
24 | j := rsub(j)
25 | i < j :: {
26 | false -> j
27 | true -> (
28 | ` inlined swap! `
29 | tmp := v.(i)
30 | tmpPred := vPred.(i)
31 | v.(i) := v.(j)
32 | v.(j) := tmp
33 | vPred.(i) := vPred.(j)
34 | vPred.(j) := tmpPred
35 |
36 | sub(i + 1, j - 1)
37 | )
38 | }
39 | ))(lo, hi)
40 | )
41 | (quicksort := (v, lo, hi) => len(v) :: {
42 | 0 -> v
43 | _ -> lo < hi :: {
44 | false -> v
45 | true -> (
46 | p := partition(v, lo, hi)
47 | quicksort(v, lo, p)
48 | quicksort(v, p + 1, hi)
49 | )
50 | }
51 | })(v, 0, len(v) - 1)
52 | )
53 |
54 | sort! := v => sortBy(v, x => x)
55 |
56 | sort := v => sort!(clone(v))
57 |
--------------------------------------------------------------------------------
/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) => i < 0 :: {
137 | true -> acc
138 | _ -> sub(acc.len(acc) := list.(i), i - 1)
139 | })([], 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 |
--------------------------------------------------------------------------------
/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/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('../vendor/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 |
--------------------------------------------------------------------------------