├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── assets
├── chat_icon.png
├── create_gizmo_icon.png
├── create_icon.png
├── default_gizmo_icon.png
├── dot_grid.svg
├── image_gallery_icon.png
├── note_icon.png
├── todo_icon.png
└── tutorial_icon.png
├── bin
├── _includes.sh
└── types.sh
├── docs
└── gizmoDirectory.json
├── elm.json
├── package.json
├── prettier.config.js
├── src
├── electron
│ └── index.ts
├── elm
│ ├── Bot.elm
│ ├── BotHarness.elm
│ ├── Clipboard.elm
│ ├── Colors.elm
│ ├── DataTransfer.elm
│ ├── Doc.elm
│ ├── Draggable.elm
│ ├── Extra
│ │ ├── Array.elm
│ │ └── List.elm
│ ├── FarmUrl.elm
│ ├── Gizmo.elm
│ ├── Harness.elm
│ ├── History.elm
│ ├── IO.elm
│ ├── Keyboard.elm
│ ├── Link.elm
│ ├── ListSet.elm
│ ├── Navigation.elm
│ ├── Notification.elm
│ ├── RealmUrl.elm
│ ├── Repo.elm
│ ├── Tooltip.elm
│ ├── Uri.elm
│ ├── UriParser.elm
│ ├── Value.elm
│ ├── VsCode.elm
│ ├── examples
│ │ ├── Authors.elm
│ │ ├── Avatar.elm
│ │ ├── Board.elm
│ │ ├── CatBot.elm
│ │ ├── Chat.elm
│ │ ├── Counter.elm
│ │ ├── CounterTutorial.elm
│ │ ├── CreateExample.elm
│ │ ├── CreatePicker.elm
│ │ ├── Cube.elm
│ │ ├── DummyBoard.elm
│ │ ├── EditableTitle.elm
│ │ ├── EmptyGizmo.elm
│ │ ├── Essay.elm
│ │ ├── GalleryTutorial.elm
│ │ ├── GizmoDirectory.elm
│ │ ├── GizmoTemplate.elm
│ │ ├── HistoryViewer.elm
│ │ ├── Icon.elm
│ │ ├── Image.elm
│ │ ├── Koala.elm
│ │ ├── Launcher.elm
│ │ ├── LiveEdit.elm
│ │ ├── NavigationBar.elm
│ │ ├── Navigator.elm
│ │ ├── Note.elm
│ │ ├── Oblique.elm
│ │ ├── PickerItem.elm
│ │ ├── Property.elm
│ │ ├── RendererPicker.elm
│ │ ├── SimpleAvatar.elm
│ │ ├── SimpleImageGallery.elm
│ │ ├── SmallAvatar.elm
│ │ ├── SuperboxDefault.elm
│ │ ├── SuperboxEdit.elm
│ │ ├── Title.elm
│ │ ├── TitledMarkdownNote.elm
│ │ ├── TodoList.elm
│ │ ├── Wiki.elm
│ │ ├── WindowManager.elm
│ │ └── Workspace.elm
│ └── vendor
│ │ ├── Csv.elm
│ │ └── Csv
│ │ ├── Decode.elm
│ │ └── Decoder.elm
├── hypermerge-devtools
│ ├── main.ts
│ ├── manifest.json
│ └── panel.ts
├── js
│ ├── App.ts
│ ├── AsyncQueue.ts
│ ├── Author.ts
│ ├── Bot.ts
│ ├── Code.ts
│ ├── Compiler.ts
│ ├── Diff.ts
│ ├── Digest.ts
│ ├── Draggable.ts
│ ├── ElmGizmo.ts
│ ├── FakeWorker.ts
│ ├── FarmUrl.ts
│ ├── Gizmo.ts
│ ├── GizmoWindow.ts
│ ├── Link.ts
│ ├── Msg.ts
│ ├── Queue.ts
│ ├── QueuedPort.ts
│ ├── QueuedResource.ts
│ ├── QueuedWorker.ts
│ ├── Repo.ts
│ ├── Subscription.ts
│ ├── bootstrap
│ │ ├── Workspace.ts
│ │ └── index.ts
│ ├── cli
│ │ └── farm.ts
│ ├── compile.worker.ts
│ ├── index.ts
│ └── repo.worker.ts
├── node_modules
│ └── @types
│ │ ├── dat-swarm-defaults
│ │ └── index.d.ts
│ │ ├── discovery-swarm
│ │ └── index.d.ts
│ │ ├── electron-context-menu
│ │ └── index.d.ts
│ │ ├── elm-format
│ │ └── index.d.ts
│ │ ├── hypercore
│ │ └── index.d.ts
│ │ ├── node-elm-compiler
│ │ └── index.d.ts
│ │ ├── pseudo-worker
│ │ └── index.d.ts
│ │ ├── random-access-file
│ │ └── index.d.ts
│ │ └── xmlhttprequest
│ │ └── index.d.ts
├── server
│ └── index.ts
└── vscode
│ ├── package.json
│ └── yarn.lock
├── tsconfig.json
├── webpack.config.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /src/vscode/node_modules
3 | /elm-stuff
4 | /dist
5 | .data*
6 | .tmp
7 | .ts-cache
8 | .cache
9 | .DS_Store
10 | *.log
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "typescript.format.enable": false,
4 | "[elm]": {
5 | "editor.detectIndentation": false,
6 | "editor.insertSpaces": true,
7 | "editor.tabSize": 4
8 | },
9 | "elm.formatCommand": "./node_modules/.bin/elm-format",
10 | "hypermergefs.roots": [
11 | // "hypermerge:/DLPW4Kotn3Z9Hz2YQiMW5j97ofccyuhpFiFriJh4P2Si",
12 | // "hypermerge:/2ukn1Myu2Z6MWF5BvyFMHk3S7q52egryL2XYvfxzu558",
13 | // "hypermerge:/HSwDVsGzJbpPD88SF7FMNDZgRhuF6SpvQfnVsM6RogTo",
14 | // "hypermerge:/J3pbWADhHf544gAZ7H7wygwiRip56JGWJ3C7BbTkMfsc",
15 | // "hypermerge:/9wUHvu5xFTZmdCkmjy9kDqy6CMq5Dn1uu6nEhvnKt4bx",
16 | // "hypermerge:/2f811DqW9VhM4xzASENiQo7oTcvMpGq9irGmewVz6tr3",
17 | // "hypermerge:/RxJmxe9P7dZrWLcfFYp7VqF7uHf7EGW2xjs7CrCv7xD"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Farm
2 |
3 | Farm is an experiment in distributed peer-to-peer computing. Farm is an extensible, programmable environment with real-time and offline collaboration with other users and no mandatory infrastructure. You can program Farm using the Elm programming language, and changes you make to the code will be shared in real-time with users anywhere in the world.
4 |
5 | Farm also includes a demonstration application inspired by tools like Google Keep, Milanote, or Trello.
6 |
7 | It partners particularly well with our [vscode plugin](https://github.com/inkandswitch/vscode-hypermerge/).
8 |
9 | ## Caveat Emptor
10 |
11 | Warning! Farm is experimental software. It makes no guarantees about performance, security, or stability. Farm will probably consume all the memory on your computer, leak your data to your worst enemy, and then delete it.
12 |
13 | That said, if you encounter Farm bugs or defects, please let us know. We're interested in hearing from people who try the software.
14 |
15 | ## Trying Farm
16 |
17 | Clone this repo, then start the application.
18 |
19 | ```bash
20 | yarn
21 | yarn start
22 | ```
23 |
24 | You should see a "welcome" board with a navigation URL. This is the Farm demonstration application. Everything you see is self-hosted in Farm and can be edited by you, from the navigation bar at the top to the typeface used on the welcome card.
25 |
26 | ## Using FarmPin
27 |
28 | FarmPin is a tool for collecting and sharing ideas. You can use it to plan a trip or a project, create a mood board, or to improvise an ad-hoc user interface for an application you're working on.
29 |
30 | - You can right-click to create new notes on your board or resize and drag notes around by clicking on the bar at the top.
31 | - You can make the current card full-screen by double clicking on the top 20px of the card navigate back (or forward) to the previous view with the arrow buttons left of the title bar.
32 | - Delete a card by right-clicking on the top 20px and clicking "Remove".
33 | - Share a link to your current view (and it's code!) by copying the URL out of the title bar and pasting it to another user. Be careful -- anyone with the link to a Farm document can not only view it now but all future versions as well and their modifications to the code or data will be merged with your own.
34 |
35 | ## Working on Farm Applications
36 |
37 | Farm applications are built out of Gizmos. A Gizmo is the combination of some data with a small Elm program to render it as a Web Component. Changes to the Elm code for a Gizmo are compiled automatically into Javascript by the Farm runtime, and changes to the data document will trigger a re-rendering of the content as well.
38 |
39 | All documents in Farm, both data and code, are Hypermerge documents. A Hypermerge document is identified by its URL, and anyone with the URL is able to make changes to it and should expect them to be synchronized everwhere in the world. All hypermerge documents are constructed out of their full history.
40 |
41 | The best supported way of working on a Farm application is through the Hypermerge VSCode extension. To import your data and code into the VSCode extension, paste it into the "Open Document" dialogue for the extension. Further details on this process are describe in the README for that project.
42 |
43 | A farm:// URL has two parts -- the first half tells Farm which code to run, and the second half describes the data document to render with that code. You can pair any code with any document and Farm will do its best to make it work.
44 |
45 | When editing Farm code in VSCode changes made to a Source.elm key will be synchronized to Farm which will attempt to compile them with the Elm compiler. If the compile is successful, the result will be written to Source.js. If the compile fails, the errors will be written to a hypermergeFsDiagnostics key which VSCode will render as code error highlighting within the relevant buffer.
46 |
47 | Good luck! If you have questions, don't hesitate to ask here in the Github Issues or on the automerge slack.
48 |
49 | ### Starting multiple instances of Farm on one machine
50 |
51 | Each instance of Farm must have it's own [Repo][repo] directory.
52 | By setting the `REPO_ROOT` environment variable, we can open a second instance
53 | of Farm:
54 |
55 | ```sh
56 | REPO_ROOT=.data2 yarn start
57 | ```
58 |
59 | The default `REPO_ROOT` value is `.data`.
60 |
61 | ### Code highlighting & formatting for Elm code in Hypermerge
62 |
63 | The upstream Elm extension assumes files are written to disk, which Hypermerge documents are not. As a result, when working on Hypermerge documents you'll want to use our patched version of the Elm vscode extension. Download the [latest Release](https://github.com/inkandswitch/vscode-elm/releases/latest). Install by selecting
64 | "Extensions: Install from VSX..." from the Command Palette, and selecting the downloaded
65 | .vsx file.
66 |
67 | [repo]: https://github.com/automerge/hypermerge/tree/fork#concepts
68 |
69 | ### Windows
70 |
71 | Farm uses several packages which will require a native rebuild on Windows. You should only need to do this once. To rebuild, from the repo root run:
72 | ```
73 | npx electron-rebuild
74 | ```
--------------------------------------------------------------------------------
/assets/chat_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/chat_icon.png
--------------------------------------------------------------------------------
/assets/create_gizmo_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/create_gizmo_icon.png
--------------------------------------------------------------------------------
/assets/create_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/create_icon.png
--------------------------------------------------------------------------------
/assets/default_gizmo_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/default_gizmo_icon.png
--------------------------------------------------------------------------------
/assets/dot_grid.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/assets/image_gallery_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/image_gallery_icon.png
--------------------------------------------------------------------------------
/assets/note_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/note_icon.png
--------------------------------------------------------------------------------
/assets/todo_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/todo_icon.png
--------------------------------------------------------------------------------
/assets/tutorial_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inkandswitch/farm/4e20f604b9cdc6d94cab5088e518a2809781e470/assets/tutorial_icon.png
--------------------------------------------------------------------------------
/bin/_includes.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | show() {
4 | printf "\n$@\n\n"
5 | }
6 |
7 | require() {
8 | [[ -z "$2" ]] && usage && show "Argument <$1> is required." && exit 1
9 | eval "$1=$2"
10 | }
11 |
--------------------------------------------------------------------------------
/bin/types.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | source "${BASH_SOURCE%/*}/_includes.sh"
4 |
5 | usage() {
6 | cat <<- EOF
7 |
8 | Usage:
9 |
10 | $0
11 |
12 | Installs the '@types/' package or creates a
13 | declaration file if it doesn't exist.
14 |
15 | EOF
16 | }
17 |
18 | types() {
19 | install_types "$@" || create_stub "$@"
20 | }
21 |
22 | install_types() {
23 | require "name" $1
24 |
25 | yarn add --dev @types/$name &>/dev/null \
26 | && show "Installed '@types/$name'." \
27 | || (echo "Package '@types/$1' not found..." && return 1)
28 | }
29 |
30 | create_stub() {
31 | require "name" $1
32 | module="${name%%/*}"
33 |
34 | path="./src/node_modules/@types/$module"
35 | index="$path/index.d.ts"
36 |
37 | if [ -f $index ]; then
38 | show "Found $index"
39 | return 0
40 | fi
41 |
42 | mkdir -p $path
43 | cat <<- EOF > $index
44 | declare module "$name"
45 | EOF
46 |
47 | show "Created '$name' module at $index"
48 | }
49 |
50 | types "$@"
51 |
--------------------------------------------------------------------------------
/docs/gizmoDirectory.json:
--------------------------------------------------------------------------------
1 | {
2 | "gizmos": [
3 | {
4 | "gizmo": "hypermerge:/HSwDVsGzJbpPD88SF7FMNDZgRhuF6SpvQfnVsM6RogTo/",
5 | "documents": [
6 | "hypermerge:/3xmrSG1j1YbmkwiExsm7mwAsR9qkJMkUxtG2CWt3sAcy/",
7 | "hypermerge:/EXD4GJrrjLRbLLHoczPPVUTvFCsqmCsF12aU5zMH1MyJ/",
8 | "hypermerge:/GVjJdLgZxZcRtdwhK5aKRKe9UuR43FFxHCGBdr2fzF8/"
9 | ]
10 | },
11 | {
12 | "gizmo": "hypermerge:/9wUHvu5xFTZmdCkmjy9kDqy6CMq5Dn1uu6nEhvnKt4bx/",
13 | "documents": [
14 | "hypermerge:/2LYdYCW832AkwEVMYDHwQPWs3CndKdYGB4u3dp1wUeEN/",
15 | "hypermerge:/2XkrLwC6zKYhqYAy1T8h4FnvDhkkW3ZYmnYQYVefdBDt/",
16 | "hypermerge:/3BFGVhsKVoudtkNAmoXdwHa9PRtbrxEXpR3C6ddZJnPV/",
17 | "hypermerge:/2jUPef8EJ1bwGE2CiwpbtgLXvpuDVtpCoXPUYnDBrKzR/"
18 | ]
19 | },
20 | {
21 | "gizmo": "hypermerge:/J3pbWADhHf544gAZ7H7wygwiRip56JGWJ3C7BbTkMfsc/",
22 | "documents": ["hypermerge:/3tsjxbDoo7NdZaWyMK7rDpxt8o3kMn6EqQALF5mQq5cF/"]
23 | },
24 | {
25 | "gizmo": "hypermerge:/2ukn1Myu2Z6MWF5BvyFMHk3S7q52egryL2XYvfxzu558/",
26 | "documents": [
27 | "hypermerge:/BAm3Dgki1bVnuizyPfGFezFMzSUy1bx9gHMAjZ8urhPT/",
28 | "hypermerge:/7ZymcxjVhKee8DE8AiRyHkSeuLPLYo58d6tm6X7eGa7b/",
29 | "hypermerge:/7vURtAwTqZq9jxz5jDmQA1niHuWAGVRxFWVcEkynsBKR/"
30 | ]
31 | },
32 | {
33 | "gizmo": "hypermerge:/E19jZZNm4QceSWwwiZGLtrduFVDwmdtM3PGFAfMMS55S/",
34 | "documents": [
35 | "hypermerge:/7Qv2YyfAkiBZEZNHK1xCbCXQK1toywbdzJzEX9AL2F9T/",
36 | "hypermerge:/FvsM8JvaCvog3zXXr2qig6D5oZECtVRAyrHSBqoFzNy7/",
37 | "hypermerge:/D7ciYve4NpUvCYQU2Bd6JKFLtRUeenyYbsb6tPwkjxMJ/"
38 | ]
39 | },
40 | {
41 | "gizmo": "hypermerge:/9DJcvkXLyRU8KmqvyzhNY3zCpkKXVkHVJ8vBNr6iizGQ/",
42 | "documents": [
43 | "hypermerge:/DBvjC1Sf7GNeGFbSRocHaMkNZ1NEV8W8XkihdbAq5c89/",
44 | "hypermerge:/5HZVmnEF4BqqkTLCEVfZbsBSQod7E3EwGgT5c1iHJcjH/",
45 | "hypermerge:/CeJsEwddjvYLUh9eDx5xJaKD8QQjVRHjw3NbQ1jzcGdn/"
46 | ]
47 | },
48 | {
49 | "gizmo": "hypermerge:/5NiEp2QK9k6N5BtVzUSXUELxryK6CxA78hP14mvZuANN/",
50 | "documents": ["hypermerge:/Gr856Q9xfhnAtnBDPiE83jPnAXFGUzXCMfVm32zfiTaT/"]
51 | },
52 | {
53 | "documents": [],
54 | "gizmo": "hypermerge:/H9DXbAsUnLV5QyR3drP7Si1GmkadCaCcFHUZLushrpS1/"
55 | },
56 | {
57 | "documents": [],
58 | "gizmo": "hypermerge:/DJZN6Xb5fZwkqzKtjwE1hHSajYiWj5oMVbMyDu818Jwb/"
59 | },
60 | {
61 | "documents": [
62 | "hypermerge:/39h3kRwC62PbtLdVqoRXuYNxTWPqwkb1ErKpR9jZ8RTd/",
63 | "hypermerge:/GPrkojydXXcytSWgN2umHhBh9MkxT5kNUmEvq7xMKZ14/"
64 | ],
65 | "gizmo": "hypermerge:/FhUiyTwrA3f2Hw33y2AAYYmzUX3vPsiEYtwSx7uvYL2e/"
66 | },
67 | {
68 | "documents": [],
69 | "gizmo": "hypermerge:/C1MTpUP4rxoP12mhhp8CRYcUv7o4WX95xjS6CXDtbzp3/"
70 | },
71 | {
72 | "documents": [
73 | "hypermerge:/8wg25rAsUfukvcegfqwTdxRq7sU2RtTE2dtzftc6jZnU/"
74 | ],
75 | "gizmo": "hypermerge:/B3JZ35aBFuwxJHnCYQzKJ6ux5GhyCkzy3mA7f7ikwGha/"
76 | },
77 | {
78 | "documents": [],
79 | "gizmo": "hypermerge:/CVxHXJoBJiEfrRqERfeGTZUUVgjSpKUFmvS6yVzAnu5D/"
80 | },
81 | {
82 | "documents": [],
83 | "gizmo": "hypermerge:/FpeVPqFydiGk2ZYP28oemJ6xu7T7KDMBJo9dwPs64ejg/"
84 | },
85 | {
86 | "documents": [
87 | "hypermerge:/7LBiFEqBTUjbx2NSmhBSuWkyUPb26xrJzahjUfsqn7Px/"
88 | ],
89 | "gizmo": "hypermerge:/gkkWiL4qp5ZuQDdZhqNCcBG4rF9CWBkLjCxMJrVN23i/"
90 | }
91 | ],
92 | "title": "Shared Gizmo Directory",
93 | "currentDocument": null,
94 | "currentGizmo": null
95 | }
96 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src/elm/vendor",
5 | "src/elm",
6 | ".tmp"
7 | ],
8 | "elm-version": "0.19.0",
9 | "dependencies": {
10 | "direct": {
11 | "NoRedInk/elm-json-decode-pipeline": "1.0.0",
12 | "elm/browser": "1.0.1",
13 | "elm/core": "1.0.2",
14 | "elm/file": "1.0.1",
15 | "elm/html": "1.0.0",
16 | "elm/json": "1.1.2",
17 | "elm/parser": "1.1.0",
18 | "elm/random": "1.0.0",
19 | "elm/time": "1.0.0",
20 | "elm/url": "1.0.0",
21 | "elm-community/list-extra": "8.1.0",
22 | "elm-explorations/linear-algebra": "1.0.3",
23 | "elm-explorations/markdown": "1.0.0",
24 | "elm-explorations/webgl": "1.0.1",
25 | "mdgriffith/elm-ui": "1.1.0",
26 | "mpizenberg/elm-pointer-events": "4.0.0",
27 | "rtfeldman/elm-css": "16.0.0",
28 | "ryannhg/date-format": "2.3.0",
29 | "stil4m/elm-syntax": "7.0.4"
30 | },
31 | "indirect": {
32 | "Skinney/murmur3": "2.0.8",
33 | "elm/bytes": "1.0.7",
34 | "elm/virtual-dom": "1.0.2",
35 | "elm-community/json-extra": "4.0.0",
36 | "rtfeldman/elm-hex": "1.0.0",
37 | "rtfeldman/elm-iso8601-date-strings": "1.1.2",
38 | "stil4m/structured-writer": "1.0.2"
39 | }
40 | },
41 | "test-dependencies": {
42 | "direct": {},
43 | "indirect": {
44 | "elm/regex": "1.0.0"
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "farm",
3 | "version": "1.0.1",
4 | "description": "Runtime-editable elm.",
5 | "main": "dist/electron.js",
6 | "repository": "https://github.com/inkandswitch/farm",
7 | "author": "Jeff Peterson , Peter van Hardenberg , Matt Tognetti ",
8 | "license": "BSD",
9 | "private": true,
10 | "scripts": {
11 | "start": "yarn && yarn build && electron .",
12 | "build": "cross-env TS_NODE_CACHE_DIRECTORY=.ts-cache webpack",
13 | "app": "yarn build && yarn start-app",
14 | "types": "./bin/types.sh",
15 | "farm": "yarn build && cross-env REPO_ROOT=./.data/bots node ./dist/farm.js",
16 | "bot": "yarn farm bot"
17 | },
18 | "devDependencies": {
19 | "@types/bs58": "^3.0.30",
20 | "@types/chrome": "^0.0.78",
21 | "@types/commander": "^2.12.2",
22 | "@types/copy-webpack-plugin": "^4.4.2",
23 | "@types/debug": "^0.0.31",
24 | "@types/electron": "^1.6.10",
25 | "@types/express": "^4.16.0",
26 | "@types/hard-source-webpack-plugin": "^1.0.0",
27 | "@types/html-webpack-plugin": "^3.2.0",
28 | "@types/lodash": "^4.14.118",
29 | "@types/mime-types": "^2.1.0",
30 | "@types/node": "^10.12.0",
31 | "@types/proper-lockfile": "^3.0.0",
32 | "@types/webpack": "^4.4.17",
33 | "@types/webpack-dev-middleware": "^2.0.2",
34 | "@types/webpack-node-externals": "^1.6.3",
35 | "copy-webpack-plugin": "^4.6.0",
36 | "cross-env": "^5.2.0",
37 | "electron": "^3.0.7",
38 | "electron-rebuild": "^1.8.2",
39 | "elm-webpack-loader": "^5.0.0",
40 | "express": "^4.16.4",
41 | "hard-source-webpack-plugin": "^0.13.1",
42 | "html-webpack-plugin": "^3.2.0",
43 | "prettier": "^1.14.3",
44 | "prettier-plugin-elm": "^0.4.1",
45 | "ts-loader": "^5.2.2",
46 | "ts-node": "^7.0.1",
47 | "typescript": "^3.1.3",
48 | "webpack": "^4.23.1",
49 | "webpack-cli": "^3.1.2",
50 | "webpack-dev-middleware": "^3.4.0",
51 | "webpack-node-externals": "^1.7.2"
52 | },
53 | "dependencies": {
54 | "@types/text-encoding": "^0.0.35",
55 | "automerge": "^0.9.2",
56 | "bs58": "^4.0.1",
57 | "commander": "^2.19.0",
58 | "dat-swarm-defaults": "^1.0.1",
59 | "debug": "^4.1.0",
60 | "discovery-cloud-client": "github:inkandswitch/discovery-cloud-client",
61 | "discovery-swarm": "^5.1.2",
62 | "electron-context-menu": "^0.10.1",
63 | "elm": "^0.19.0-bugfix2",
64 | "elm-format": "^0.8.1",
65 | "hypercore": "^6.21.0",
66 | "hypermerge": "github:automerge/hypermerge#fork",
67 | "lodash": "^4.17.11",
68 | "mime-types": "^2.1.21",
69 | "node-elm-compiler": "^5.0.1",
70 | "odiff": "github:inkandswitch/odiff",
71 | "proper-lockfile": "^3.2.0",
72 | "pseudo-worker": "^1.2.0",
73 | "random-access-file": "^2.0.1",
74 | "random-access-memory": "^3.0.0",
75 | "text-encoding": "^0.7.0",
76 | "tiny-worker": "^2.1.2",
77 | "utp-native": "^2.1.3",
78 | "xmlhttprequest": "^1.8.0"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | overrides: [
3 | {
4 | files: "*.{ts,js}",
5 | options: {
6 | semi: false,
7 | trailingComma: "all",
8 | },
9 | },
10 | ],
11 | }
12 |
--------------------------------------------------------------------------------
/src/electron/index.ts:
--------------------------------------------------------------------------------
1 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "1"
2 |
3 | console.log("App starting...")
4 |
5 | import {
6 | app,
7 | shell,
8 | Menu,
9 | BrowserWindow,
10 | MenuItemConstructorOptions,
11 | } from "electron"
12 | import contextMenu from "electron-context-menu"
13 | import path from "path"
14 |
15 | app.on("ready", createWindow)
16 |
17 | app.setAsDefaultProtocolClient("farm")
18 |
19 | // If we are running a non-packaged version of the app
20 | if (process.defaultApp) {
21 | // If we have the path to our app we set the protocol client to launch electron.exe with the path to our app
22 | if (process.argv.length >= 2) {
23 | app.setAsDefaultProtocolClient("farm", process.execPath, [
24 | path.resolve(process.argv[1]),
25 | ])
26 | }
27 | } else {
28 | app.setAsDefaultProtocolClient("farm")
29 | }
30 |
31 | app.on("open-url", (_event, url) => {
32 | getWindow().webContents.send("open-url", url)
33 | })
34 |
35 | contextMenu({})
36 |
37 | function createWindow(): BrowserWindow {
38 | // BrowserWindow.addDevToolsExtension(path.resolve("./dist/hypermerge-devtools"))
39 |
40 | const win = new BrowserWindow({
41 | width: 800,
42 | height: 600,
43 | webPreferences: {
44 | sandbox: false,
45 | nodeIntegration: true,
46 | nodeIntegrationInWorker: true,
47 | nativeWindowOpen: true,
48 | webSecurity: false,
49 | experimentalFeatures: true,
50 | },
51 | })
52 |
53 | win.loadFile("./dist/index.html") // production
54 |
55 | win.webContents.on("will-navigate", (e, url) => {
56 | console.log("Opening externally...", url)
57 | e.preventDefault()
58 | shell.openExternal(url)
59 | })
60 |
61 | const template: MenuItemConstructorOptions[] = [
62 | {
63 | label: "Edit",
64 | submenu: [
65 | { role: "undo" },
66 | { role: "redo" },
67 | { type: "separator" },
68 | { role: "cut" },
69 | { role: "copy" },
70 | { role: "paste" },
71 | { role: "pasteandmatchstyle" },
72 | { role: "delete" },
73 | { role: "selectall" },
74 | ],
75 | },
76 | {
77 | label: "View",
78 | submenu: [
79 | { role: "reload" },
80 | { role: "forcereload" },
81 | { role: "toggledevtools" },
82 | { type: "separator" },
83 | { role: "resetzoom" },
84 | { role: "zoomin" },
85 | { role: "zoomout" },
86 | { type: "separator" },
87 | { role: "togglefullscreen" },
88 | ],
89 | },
90 | {
91 | role: "window",
92 | submenu: [{ role: "minimize" }, { role: "close" }],
93 | },
94 | {
95 | role: "help",
96 | submenu: [
97 | {
98 | label: "Farm on Github",
99 | click() {
100 | shell.openExternal("https://github.com/inkandswitch/farm")
101 | },
102 | },
103 | ],
104 | },
105 | ]
106 |
107 | if (process.platform === "darwin") {
108 | template.unshift({
109 | label: app.getName(),
110 | submenu: [
111 | { role: "about" },
112 | { type: "separator" },
113 | { role: "services", submenu: [] },
114 | { type: "separator" },
115 | { role: "hide" },
116 | { role: "hideothers" },
117 | { role: "unhide" },
118 | { type: "separator" },
119 | { role: "quit" },
120 | ],
121 | })
122 | }
123 |
124 | Menu.setApplicationMenu(Menu.buildFromTemplate(template))
125 |
126 | return win
127 | }
128 |
129 | function getWindow(): BrowserWindow {
130 | return (
131 | BrowserWindow.getFocusedWindow() ||
132 | BrowserWindow.getAllWindows()[0] ||
133 | createWindow()
134 | )
135 | }
136 |
--------------------------------------------------------------------------------
/src/elm/Bot.elm:
--------------------------------------------------------------------------------
1 | module Bot exposing (Flags, InputFlags, Model, Msg(..), Program, create, decodeFlags)
2 |
3 | import Dict exposing (Dict)
4 | import Json.Decode as Json
5 | import Repo
6 |
7 |
8 | type alias Attrs =
9 | Dict String String
10 |
11 |
12 | type alias Flags =
13 | { code : String
14 | , data : String
15 | , config : Dict String String
16 | , all : Attrs
17 | }
18 |
19 |
20 | type alias InputFlags =
21 | { code : String
22 | , data : String
23 | , config : Json.Value
24 | , all : Json.Value
25 | }
26 |
27 |
28 | type alias Model state doc =
29 | { doc : doc
30 | , state : state
31 | , flags : Flags
32 | }
33 |
34 |
35 | type Msg doc msg
36 | = Custom msg
37 | | LoadDoc doc
38 |
39 |
40 | type alias Program state doc msg =
41 | { init : Flags -> ( state, doc, Cmd msg )
42 | , update : msg -> Model state doc -> ( state, doc, Cmd msg )
43 | , onDoc : Model state doc -> ( state, Cmd msg )
44 | , subscriptions : Model state doc -> Sub msg
45 | }
46 |
47 |
48 | sandbox :
49 | { init : ( state, doc )
50 | , onDoc : Model state doc -> state
51 | , update : msg -> Model state doc -> ( state, doc )
52 | }
53 | -> Program state doc msg
54 | sandbox { init, onDoc, update } =
55 | { init = always (init |> withThird Cmd.none)
56 | , update = \msg model -> update msg model |> withThird Cmd.none
57 | , onDoc = \model -> ( onDoc model, Cmd.none )
58 | , subscriptions = always Sub.none
59 | }
60 |
61 |
62 | create : Program state doc msg -> Program state doc msg
63 | create =
64 | identity
65 |
66 |
67 | withThird : c -> ( a, b ) -> ( a, b, c )
68 | withThird c ( a, b ) =
69 | ( a, b, c )
70 |
71 |
72 | decodeFlags : InputFlags -> Flags
73 | decodeFlags fl =
74 | { code = fl.code
75 | , data = fl.data
76 | , all =
77 | fl.all
78 | |> Json.decodeValue attrsDecoder
79 | |> Result.withDefault Dict.empty
80 | }
81 |
82 |
83 | attrsDecoder : Json.Decoder Attrs
84 | attrsDecoder =
85 | Json.dict <|
86 | Json.oneOf
87 | [ Json.string
88 | , Json.succeed ""
89 | ]
90 |
--------------------------------------------------------------------------------
/src/elm/BotHarness.elm:
--------------------------------------------------------------------------------
1 | port module BotHarness exposing (main)
2 |
3 | import Bot exposing (Flags, InputFlags, Msg(..), decodeFlags)
4 | import Repo
5 | import Source as S exposing (Doc, State, bot)
6 |
7 |
8 | port initDoc : Doc -> Cmd msg
9 |
10 |
11 | port saveDoc : { doc : Doc, prevDoc : Doc } -> Cmd msg
12 |
13 |
14 | port loadDoc : (Doc -> msg) -> Sub msg
15 |
16 |
17 | type alias InputMsg =
18 | { doc : Maybe Doc
19 | }
20 |
21 |
22 | type alias Model =
23 | Bot.Model State Doc
24 |
25 |
26 | init : InputFlags -> ( Model, Cmd Msg )
27 | init iFlags =
28 | let
29 | flags =
30 | decodeFlags iFlags
31 |
32 | ( state, doc, cmd ) =
33 | bot.init flags
34 | in
35 | ( { doc = doc
36 | , state = state
37 | , flags = flags
38 | }
39 | , Cmd.batch
40 | [ cmd |> Cmd.map Custom
41 | , initDoc doc
42 | ]
43 | )
44 |
45 |
46 | type alias Msg =
47 | Bot.Msg Doc S.Msg
48 |
49 |
50 | update : Msg -> Model -> ( Model, Cmd Msg )
51 | update msg model =
52 | case msg of
53 | Custom sMsg ->
54 | let
55 | ( state, doc, cmd ) =
56 | bot.update sMsg model
57 | in
58 | ( { model | state = state, doc = doc }
59 | , Cmd.batch
60 | [ cmd |> Cmd.map Custom
61 | , saveIfChanged model.doc doc
62 | ]
63 | )
64 |
65 | LoadDoc doc ->
66 | let
67 | newModel =
68 | { model | doc = doc }
69 |
70 | ( state, cmd ) =
71 | bot.onDoc newModel
72 | in
73 | ( newModel, cmd |> Cmd.map Custom )
74 |
75 |
76 | subscriptions : Model -> Sub Msg
77 | subscriptions model =
78 | Sub.batch
79 | [ loadDoc LoadDoc
80 | , bot.subscriptions model |> Sub.map Custom
81 | ]
82 |
83 |
84 | main : Platform.Program InputFlags Model Msg
85 | main =
86 | Platform.worker
87 | { init = init
88 | , update = update
89 | , subscriptions = subscriptions
90 | }
91 |
92 |
93 | saveIfChanged : Doc -> Doc -> Cmd Msg
94 | saveIfChanged prevDoc doc =
95 | if doc /= prevDoc then
96 | saveDoc { doc = doc, prevDoc = prevDoc }
97 |
98 | else
99 | Cmd.none
100 |
--------------------------------------------------------------------------------
/src/elm/Clipboard.elm:
--------------------------------------------------------------------------------
1 | port module Clipboard exposing
2 | ( copy
3 | , pasted
4 | )
5 |
6 | {-| This module allows the copying of a to the user's
7 | clipboard.
8 |
9 |
10 | # Writing to clipboard
11 |
12 | @docs copy
13 |
14 | -}
15 |
16 | import Gizmo
17 | import Json.Decode as D
18 |
19 |
20 | port pasted : (D.Value -> msg) -> Sub msg
21 |
22 |
23 | {-| Copy the given String to the user's clipboard.
24 | -}
25 | copy : String -> Cmd msg
26 | copy str =
27 | Gizmo.command ( "Copy", str )
28 |
--------------------------------------------------------------------------------
/src/elm/Colors.elm:
--------------------------------------------------------------------------------
1 | module Colors exposing (
2 | primary
3 | , darkerPrimary
4 | , farmGreen
5 | , darkerFarmGreen
6 | , hotPink
7 | , darkerHotPink
8 | , blueBlack
9 | , darkGrey
10 | , darkerGrey
11 | )
12 |
13 | primary : String
14 | primary =
15 | farmGreen
16 |
17 | darkerPrimary : String
18 | darkerPrimary =
19 | darkerFarmGreen
20 |
21 | farmGreen: String
22 | farmGreen =
23 | "#02b200"
24 |
25 | darkerFarmGreen : String
26 | darkerFarmGreen =
27 | "#039400"
28 |
29 | hotPink : String
30 | hotPink =
31 | "#ff69b4"
32 |
33 |
34 | darkerHotPink : String
35 | darkerHotPink =
36 | "#ff1a8c"
37 |
38 |
39 | blueBlack : String
40 | blueBlack =
41 | "#102542"
42 |
43 | darkerGrey : String
44 | darkerGrey =
45 | "#333"
46 |
47 | darkGrey : String
48 | darkGrey =
49 | "#777"
--------------------------------------------------------------------------------
/src/elm/DataTransfer.elm:
--------------------------------------------------------------------------------
1 | module DataTransfer exposing (elmFileDecoder, toFileType)
2 |
3 | import File exposing (File)
4 | import Json.Decode as Json exposing (Decoder)
5 |
6 |
7 | type FileType
8 | = Image File
9 | | FarmUrl File
10 | | DocumentUrl File
11 | | File File
12 |
13 |
14 | toFileType : File -> FileType
15 | toFileType file =
16 | case String.split "/" (File.mime file) of
17 | ["image", _] ->
18 | Image file
19 | ["application", "farm-url"] ->
20 | FarmUrl file
21 | ["application", "hypermerge-url"] ->
22 | DocumentUrl file
23 | _ ->
24 | File file
25 |
26 |
27 |
28 | elmFileDecoder : Decoder (List File)
29 | elmFileDecoder =
30 | Json.field "elmFiles" (Json.list File.decoder)
--------------------------------------------------------------------------------
/src/elm/Doc.elm:
--------------------------------------------------------------------------------
1 | module Doc exposing (Doc, RawDoc, asString, debug, decode, decoder, empty, encode, get, rawEmpty)
2 |
3 | import Dict exposing (Dict)
4 | import Json.Decode as D
5 | import Json.Encode as E
6 | import Value exposing (Value)
7 |
8 |
9 | type alias Doc =
10 | Dict String Value
11 |
12 |
13 | type alias RawDoc =
14 | D.Value
15 |
16 |
17 | empty : Doc
18 | empty =
19 | Dict.empty
20 |
21 |
22 | rawEmpty : RawDoc
23 | rawEmpty =
24 | E.null
25 |
26 |
27 | get : String -> Doc -> Value
28 | get k =
29 | Dict.get k >> Maybe.withDefault Value.Null
30 |
31 |
32 | rawGet : String -> RawDoc -> Value
33 | rawGet k =
34 | decode >> get k
35 |
36 |
37 |
38 | -- at : List String -> Doc -> Value
39 | -- at keys doc =
40 | -- Dict.get k >> Maybe.withDefault Null
41 | -- atV : List String -> Value -> Value
42 | -- atV path val =
43 | -- case path of
44 | -- [] ->
45 | -- val
46 | -- k :: rest ->
47 | -- case val of
48 | -- Dict dict ->
49 | -- Debug
50 |
51 |
52 | debug : Doc -> Doc
53 | debug doc =
54 | Debug.log (asString doc) doc
55 |
56 |
57 | asString : Doc -> String
58 | asString doc =
59 | Value.Dict doc |> Value.toString
60 |
61 |
62 | encode : Doc -> RawDoc
63 | encode doc =
64 | Value.Dict doc |> Value.encode
65 |
66 |
67 | decode : RawDoc -> Doc
68 | decode =
69 | D.decodeValue decoder >> Result.withDefault empty
70 |
71 |
72 | decoder : D.Decoder Doc
73 | decoder =
74 | D.dict Value.decoder
--------------------------------------------------------------------------------
/src/elm/Draggable.elm:
--------------------------------------------------------------------------------
1 | module Draggable exposing (draggable)
2 |
3 | import Html.Styled as Html exposing (..)
4 | import Html.Styled.Attributes as Attrs
5 |
6 |
7 | draggable : (String, String) -> List (Html msg) -> Html msg
8 | draggable (datatype, data) children =
9 | Html.node "farm-draggable"
10 | [ Attrs.draggable "true"
11 | , Attrs.attribute "dragdata" data
12 | , Attrs.attribute "dragtype" datatype
13 | ]
14 | children
--------------------------------------------------------------------------------
/src/elm/Extra/Array.elm:
--------------------------------------------------------------------------------
1 | module Extra.Array exposing (remove, update)
2 |
3 | import Array exposing (..)
4 |
5 |
6 | update : Int -> (a -> a) -> Array a -> Array a
7 | update i fn arr =
8 | case get i arr of
9 | Nothing ->
10 | arr
11 |
12 | Just a ->
13 | set i (fn a) arr
14 |
15 |
16 | remove : Int -> Array a -> Array a
17 | remove n arr =
18 | append
19 | (slice 0 n arr)
20 | (slice (n + 1) (Array.length arr) arr)
21 |
--------------------------------------------------------------------------------
/src/elm/Extra/List.elm:
--------------------------------------------------------------------------------
1 | module Extra.List exposing (consTo, init, last)
2 |
3 | import List exposing (..)
4 |
5 |
6 | {-| Extract the last element of a list.
7 | last [ 1, 2, 3 ]
8 | --> Just 3
9 | last []
10 | --> Nothing
11 | -}
12 | last : List a -> Maybe a
13 | last items =
14 | case items of
15 | [] ->
16 | Nothing
17 |
18 | [ x ] ->
19 | Just x
20 |
21 | _ :: rest ->
22 | last rest
23 |
24 |
25 | {-| Return all elements of the list except the last one.
26 | init [ 1, 2, 3 ]
27 | --> Just [ 1, 2 ]
28 | init []
29 | --> Nothing
30 | -}
31 | init : List a -> Maybe (List a)
32 | init items =
33 | case items of
34 | [] ->
35 | Nothing
36 |
37 | nonEmptyList ->
38 | nonEmptyList
39 | |> List.reverse
40 | |> List.tail
41 | |> Maybe.map List.reverse
42 |
43 |
44 | consTo : List a -> a -> List a
45 | consTo items item =
46 | item :: items
47 |
--------------------------------------------------------------------------------
/src/elm/FarmUrl.elm:
--------------------------------------------------------------------------------
1 | module FarmUrl exposing (create, fromIds, parse, parseIds)
2 |
3 | import Extra.List as List
4 | import Link
5 | import UriParser exposing (Uri)
6 |
7 |
8 | create : { a | code : String, data : String } -> Result String String
9 | create { code, data } =
10 | Result.map2 fromIds
11 | (Link.getId code)
12 | (Link.getId data)
13 |
14 |
15 | parse : String -> Result String { code : String, data : String }
16 | parse url =
17 | parseIds url
18 | |> Result.map
19 | (\{ codeId, dataId } ->
20 | { code = Link.create codeId
21 | , data = Link.create dataId
22 | }
23 | )
24 |
25 |
26 | parseIds : String -> Result String { codeId : String, dataId : String }
27 | parseIds url =
28 | UriParser.parse url
29 | |> Result.andThen checkScheme
30 | |> Result.andThen extractIds
31 |
32 |
33 | extractIds : Uri -> Result String { codeId : String, dataId : String }
34 | extractIds uri =
35 | Maybe.map2 idPair
36 | (extractCode uri)
37 | (List.last uri.path)
38 | |> Result.fromMaybe "An id is missing"
39 |
40 |
41 | extractCode : Uri -> Maybe String
42 | extractCode uri =
43 | uri.authority
44 | |> Maybe.map
45 | (List.consTo (List.init uri.path |> Maybe.withDefault []))
46 | |> Maybe.map (String.join "/")
47 |
48 |
49 | idPair : String -> String -> { codeId : String, dataId : String }
50 | idPair codeId dataId =
51 | { codeId = codeId, dataId = dataId }
52 |
53 |
54 | fromIds : String -> String -> String
55 | fromIds codeId dataId =
56 | "farm://" ++ codeId ++ "/" ++ dataId
57 |
58 |
59 | checkScheme : Uri -> Result String Uri
60 | checkScheme uri =
61 | case uri.scheme of
62 | "farm" ->
63 | Ok uri
64 |
65 | "realm" ->
66 | Ok uri
67 |
68 | _ ->
69 | Err "scheme must be 'farm'"
70 |
--------------------------------------------------------------------------------
/src/elm/Gizmo.elm:
--------------------------------------------------------------------------------
1 | port module Gizmo exposing
2 | ( Attrs
3 | , EmitDetail
4 | , Flags
5 | , InputFlags
6 | , Model
7 | , Msg(..)
8 | , Program
9 | , attr
10 | , command
11 | , decodeFlags
12 | , decodedMsgs
13 | , element
14 | , emit
15 | , onEmit
16 | , portal
17 | , portalTo
18 | , render
19 | , renderWindow
20 | , renderWith
21 | , sandbox
22 | , send
23 | )
24 |
25 | import Dict exposing (Dict)
26 | import Doc
27 | import Html.Styled as Html exposing (Html)
28 | import Html.Styled.Attributes as Attr
29 | import Html.Styled.Events as Events
30 | import Json.Decode as Json
31 | import Json.Encode as E
32 | import Repo exposing (Url)
33 | import Task
34 |
35 |
36 | port command : ( String, String ) -> Cmd msg
37 |
38 |
39 | port msgs : (Json.Value -> msg) -> Sub msg
40 |
41 |
42 | port emitted : ( String, E.Value ) -> Cmd msg
43 |
44 |
45 | decodedMsgs : (String -> Msg doc msg) -> Sub (Msg doc msg)
46 | decodedMsgs errMsg =
47 | msgs (Json.decodeValue msgDecoder)
48 | |> Sub.map
49 | (\res ->
50 | case res of
51 | Ok v ->
52 | v
53 |
54 | Err x ->
55 | errMsg (Json.errorToString x)
56 | )
57 |
58 |
59 | emit : String -> E.Value -> Cmd msg
60 | emit name value =
61 | emitted ( name, value )
62 |
63 |
64 | type alias EmitDetail =
65 | { name : String
66 | , value : E.Value
67 | , code : Url
68 | , data : Url
69 | }
70 |
71 |
72 | onEmit : String -> (EmitDetail -> msg) -> Html.Attribute msg
73 | onEmit name mkMsg =
74 | Events.stopPropagationOn name
75 | (emitDecoder
76 | |> Json.map mkMsg
77 | |> Json.map (\msg -> ( msg, True ))
78 | )
79 |
80 |
81 | emitDecoder : Json.Decoder EmitDetail
82 | emitDecoder =
83 | Json.map4 EmitDetail
84 | (Json.at [ "detail", "name" ] Json.string)
85 | (Json.at [ "detail", "value" ] Json.value)
86 | (Json.at [ "detail", "code" ] Json.string)
87 | (Json.at [ "detail", "data" ] Json.string)
88 |
89 |
90 | type alias Attrs =
91 | Dict String String
92 |
93 |
94 | type alias InputFlags =
95 | { code : Url
96 | , data : Url
97 | , self : Url
98 | , config : Json.Value
99 | , doc : Doc.RawDoc
100 | , all : Json.Value
101 | }
102 |
103 |
104 | type alias Flags =
105 | { code : Url
106 | , data : Url
107 | , self : Url
108 | , config : Dict String String
109 | , doc : Doc.Doc
110 | , rawDoc : Doc.RawDoc
111 | , all : Attrs
112 | }
113 |
114 |
115 | type alias Model state doc =
116 | { isMounted : Bool
117 | , doc : doc
118 | , state : state
119 | , flags : Flags
120 | }
121 |
122 |
123 | type Msg doc msg
124 | = NoOp
125 | | Custom msg
126 | | Unmount
127 | | LoadDoc doc
128 |
129 |
130 | type alias Program state doc msg =
131 | { init : Flags -> ( state, doc, Cmd msg )
132 | , update : msg -> Model state doc -> ( state, doc, Cmd msg )
133 | , view : Model state doc -> Html msg
134 | , subscriptions : Model state doc -> Sub msg
135 | }
136 |
137 |
138 | sandbox :
139 | { init : ( state, doc )
140 | , view : Model state doc -> Html msg
141 | , update : msg -> Model state doc -> ( state, doc )
142 | }
143 | -> Program state doc msg
144 | sandbox { init, view, update } =
145 | { init = always (init |> withThird Cmd.none)
146 | , update = \msg model -> update msg model |> withThird Cmd.none
147 | , view = view
148 | , subscriptions = always Sub.none
149 | }
150 |
151 |
152 | element : Program state doc msg -> Program state doc msg
153 | element =
154 | identity
155 |
156 |
157 | withThird : c -> ( a, b ) -> ( a, b, c )
158 | withThird c ( a, b ) =
159 | ( a, b, c )
160 |
161 |
162 | render : Url -> Url -> Html msg
163 | render =
164 | renderWith []
165 |
166 |
167 | renderWith : List (Html.Attribute msg) -> Url -> Url -> Html msg
168 | renderWith attrs code data =
169 | Html.node "farm-ui"
170 | (attr "code" code
171 | :: attr "data" data
172 | :: attrs
173 | )
174 | []
175 |
176 |
177 | renderWindow : Url -> Url -> msg -> Html msg
178 | renderWindow code data closeMsg =
179 | Html.node "farm-window"
180 | [ attr "code" code
181 | , attr "data" data
182 | , Events.on "windowclose" (Json.succeed closeMsg)
183 | ]
184 | []
185 |
186 |
187 | portal : Html.Attribute msg
188 | portal =
189 | portalTo "body"
190 |
191 |
192 | {-| Render a gizmo as a child of an element specified by a CSS selector.
193 | Note: Uses `document.querySelector` under the hood.
194 | -}
195 | portalTo : String -> Html.Attribute msg
196 | portalTo =
197 | attr "portaltarget"
198 |
199 |
200 | attr : String -> String -> Html.Attribute msg
201 | attr =
202 | Attr.attribute
203 |
204 |
205 | decodeFlags : InputFlags -> Flags
206 | decodeFlags fl =
207 | { code = fl.code
208 | , data = fl.data
209 | , self = fl.self
210 | , config =
211 | fl.config
212 | |> Json.decodeValue (Json.dict Json.string)
213 | |> Result.withDefault Dict.empty
214 | , doc = fl.doc |> Doc.decode
215 | , rawDoc = fl.doc
216 | , all =
217 | fl.all
218 | |> Json.decodeValue attrsDecoder
219 | |> Result.withDefault Dict.empty
220 | }
221 |
222 |
223 | attrsDecoder : Json.Decoder Attrs
224 | attrsDecoder =
225 | Json.dict <|
226 | Json.oneOf
227 | [ Json.string
228 | , Json.succeed ""
229 | ]
230 |
231 |
232 | msgDecoder : Json.Decoder (Msg doc msg)
233 | msgDecoder =
234 | Json.field "t" Json.string
235 | |> Json.andThen
236 | (\t ->
237 | case t of
238 | "Unmount" ->
239 | Json.succeed Unmount
240 |
241 | _ ->
242 | Json.fail "Not a valid incoming Msg."
243 | )
244 |
245 |
246 | send : msg -> Cmd msg
247 | send =
248 | Task.succeed >> Task.perform identity
249 |
--------------------------------------------------------------------------------
/src/elm/Harness.elm:
--------------------------------------------------------------------------------
1 | port module Harness exposing (main)
2 |
3 | import Browser
4 | import Gizmo exposing (Flags, InputFlags, Msg(..), decodeFlags)
5 | import Html.Styled as Html exposing (Html)
6 | import Repo
7 | import Source as S exposing (Doc, State, gizmo)
8 |
9 |
10 | port initDoc : Doc -> Cmd msg
11 |
12 |
13 | port saveDoc : { doc : Doc, prevDoc : Doc } -> Cmd msg
14 |
15 |
16 | port loadDoc : (Doc -> msg) -> Sub msg
17 |
18 |
19 | type alias InputMsg =
20 | { doc : Maybe Doc
21 | }
22 |
23 |
24 | type alias Model =
25 | Gizmo.Model State Doc
26 |
27 |
28 | init : InputFlags -> ( Model, Cmd Msg )
29 | init iFlags =
30 | let
31 | flags =
32 | decodeFlags iFlags
33 |
34 | ( state, doc, cmd ) =
35 | gizmo.init flags
36 | in
37 | ( { isMounted = True
38 | , doc = doc
39 | , state = state
40 | , flags = flags
41 | }
42 | , Cmd.batch
43 | [ cmd |> Cmd.map Custom
44 | , initDoc doc
45 | ]
46 | )
47 |
48 |
49 | type alias Msg =
50 | Gizmo.Msg Doc S.Msg
51 |
52 |
53 | update : Msg -> Model -> ( Model, Cmd Msg )
54 | update msg model =
55 | case msg of
56 | NoOp ->
57 | ( model, Cmd.none )
58 |
59 | Custom sMsg ->
60 | let
61 | ( state, doc, cmd ) =
62 | gizmo.update sMsg model
63 | in
64 | ( { model | state = state, doc = doc }
65 | , Cmd.batch
66 | [ cmd |> Cmd.map Custom
67 | , saveIfChanged model.doc doc
68 | ]
69 | )
70 |
71 | LoadDoc doc ->
72 | ( { model | doc = doc }, Cmd.none )
73 |
74 | Unmount ->
75 | ( { model | isMounted = False }, Cmd.none )
76 |
77 |
78 | subscriptions : Model -> Sub Msg
79 | subscriptions model =
80 | Sub.batch
81 | [ Gizmo.decodedMsgs (Debug.log "Invalid msg" >> always NoOp)
82 | , if model.isMounted then
83 | Sub.batch
84 | [ loadDoc LoadDoc
85 | , gizmo.subscriptions model |> Sub.map Custom
86 | ]
87 |
88 | else
89 | Sub.none
90 | ]
91 |
92 |
93 | view : Model -> Html Msg
94 | view model =
95 | if model.isMounted then
96 | gizmo.view model
97 | |> Html.map Custom
98 |
99 | else
100 | Html.text ""
101 |
102 |
103 | main : Platform.Program InputFlags Model Msg
104 | main =
105 | Browser.element
106 | { init = init
107 | , update = update
108 | , view = Html.toUnstyled << view
109 | , subscriptions = subscriptions
110 | }
111 |
112 |
113 | saveIfChanged : Doc -> Doc -> Cmd Msg
114 | saveIfChanged prevDoc doc =
115 | if doc /= prevDoc then
116 | saveDoc { doc = doc, prevDoc = prevDoc }
117 |
118 | else
119 | Cmd.none
120 |
--------------------------------------------------------------------------------
/src/elm/History.elm:
--------------------------------------------------------------------------------
1 | module History exposing (History, empty, current, hasSeen, hasBack, hasForward, push, replace, back, forward)
2 |
3 | import List exposing (length, isEmpty)
4 | import ListSet exposing (ListSet)
5 |
6 | {- NOTE: Allows going back to the empty state -}
7 |
8 | type alias History a =
9 | { backward : List a
10 | , forward : List a
11 | , seen : ListSet a
12 | }
13 |
14 |
15 | empty : History a
16 | empty =
17 | { backward = []
18 | , forward = []
19 | , seen = ListSet.empty
20 | }
21 |
22 |
23 | current : History a -> Maybe a
24 | current =
25 | .backward >> List.head
26 |
27 |
28 | hasBack : History a -> Bool
29 | hasBack =
30 | .backward >> ((not) << isEmpty)
31 |
32 |
33 | hasForward : History a -> Bool
34 | hasForward =
35 | .forward >> ((not) << isEmpty)
36 |
37 |
38 | hasSeen : History a -> Bool
39 | hasSeen =
40 | .seen >> ((not) << isEmpty)
41 |
42 |
43 | push : a -> History a -> History a
44 | push val history =
45 | { history
46 | | backward = val :: history.backward
47 | , forward = []
48 | , seen = (ListSet.insert val history.seen)
49 | }
50 |
51 |
52 | replace : a -> History a -> History a
53 | replace val history =
54 | { history | backward = val :: (Maybe.withDefault [] <| List.tail history.backward) }
55 |
56 |
57 | back : History a -> History a
58 | back history =
59 | case List.head history.backward of
60 | Just prevCurrent ->
61 | { history
62 | | backward = tailOrEmpty history.backward
63 | , forward = prevCurrent :: history.forward
64 | }
65 | Nothing ->
66 | history
67 |
68 |
69 | forward : History a -> History a
70 | forward history =
71 | case List.head history.forward of
72 | Just next ->
73 | { history
74 | | backward = next :: history.backward
75 | , forward = tailOrEmpty history.forward
76 | }
77 | Nothing ->
78 | history
79 |
80 |
81 | tailOrEmpty : List a -> List a
82 | tailOrEmpty list =
83 | Maybe.withDefault [] (List.tail list)
--------------------------------------------------------------------------------
/src/elm/IO.elm:
--------------------------------------------------------------------------------
1 | port module IO exposing (log, logValue, output)
2 |
3 | import Json.Encode as Json exposing (Value)
4 |
5 |
6 | port output : List Value -> Cmd msg
7 |
8 |
9 | port input : (List String -> msg) -> Sub msg
10 |
11 |
12 | log : String -> Cmd msg
13 | log =
14 | Json.string >> logValue
15 |
16 |
17 | logValue : Value -> Cmd msg
18 | logValue val =
19 | output [ val ]
20 |
--------------------------------------------------------------------------------
/src/elm/Link.elm:
--------------------------------------------------------------------------------
1 | module Link exposing (create, getId)
2 |
3 | import UriParser exposing (Uri, parse)
4 |
5 |
6 | create : String -> String
7 | create id =
8 | "hypermerge:/" ++ id
9 |
10 |
11 | getId : String -> Result String String
12 | getId str =
13 | parse str
14 | |> Result.andThen checkScheme
15 | |> Result.map extractPath
16 |
17 |
18 | extractPath : Uri -> String
19 | extractPath =
20 | .path
21 | >> String.join "/"
22 |
23 |
24 | extractId : Uri -> Result String String
25 | extractId =
26 | .path
27 | >> List.head
28 | >> Result.fromMaybe "link has no id"
29 |
30 |
31 | checkScheme : Uri -> Result String Uri
32 | checkScheme uri =
33 | case uri.scheme of
34 | "hypermerge" ->
35 | Ok uri
36 |
37 | _ ->
38 | Err "scheme must be 'hypermerge'"
39 |
40 |
41 | checkId : String -> Result String String
42 | checkId str =
43 | if String.length str < 10 then
44 | Err "not a valid ID"
45 |
46 | else
47 | Ok str
48 |
--------------------------------------------------------------------------------
/src/elm/ListSet.elm:
--------------------------------------------------------------------------------
1 | module ListSet exposing (ListSet, empty, insert, remove, member)
2 |
3 | import List
4 |
5 |
6 | type alias ListSet a =
7 | List a
8 |
9 |
10 | empty : ListSet a
11 | empty =
12 | []
13 |
14 |
15 | insert : a -> ListSet a -> ListSet a
16 | insert val list =
17 | if List.member val list then list else (val :: list)
18 |
19 |
20 | remove : a -> ListSet a -> ListSet a
21 | remove val =
22 | (/=) val |> List.filter
23 |
24 |
25 | member : a -> ListSet a -> Bool
26 | member =
27 | List.member
--------------------------------------------------------------------------------
/src/elm/Navigation.elm:
--------------------------------------------------------------------------------
1 | port module Navigation exposing (navigateToUrl, currentUrl)
2 |
3 |
4 | port navigateToUrl : String -> Cmd msg
5 |
6 |
7 | port navigatedUrls : (String -> msg) -> Sub msg
8 |
9 |
10 | currentUrl : (String -> msg) -> Sub msg
11 | currentUrl =
12 | navigatedUrls
--------------------------------------------------------------------------------
/src/elm/Notification.elm:
--------------------------------------------------------------------------------
1 | port module Notification exposing (Notification, onClick, send)
2 |
3 |
4 | type alias Notification =
5 | { ref : String
6 | , title : String
7 | , body : String
8 | }
9 |
10 |
11 | port sentNotifications : Notification -> Cmd msg
12 |
13 |
14 | port notificationClicked : (String -> msg) -> Sub msg
15 |
16 |
17 | send : Notification -> Cmd msg
18 | send =
19 | sentNotifications
20 |
21 |
22 | onClick : (String -> msg) -> Sub msg
23 | onClick =
24 | notificationClicked
25 |
--------------------------------------------------------------------------------
/src/elm/RealmUrl.elm:
--------------------------------------------------------------------------------
1 | module RealmUrl exposing (create, fromIds, parse, parseIds)
2 |
3 | import FarmUrl
4 |
5 |
6 | create =
7 | FarmUrl.create << Debug.log "RealmUrl is deprecated. Use FarmUrl"
8 |
9 |
10 | fromIds =
11 | FarmUrl.fromIds << Debug.log "RealmUrl is deprecated. Use FarmUrl"
12 |
13 |
14 | parse =
15 | FarmUrl.parse << Debug.log "RealmUrl is deprecated. Use FarmUrl"
16 |
17 |
18 | parseIds =
19 | FarmUrl.parseIds << Debug.log "RealmUrl is deprecated. Use FarmUrl"
20 |
--------------------------------------------------------------------------------
/src/elm/Repo.elm:
--------------------------------------------------------------------------------
1 | port module Repo exposing (Props, Ref, Url, clone, create, createWithProps, created, docs, fork, open, rawDocs)
2 |
3 | import Doc exposing (Doc, RawDoc)
4 | import Json.Decode as D
5 | import Json.Encode as E
6 | import Result
7 | import Task
8 |
9 |
10 | type alias Url =
11 | String
12 |
13 |
14 | type alias Ref =
15 | String
16 |
17 |
18 | type alias Props =
19 | List ( String, E.Value )
20 |
21 |
22 | port created : (( Ref, List Url ) -> msg) -> Sub msg
23 |
24 |
25 | port rawDocs : (( Url, RawDoc ) -> msg) -> Sub msg
26 |
27 |
28 | docs : (( Url, Doc ) -> msg) -> Sub msg
29 | docs mkMsg =
30 | rawDocs (Tuple.mapSecond Doc.decode)
31 | |> Sub.map mkMsg
32 |
33 |
34 | create : Ref -> Int -> Cmd msg
35 | create ref n =
36 | createWithProps ref n []
37 |
38 |
39 | createWithProps : Ref -> Int -> Props -> Cmd msg
40 | createWithProps ref n props =
41 | send <| Create ref n props
42 |
43 |
44 | open : Url -> Cmd msg
45 | open url =
46 | send <| Open url
47 |
48 |
49 | clone : Ref -> Url -> Cmd msg
50 | clone ref url =
51 | send <| Clone ref url
52 |
53 |
54 | fork : Ref -> Url -> Cmd msg
55 | fork ref url =
56 | send <| Fork ref url
57 |
58 |
59 |
60 | -- type alias Model msg =
61 | -- { createQ : List (List String -> msg)
62 | -- }
63 | -- type Msg
64 | -- = Created (List String)
65 | -- | Error String
66 |
67 |
68 | port repoOut : E.Value -> Cmd msg
69 |
70 |
71 |
72 | -- update : Msg -> Model msg -> ( Model msg, Cmd msg )
73 | -- update msg model =
74 | -- case msg of
75 | -- Created ids ->
76 | -- case model.createQ of
77 | -- mkMsg :: createQ ->
78 | -- ( { model | createQ = createQ }, mkMsg ids |> Task.succeed |> Task.perform identity )
79 | -- Outgoing Messages
80 |
81 |
82 | type OutMsg
83 | = Create Ref Int Props -- String ref and number of docs to create
84 | | Clone Ref Url -- String ref and url of document
85 | | Fork Ref Url -- String ref and url of document to fork
86 | | Open Url
87 |
88 |
89 | encodeOut : OutMsg -> E.Value
90 | encodeOut msg =
91 | case msg of
92 | Create ref n props ->
93 | E.object
94 | [ ( "t", E.string "Create" )
95 | , ( "ref", E.string ref )
96 | , ( "n", E.int n )
97 | , ( "p", E.object props )
98 | ]
99 |
100 | Fork ref url ->
101 | E.object
102 | [ ( "t", E.string "Fork" )
103 | , ( "ref", E.string ref )
104 | , ( "url", E.string url )
105 | ]
106 |
107 | Clone ref url ->
108 | E.object
109 | [ ( "t", E.string "Clone" )
110 | , ( "ref", E.string ref )
111 | , ( "url", E.string url )
112 | ]
113 |
114 | Open url ->
115 | E.object
116 | [ ( "t", E.string "Open" )
117 | , ( "url", E.string url )
118 | ]
119 |
120 |
121 | send : OutMsg -> Cmd msg
122 | send =
123 | encodeOut >> repoOut
124 |
125 |
126 |
127 | -- Incoming Messages
128 | -- port repoIn : (D.Value -> msg) -> Sub msg
129 | -- msgDecoder : D.Decoder Msg
130 | -- msgDecoder =
131 | -- D.field "t" D.string
132 | -- |> D.andThen typeDecoder
133 | -- typeDecoder : String -> D.Decoder Msg
134 | -- typeDecoder t =
135 | -- case t of
136 | -- "Created" ->
137 | -- D.map Created
138 | -- (D.field "ids" (D.list D.string))
139 | -- _ ->
140 | -- D.fail "Not a valid Repo.Msg type."
141 | -- decodeMsg : D.Value -> Msg
142 | -- decodeMsg val =
143 | -- case D.decodeValue msgDecoder val of
144 | -- Err err ->
145 | -- Error (D.errorToString err)
146 | -- Ok msg ->
147 | -- msg
148 | -- incoming : Sub Msg
149 | -- incoming =
150 | -- repoIn decodeMsg
151 |
--------------------------------------------------------------------------------
/src/elm/Tooltip.elm:
--------------------------------------------------------------------------------
1 | module Tooltip exposing (Position(..), tooltip)
2 |
3 | import Css exposing (..)
4 |
5 | type Position
6 | = Top
7 | | TopLeft
8 | | TopRight
9 | | Right
10 | | Bottom
11 | | BottomLeft
12 | | BottomRight
13 | | Left
14 |
15 | defaultStyle : List Style
16 | defaultStyle =
17 | [ fontSize (Css.em 0.75)
18 | , color (hex "fff")
19 | , backgroundColor (hex "333")
20 | , borderRadius (px 5)
21 | , padding (px 5)
22 | ]
23 |
24 |
25 | tooltip : Position -> String -> List Style
26 | tooltip pos tip =
27 | [ position relative
28 | , after
29 | ([ display none
30 | , position absolute
31 | , opacity (num 1)
32 | , zIndex (int 999999)
33 | , property "content" (Debug.toString tip)
34 | , textAlign center
35 | , property "word-wrap" "break-word"
36 | , whiteSpace pre
37 | , pointerEvents none
38 | ]
39 | ++ styleForPosition pos
40 | ++ defaultStyle
41 | )
42 | , hover
43 | [ after
44 | [ display inlineBlock
45 | , textDecoration none
46 | ]
47 | ]
48 | ]
49 |
50 | styleForPosition : Position -> List Style
51 | styleForPosition position =
52 | case position of
53 | Top ->
54 | [ bottom (pct 100)
55 | ]
56 | TopRight ->
57 | [ bottom (pct 100)
58 | , left (pct 50)
59 | , right auto
60 | ]
61 | Right ->
62 | [ left (pct 100)
63 | ]
64 | BottomRight ->
65 | [ top (pct 100)
66 | , left (pct 50)
67 | , right auto
68 | ]
69 | Bottom ->
70 | [ top (pct 100)
71 | , left (px 0)
72 | ]
73 | BottomLeft ->
74 | [ top (pct 100)
75 | , right (pct 50)
76 | , left auto
77 | ]
78 | Left ->
79 | [ right (pct 100)
80 | ]
81 | TopLeft ->
82 | [ top (pct 100)
83 | , right (pct 50)
84 | , left auto
85 | ]
86 |
87 |
--------------------------------------------------------------------------------
/src/elm/Uri.elm:
--------------------------------------------------------------------------------
1 | module Uri exposing (create, parse, resolve)
2 |
3 | import UriParser exposing (Uri)
4 |
5 |
6 | parse : String -> Result String Uri
7 | parse =
8 | UriParser.parse
9 |
10 |
11 | {-| Not quite accurate. We should be parsing both arguments
12 | and returning a Result.
13 | -}
14 | resolve : String -> String -> String
15 | resolve base other =
16 | case parse other of
17 | Ok _ ->
18 | other
19 |
20 | Err _ ->
21 | base ++ "/" ++ other
22 |
23 |
24 | create : Uri -> String
25 | create uri =
26 | case uri.authority of
27 | Just authority ->
28 | uri.scheme ++ "//" ++ authority ++ "/" ++ (uri.path |> String.join "/")
29 |
30 | Nothing ->
31 | uri.scheme ++ "/" ++ (uri.path |> String.join "/")
32 |
--------------------------------------------------------------------------------
/src/elm/UriParser.elm:
--------------------------------------------------------------------------------
1 | module UriParser exposing (Uri, parse)
2 |
3 | import Parser exposing (..)
4 |
5 |
6 |
7 | -- hypermerge:/abc123/Source.elm
8 |
9 |
10 | type alias Uri =
11 | { scheme : String
12 | , authority : Maybe String
13 | , path : List String
14 | }
15 |
16 |
17 | parse : String -> Result String Uri
18 | parse =
19 | run uri >> Result.mapError deadEndsToString
20 |
21 |
22 | uri : Parser Uri
23 | uri =
24 | succeed Uri
25 | |= getChompedString (chompUntil ":")
26 | |. symbol ":"
27 | |= authority
28 | |= path
29 |
30 |
31 | authority : Parser (Maybe String)
32 | authority =
33 | oneOf
34 | [ succeed Just
35 | |. symbol "//"
36 | |= getChompedString (chompUntilEndOr "/")
37 | , succeed Nothing
38 | ]
39 |
40 |
41 | path : Parser (List String)
42 | path =
43 | loop [] pathHelp
44 |
45 |
46 | pathHelp : List String -> Parser (Step (List String) (List String))
47 | pathHelp revPath =
48 | oneOf
49 | [ succeed (\k -> Loop (k :: revPath))
50 | |. symbol "/"
51 | |= key
52 | , succeed ()
53 | |> map (\_ -> Done (List.reverse revPath))
54 | ]
55 |
56 |
57 | key : Parser String
58 | key =
59 | getChompedString <|
60 | succeed ()
61 | |. chompUntilEndOr "/"
62 |
63 |
64 | deadEndsToString : List DeadEnd -> String
65 | deadEndsToString =
66 | deadEndsToStrings >> String.join "\n"
67 |
68 |
69 | deadEndsToStrings : List DeadEnd -> List String
70 | deadEndsToStrings =
71 | List.map deadEndToString
72 |
73 |
74 | deadEndToString : DeadEnd -> String
75 | deadEndToString dead =
76 | case dead.problem of
77 | Expecting str ->
78 | "Expecting '" ++ str ++ "'"
79 |
80 | ExpectingSymbol str ->
81 | "Expecting symbol: " ++ str
82 |
83 | ExpectingKeyword str ->
84 | "Expecting a keyword: " ++ str
85 |
86 | ExpectingEnd ->
87 | "Expecting the end of input"
88 |
89 | UnexpectedChar ->
90 | "Unexpected character in input"
91 |
92 | Problem str ->
93 | str
94 |
95 | _ ->
96 | "Parsing error"
97 |
--------------------------------------------------------------------------------
/src/elm/Value.elm:
--------------------------------------------------------------------------------
1 | module Value exposing (Value(..), toString, encode, decoder)
2 |
3 | import Dict exposing (Dict)
4 | import Json.Encode as E
5 | import Json.Decode as D
6 |
7 | type alias Text =
8 | List String
9 |
10 |
11 | type Value
12 | = String String
13 | | Float Float
14 | | Bool Bool
15 | | Dict (Dict String Value)
16 | | List (List Value)
17 | | Text Text
18 | | Null
19 |
20 |
21 | toString : Value -> String
22 | toString val =
23 | case val of
24 | String x ->
25 | x
26 |
27 | Float x ->
28 | x |> String.fromFloat
29 |
30 | Bool x ->
31 | if x then "True" else "False"
32 |
33 | Dict x ->
34 | x |> Debug.toString
35 |
36 | List x ->
37 | x |> Debug.toString
38 |
39 | Text x ->
40 | x |> String.join ""
41 |
42 | Null ->
43 | "Null"
44 |
45 | encode : Value -> E.Value
46 | encode val =
47 | case val of
48 | String x ->
49 | x |> E.string
50 |
51 | Float x ->
52 | x |> E.float
53 |
54 | Bool x ->
55 | x |> E.bool
56 |
57 | Dict x ->
58 | x |> E.dict identity encode
59 |
60 | List x ->
61 | x |> E.list encode
62 |
63 | Text x ->
64 | x |> E.list E.string
65 |
66 | Null ->
67 | E.null
68 |
69 |
70 | decoder : D.Decoder Value
71 | decoder =
72 | D.oneOf
73 | [ D.string |> D.map String
74 | , D.float |> D.map Float
75 | , D.bool |> D.map Bool
76 | , D.lazy (\_ -> D.dict decoder |> D.map Dict)
77 | , D.lazy (\_ -> D.list decoder |> D.map List)
78 | , D.list D.string |> D.map Text
79 | , D.succeed Null
80 | ]
--------------------------------------------------------------------------------
/src/elm/VsCode.elm:
--------------------------------------------------------------------------------
1 | module VsCode exposing (link, open)
2 |
3 | import Gizmo
4 |
5 |
6 | link : String -> String
7 | link url =
8 | "vscode://inkandswitch.hypermerge/" ++ url
9 |
10 |
11 | open : String -> Cmd msg
12 | open url =
13 | Gizmo.command ( "OpenExternal", link url )
14 |
--------------------------------------------------------------------------------
/src/elm/examples/Authors.elm:
--------------------------------------------------------------------------------
1 | module Authors exposing (Doc, Msg, State, gizmo)
2 |
3 | import Colors
4 | import Config
5 | import Css exposing (..)
6 | import Gizmo
7 | import Html.Styled as Html exposing (..)
8 | import Html.Styled.Attributes exposing (css)
9 |
10 |
11 | maxAuthors : Int
12 | maxAuthors =
13 | 3
14 |
15 |
16 | gizmo : Gizmo.Program State Doc Msg
17 | gizmo =
18 | Gizmo.element
19 | { init = init
20 | , update = update
21 | , view = view
22 | , subscriptions = subscriptions
23 | }
24 |
25 |
26 | {-| Internal state not persisted to a document
27 | -}
28 | type alias State =
29 | {}
30 |
31 |
32 | {-| Document state
33 | -}
34 | type alias Doc =
35 | { authors : Maybe (List String)
36 | }
37 |
38 |
39 | {-| Message type for modifying State and Doc inside update
40 | -}
41 | type Msg
42 | = NoOp
43 |
44 |
45 | init : Gizmo.Flags -> ( State, Doc, Cmd Msg )
46 | init =
47 | always
48 | ( {}
49 | , { authors = Nothing
50 | }
51 | , Cmd.none
52 | )
53 |
54 |
55 | update : Msg -> Gizmo.Model State Doc -> ( State, Doc, Cmd Msg )
56 | update msg { state, doc } =
57 | case msg of
58 | NoOp ->
59 | ( state
60 | , doc
61 | , Cmd.none
62 | )
63 |
64 |
65 | subscriptions : Gizmo.Model State Doc -> Sub Msg
66 | subscriptions { state, doc } =
67 | Sub.none
68 |
69 |
70 | view : Gizmo.Model State Doc -> Html Msg
71 | view { flags, state, doc } =
72 | case doc.authors of
73 | Just authors ->
74 | viewAuthors authors
75 |
76 | Nothing ->
77 | viewEmpty
78 |
79 |
80 | viewAuthors : List String -> Html Msg
81 | viewAuthors authors =
82 | if List.length authors > maxAuthors then
83 | div
84 | [ css
85 | [ displayFlex
86 | , flexDirection row
87 | , alignItems center
88 | ]
89 | ]
90 | [ viewAuthorList <| List.take maxAuthors authors
91 | , viewRemaining maxAuthors authors
92 | ]
93 |
94 | else
95 | viewAuthorList authors
96 |
97 |
98 | viewAuthorList : List String -> Html Msg
99 | viewAuthorList authors =
100 | div
101 | [ css
102 | [ displayFlex
103 | , flexDirection row
104 | , padding2 (px 3) (px 0)
105 | ]
106 | ]
107 | (List.map viewAuthor authors)
108 |
109 |
110 | viewAuthor : String -> Html Msg
111 | viewAuthor author =
112 | div
113 | [ css
114 | [ marginRight (px 3)
115 | ]
116 | ]
117 | [ Gizmo.render Config.smallAvatar author
118 | ]
119 |
120 |
121 | viewRemaining : Int -> List String -> Html Msg
122 | viewRemaining max authors =
123 | let
124 | remaining =
125 | List.length authors - max
126 | in
127 | span
128 | [ css
129 | [ color (hex "aaa")
130 | , fontSize (Css.em 0.8)
131 | ]
132 | ]
133 | [ text <| "and " ++ String.fromInt remaining ++ " more"
134 | ]
135 |
136 |
137 | viewEmpty : Html Msg
138 | viewEmpty =
139 | Html.text "Unknown author"
140 |
--------------------------------------------------------------------------------
/src/elm/examples/Avatar.elm:
--------------------------------------------------------------------------------
1 | module Avatar exposing (Doc, Msg, State, gizmo)
2 |
3 | import Colors
4 | import Css exposing (..)
5 | import File exposing (File)
6 | import File.Select as Select
7 | import Gizmo
8 | import Html
9 | import Html.Styled exposing (..)
10 | import Html.Styled.Attributes as Attr exposing (autofocus, css, href, placeholder, src, value)
11 | import Html.Styled.Events exposing (keyCode, on, onBlur, onClick, onInput)
12 | import Json.Decode as Json
13 | import Task
14 |
15 |
16 | defaultName : String
17 | defaultName =
18 | "Mysterious Stranger"
19 |
20 |
21 | gizmo : Gizmo.Program State Doc Msg
22 | gizmo =
23 | Gizmo.element
24 | { init = init
25 | , update = update
26 | , view = view
27 | , subscriptions = subscriptions
28 | }
29 |
30 |
31 | {-| Internal state not persisted to a document
32 | -}
33 | type alias State =
34 | { editing : Bool
35 | , input : Maybe String
36 | }
37 |
38 |
39 | {-| Document state
40 | -}
41 | type alias Doc =
42 | { title : Maybe String
43 | , imageData : Maybe String
44 | }
45 |
46 |
47 | {-| Message type for modifying State and Doc inside update
48 | -}
49 | type Msg
50 | = PickImage
51 | | GotFiles File (List File)
52 | | GotPreviews (List String)
53 | | NoOp
54 |
55 |
56 | init : Gizmo.Flags -> ( State, Doc, Cmd Msg )
57 | init =
58 | always
59 | ( { editing = False
60 | , input = Nothing
61 | }
62 | , { title = Nothing
63 | , imageData = Nothing
64 | }
65 | , Cmd.none
66 | )
67 |
68 |
69 | update : Msg -> Gizmo.Model State Doc -> ( State, Doc, Cmd Msg )
70 | update msg { state, doc } =
71 | case Debug.log "update" msg of
72 | PickImage ->
73 | ( state, doc, Select.files [ "image/*" ] GotFiles )
74 |
75 | GotFiles file files ->
76 | ( state
77 | , doc
78 | , Task.perform GotPreviews <|
79 | Task.sequence <|
80 | List.map File.toUrl (file :: files)
81 | )
82 |
83 | GotPreviews urls ->
84 | ( state
85 | , { doc | imageData = List.head urls }
86 | , Cmd.none
87 | )
88 |
89 | NoOp ->
90 | ( state, doc, Cmd.none )
91 |
92 |
93 | subscriptions : Gizmo.Model State Doc -> Sub Msg
94 | subscriptions { state, doc } =
95 | Sub.none
96 |
97 |
98 | view : Gizmo.Model State Doc -> Html Msg
99 | view { state, doc } =
100 | case doc.imageData of
101 | Just data ->
102 | imageAvatar data
103 |
104 | Nothing ->
105 | case doc.title of
106 | Just name ->
107 | textAvatar <| defaultIfEmpty "Mysterious Stanger" name
108 |
109 | Nothing ->
110 | textAvatar "Mysterious Stranger"
111 |
112 |
113 | imageAvatar : String -> Html Msg
114 | imageAvatar imageSrc =
115 | div
116 | [ css
117 | [ width (px 36)
118 | , height (px 36)
119 | , backgroundImage (url imageSrc)
120 | , backgroundPosition center
121 | , backgroundRepeat noRepeat
122 | , backgroundSize contain
123 | ]
124 | ]
125 | []
126 |
127 |
128 | textAvatar : String -> Html Msg
129 | textAvatar name =
130 | div
131 | [ onClick PickImage
132 | , css
133 | [ width (px 36)
134 | , height (px 36)
135 | , borderRadius (pct 50)
136 | , border3 (px 1) solid (hex Colors.primary)
137 | , color (hex Colors.primary)
138 | , padding (px 0)
139 | , displayFlex
140 | , alignItems center
141 | , justifyContent center
142 | , fontSize (Css.em 0.9)
143 | , overflow hidden
144 | ]
145 | ]
146 | [ text <| initials name
147 | ]
148 |
149 |
150 | initials : String -> String
151 | initials name =
152 | name
153 | |> String.words
154 | |> List.map (String.left 1)
155 | |> String.join ""
156 |
157 |
158 | defaultIfEmpty : String -> String -> String
159 | defaultIfEmpty default str =
160 | if String.isEmpty str then
161 | default
162 |
163 | else
164 | str
165 |
--------------------------------------------------------------------------------
/src/elm/examples/CatBot.elm:
--------------------------------------------------------------------------------
1 | module CatBot exposing (Doc, Msg, State, bot)
2 |
3 | import Bot exposing (Flags, Model)
4 | import Doc
5 | import IO
6 |
7 |
8 | bot : Bot.Program State Doc Msg
9 | bot =
10 | Bot.create
11 | { init = init
12 | , update = update
13 | , onDoc = onDoc
14 | , subscriptions = subscriptions
15 | }
16 |
17 |
18 | {-| Ephemeral state not saved to the doc
19 | -}
20 | type alias State =
21 | {}
22 |
23 |
24 | {-| Document state
25 | -}
26 | type alias Doc =
27 | Doc.RawDoc
28 |
29 |
30 | init : Flags -> ( State, Doc, Cmd Msg )
31 | init _ =
32 | ( {}
33 | , Doc.encode Doc.empty
34 | , Cmd.none
35 | )
36 |
37 |
38 | {-| Message type for modifying State and Doc inside update
39 | -}
40 | type Msg
41 | = NoOp
42 |
43 |
44 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
45 | update msg { state, doc } =
46 | case msg of
47 | NoOp ->
48 | ( state, doc, Cmd.none )
49 |
50 |
51 | subscriptions : Model State Doc -> Sub Msg
52 | subscriptions { state, doc } =
53 | Sub.none
54 |
55 |
56 | onDoc : Model State Doc -> ( State, Cmd Msg )
57 | onDoc { state, doc } =
58 | ( state, IO.log (Doc.asString (Doc.decode doc)) )
59 |
--------------------------------------------------------------------------------
/src/elm/examples/Counter.elm:
--------------------------------------------------------------------------------
1 | module Counter exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Model)
5 | import Html.Styled exposing (..)
6 | import Html.Styled.Attributes exposing (..)
7 | import Html.Styled.Events exposing (..)
8 |
9 |
10 | gizmo : Gizmo.Program State Doc Msg
11 | gizmo =
12 | Gizmo.sandbox
13 | { init = init
14 | , update = update
15 | , view = view
16 | }
17 |
18 |
19 | {-| Ephemeral state not saved to the doc
20 | -}
21 | type alias State =
22 | {}
23 |
24 |
25 | {-| Document state
26 | -}
27 | type alias Doc =
28 | { counter : Int
29 | }
30 |
31 |
32 | init : ( State, Doc )
33 | init =
34 | ( {}
35 | , { counter = 0
36 | }
37 | )
38 |
39 |
40 | {-| Message type for modifying State and Doc inside update
41 | -}
42 | type Msg
43 | = Inc
44 |
45 |
46 | update : Msg -> Model State Doc -> ( State, Doc )
47 | update msg { state, doc } =
48 | case msg of
49 | Inc ->
50 | ( state, { doc | counter = doc.counter + 1 } )
51 |
52 |
53 | view : Model State Doc -> Html Msg
54 | view { state, doc } =
55 | div []
56 | [ button [ onClick Inc ] [ text (String.fromInt doc.counter) ]
57 | ]
58 |
--------------------------------------------------------------------------------
/src/elm/examples/CreateExample.elm:
--------------------------------------------------------------------------------
1 | module CreateExample exposing (Doc, Msg, State, gizmo)
2 |
3 | import Gizmo exposing (Flags, Model)
4 | import Html exposing (Html, button, div, text)
5 | import Html.Events exposing (onClick)
6 | import Repo exposing (Ref, Url)
7 |
8 |
9 | gizmo : Gizmo.Program State Doc Msg
10 | gizmo =
11 | Gizmo.element
12 | { init = init
13 | , update = update
14 | , view = view
15 | , subscriptions = subscriptions
16 | }
17 |
18 |
19 | {-| Ephemeral state not saved to the doc
20 | -}
21 | type alias State =
22 | {}
23 |
24 |
25 | {-| Document state
26 | -}
27 | type alias Doc =
28 | { urls : List String
29 | }
30 |
31 |
32 | init : Flags -> ( State, Doc, Cmd Msg )
33 | init flags =
34 | ( {}
35 | , { urls = []
36 | }
37 | , Cmd.none
38 | )
39 |
40 |
41 | {-| Message type for modifying State and Doc inside update
42 | -}
43 | type Msg
44 | = Created ( Ref, List String )
45 | | Create Int
46 | | Clone Url
47 |
48 |
49 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
50 | update msg { state, doc } =
51 | case msg of
52 | Create n ->
53 | ( state, doc, Repo.create "CreateOne" n )
54 |
55 | Clone url ->
56 | ( state, doc, Repo.clone "Clone" url )
57 |
58 | Created ( ref, urls ) ->
59 | ( state, { doc | urls = doc.urls ++ urls }, Cmd.none )
60 |
61 |
62 | view : Model State Doc -> Html Msg
63 | view { doc } =
64 | div []
65 | [ button [ onClick <| Create 1 ] [ text "Create empty doc" ]
66 | , Html.ul []
67 | (doc.urls
68 | |> List.map
69 | (\url ->
70 | Html.li []
71 | [ text url
72 | , button [ onClick <| Clone url ] [ text "Clone" ]
73 | ]
74 | )
75 | )
76 | ]
77 |
78 |
79 | subscriptions : Model State Doc -> Sub Msg
80 | subscriptions model =
81 | Repo.created Created
82 |
--------------------------------------------------------------------------------
/src/elm/examples/CreatePicker.elm:
--------------------------------------------------------------------------------
1 | module CreatePicker exposing (Doc, Msg, State, gizmo)
2 |
3 | import Browser.Dom as Dom
4 | import Clipboard
5 | import Colors
6 | import Config
7 | import Css exposing (..)
8 | import Draggable
9 | import Gizmo exposing (Flags, Model)
10 | import History exposing (History)
11 | import Html.Styled as Html exposing (..)
12 | import Html.Styled.Attributes exposing (css, id, placeholder, value)
13 | import Html.Styled.Events exposing (..)
14 | import IO
15 | import Json.Decode as D
16 | import Json.Encode as E
17 | import Keyboard exposing (Combo(..))
18 | import Link
19 | import ListSet exposing (ListSet)
20 | import Navigation
21 | import RealmUrl
22 | import Task
23 |
24 |
25 | focusColor =
26 | "#f0f0f0"
27 |
28 |
29 | gizmo : Gizmo.Program State Doc Msg
30 | gizmo =
31 | Gizmo.element
32 | { init = init
33 | , update = update
34 | , view = view
35 | , subscriptions = subscriptions
36 | }
37 |
38 |
39 | {-| Ephemeral state not saved to the doc
40 | -}
41 | type alias State =
42 | {}
43 |
44 |
45 | {-| Document state
46 | -}
47 | type alias Doc =
48 | { codeDocs : ListSet String
49 | }
50 |
51 |
52 | init : Flags -> ( State, Doc, Cmd Msg )
53 | init flags =
54 | ( {}
55 | , { codeDocs = ListSet.empty
56 | }
57 | , Cmd.none
58 | )
59 |
60 |
61 | {-| Message type for modifying State and Doc inside update
62 | -}
63 | type Msg
64 | = NoOp
65 | | Select String
66 |
67 |
68 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
69 | update msg ({ flags, state, doc } as model) =
70 | case msg of
71 | NoOp ->
72 | ( state
73 | , doc
74 | , Cmd.none
75 | )
76 |
77 | Select val ->
78 | ( state
79 | , doc
80 | , Gizmo.emit "select" (E.string val)
81 | )
82 |
83 |
84 | view : Model State Doc -> Html Msg
85 | view ({ doc, state } as model) =
86 | div
87 | []
88 | (List.map viewItem doc.codeDocs)
89 |
90 |
91 | viewItem : String -> Html Msg
92 | viewItem url =
93 | Draggable.draggable ( "application/hypermerge-url", url )
94 | [ div
95 | [ onStopPropagationClick (Select url)
96 | ]
97 | [ Gizmo.render Config.pickerItem url
98 | ]
99 | ]
100 |
101 |
102 | viewProperty : String -> String -> String -> Html Msg
103 | viewProperty prop default url =
104 | let
105 | attrs =
106 | [ Gizmo.attr "prop" prop
107 | , Gizmo.attr "default" default
108 | ]
109 | in
110 | Gizmo.renderWith attrs Config.property url
111 |
112 |
113 | onStopPropagationClick : Msg -> Html.Attribute Msg
114 | onStopPropagationClick msg =
115 | stopPropagationOn "click" (D.succeed ( msg, True ))
116 |
117 |
118 | subscriptions : Model State Doc -> Sub Msg
119 | subscriptions { state } =
120 | Sub.none
121 |
--------------------------------------------------------------------------------
/src/elm/examples/Cube.elm:
--------------------------------------------------------------------------------
1 | module Cube exposing (main)
2 |
3 | {-
4 | Rotating cube with colored sides.
5 | -}
6 |
7 | import Browser
8 | import Browser.Events exposing (onAnimationFrameDelta)
9 | import Html exposing (Html)
10 | import Html.Attributes exposing (height, style, width)
11 | import Json.Decode exposing (Value)
12 | import Math.Matrix4 as Mat4 exposing (Mat4)
13 | import Math.Vector3 as Vec3 exposing (Vec3, vec3)
14 | import WebGL exposing (Mesh, Shader)
15 |
16 |
17 | main : Program Value Float Float
18 | main =
19 | Browser.element
20 | { init = \_ -> ( 0, Cmd.none )
21 | , view = view
22 | , subscriptions = \_ -> onAnimationFrameDelta Basics.identity
23 | , update = \dt theta -> ( theta + dt / 5000, Cmd.none )
24 | }
25 |
26 |
27 | view : Float -> Html Float
28 | view theta =
29 | WebGL.toHtml
30 | [ width 400
31 | , height 400
32 | , style "display" "block"
33 | ]
34 | [ WebGL.entity
35 | vertexShader
36 | fragmentShader
37 | cubeMesh
38 | (uniforms theta)
39 | ]
40 |
41 |
42 | type alias Uniforms =
43 | { rotation : Mat4
44 | , perspective : Mat4
45 | , camera : Mat4
46 | , shade : Float
47 | }
48 |
49 |
50 | uniforms : Float -> Uniforms
51 | uniforms theta =
52 | { rotation =
53 | Mat4.mul
54 | (Mat4.makeRotate (3 * theta) (vec3 0 1 0))
55 | (Mat4.makeRotate (2 * theta) (vec3 1 0 0))
56 | , perspective = Mat4.makePerspective 45 1 0.01 100
57 | , camera = Mat4.makeLookAt (vec3 0 0 5) (vec3 0 0 0) (vec3 0 1 0)
58 | , shade = 0.8
59 | }
60 |
61 |
62 |
63 | -- Mesh
64 |
65 |
66 | type alias Vertex =
67 | { color : Vec3
68 | , position : Vec3
69 | }
70 |
71 |
72 | cubeMesh : Mesh Vertex
73 | cubeMesh =
74 | let
75 | rft =
76 | vec3 1 1 1
77 |
78 | lft =
79 | vec3 -1 1 1
80 |
81 | lbt =
82 | vec3 -1 -1 1
83 |
84 | rbt =
85 | vec3 1 -1 1
86 |
87 | rbb =
88 | vec3 1 -1 -1
89 |
90 | rfb =
91 | vec3 1 1 -1
92 |
93 | lfb =
94 | vec3 -1 1 -1
95 |
96 | lbb =
97 | vec3 -1 -1 -1
98 | in
99 | [ face (vec3 115 210 22) rft rfb rbb rbt -- green
100 | , face (vec3 52 101 164) rft rfb lfb lft -- blue
101 | , face (vec3 237 212 0) rft lft lbt rbt -- yellow
102 | , face (vec3 204 0 0) rfb lfb lbb rbb -- red
103 | , face (vec3 117 80 123) lft lfb lbb lbt -- purple
104 | , face (vec3 245 121 0) rbt rbb lbb lbt -- orange
105 | ]
106 | |> List.concat
107 | |> WebGL.triangles
108 |
109 |
110 | face : Vec3 -> Vec3 -> Vec3 -> Vec3 -> Vec3 -> List ( Vertex, Vertex, Vertex )
111 | face color a b c d =
112 | let
113 | vertex position =
114 | Vertex (Vec3.scale (1 / 255) color) position
115 | in
116 | [ ( vertex a, vertex b, vertex c )
117 | , ( vertex c, vertex d, vertex a )
118 | ]
119 |
120 |
121 |
122 | -- Shaders
123 |
124 |
125 | vertexShader : Shader Vertex Uniforms { vcolor : Vec3 }
126 | vertexShader =
127 | [glsl|
128 | attribute vec3 position;
129 | attribute vec3 color;
130 | uniform mat4 perspective;
131 | uniform mat4 camera;
132 | uniform mat4 rotation;
133 | varying vec3 vcolor;
134 | void main () {
135 | gl_Position = perspective * camera * rotation * vec4(position, 1.0);
136 | vcolor = color;
137 | }
138 | |]
139 |
140 |
141 | fragmentShader : Shader {} Uniforms { vcolor : Vec3 }
142 | fragmentShader =
143 | [glsl|
144 | precision mediump float;
145 | uniform float shade;
146 | varying vec3 vcolor;
147 | void main () {
148 | gl_FragColor = shade * vec4(vcolor, 1.0);
149 | }
150 | |]
151 |
--------------------------------------------------------------------------------
/src/elm/examples/DummyBoard.elm:
--------------------------------------------------------------------------------
1 | module DummyBoard exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Flags, Model)
5 | import Html.Styled as Html exposing (..)
6 | import Html.Styled.Attributes exposing (css)
7 |
8 |
9 | gizmo : Gizmo.Program State Doc Msg
10 | gizmo =
11 | Gizmo.element
12 | { init = init
13 | , update = update
14 | , view = view
15 | , subscriptions = subscriptions
16 | }
17 |
18 |
19 | {-| Ephemeral state not saved to the doc
20 | -}
21 | type alias State =
22 | {}
23 |
24 |
25 | {-| Document state
26 | -}
27 | type alias Doc =
28 | { content : String
29 | }
30 |
31 |
32 | init : Flags -> ( State, Doc, Cmd Msg )
33 | init flags =
34 | ( {}
35 | , { content = "" }
36 | , Cmd.none
37 | )
38 |
39 |
40 | {-| Message type for modifying State and Doc inside update
41 | -}
42 | type Msg
43 | = NoOp
44 |
45 |
46 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
47 | update msg { state, doc } =
48 | case msg of
49 | NoOp ->
50 | ( state
51 | , doc
52 | , Cmd.none
53 | )
54 |
55 |
56 | view : Model State Doc -> Html Msg
57 | view { doc } =
58 | div
59 | [ css
60 | [ backgroundColor (hex "aaa")
61 | , height (pct 100)
62 | , width (pct 100)
63 | , displayFlex
64 | , alignItems center
65 | , justifyContent center
66 | ]
67 | ]
68 | [ h1
69 | []
70 | [ text doc.content
71 | ]
72 | ]
73 |
74 |
75 | subscriptions : Model State Doc -> Sub Msg
76 | subscriptions model =
77 | Sub.none
78 |
--------------------------------------------------------------------------------
/src/elm/examples/EditableTitle.elm:
--------------------------------------------------------------------------------
1 | module EditableTitle exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Flags, Model)
5 | import Html.Styled as Html exposing (..)
6 | import Html.Styled.Attributes exposing (autofocus, css, placeholder, value)
7 | import Html.Styled.Events exposing (..)
8 | import Json.Decode as D
9 |
10 |
11 | gizmo : Gizmo.Program State Doc Msg
12 | gizmo =
13 | Gizmo.element
14 | { init = init
15 | , update = update
16 | , view = view
17 | , subscriptions = subscriptions
18 | }
19 |
20 |
21 | {-| Ephemeral state not saved to the doc
22 | -}
23 | type alias State =
24 | { isEditing : Bool
25 | }
26 |
27 |
28 | {-| Persisted state
29 | -}
30 | type alias Doc =
31 | { title : String
32 | }
33 |
34 |
35 | {-| What are Flags?
36 | -}
37 | init : Flags -> ( State, Doc, Cmd Msg )
38 | init flags =
39 | ( { isEditing = False }
40 | , { title = "" }
41 | , Cmd.none
42 | )
43 |
44 |
45 | type Msg
46 | = NoOp
47 | | StartTitleEdit
48 | | ChangeTitle String
49 | | EndTitleEdit
50 | | KeyDown Int
51 |
52 |
53 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
54 | update msg { state, doc } =
55 | case msg of
56 | NoOp ->
57 | ( state
58 | , doc
59 | , Cmd.none
60 | )
61 |
62 | StartTitleEdit ->
63 | ( { state | isEditing = True }
64 | , doc
65 | , Cmd.none
66 | )
67 |
68 | EndTitleEdit ->
69 | ( { state | isEditing = False }
70 | , doc
71 | , Cmd.none
72 | )
73 |
74 | ChangeTitle title ->
75 | ( state
76 | , { doc | title = title }
77 | , Cmd.none
78 | )
79 |
80 | KeyDown code ->
81 | case code of
82 | 27 ->
83 | ( { state | isEditing = False }
84 | , doc
85 | , Cmd.none
86 | )
87 |
88 | 13 ->
89 | ( { state | isEditing = False }
90 | , doc
91 | , Cmd.none
92 | )
93 |
94 | _ ->
95 | ( state
96 | , doc
97 | , Cmd.none
98 | )
99 |
100 |
101 | view : Model State Doc -> Html Msg
102 | view { flags, state, doc } =
103 | case state.isEditing of
104 | True ->
105 | input
106 | [ onBlur EndTitleEdit
107 | , onInput ChangeTitle
108 | , onKeyDown KeyDown
109 | , value doc.title
110 | , autofocus True
111 | , placeholder "Untitled"
112 | , css
113 | [ border zero
114 | , borderBottom3 (px 1) solid (hex "#ff69b4")
115 | , padding zero
116 | , paddingBottom (px 1)
117 | , fontSize inherit
118 | , fontWeight inherit
119 | , lineHeight inherit
120 | , outline none
121 | , marginBottom (px -2)
122 | ]
123 | ]
124 | []
125 |
126 | False ->
127 | div
128 | [ onClick StartTitleEdit
129 | , css
130 | [ display inlineBlock
131 | , padding2 (px 1) (px 0)
132 | , fontSize inherit
133 | , fontWeight inherit
134 | ]
135 | ]
136 | [ text
137 | (if String.isEmpty doc.title then
138 | "Untitled"
139 |
140 | else
141 | doc.title
142 | )
143 | ]
144 |
145 |
146 | onKeyDown : (Int -> msg) -> Attribute msg
147 | onKeyDown tagger =
148 | on "keydown" (D.map tagger keyCode)
149 |
150 |
151 | subscriptions : Model State Doc -> Sub Msg
152 | subscriptions model =
153 | Sub.none
154 |
--------------------------------------------------------------------------------
/src/elm/examples/EmptyGizmo.elm:
--------------------------------------------------------------------------------
1 | module EmptyGizmo exposing (Doc, Msg, State, gizmo)
2 |
3 | import Gizmo exposing (Flags, Model)
4 | import Html exposing (Html, button, div, form, input, text)
5 | import Html.Attributes exposing (style, value)
6 | import Html.Events exposing (onInput, onSubmit)
7 | import Repo exposing (Ref, Url)
8 | import Json.Encode as E
9 |
10 |
11 | gizmo : Gizmo.Program State Doc Msg
12 | gizmo =
13 | Gizmo.element
14 | { init = init
15 | , update = update
16 | , view = view
17 | , subscriptions = subscriptions
18 | }
19 |
20 |
21 | {-| Ephemeral state not saved to the doc
22 | -}
23 | type alias State =
24 | { dataUrl : String
25 | }
26 |
27 |
28 | {-| Document state
29 | -}
30 | type alias Doc =
31 | {}
32 |
33 |
34 | init : Flags -> ( State, Doc, Cmd Msg )
35 | init flags =
36 | ( { dataUrl = "" }
37 | , {}
38 | , Cmd.none
39 | )
40 |
41 |
42 | {-| Message type for modifying State and Doc inside update
43 | -}
44 | type Msg
45 | = Change String
46 | | Open
47 |
48 |
49 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
50 | update msg { state, doc } =
51 | case Debug.log "msg" msg of
52 | Open ->
53 | ( state, doc, Gizmo.emit "OpenDocument" (E.string state.dataUrl) )
54 |
55 | Change str ->
56 | ( { state | dataUrl = str }, doc, Cmd.none )
57 |
58 |
59 | view : Model State Doc -> Html Msg
60 | view { doc, state } =
61 | div [ style "padding" "10px" ]
62 | [ text "This window has no data url."
63 | , form [ onSubmit Open ]
64 | [ div [] [ text "Enter a data url to open it:" ]
65 | , input [ onInput Change, value state.dataUrl ] []
66 | , button [] [ text "Open" ]
67 | ]
68 | ]
69 |
70 |
71 | subscriptions : Model State Doc -> Sub Msg
72 | subscriptions model =
73 | Sub.none
74 |
--------------------------------------------------------------------------------
/src/elm/examples/Essay.elm:
--------------------------------------------------------------------------------
1 | module Article exposing (Doc, Msg, State, gizmo)
2 |
3 | import Colors exposing (..)
4 | import Css exposing (..)
5 | import Css.Global as G
6 | import Gizmo exposing (Flags, Model)
7 | import Html.Styled as Html exposing (..)
8 | import Html.Styled.Attributes exposing (class, css, placeholder, value)
9 | import Html.Styled.Events exposing (..)
10 | import Json.Encode as Json
11 | import Markdown
12 |
13 |
14 | markdownStyles =
15 | [ G.class "MarkdownContainer"
16 | [ G.descendants
17 | [ G.everything
18 | [ fontFamilies [ "system-ui" ]
19 | ]
20 | , G.each
21 | [ G.typeSelector "h1"
22 | , G.typeSelector "h2"
23 | , G.typeSelector "h3"
24 | ]
25 | [ marginTop (px 24)
26 | , marginBottom (px 16)
27 | , lineHeight (num 1.25)
28 | ]
29 | , G.typeSelector "h1"
30 | [ fontSize (Css.em 2)
31 | , fontWeight bold
32 | ]
33 | , G.typeSelector "h2"
34 | [ fontSize (Css.em 1.5)
35 | , fontWeight bold
36 | ]
37 | , G.typeSelector "h2"
38 | [ fontSize (Css.em 1.25)
39 | , fontWeight bold
40 | ]
41 | , G.typeSelector "p"
42 | [ marginTop (px 0)
43 | , marginBottom (px 16)
44 | ]
45 | , G.each
46 | [ G.typeSelector "ul"
47 | , G.typeSelector "ol"
48 | ]
49 | [ paddingLeft (Css.em 2)
50 | , marginTop (px 0)
51 | , marginBottom (px 0)
52 | ]
53 | , G.typeSelector "ul"
54 | [ listStyle disc
55 | ]
56 | , G.typeSelector "li"
57 | [ property "word-wrap" "break-all"
58 | ]
59 | , G.selector "li+li"
60 | [ marginTop (Css.em 0.25)
61 | ]
62 | , G.typeSelector "em"
63 | [ fontStyle italic
64 | ]
65 | , G.typeSelector "bold"
66 | [ fontWeight bold
67 | ]
68 | ]
69 | ]
70 | ]
71 |
72 |
73 | gizmo : Gizmo.Program State Doc Msg
74 | gizmo =
75 | Gizmo.element
76 | { init = init
77 | , update = update
78 | , view = view
79 | , subscriptions = subscriptions
80 | }
81 |
82 |
83 | {-| Ephemeral state not saved to the doc
84 | -}
85 | type alias State =
86 | { isEditing : Bool
87 | }
88 |
89 |
90 | {-| Document state
91 | -}
92 | type alias Doc =
93 | { title : String
94 | , body : String
95 | }
96 |
97 |
98 | init : Flags -> ( State, Doc, Cmd Msg )
99 | init flags =
100 | ( { isEditing = False }
101 | , { title = "", body = "" }
102 | , Cmd.none
103 | )
104 |
105 |
106 | {-| Message type for modifying State and Doc inside update
107 | -}
108 | type Msg
109 | = NoOp
110 | | SetBody String
111 | | ToggleEdit
112 |
113 |
114 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
115 | update msg { state, doc } =
116 | case msg of
117 | NoOp ->
118 | ( state
119 | , doc
120 | , Cmd.none
121 | )
122 |
123 | SetBody body ->
124 | ( state
125 | , { doc | body = body }
126 | , Cmd.none
127 | )
128 |
129 | ToggleEdit ->
130 | ( { state | isEditing = not state.isEditing }
131 | , doc
132 | , Cmd.none
133 | )
134 |
135 |
136 | updateTitleEmitValue : String -> String -> Json.Value
137 | updateTitleEmitValue old new =
138 | Json.object
139 | [ ( "old", Json.string old )
140 | , ( "new", Json.string new )
141 | ]
142 |
143 |
144 | textColor =
145 | hex "#333"
146 |
147 |
148 | view : Model State Doc -> Html Msg
149 | view { state, doc } =
150 | div
151 | [ css
152 | [ displayFlex
153 | , flexDirection column
154 | , padding (px 10)
155 | , margin (px 10)
156 | , backgroundColor (hex "fff")
157 | , fontFamilies [ "system-ui" ]
158 | ]
159 | ]
160 | [ div
161 | [ css
162 | [ display block
163 | , position absolute
164 | , right (px 10)
165 | , top (px 10)
166 | ]
167 | ]
168 | [ span
169 | [ onClick ToggleEdit
170 | , css
171 | [ color (hex hotPink)
172 | , cursor pointer
173 | , padding (px 2)
174 | , hover
175 | [ color (hex darkerHotPink)
176 | ]
177 | ]
178 | ]
179 | [ text
180 | (if state.isEditing then
181 | "View"
182 |
183 | else
184 | "Edit"
185 | )
186 | ]
187 | ]
188 | , if state.isEditing then
189 | textarea
190 | [ css
191 | [ flexGrow (num 1)
192 | , border zero
193 | , width (pct 100)
194 | , height (vh 80)
195 | , fontSize (Css.em 1)
196 | , fontFamilies ["Lucida Console"]
197 | , color textColor
198 | ]
199 | , onInput SetBody
200 | , value doc.body
201 | , placeholder "Your note here..."
202 | ]
203 | []
204 |
205 | else
206 | div
207 | [ class "MarkdownContainer"
208 | ]
209 | [ G.global markdownStyles
210 | , Html.fromUnstyled <| Markdown.toHtml [] doc.body
211 | ]
212 | ]
213 |
214 |
215 | subscriptions : Model State Doc -> Sub Msg
216 | subscriptions model =
217 | Sub.none
218 |
--------------------------------------------------------------------------------
/src/elm/examples/GizmoTemplate.elm:
--------------------------------------------------------------------------------
1 | module GizmoTemplate exposing (Doc, Msg, State, gizmo)
2 |
3 | import Config
4 | import Css exposing (..)
5 | import Gizmo exposing (Flags, Model)
6 | import Html.Styled as Html exposing (..)
7 | import Html.Styled.Attributes as Attr exposing (css)
8 |
9 |
10 | gizmo : Gizmo.Program State Doc Msg
11 | gizmo =
12 | Gizmo.element
13 | { init = init
14 | , update = update
15 | , view = view
16 | , subscriptions = subscriptions
17 | }
18 |
19 |
20 | {-| Ephemeral state not saved to the doc
21 | -}
22 | type alias State =
23 | {}
24 |
25 |
26 | {-| Document state
27 | -}
28 | type alias Doc =
29 | {}
30 |
31 |
32 | init : Flags -> ( State, Doc, Cmd Msg )
33 | init flags =
34 | ( {} {- initial State -}
35 | , {} {- initial Doc -}
36 | , Cmd.none {- initial Cmd -}
37 | )
38 |
39 |
40 | subscriptions : Model State Doc -> Sub Msg
41 | subscriptions model =
42 | Sub.none
43 |
44 |
45 | {-| Message type for modifying State and Doc inside update
46 | -}
47 | type Msg
48 | = NoOp
49 |
50 |
51 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
52 | update msg { state, doc } =
53 | case msg of
54 | NoOp ->
55 | ( state
56 | , doc
57 | , Cmd.none
58 | )
59 |
60 |
61 | view : Model State Doc -> Html Msg
62 | view { flags, doc, state } =
63 | div
64 | []
65 | [ h1 [] [ Gizmo.render Config.editableTitle flags.data ]
66 | , h2 [] [ text "Authors" ]
67 | , div []
68 | [ Gizmo.render Config.authors flags.code ]
69 | ]
70 |
--------------------------------------------------------------------------------
/src/elm/examples/Icon.elm:
--------------------------------------------------------------------------------
1 | module Icon exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Dict
5 | import Gizmo exposing (Flags, Model)
6 | import Html.Styled as Html exposing (..)
7 | import Html.Styled.Attributes exposing (css, placeholder, value)
8 | import Html.Styled.Events exposing (..)
9 | import Maybe
10 |
11 |
12 | gizmo : Gizmo.Program State Doc Msg
13 | gizmo =
14 | Gizmo.element
15 | { init = init
16 | , update = update
17 | , view = view
18 | , subscriptions = subscriptions
19 | }
20 |
21 |
22 | {-| Ephemeral state not saved to the doc
23 | -}
24 | type alias State =
25 | {}
26 |
27 |
28 | {-| Document state
29 | -}
30 | type alias Doc =
31 | { icon : Maybe String
32 | }
33 |
34 |
35 | init : Flags -> ( State, Doc, Cmd Msg )
36 | init flags =
37 | ( {}
38 | , { icon = Nothing }
39 | , Cmd.none
40 | )
41 |
42 |
43 | {-| Message type for modifying State and Doc inside update
44 | -}
45 | type Msg
46 | = NoOp
47 |
48 |
49 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
50 | update msg { state, doc } =
51 | case msg of
52 | NoOp ->
53 | ( state
54 | , doc
55 | , Cmd.none
56 | )
57 |
58 |
59 | view : Model State Doc -> Html Msg
60 | view { flags, doc } =
61 | let
62 | defaultIconSrc =
63 | Maybe.withDefault "" (Dict.get "defaultIcon" flags.config)
64 | in
65 | div
66 | [ css
67 | [ width (pct 100)
68 | , height (pct 100)
69 | , backgroundImage (url <| Maybe.withDefault defaultIconSrc doc.icon)
70 | , backgroundPosition center
71 | , backgroundSize cover
72 | , cursor pointer
73 | ]
74 | ]
75 | []
76 |
77 |
78 | subscriptions : Model State Doc -> Sub Msg
79 | subscriptions model =
80 | Sub.none
81 |
--------------------------------------------------------------------------------
/src/elm/examples/Image.elm:
--------------------------------------------------------------------------------
1 | module GizmoTemplate exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Flags, Model)
5 | import Html.Styled as Html exposing (..)
6 | import Html.Styled.Attributes exposing (css, src)
7 |
8 |
9 | gizmo : Gizmo.Program State Doc Msg
10 | gizmo =
11 | Gizmo.element
12 | { init = init
13 | , update = update
14 | , view = view
15 | , subscriptions = subscriptions
16 | }
17 |
18 |
19 | {-| Ephemeral state not saved to the doc
20 | -}
21 | type alias State =
22 | {}
23 |
24 |
25 | {-| Document state
26 | -}
27 | type alias Doc =
28 | { src : String
29 | }
30 |
31 |
32 | init : Flags -> ( State, Doc, Cmd Msg )
33 | init flags =
34 | ( {}
35 | , { src = ""
36 | }
37 | , Cmd.none
38 | )
39 |
40 |
41 | {-| Message type for modifying State and Doc inside update
42 | -}
43 | type Msg
44 | = NoOp
45 |
46 |
47 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
48 | update msg { state, doc } =
49 | case msg of
50 | NoOp ->
51 | ( state
52 | , doc
53 | , Cmd.none
54 | )
55 |
56 |
57 | view : Model State Doc -> Html Msg
58 | view { doc } =
59 | img
60 | [ src doc.src
61 | , css
62 | [ property "object-fit" "cover"
63 | , position absolute
64 | , top zero
65 | , left zero
66 | , width (pct 100)
67 | , height (pct 100)
68 | , pointerEvents none
69 | ]
70 | ]
71 | []
72 |
73 |
74 | subscriptions : Model State Doc -> Sub Msg
75 | subscriptions model =
76 | Sub.none
77 |
--------------------------------------------------------------------------------
/src/elm/examples/LiveEdit.elm:
--------------------------------------------------------------------------------
1 | module LiveEdit exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Dict
5 | import Doc
6 | import Gizmo exposing (Flags, Model)
7 | import Html.Styled as Html exposing (..)
8 | import Html.Styled.Attributes exposing (css, id, placeholder, value)
9 | import Html.Styled.Events exposing (onInput)
10 | import Json.Decode as D
11 | import Json.Encode as E
12 | import Value exposing (Value)
13 |
14 |
15 | propAttr : String
16 | propAttr =
17 | "prop"
18 |
19 |
20 | defaultAttr : String
21 | defaultAttr =
22 | "default"
23 |
24 |
25 | inputIdAttr : String
26 | inputIdAttr =
27 | "input-id"
28 |
29 |
30 | gizmo : Gizmo.Program State Doc Msg
31 | gizmo =
32 | Gizmo.element
33 | { init = init
34 | , update = update
35 | , view = view
36 | , subscriptions = subscriptions
37 | }
38 |
39 |
40 | {-| Ephemeral state not saved to the doc
41 | -}
42 | type alias State =
43 | {}
44 |
45 |
46 | {-| Document state
47 | -}
48 | type alias Doc =
49 | E.Value
50 |
51 |
52 | init : Flags -> ( State, Doc, Cmd Msg )
53 | init flags =
54 | ( {}
55 | , E.null
56 | , Cmd.none
57 | )
58 |
59 |
60 | {-| Message type for modifying State and Doc inside update
61 | -}
62 | type Msg
63 | = NoOp
64 | | SetValue String
65 |
66 |
67 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
68 | update msg { flags, state, doc } =
69 | case msg of
70 | NoOp ->
71 | ( state
72 | , doc
73 | , Cmd.none
74 | )
75 |
76 | SetValue value ->
77 | case Dict.get propAttr flags.all of
78 | Just prop ->
79 | ( state
80 | , set prop (Value.String value) doc
81 | , Cmd.none
82 | )
83 |
84 | Nothing ->
85 | ( state
86 | , doc
87 | , Cmd.none
88 | )
89 |
90 |
91 | set : String -> Value -> E.Value -> E.Value
92 | set key value doc =
93 | doc
94 | |> Doc.decode
95 | |> Dict.insert key value
96 | |> Doc.encode
97 |
98 |
99 | view : Model State Doc -> Html Msg
100 | view { flags, doc } =
101 | let
102 | prop =
103 | Dict.get propAttr flags.all
104 |
105 | default =
106 | Maybe.withDefault "" <| Dict.get defaultAttr flags.all
107 |
108 | inputId =
109 | Maybe.withDefault flags.data <| Dict.get inputIdAttr flags.all
110 | in
111 | case prop of
112 | Just propName ->
113 | let
114 | val =
115 | Value.toString <| getProp "" propName doc
116 | in
117 | input
118 | [ value val
119 | , onInput SetValue
120 | , placeholder default
121 | , id inputId
122 | , css
123 | [ all inherit
124 | , display initial
125 | , width (pct 100)
126 | , height (pct 100)
127 | ]
128 | ]
129 | []
130 |
131 | Nothing ->
132 | Html.text <| "Must define a " ++ propAttr ++ " attribute."
133 |
134 |
135 | getProp : String -> String -> Doc -> Value
136 | getProp default prop doc =
137 | Result.withDefault (Value.String default) <|
138 | D.decodeValue (propDecoder prop) doc
139 |
140 |
141 | propDecoder : String -> D.Decoder Value
142 | propDecoder prop =
143 | D.field prop Value.decoder
144 |
145 |
146 | subscriptions : Model State Doc -> Sub Msg
147 | subscriptions model =
148 | Sub.none
149 |
--------------------------------------------------------------------------------
/src/elm/examples/Navigator.elm:
--------------------------------------------------------------------------------
1 | module Navigator exposing (Doc, Msg, State, gizmo)
2 |
3 | import FarmUrl
4 | import Gizmo exposing (Flags, Model)
5 | import Html exposing (Html, text)
6 | import IO
7 | import Navigation
8 |
9 |
10 | gizmo : Gizmo.Program State Doc Msg
11 | gizmo =
12 | Gizmo.element
13 | { init = init
14 | , update = update
15 | , view = view
16 | , subscriptions = subscriptions
17 | }
18 |
19 |
20 | {-| Ephemeral state not saved to the doc
21 | -}
22 | type alias State =
23 | { error : Maybe ( String, String )
24 | }
25 |
26 |
27 | type alias Pair =
28 | { code : String
29 | , data : String
30 | }
31 |
32 |
33 | {-| Document state
34 | -}
35 | type alias Doc =
36 | { history : List Pair
37 | }
38 |
39 |
40 | init : Flags -> ( State, Doc, Cmd Msg )
41 | init flags =
42 | ( { error = Nothing
43 | }
44 | , { history = []
45 | }
46 | , Cmd.none
47 | )
48 |
49 |
50 | {-| Message type for modifying State and Doc inside update
51 | -}
52 | type Msg
53 | = NavigateTo String
54 |
55 |
56 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
57 | update msg { state, doc } =
58 | case msg of
59 | NavigateTo url ->
60 | case FarmUrl.parse url of
61 | Ok pair ->
62 | ( state
63 | , { doc | history = pair :: doc.history }
64 | , IO.log <| "Navigating to " ++ url
65 | )
66 |
67 | Err err ->
68 | ( { state | error = Just ( url, err ) }
69 | , doc
70 | , IO.log <| "Could not navigate to " ++ url ++ ". " ++ err
71 | )
72 |
73 |
74 | view : Model State Doc -> Html Msg
75 | view { doc, state } =
76 | case state.error of
77 | Just ( url, err ) ->
78 | text <| "'" ++ url ++ "' could not be parsed: " ++ err
79 |
80 | Nothing ->
81 | case current doc of
82 | Just ({ code, data } as pair) ->
83 | let
84 | url =
85 | Debug.log "Viewing " <| FarmUrl.create pair
86 | in
87 | Gizmo.render code data
88 |
89 | Nothing ->
90 | text "You haven't navigated to anything. Click a farm link."
91 |
92 |
93 | subscriptions : Model State Doc -> Sub Msg
94 | subscriptions model =
95 | Navigation.currentUrl NavigateTo
96 |
97 |
98 | current : Doc -> Maybe Pair
99 | current =
100 | .history >> List.head
101 |
--------------------------------------------------------------------------------
/src/elm/examples/Note.elm:
--------------------------------------------------------------------------------
1 | module Note exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Flags, Model)
5 | import Html.Styled as Html exposing (..)
6 | import Html.Styled.Attributes exposing (css, placeholder, value)
7 | import Html.Styled.Events exposing (..)
8 |
9 |
10 | gizmo : Gizmo.Program State Doc Msg
11 | gizmo =
12 | Gizmo.element
13 | { init = init
14 | , update = update
15 | , view = view
16 | , subscriptions = subscriptions
17 | }
18 |
19 |
20 | {-| Ephemeral state not saved to the doc
21 | -}
22 | type alias State =
23 | {}
24 |
25 |
26 | {-| Document state
27 | -}
28 | type alias Doc =
29 | { title : String
30 | , body : String
31 | }
32 |
33 |
34 | init : Flags -> ( State, Doc, Cmd Msg )
35 | init flags =
36 | ( {}
37 | , { title = "", body = "" }
38 | , Cmd.none
39 | )
40 |
41 |
42 | {-| Message type for modifying State and Doc inside update
43 | -}
44 | type Msg
45 | = NoOp
46 | | SetTitle String
47 | | SetBody String
48 |
49 |
50 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
51 | update msg { state, doc } =
52 | case msg of
53 | NoOp ->
54 | ( state
55 | , doc
56 | , Cmd.none
57 | )
58 |
59 | SetTitle title ->
60 | ( state
61 | , { doc | title = title }
62 | , Cmd.none
63 | )
64 |
65 | SetBody body ->
66 | ( state
67 | , { doc | body = body }
68 | , Cmd.none
69 | )
70 |
71 |
72 | textColor =
73 | hex "#333"
74 |
75 |
76 | view : Model State Doc -> Html Msg
77 | view { doc } =
78 | div
79 | [ css
80 | [ padding (px 5)
81 | , displayFlex
82 | , flexDirection column
83 | , height (pct 100)
84 | ]
85 | ]
86 | [ textarea
87 | [ css
88 | [ flexGrow (num 1)
89 | , border zero
90 | , width (pct 100)
91 | , fontSize (Css.em 1)
92 | , color textColor
93 | , resize none
94 | , fontFamily inherit
95 | ]
96 | , onInput SetBody
97 | , value doc.body
98 | , placeholder "Your note here..."
99 | ]
100 | []
101 | ]
102 |
103 |
104 | subscriptions : Model State Doc -> Sub Msg
105 | subscriptions model =
106 | Sub.none
107 |
--------------------------------------------------------------------------------
/src/elm/examples/Oblique.elm:
--------------------------------------------------------------------------------
1 | module ObliqueStrategiesViewer exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Flags, Model)
5 | import Html.Styled as Html exposing (..)
6 | import Html.Styled.Attributes exposing (css)
7 | import Html.Styled.Events exposing (..)
8 | import Random
9 |
10 |
11 | gizmo : Gizmo.Program State Doc Msg
12 | gizmo =
13 | Gizmo.element
14 | { init = init
15 | , update = update
16 | , view = view
17 | , subscriptions = subscriptions
18 | }
19 |
20 |
21 | {-| Ephemeral state not saved to the doc
22 | -}
23 | type alias State =
24 | { currentStrategy : Maybe String }
25 |
26 |
27 | {-| Document state
28 | -}
29 | type alias Doc =
30 | { strategies : List String }
31 |
32 |
33 | init : Flags -> ( State, Doc, Cmd Msg )
34 | init flags =
35 | ( { currentStrategy = Nothing }
36 | , { strategies = [] }
37 | , Cmd.none
38 | )
39 |
40 |
41 | {-| Message type for modifying State and Doc inside update
42 | -}
43 | type Msg
44 | = PickAgain
45 | | StrategyPicked String
46 |
47 |
48 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
49 | update msg { state, doc } =
50 | case msg of
51 | PickAgain ->
52 | ( state
53 | , doc
54 | , Random.generate StrategyPicked (Random.uniform "" doc.strategies)
55 | )
56 |
57 | StrategyPicked strat ->
58 | ( { state | currentStrategy = Just strat }, doc, Cmd.none )
59 |
60 |
61 | view : Model State Doc -> Html Msg
62 | view { doc, state } =
63 | div
64 | [ css
65 | [ justifyContent center
66 | , fontSize (px 48)
67 | , displayFlex
68 | , alignItems center
69 | , height (pct 100)
70 | ]
71 | ]
72 | [ h1 [] [ text (Maybe.withDefault "pick one" state.currentStrategy) ]
73 | , button
74 | [ onClick PickAgain
75 | , css
76 | [ position absolute
77 | , right (px 5)
78 | , bottom (px 5)
79 | ]
80 | ]
81 | [ text "again" ]
82 | ]
83 |
84 |
85 | subscriptions : Model State Doc -> Sub Msg
86 | subscriptions model =
87 | Sub.none
88 |
--------------------------------------------------------------------------------
/src/elm/examples/PickerItem.elm:
--------------------------------------------------------------------------------
1 | module PickerItem exposing (Doc, Msg, State, gizmo)
2 |
3 | import Config
4 | import Css exposing (..)
5 | import DateFormat.Relative exposing (relativeTime)
6 | import Gizmo exposing (Flags, Model)
7 | import Html.Styled as Html exposing (..)
8 | import Html.Styled.Attributes exposing (css, placeholder, value)
9 | import Html.Styled.Events exposing (..)
10 | import String
11 | import Task
12 | import Time exposing (Posix)
13 |
14 |
15 | gizmo : Gizmo.Program State Doc Msg
16 | gizmo =
17 | Gizmo.element
18 | { init = init
19 | , update = update
20 | , view = view
21 | , subscriptions = subscriptions
22 | }
23 |
24 |
25 | {-| Ephemeral state not saved to the doc
26 | -}
27 | type alias State =
28 | { currentTime : Maybe Posix }
29 |
30 |
31 | {-| Document state
32 | -}
33 | type alias Doc =
34 | { title : Maybe String
35 | , authors : List String
36 | , lastEditTimestamp : Maybe Int
37 | }
38 |
39 |
40 | init : Flags -> ( State, Doc, Cmd Msg )
41 | init flags =
42 | ( { currentTime = Nothing
43 | }
44 | , { title = Nothing
45 | , authors = []
46 | , lastEditTimestamp = Nothing
47 | }
48 | , Task.perform ReceiveCurrentTime Time.now
49 | )
50 |
51 |
52 | {-| Message type for modifying State and Doc inside update
53 | -}
54 | type Msg
55 | = NoOp
56 | | ReceiveCurrentTime Posix
57 |
58 |
59 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
60 | update msg { state, doc } =
61 | case msg of
62 | NoOp ->
63 | ( state
64 | , doc
65 | , Cmd.none
66 | )
67 |
68 | ReceiveCurrentTime now ->
69 | ( { state | currentTime = Just now }
70 | , doc
71 | , Cmd.none
72 | )
73 |
74 |
75 | view : Model State Doc -> Html Msg
76 | view { flags, doc, state } =
77 | div
78 | [ css
79 | [ displayFlex
80 | , flexDirection column
81 | , padding (px 15)
82 | , fontSize (Css.em 0.9)
83 | , textOverflow ellipsis
84 | , property "white-space" "nowrap"
85 | , overflow hidden
86 | , cursor pointer
87 | , borderTop3 (px 1) solid (hex "ddd")
88 | , backgroundColor (hex "fff")
89 | , hover
90 | [ backgroundColor (hex "f5f5f5")
91 | ]
92 | ]
93 | ]
94 | [ div
95 | [ css
96 | [ flexGrow (int 1)
97 | , marginBottom (px 5)
98 | ]
99 | ]
100 | [ text <| Maybe.withDefault "No title" doc.title
101 | ]
102 | , div
103 | [ css
104 | [ displayFlex
105 | , flexDirection row
106 | , alignItems center
107 | ]
108 | ]
109 | [ div
110 | [ css
111 | [ flexGrow (int 1)
112 | ]
113 | ]
114 | [ Gizmo.render Config.authors flags.data
115 | ]
116 | , viewLastEdit state.currentTime doc.lastEditTimestamp
117 | ]
118 | ]
119 |
120 |
121 | viewLastEdit : Maybe Posix -> Maybe Int -> Html Msg
122 | viewLastEdit currentTime lastEditTimestamp =
123 | span
124 | [ css
125 | [ color (hex "aaa")
126 | , fontSize (Css.em 0.8)
127 | ]
128 | ]
129 | [ case ( currentTime, lastEditTimestamp ) of
130 | ( Just now, Just timestamp ) ->
131 | text <| relativeTime now <| Time.millisToPosix timestamp
132 |
133 | _ ->
134 | text ""
135 | ]
136 |
137 |
138 | subscriptions : Model State Doc -> Sub Msg
139 | subscriptions model =
140 | Sub.none
141 |
--------------------------------------------------------------------------------
/src/elm/examples/Property.elm:
--------------------------------------------------------------------------------
1 | module Property exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Dict
5 | import Gizmo exposing (Flags, Model)
6 | import Html.Styled as Html exposing (..)
7 | import Html.Styled.Attributes exposing (css)
8 | import Json.Decode as D
9 | import Json.Encode as E
10 | import Value exposing (Value)
11 |
12 |
13 | attr : String
14 | attr =
15 | "prop"
16 |
17 |
18 | defaultAttr : String
19 | defaultAttr =
20 | "default"
21 |
22 |
23 | gizmo : Gizmo.Program State Doc Msg
24 | gizmo =
25 | Gizmo.element
26 | { init = init
27 | , update = update
28 | , view = view
29 | , subscriptions = subscriptions
30 | }
31 |
32 |
33 | {-| Ephemeral state not saved to the doc
34 | -}
35 | type alias State =
36 | {}
37 |
38 |
39 | {-| Document state
40 | -}
41 | type alias Doc =
42 | E.Value
43 |
44 |
45 | init : Flags -> ( State, Doc, Cmd Msg )
46 | init flags =
47 | ( {}
48 | , E.null
49 | , Cmd.none
50 | )
51 |
52 |
53 | {-| Message type for modifying State and Doc inside update
54 | -}
55 | type Msg
56 | = NoOp
57 |
58 |
59 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
60 | update msg { state, doc } =
61 | case msg of
62 | NoOp ->
63 | ( state
64 | , doc
65 | , Cmd.none
66 | )
67 |
68 |
69 | view : Model State Doc -> Html Msg
70 | view { flags, doc } =
71 | let
72 | prop =
73 | Dict.get attr flags.all
74 |
75 | default =
76 | Maybe.withDefault "" <| Dict.get defaultAttr flags.all
77 | in
78 | case prop of
79 | Just propName ->
80 | Html.text <| Value.toString <| getProp default propName doc
81 |
82 | Nothing ->
83 | Html.text <| "Must define a " ++ attr ++ " attribute."
84 |
85 |
86 | getProp : String -> String -> Doc -> Value
87 | getProp default prop doc =
88 | Result.withDefault (Value.String default) <|
89 | D.decodeValue (propDecoder prop) doc
90 |
91 |
92 | propDecoder : String -> D.Decoder Value
93 | propDecoder prop =
94 | D.field prop Value.decoder
95 |
96 |
97 | subscriptions : Model State Doc -> Sub Msg
98 | subscriptions model =
99 | Sub.none
100 |
--------------------------------------------------------------------------------
/src/elm/examples/RendererPicker.elm:
--------------------------------------------------------------------------------
1 | module RendererPicker exposing (Doc, Msg, State, gizmo)
2 |
3 | import Browser.Dom as Dom
4 | import Clipboard
5 | import Colors
6 | import Config
7 | import Css exposing (..)
8 | import Gizmo exposing (Flags, Model)
9 | import History exposing (History)
10 | import Html.Styled as Html exposing (..)
11 | import Html.Styled.Attributes exposing (css, id, placeholder, value)
12 | import Html.Styled.Events exposing (..)
13 | import IO
14 | import Json.Decode as D
15 | import Json.Encode as E
16 | import Keyboard exposing (Combo(..))
17 | import Link
18 | import ListSet exposing (ListSet)
19 | import Navigation
20 | import RealmUrl
21 | import Task
22 |
23 |
24 | focusColor =
25 | "#f0f0f0"
26 |
27 |
28 | gizmo : Gizmo.Program State Doc Msg
29 | gizmo =
30 | Gizmo.element
31 | { init = init
32 | , update = update
33 | , view = view
34 | , subscriptions = subscriptions
35 | }
36 |
37 |
38 | {-| Ephemeral state not saved to the doc
39 | -}
40 | type alias State =
41 | {}
42 |
43 |
44 | {-| Document state
45 | -}
46 | type alias Doc =
47 | { codeDocs : ListSet String
48 | }
49 |
50 |
51 | init : Flags -> ( State, Doc, Cmd Msg )
52 | init flags =
53 | ( {}
54 | , { codeDocs = ListSet.empty
55 | }
56 | , Cmd.none
57 | )
58 |
59 |
60 | {-| Message type for modifying State and Doc inside update
61 | -}
62 | type Msg
63 | = NoOp
64 | | Select String
65 |
66 |
67 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
68 | update msg ({ flags, state, doc } as model) =
69 | case msg of
70 | NoOp ->
71 | ( state
72 | , doc
73 | , Cmd.none
74 | )
75 |
76 | Select val ->
77 | ( state
78 | , doc
79 | , Gizmo.emit "select" (E.string val)
80 | )
81 |
82 |
83 | view : Model State Doc -> Html Msg
84 | view ({ doc, state } as model) =
85 | div
86 | [ css
87 | [ boxShadow5 zero (px 2) (px 8) zero (rgba 0 0 0 0.12)
88 | , border3 (px 1) solid (hex "ddd")
89 | , borderRadius (px 5)
90 | , maxHeight (px 400)
91 | , width (pct 100)
92 | , backgroundColor (hex "#fff")
93 | , overflowX hidden
94 | , overflowY auto
95 | , fontFamilies [ "system-ui" ]
96 | ]
97 | ]
98 | [ div
99 | []
100 | (List.map viewItem doc.codeDocs)
101 | ]
102 |
103 |
104 | viewItem : String -> Html Msg
105 | viewItem url =
106 | div
107 | [ onStopPropagationClick (Select url)
108 | ]
109 | [ Gizmo.render Config.pickerItem url
110 | ]
111 |
112 |
113 | viewProperty : String -> String -> String -> Html Msg
114 | viewProperty prop default url =
115 | let
116 | attrs =
117 | [ Gizmo.attr "prop" prop
118 | , Gizmo.attr "default" default
119 | ]
120 | in
121 | Gizmo.renderWith attrs Config.property url
122 |
123 |
124 | onStopPropagationClick : Msg -> Html.Attribute Msg
125 | onStopPropagationClick msg =
126 | stopPropagationOn "click" (D.succeed ( msg, True ))
127 |
128 |
129 | subscriptions : Model State Doc -> Sub Msg
130 | subscriptions { state } =
131 | Sub.none
132 |
--------------------------------------------------------------------------------
/src/elm/examples/SimpleAvatar.elm:
--------------------------------------------------------------------------------
1 | module SimpleAvatar exposing (Doc, Msg, State, gizmo)
2 |
3 | import Colors
4 | import Css exposing (..)
5 | import File exposing (File)
6 | import File.Select as Select
7 | import Gizmo
8 | import Html
9 | import Html.Styled exposing (..)
10 | import Html.Styled.Attributes as Attr exposing (autofocus, css, href, placeholder, src, value)
11 | import Html.Styled.Events exposing (keyCode, on, onBlur, onClick, onInput)
12 | import Json.Decode as Json
13 | import Task
14 |
15 |
16 | defaultName : String
17 | defaultName =
18 | "Mysterious Stranger"
19 |
20 |
21 | gizmo : Gizmo.Program State Doc Msg
22 | gizmo =
23 | Gizmo.element
24 | { init = init
25 | , update = update
26 | , view = view
27 | , subscriptions = subscriptions
28 | }
29 |
30 |
31 | {-| Internal state not persisted to a document
32 | -}
33 | type alias State =
34 | { editing : Bool
35 | , input : Maybe String
36 | }
37 |
38 |
39 | {-| Document state
40 | -}
41 | type alias Doc =
42 | { title : Maybe String
43 | , imageData : Maybe String
44 | }
45 |
46 |
47 | {-| Message type for modifying State and Doc inside update
48 | -}
49 | type Msg
50 | = PickImage
51 | | GotFiles File (List File)
52 | | GotPreviews (List String)
53 | | NoOp
54 |
55 |
56 | init : Gizmo.Flags -> ( State, Doc, Cmd Msg )
57 | init =
58 | always
59 | ( { editing = False
60 | , input = Nothing
61 | }
62 | , { title = Nothing
63 | , imageData = Nothing
64 | }
65 | , Cmd.none
66 | )
67 |
68 |
69 | update : Msg -> Gizmo.Model State Doc -> ( State, Doc, Cmd Msg )
70 | update msg { state, doc } =
71 | case Debug.log "update" msg of
72 | PickImage ->
73 | ( state, doc, Select.files [ "image/*" ] GotFiles )
74 |
75 | GotFiles file files ->
76 | ( state
77 | , doc
78 | , Task.perform GotPreviews <|
79 | Task.sequence <|
80 | List.map File.toUrl (file :: files)
81 | )
82 |
83 | GotPreviews urls ->
84 | ( state
85 | , { doc | imageData = List.head urls }
86 | , Cmd.none
87 | )
88 |
89 | NoOp ->
90 | ( state, doc, Cmd.none )
91 |
92 |
93 | subscriptions : Gizmo.Model State Doc -> Sub Msg
94 | subscriptions { state, doc } =
95 | Sub.none
96 |
97 |
98 | view : Gizmo.Model State Doc -> Html Msg
99 | view { state, doc } =
100 | case doc.imageData of
101 | Just data ->
102 | imageAvatar data
103 |
104 | Nothing ->
105 | case doc.title of
106 | Just name ->
107 | textAvatar <| defaultIfEmpty "Mysterious Stanger" name
108 |
109 | Nothing ->
110 | textAvatar "Mysterious Stranger"
111 |
112 |
113 | imageAvatar : String -> Html Msg
114 | imageAvatar imageSrc =
115 | div
116 | [ css
117 | [ width (px 36)
118 | , height (px 36)
119 | , backgroundImage (url imageSrc)
120 | , backgroundPosition center
121 | , backgroundRepeat noRepeat
122 | , backgroundSize contain
123 | ]
124 | ]
125 | []
126 |
127 |
128 | textAvatar : String -> Html Msg
129 | textAvatar name =
130 | div
131 | [ onClick PickImage
132 | , css
133 | [ width (px 36)
134 | , height (px 36)
135 | , borderRadius (pct 50)
136 | , border3 (px 1) solid (hex Colors.primary)
137 | , color (hex Colors.primary)
138 | , padding (px 0)
139 | , displayFlex
140 | , alignItems center
141 | , justifyContent center
142 | , fontSize (Css.em 0.9)
143 | , overflow hidden
144 | ]
145 | ]
146 | [ text <| initials name
147 | ]
148 |
149 |
150 | initials : String -> String
151 | initials name =
152 | name
153 | |> String.words
154 | |> List.map (String.left 1)
155 | |> String.join ""
156 |
157 |
158 | defaultIfEmpty : String -> String -> String
159 | defaultIfEmpty default str =
160 | if String.isEmpty str then
161 | default
162 |
163 | else
164 | str
165 |
--------------------------------------------------------------------------------
/src/elm/examples/SmallAvatar.elm:
--------------------------------------------------------------------------------
1 | module SmallAvatar exposing (Doc, Msg, State, gizmo)
2 |
3 | import Colors
4 | import Css exposing (..)
5 | import File exposing (File)
6 | import File.Select as Select
7 | import Gizmo
8 | import Html
9 | import Html.Styled exposing (..)
10 | import Html.Styled.Attributes as Attr exposing (autofocus, css, href, placeholder, src, title, value)
11 | import Json.Decode as Json
12 | import Task
13 |
14 |
15 | defaultName : String
16 | defaultName =
17 | "Mysterious Stranger"
18 |
19 |
20 | gizmo : Gizmo.Program State Doc Msg
21 | gizmo =
22 | Gizmo.element
23 | { init = init
24 | , update = update
25 | , view = view
26 | , subscriptions = subscriptions
27 | }
28 |
29 |
30 | {-| Internal state not persisted to a document
31 | -}
32 | type alias State =
33 | {}
34 |
35 |
36 | {-| Document state
37 | -}
38 | type alias Doc =
39 | { title : Maybe String
40 | , imageData : Maybe String
41 | }
42 |
43 |
44 | {-| Message type for modifying State and Doc inside update
45 | -}
46 | type Msg
47 | = NoOp
48 |
49 |
50 | init : Gizmo.Flags -> ( State, Doc, Cmd Msg )
51 | init =
52 | always
53 | ( {}
54 | , { title = Nothing
55 | , imageData = Nothing
56 | }
57 | , Cmd.none
58 | )
59 |
60 |
61 | update : Msg -> Gizmo.Model State Doc -> ( State, Doc, Cmd Msg )
62 | update msg { state, doc } =
63 | case msg of
64 | NoOp ->
65 | ( state, doc, Cmd.none )
66 |
67 |
68 | subscriptions : Gizmo.Model State Doc -> Sub Msg
69 | subscriptions { state, doc } =
70 | Sub.none
71 |
72 |
73 | view : Gizmo.Model State Doc -> Html Msg
74 | view { state, doc } =
75 | let
76 | name =
77 | Maybe.withDefault "Mysterious Stranger" doc.title
78 | in
79 | case doc.imageData of
80 | Just data ->
81 | imageAvatar name data
82 |
83 | Nothing ->
84 | textAvatar <| defaultIfEmpty "Mysterious Stanger" name
85 |
86 |
87 | imageAvatar : String -> String -> Html Msg
88 | imageAvatar name imageSrc =
89 | div
90 | [ title name
91 | , css
92 | [ width (px 15)
93 | , height (px 15)
94 | , backgroundImage (url imageSrc)
95 | , backgroundPosition center
96 | , backgroundRepeat noRepeat
97 | , backgroundSize contain
98 | ]
99 | ]
100 | []
101 |
102 |
103 | textAvatar : String -> Html Msg
104 | textAvatar name =
105 | div
106 | [ title name
107 | , css
108 | [ width (px 15)
109 | , height (px 15)
110 | , borderRadius (pct 50)
111 | , border3 (px 1) solid (hex Colors.primary)
112 | , color (hex Colors.primary)
113 | , padding (px 0)
114 | , displayFlex
115 | , alignItems center
116 | , justifyContent center
117 | , fontSize (Css.em 0.5)
118 | , overflow hidden
119 | ]
120 | ]
121 | [ text <| initials name
122 | ]
123 |
124 |
125 | initials : String -> String
126 | initials name =
127 | name
128 | |> String.words
129 | |> List.map (String.left 1)
130 | |> String.join ""
131 |
132 |
133 | defaultIfEmpty : String -> String -> String
134 | defaultIfEmpty default str =
135 | if String.isEmpty str then
136 | default
137 |
138 | else
139 | str
140 |
--------------------------------------------------------------------------------
/src/elm/examples/SuperboxDefault.elm:
--------------------------------------------------------------------------------
1 | module SuperboxDefault exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Flags, Model)
5 | import Html.Styled as Html exposing (..)
6 | import Html.Styled.Attributes exposing (css)
7 | import Html.Styled.Events exposing (..)
8 | import Json.Encode as E
9 |
10 |
11 | gizmo : Gizmo.Program State Doc Msg
12 | gizmo =
13 | Gizmo.element
14 | { init = init
15 | , update = update
16 | , view = view
17 | , subscriptions = subscriptions
18 | }
19 |
20 |
21 | {-| Ephemeral state not saved to the doc
22 | -}
23 | type alias State =
24 | {}
25 |
26 |
27 | {-| Document state
28 | -}
29 | type alias Doc =
30 | { title : String }
31 |
32 |
33 | init : Flags -> ( State, Doc, Cmd Msg )
34 | init flags =
35 | ( {}
36 | , { title = "No title" }
37 | , Cmd.none
38 | )
39 |
40 |
41 | {-| Message type for modifying State and Doc inside update
42 | -}
43 | type Msg
44 | = NoOp
45 | | RequestEditMode
46 | | RequestSearchMode
47 |
48 |
49 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
50 | update msg { state, doc } =
51 | case msg of
52 | NoOp ->
53 | ( state
54 | , doc
55 | , Cmd.none
56 | )
57 |
58 | RequestEditMode ->
59 | ( state
60 | , doc
61 | , Gizmo.emit "editmode" E.null
62 | )
63 |
64 | RequestSearchMode ->
65 | ( state
66 | , doc
67 | , Gizmo.emit "searchmode" E.null
68 | )
69 |
70 |
71 | view : Model State Doc -> Html Msg
72 | view { doc } =
73 | div
74 | [ css
75 | [ width (pct 100)
76 | , position relative
77 | , alignItems center
78 | ]
79 | ]
80 | [ div
81 | [ onClick RequestSearchMode
82 | , css
83 | [ width (pct 100)
84 | , cursor pointer
85 | , flexGrow (num 1)
86 | ]
87 | ]
88 | [ text <| emptyWithDefault "No title" doc.title
89 | ]
90 | , div
91 | [ onClick RequestEditMode
92 | , css
93 | [ cursor pointer
94 | , position absolute
95 | , right (px 0)
96 | , top (px 0)
97 | ]
98 | ]
99 | [ text "📝"
100 | ]
101 | ]
102 |
103 |
104 | emptyWithDefault : String -> String -> String
105 | emptyWithDefault default str =
106 | if str == "" then
107 | default
108 |
109 | else
110 | str
111 |
112 |
113 | subscriptions : Model State Doc -> Sub Msg
114 | subscriptions model =
115 | Sub.none
116 |
--------------------------------------------------------------------------------
/src/elm/examples/SuperboxEdit.elm:
--------------------------------------------------------------------------------
1 | module SuperboxEdit exposing (Doc, Msg, State, gizmo)
2 |
3 | import Colors
4 | import Css exposing (..)
5 | import Gizmo exposing (Flags, Model)
6 | import Html.Styled as Html exposing (..)
7 | import Html.Styled.Attributes exposing (autofocus, css, value)
8 | import Html.Styled.Events exposing (..)
9 | import Json.Decode as D
10 | import Json.Encode as E
11 |
12 |
13 | gizmo : Gizmo.Program State Doc Msg
14 | gizmo =
15 | Gizmo.element
16 | { init = init
17 | , update = update
18 | , view = view
19 | , subscriptions = subscriptions
20 | }
21 |
22 |
23 | {-| Ephemeral state not saved to the doc
24 | -}
25 | type alias State =
26 | { title : Maybe String }
27 |
28 |
29 | {-| Document state
30 | -}
31 | type alias Doc =
32 | { title : String }
33 |
34 |
35 | init : Flags -> ( State, Doc, Cmd Msg )
36 | init flags =
37 | ( { title = Nothing }
38 | , { title = "No title" }
39 | , Cmd.none
40 | )
41 |
42 |
43 | {-| Message type for modifying State and Doc inside update
44 | -}
45 | type Msg
46 | = NoOp
47 | | SetTitle String
48 | | SaveTitle
49 |
50 |
51 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
52 | update msg { state, doc } =
53 | case msg of
54 | NoOp ->
55 | ( state
56 | , doc
57 | , Cmd.none
58 | )
59 |
60 | SetTitle title ->
61 | ( { state | title = Just title }
62 | , doc
63 | , Cmd.none
64 | )
65 |
66 | SaveTitle ->
67 | case state.title of
68 | Just title ->
69 | ( state
70 | , { doc | title = title }
71 | , Debug.log "Saving new title" Gizmo.emit "defaultmode" E.null
72 | )
73 |
74 | Nothing ->
75 | ( state
76 | , doc
77 | , Gizmo.emit "defaultmode" E.null
78 | )
79 |
80 |
81 | view : Model State Doc -> Html Msg
82 | view { state, doc } =
83 | input
84 | [ autofocus True
85 | , onInput SetTitle
86 | , onEnter SaveTitle
87 | , value <| Maybe.withDefault doc.title state.title
88 | , css
89 | [ width (pct 100)
90 | , margin zero
91 | , border zero
92 | , padding zero
93 | , backgroundColor transparent
94 | , textAlign center
95 | , fontSize (Css.em 1)
96 | , color (hex Colors.blueBlack)
97 | ]
98 | ]
99 | []
100 |
101 |
102 | onEnter : Msg -> Attribute Msg
103 | onEnter msg =
104 | on "keypress" (D.andThen (enter msg) keyCode)
105 |
106 |
107 | enter : msg -> Int -> D.Decoder msg
108 | enter msg keycode =
109 | if keycode == 13 then
110 | D.succeed msg
111 |
112 | else
113 | D.fail "Not the enter key"
114 |
115 |
116 | subscriptions : Model State Doc -> Sub Msg
117 | subscriptions model =
118 | Sub.none
119 |
--------------------------------------------------------------------------------
/src/elm/examples/Title.elm:
--------------------------------------------------------------------------------
1 | module GizmoTemplate exposing (Doc, Msg, State, gizmo)
2 |
3 | import Css exposing (..)
4 | import Gizmo exposing (Flags, Model)
5 | import Html.Styled as Html exposing (..)
6 | import Html.Styled.Attributes exposing (css, placeholder, value)
7 | import Html.Styled.Events exposing (..)
8 | import String
9 |
10 |
11 | gizmo : Gizmo.Program State Doc Msg
12 | gizmo =
13 | Gizmo.element
14 | { init = init
15 | , update = update
16 | , view = view
17 | , subscriptions = subscriptions
18 | }
19 |
20 |
21 | {-| Ephemeral state not saved to the doc
22 | -}
23 | type alias State =
24 | {}
25 |
26 |
27 | {-| Document state
28 | -}
29 | type alias Doc =
30 | { title : String
31 | }
32 |
33 |
34 | init : Flags -> ( State, Doc, Cmd Msg )
35 | init flags =
36 | ( {}
37 | , { title = "" }
38 | , Cmd.none
39 | )
40 |
41 |
42 | {-| Message type for modifying State and Doc inside update
43 | -}
44 | type Msg
45 | = NoOp
46 |
47 |
48 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
49 | update msg { state, doc } =
50 | case msg of
51 | NoOp ->
52 | ( state
53 | , doc
54 | , Cmd.none
55 | )
56 |
57 |
58 | view : Model State Doc -> Html Msg
59 | view { doc } =
60 | span
61 | []
62 | [ text
63 | (if String.isEmpty doc.title then
64 | "Untitled"
65 |
66 | else
67 | doc.title
68 | )
69 | ]
70 |
71 |
72 | subscriptions : Model State Doc -> Sub Msg
73 | subscriptions model =
74 | Sub.none
75 |
--------------------------------------------------------------------------------
/src/elm/examples/TitledMarkdownNote.elm:
--------------------------------------------------------------------------------
1 | module TitledMarkdownNote exposing (Doc, Msg, State, gizmo)
2 |
3 | import Colors exposing (..)
4 | import Config
5 | import Css exposing (..)
6 | import Css.Global as G
7 | import Gizmo exposing (Flags, Model)
8 | import Html.Styled as Html exposing (..)
9 | import Html.Styled.Attributes exposing (class, css, placeholder, value)
10 | import Html.Styled.Events exposing (..)
11 | import Json.Encode as Json
12 | import Markdown
13 |
14 |
15 | markdownStyles =
16 | [ G.class "MarkdownContainer"
17 | [ G.descendants
18 | [ G.everything
19 | [ fontFamilies [ "system-ui" ]
20 | ]
21 | , G.each
22 | [ G.typeSelector "h1"
23 | , G.typeSelector "h2"
24 | , G.typeSelector "h3"
25 | ]
26 | [ marginTop (px 24)
27 | , marginBottom (px 16)
28 | , lineHeight (num 1.25)
29 | ]
30 | , G.typeSelector "h1"
31 | [ fontSize (Css.em 2)
32 | , fontWeight bold
33 | ]
34 | , G.typeSelector "h2"
35 | [ fontSize (Css.em 1.5)
36 | , fontWeight bold
37 | ]
38 | , G.typeSelector "h2"
39 | [ fontSize (Css.em 1.25)
40 | , fontWeight bold
41 | ]
42 | , G.typeSelector "p"
43 | [ marginTop (px 0)
44 | , marginBottom (px 16)
45 | ]
46 | , G.each
47 | [ G.typeSelector "ul"
48 | , G.typeSelector "ol"
49 | ]
50 | [ paddingLeft (Css.em 2)
51 | , marginTop (px 0)
52 | , marginBottom (px 0)
53 | ]
54 | , G.typeSelector "ul"
55 | [ listStyle disc
56 | ]
57 | , G.typeSelector "li"
58 | [ property "word-wrap" "break-all"
59 | ]
60 | , G.selector "li+li"
61 | [ marginTop (Css.em 0.25)
62 | ]
63 | , G.typeSelector "em"
64 | [ fontStyle italic
65 | ]
66 | , G.typeSelector "bold"
67 | [ fontWeight bold
68 | ]
69 | ]
70 | ]
71 | ]
72 |
73 |
74 | gizmo : Gizmo.Program State Doc Msg
75 | gizmo =
76 | Gizmo.element
77 | { init = init
78 | , update = update
79 | , view = view
80 | , subscriptions = subscriptions
81 | }
82 |
83 |
84 | {-| Ephemeral state not saved to the doc
85 | -}
86 | type alias State =
87 | { isEditing : Bool
88 | }
89 |
90 |
91 | {-| Document state
92 | -}
93 | type alias Doc =
94 | { title : String
95 | , body : String
96 | }
97 |
98 |
99 | init : Flags -> ( State, Doc, Cmd Msg )
100 | init flags =
101 | ( { isEditing = False }
102 | , { title = "", body = "" }
103 | , Cmd.none
104 | )
105 |
106 |
107 | {-| Message type for modifying State and Doc inside update
108 | -}
109 | type Msg
110 | = NoOp
111 | | SetTitle String
112 | | SetBody String
113 | | ToggleEdit
114 |
115 |
116 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
117 | update msg { state, doc } =
118 | case msg of
119 | NoOp ->
120 | ( state
121 | , doc
122 | , Cmd.none
123 | )
124 |
125 | SetTitle title ->
126 | ( state
127 | , { doc | title = title }
128 | , Cmd.none
129 | )
130 |
131 | SetBody body ->
132 | ( state
133 | , { doc | body = body }
134 | , Cmd.none
135 | )
136 |
137 | ToggleEdit ->
138 | ( { state | isEditing = not state.isEditing }
139 | , doc
140 | , Cmd.none
141 | )
142 |
143 |
144 | textColor =
145 | hex "#333"
146 |
147 |
148 | view : Model State Doc -> Html Msg
149 | view { state, doc } =
150 | div
151 | [ css
152 | [ displayFlex
153 | , flexDirection column
154 | , padding (px 10)
155 | , backgroundColor (hex "fff")
156 | , fontFamilies [ "system-ui" ]
157 | ]
158 | ]
159 | [ div
160 | [ css
161 | [ displayFlex
162 | , flexDirection row
163 | , marginBottom (px 15)
164 | , borderBottom3 (px 1) solid (hex "#aaa")
165 | , paddingBottom (px 10)
166 | , alignItems center
167 | ]
168 | ]
169 | [ input
170 | [ css
171 | [ border zero
172 | , flexGrow (num 1)
173 | , fontSize (Css.em 1.5)
174 | , color textColor
175 | ]
176 | , onInput SetTitle
177 | , value doc.title
178 | , placeholder "Title"
179 | ]
180 | []
181 | , span
182 | [ onClick ToggleEdit
183 | , css
184 | [ color (hex Colors.primary)
185 | , cursor pointer
186 | , padding (px 2)
187 | , hover
188 | [ color (hex Colors.darkerPrimary)
189 | ]
190 | ]
191 | ]
192 | [ text
193 | (if state.isEditing then
194 | "View"
195 |
196 | else
197 | "Edit"
198 | )
199 | ]
200 | ]
201 | , if state.isEditing then
202 | textarea
203 | [ css
204 | [ flexGrow (num 1)
205 | , border zero
206 | , width (pct 100)
207 | , height (vh 80)
208 | , fontSize (Css.em 1)
209 | , fontFamilies [ "Lucida Console" ]
210 | , color textColor
211 | ]
212 | , onInput SetBody
213 | , value doc.body
214 | , placeholder "Your note here..."
215 | ]
216 | []
217 |
218 | else
219 | div
220 | [ class "MarkdownContainer"
221 | ]
222 | [ G.global markdownStyles
223 | , Html.fromUnstyled <| Markdown.toHtml [] doc.body
224 | ]
225 | ]
226 |
227 |
228 | subscriptions : Model State Doc -> Sub Msg
229 | subscriptions model =
230 | Sub.none
231 |
--------------------------------------------------------------------------------
/src/elm/examples/TodoList.elm:
--------------------------------------------------------------------------------
1 | module TodoList exposing (Doc, Msg, State, gizmo)
2 |
3 | import Array exposing (Array)
4 | import Extra.Array exposing (remove)
5 | import Css exposing (..)
6 | import Extra.Array as Array
7 | import Gizmo exposing (Flags, Model)
8 | import Html.Styled as Html exposing (Html, button, div, input, text)
9 | import Html.Styled.Attributes as Attr exposing (checked, css, value, id)
10 | import Html.Styled.Events as Events exposing (onCheck, onClick, onInput)
11 | import Keyboard exposing (Combo(..))
12 | import Browser.Dom as Dom
13 | import Task exposing (Task)
14 |
15 |
16 | import Random exposing (Generator, list, int)
17 |
18 | randomString : Int -> Generator String
19 | randomString stringLength =
20 | Random.map String.fromList <| Random.list stringLength randomChar
21 |
22 | randomChar : Generator Char
23 | randomChar =
24 | Random.map (\n -> Char.fromCode (n + 65)) (int 0 51)
25 |
26 | gizmo : Gizmo.Program State Doc Msg
27 | gizmo =
28 | Gizmo.element
29 | { init = init
30 | , update = update
31 | , view = view
32 | , subscriptions = subscriptions
33 | }
34 |
35 | subscriptions : Model State Doc -> Sub Msg
36 | subscriptions { state } =
37 | Sub.none
38 |
39 | {-| Ephemeral state not saved to the doc
40 | -}
41 | type alias State =
42 | {}
43 |
44 | type alias Todo =
45 | { done : Bool
46 | , id : String
47 | , title : String
48 | }
49 |
50 |
51 | {-| Document state
52 | -}
53 | type alias Doc =
54 | { todos : Array Todo
55 | }
56 |
57 |
58 | init : Flags -> ( State, Doc, Cmd Msg )
59 | init flags =
60 | ( {}
61 | , { todos = Array.empty
62 | }
63 | , Cmd.none
64 | )
65 |
66 | {-| Message type for modifying State and Doc inside update
67 | -}
68 | type Msg
69 | = SetDone String Bool
70 | | SetTitle String String
71 | | CreateTodo
72 | | NewTodo String
73 | | DeleteTodo String
74 | | NoOp
75 |
76 |
77 | update : Msg -> Model State Doc -> ( State, Doc, Cmd Msg )
78 | update msg { state, doc } =
79 | case msg of
80 | SetDone id done ->
81 | ( state, doc |> updateTodo id (setDone done), Cmd.none )
82 |
83 | SetTitle id title ->
84 | ( state, doc |> updateTodo id (setTitle title), Cmd.none )
85 |
86 |
87 | CreateTodo ->
88 | ( state, doc, Random.generate NewTodo (randomString 7))
89 |
90 | NewTodo id ->
91 | let newTodo = emptyTodo id
92 | in
93 | ( state
94 | , doc |> pushTodo newTodo
95 | , Task.attempt (\_ -> NoOp) (focusTodo newTodo.id)
96 | )
97 |
98 | DeleteTodo id ->
99 | ( state
100 | , doc |> Debug.log "delete" (deleteTodo id)
101 | , Task.attempt (\_ -> NoOp) (focusTodo id)
102 | )
103 |
104 | NoOp -> ( state, doc, Cmd.none )
105 |
106 |
107 |
108 | setDone : Bool -> Todo -> Todo
109 | setDone done todo =
110 | { todo | done = done }
111 |
112 |
113 | setTitle : String -> Todo -> Todo
114 | setTitle title todo =
115 | { todo | title = title }
116 |
117 |
118 | emptyTodo : String -> Todo
119 | emptyTodo id =
120 | { title = ""
121 | , id = id
122 | , done = False
123 | }
124 |
125 | updateElement : String -> (Todo -> Todo) -> Array Todo -> Array Todo
126 | updateElement id fn list =
127 | let
128 | toggle todo =
129 | if todo.id == id then
130 | fn todo
131 | else
132 | todo
133 | in
134 | Array.map toggle list
135 |
136 | updateTodo : String -> (Todo -> Todo) -> Doc -> Doc
137 | updateTodo id fn doc =
138 | { doc | todos = doc.todos |> updateElement id fn }
139 |
140 |
141 | deleteTodo : String -> Doc -> Doc
142 | deleteTodo id doc =
143 | { doc | todos = doc.todos |> Array.filter (\e -> e.id /= id) }
144 |
145 | pushTodo : Todo -> Doc -> Doc
146 | pushTodo todo doc =
147 | { doc | todos = doc.todos |> Array.push todo }
148 |
149 | focusTodo id =
150 | Dom.focus ("task-" ++ id)
151 |
152 |
153 | view : Model State Doc -> Html Msg
154 | view { doc } =
155 | div
156 | [ css
157 | [ property "display" "grid"
158 | , property "grid-template-rows" "1fr auto"
159 | , height (pct 100)
160 | ]
161 | ]
162 | [ div
163 | [ css
164 | [ padding2 (px 10) (px 5)
165 | , borderRadius (px 3)
166 | ]
167 | ]
168 | (doc.todos |> Array.map viewTodo |> Array.toList)
169 | , viewNewButton
170 | ]
171 |
172 |
173 | viewTodo : Todo -> Html Msg
174 | viewTodo { title, id, done } =
175 | let todoId = randomString 7
176 | in
177 | div
178 | [ css
179 | [ property "display" "grid"
180 | , property "grid-template-columns" "auto 1fr"
181 | ]
182 | ]
183 | [ input [ Attr.type_ "checkbox", checked done, onCheck (SetDone id) ] []
184 | , input
185 | [ Attr.id ("task-" ++ id)
186 | , onInput (SetTitle id)
187 | , if String.length title == 0 then
188 | Keyboard.onUp Backspace (DeleteTodo id)
189 | else
190 | Keyboard.onPress Enter CreateTodo
191 | , value title
192 | , css
193 | [ property "-webkit-appearance" "none"
194 | , border (px 0)
195 | , fontFamily inherit
196 | , fontSize inherit
197 | , if done then
198 | textDecoration lineThrough
199 |
200 | else
201 | textDecoration none
202 | , focus
203 | [ borderBottom3 (px 1) solid (hex "ddd")
204 | ]
205 | ]
206 | ]
207 | []
208 | ]
209 |
210 |
211 | viewNewButton : Html Msg
212 | viewNewButton =
213 | div
214 | [ onClick CreateTodo
215 | , css
216 | [ cursor pointer
217 | , textAlign center
218 | , padding (px 10)
219 | ]
220 | ]
221 | [ text "+ Create Todo" ]
222 |
--------------------------------------------------------------------------------
/src/elm/vendor/Csv.elm:
--------------------------------------------------------------------------------
1 | module Csv exposing
2 | ( Csv
3 | , parse
4 | , parseWith
5 | )
6 |
7 | {-| A parser for transforming CSV strings into usable input.
8 |
9 | This library does its best to support RFC 4180, however, many CSV inputs do not strictly follow the standard. There are two major deviations assumed in this library.
10 |
11 | 1. The `\n` or `\r` character may be used instead of `\r\n` for line separators.
12 | 2. The trailing new-line may be omitted.
13 |
14 | RFC 4180 grammar, for reference, with notes.
15 |
16 | The trailing newline is required, but we'll make it optional.
17 |
18 | file =
19 | [ header CRLF ] record * CRLF record [ CRLF ]
20 |
21 | header =
22 | name * COMMA name
23 |
24 | record =
25 | field * COMMA field
26 |
27 | name =
28 | field
29 |
30 | field =
31 | escaped / non - escaped
32 |
33 | There is no room for spaces around the quotes. The specification is that
34 |
35 | escaped =
36 | DQUOTE * (TEXTDATA / COMMA / CR / LF / 2 DQUOTE) DQUOTE
37 |
38 | In this specification, fields that don't have quotes surrounding them cannot have a quote inside them because it is excluded from `TEXTDATA`.
39 |
40 | non-escaped = *TEXTDATA
41 | COMMA = %x2C
42 | CR = %x0D ;as per section 6.1 of RFC 2234 [2]
43 | DQUOTE = %x22 ;as per section 6.1 of RFC 2234 [2]
44 | LF = %x0A ;as per section 6.1 of RFC 2234 [2]
45 |
46 | The spec requires that new lines be `CR + LF` but we'll let them get away with just `LF` if they want..
47 |
48 | CRLF = CR LF ;as per section 6.1 of RFC 2234 [2]
49 |
50 | All the printable characters minus the double-quote and comma, this is important above.
51 |
52 | TEXTDATA = %x20-21 / %x23-2B / %x2D-7E
53 |
54 |
55 | # Types
56 |
57 | @docs Csv
58 |
59 |
60 | # Functions
61 |
62 | @docs parse
63 | @docs parseWith
64 |
65 | -}
66 |
67 | import Parser
68 | exposing
69 | ( (|.)
70 | , (|=)
71 | , Parser
72 | , Step(..)
73 | , andThen
74 | , backtrackable
75 | , chompIf
76 | , chompWhile
77 | , float
78 | , getChompedString
79 | , keyword
80 | , lazy
81 | , loop
82 | , oneOf
83 | , run
84 | , succeed
85 | , symbol
86 | )
87 | import Result
88 | import String
89 |
90 |
91 | {-| Represents a CSV document. All CSV documents are have a header row, even if that row is empty.
92 | -}
93 | type alias Csv =
94 | { headers : List String
95 | , records : List (List String)
96 | }
97 |
98 |
99 | {-| Parse a CSV string into it's constituent fields, using comma for separator.
100 | -}
101 | parse : String -> Result (List Parser.DeadEnd) Csv
102 | parse s =
103 | parseWith ',' s
104 |
105 |
106 | {-| Parse a CSV string into it's constituent fields, using the passed Char as separator.
107 | -}
108 | parseWith : Char -> String -> Result (List Parser.DeadEnd) Csv
109 | parseWith c =
110 | addTrailingLineSep
111 | >> Parser.run (file c)
112 |
113 |
114 | {-| Gets the third element of a tuple.
115 | -}
116 | thrd : ( a, b, c ) -> c
117 | thrd ( _, _, c ) =
118 | c
119 |
120 |
121 | crs =
122 | "\u{000D}"
123 |
124 |
125 | crc =
126 | '\u{000D}'
127 |
128 |
129 | {-| Adds a trailing line separator to a string if not present.
130 | -}
131 | addTrailingLineSep : String -> String
132 | addTrailingLineSep str =
133 | if not (String.endsWith "\n" str || String.endsWith crs str) then
134 | str ++ crs ++ "\n"
135 |
136 | else
137 | str
138 |
139 |
140 | comma : Parser ()
141 | comma =
142 | symbol ","
143 |
144 |
145 | doubleQuote : Parser ()
146 | doubleQuote =
147 | symbol "\""
148 |
149 |
150 | cr : Parser ()
151 | cr =
152 | symbol crs
153 |
154 |
155 | lf : Parser ()
156 | lf =
157 | symbol "\n"
158 |
159 |
160 | lineSep : Parser ()
161 | lineSep =
162 | -- Prefer the multi-character code, but accept others.
163 | oneOf
164 | [ backtrackable <| cr |. lf
165 | , cr
166 | , lf
167 | ]
168 |
169 |
170 | doubleDoubleQuote : Parser ()
171 | doubleDoubleQuote =
172 | doubleQuote |. doubleQuote
173 |
174 |
175 | textData : Char -> Parser ()
176 | textData sepChar =
177 | chompIf <| textChar sepChar
178 |
179 |
180 | textChar : Char -> Char -> Bool
181 | textChar sepChar c =
182 | not (List.member c [ '"', sepChar, '\n', crc ])
183 |
184 |
185 | nonEscaped : Char -> Parser String
186 | nonEscaped sepChar =
187 | getChompedString (chompWhile (textChar sepChar))
188 |
189 |
190 | innerChar : Char -> Parser String
191 | innerChar sepChar =
192 | Parser.map (String.replace "\"\"" "\"") <|
193 | backtrackable <|
194 | getChompedString
195 | (oneOf [ textData sepChar, comma, cr, lf, doubleDoubleQuote ])
196 |
197 |
198 | innerString : Char -> List String -> Parser (Step (List String) String)
199 | innerString sepChar strs =
200 | oneOf
201 | [ succeed (\str -> Loop (str :: strs)) |= innerChar sepChar
202 | , succeed ()
203 | |> Parser.map (\_ -> Done (String.concat (List.reverse strs)))
204 | ]
205 |
206 |
207 | escaped : Char -> Parser String
208 | escaped sepChar =
209 | succeed identity
210 | |. doubleQuote
211 | |= loop [] (innerString sepChar)
212 | |. doubleQuote
213 |
214 |
215 | field : Char -> Parser String
216 | field sepChar =
217 | oneOf [ escaped sepChar, nonEscaped sepChar ]
218 |
219 |
220 | name : Char -> Parser String
221 | name sepChar =
222 | field sepChar
223 |
224 |
225 | recordHelper : Char -> List String -> Parser (Step (List String) (List String))
226 | recordHelper sepChar strs =
227 | oneOf
228 | [ backtrackable <|
229 | succeed (\str -> Loop (str :: strs))
230 | |= field sepChar
231 | |. symbol (String.fromChar sepChar)
232 | , succeed (\str -> Done (List.reverse (str :: strs)))
233 | |= field sepChar
234 | |. lineSep
235 | ]
236 |
237 |
238 | record : Char -> Parser (List String)
239 | record sepChar =
240 | loop [] (recordHelper sepChar)
241 |
242 |
243 | recordsHelper : Char -> List (List String) -> Parser (Step (List (List String)) (List (List String)))
244 | recordsHelper sepChar records =
245 | oneOf
246 | [ succeed (\rec -> Loop (rec :: records))
247 | |= record sepChar
248 | , succeed ()
249 | |> Parser.map (\_ -> Done (List.reverse records))
250 | ]
251 |
252 |
253 | file : Char -> Parser Csv
254 | file sepChar =
255 | succeed Csv
256 | |= record sepChar
257 | |= loop [] (recordsHelper sepChar)
258 |
--------------------------------------------------------------------------------
/src/hypermerge-devtools/main.ts:
--------------------------------------------------------------------------------
1 | type ExtensionPanel = chrome.devtools.panels.ExtensionPanel
2 |
3 | createPanelIfRepo()
4 |
5 | chrome.devtools.network.onNavigated.addListener(() => {
6 | createPanelIfRepo()
7 | })
8 |
9 | let panelCreated = false
10 | async function createPanelIfRepo() {
11 | if (panelCreated) return
12 |
13 | const hasRepo = await evalIn("main", `"repo" in window`)
14 |
15 | if (!hasRepo || panelCreated) return
16 | const panel = await createPanel("Hypermerge", "index.html")
17 | panelCreated = true
18 | }
19 |
20 | function evalIn(ctx: "main" | "worker", expr: string): Promise {
21 | return new Promise((res, rej) => {
22 | chrome.devtools.inspectedWindow.eval(expr, {}, (result, except) => {
23 | if (except.isException || except.isError) {
24 | return rej(except)
25 | }
26 | res(result)
27 | })
28 | })
29 | }
30 |
31 | function createPanel(name: string, path: string): Promise {
32 | return new Promise((res, rej) => {
33 | chrome.devtools.panels.create("Hypermerge", "", "index.html", panel => {
34 | if (panel) return res(panel)
35 | rej()
36 | })
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/src/hypermerge-devtools/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hypermerge-devtools",
3 | "version": "1.0",
4 | "minimum_chrome_version": "66",
5 | "devtools_page": "main.html",
6 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
7 | "web_accessible_resources": ["main.html", "panel.html"]
8 | }
9 |
--------------------------------------------------------------------------------
/src/hypermerge-devtools/panel.ts:
--------------------------------------------------------------------------------
1 | chrome.devtools.inspectedWindow.eval(
2 | "console.log('testing')",
3 | (result, except) => {
4 | if (except.isException) {
5 | console.log("exception")
6 | }
7 | },
8 | )
9 |
--------------------------------------------------------------------------------
/src/js/App.ts:
--------------------------------------------------------------------------------
1 | import Repo from "./Repo"
2 | import Compiler from "./Compiler"
3 | import * as Gizmo from "./Gizmo"
4 | import * as FarmUrl from "./FarmUrl"
5 | import * as GizmoWindow from "./GizmoWindow"
6 | import * as Bs from "./bootstrap"
7 | import * as Workspace from "./bootstrap/Workspace"
8 | import * as Draggable from "./Draggable"
9 |
10 | require("utp-native")
11 | require("iltorb")
12 |
13 | const REPO_ROOT = process.env.REPO_ROOT || ""
14 |
15 | // make the web worker thread-safe:
16 | ;(process).dlopen = () => {
17 | throw new Error("Load native module is not thread-safe")
18 | }
19 |
20 | export default class App {
21 | repo = new Repo("./repo.worker.js")
22 | compiler: Compiler = new Compiler(this.repo, "./compile.worker.js")
23 | root?: HTMLElement
24 |
25 | selfDataUrl = load("selfDataUrl", () => Workspace.identityData(this.repo))
26 | registryUrl = load("rootRegistryUrl", () => Workspace.registryData(this.selfDataUrl, this.repo))
27 | rootCodeUrl = load("rootCodeUrl", () => Workspace.code(this.selfDataUrl, this.repo))
28 | rootDataUrl = load("rootDataUrl", () => Workspace.data(this.selfDataUrl, this.repo))
29 |
30 | constructor() {
31 | ;(self as any).repo = this.repo
32 | Gizmo.setRepo(this.repo)
33 | Gizmo.setCompiler(this.compiler)
34 | Gizmo.setSelfDataUrl(this.selfDataUrl)
35 | Compiler.setSelfDataUrl(this.selfDataUrl)
36 |
37 | customElements.define("farm-draggable", Draggable.constructorForWindow(window))
38 | customElements.define("farm-ui", Gizmo.constructorForWindow(window))
39 | customElements.define(
40 | "farm-window",
41 | GizmoWindow.constructorForWindow(window),
42 | )
43 |
44 | // Deprecated
45 | customElements.define("realm-ui", Gizmo.constructorForWindow(window))
46 | customElements.define(
47 | "realm-window",
48 | GizmoWindow.constructorForWindow(window),
49 | )
50 |
51 | // XXX: Elm-compatible DataTransfer
52 | Object.defineProperty(DataTransfer.prototype, "elmFiles", {
53 | get() {
54 | return Array.prototype.map.call(this.items, (item: DataTransferItem) => {
55 | const file = item.getAsFile()
56 | if (file) {
57 | return file
58 | } else {
59 | return new File([this.getData(item.type)], item.type, { type: item.type })
60 | }
61 | })
62 | }
63 | })
64 |
65 |
66 | const style = document.createElement("style")
67 | style.innerHTML = css()
68 | document.body.appendChild(style)
69 |
70 | this.repo.setRegistry(this.registryUrl).then(() => {
71 | this.root = document.createElement("farm-ui")
72 | this.root.setAttribute("code", this.rootCodeUrl)
73 | this.root.setAttribute("data", this.rootDataUrl)
74 | document.body.appendChild(this.root)
75 | })
76 | }
77 |
78 | handleUrl(url: string) {
79 | this.root && (this.root).navigateTo(url)
80 | }
81 |
82 | bootstrap(name: string) {
83 | const mkCode = (Workspace)[name]
84 | const mkData =
85 | (Workspace)[name + "Data"] || ((repo: Repo) => repo.create())
86 |
87 | if (!mkCode)
88 | throw new Error(
89 | `Could not find gizmo named "${name}". Check Workspace.ts`,
90 | )
91 | const code = mkCode(this.repo)
92 | const data = mkData(this.repo)
93 | const farm = FarmUrl.create({ code, data })
94 |
95 | console.log("\n\ncode url:", code, "\n\n")
96 | console.log("\n\ndata url:", data, "\n\n")
97 | console.log("\n\nfarm url:", farm, "\n\n")
98 |
99 | return { code, data, farm }
100 | }
101 |
102 | createCode(file: string, options: Bs.Opts) {
103 | return Bs.createCode(this.selfDataUrl, this.repo, file, options)
104 | }
105 | }
106 |
107 | function load(key: string, def: () => string): string {
108 | key = REPO_ROOT + key
109 | if (localStorage[key]) return localStorage[key]
110 | const value = def()
111 | localStorage[key] = value
112 | return value
113 | }
114 |
115 | function css(): string {
116 | return `
117 | @import url('https://fonts.googleapis.com/css?family=IBM+Plex+Sans:300,300i,400,400i,700,700i');
118 |
119 | * {
120 | box-sizing: border-box;
121 | }
122 |
123 | html, body, div, span, applet, object, iframe,
124 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
125 | a, abbr, acronym, address, big, cite, code,
126 | del, dfn, em, font, img, ins, kbd, q, s, samp,
127 | small, strike, strong, sub, sup, tt, var,
128 | dl, dt, dd, ol, ul, li,
129 | fieldset, form, label, legend,
130 | table, caption, tbody, tfoot, thead, tr, th, td {
131 | margin: 0;
132 | padding: 0;
133 | border: 0;
134 | outline: 0;
135 | font-weight: inherit;
136 | font-style: inherit;
137 | font-size: 100%;
138 | font-family: inherit;
139 | vertical-align: baseline;
140 | }
141 | /* remember to define focus styles! */
142 | :focus {
143 | outline: 0;
144 | }
145 | body {
146 | line-height: 1;
147 | color: black;
148 | background: white;
149 | font-family: 'IBM Plex Sans', Helvetica, Arial, system-ui, sans-serif;
150 | }
151 | ol, ul {
152 | list-style: none;
153 | }
154 | /* tables still need 'cellspacing="0"' in the markup */
155 | table {
156 | border-collapse: separate;
157 | border-spacing: 0;
158 | }
159 | caption, th, td {
160 | text-align: left;
161 | font-weight: normal;
162 | }
163 | blockquote:before, blockquote:after,
164 | q:before, q:after {
165 | content: "";
166 | }
167 | blockquote, q {
168 | quotes: "" "";
169 | }
170 |
171 | farm-ui {
172 | display: contents;
173 | }
174 | `
175 | }
176 |
--------------------------------------------------------------------------------
/src/js/AsyncQueue.ts:
--------------------------------------------------------------------------------
1 | import Debug, { IDebugger } from "debug"
2 |
3 | export default class AsyncQueue {
4 | queue: T[] = []
5 | subscription?: (item: T) => void
6 | log: IDebugger
7 |
8 | constructor(name: string = "unknown") {
9 | this.log = Debug(`queue:${name}`)
10 | }
11 |
12 | push = (item: T): this => {
13 | if (this.subscription) {
14 | this.log("push subbed", item)
15 | this.subscription(item)
16 | delete this.subscription
17 | } else {
18 | this.log("push queued", item)
19 | this.queue.push(item)
20 | }
21 | return this
22 | }
23 |
24 | take = (subscriber: (item: T) => void): this => {
25 | if (this.subscription) {
26 | throw new Error("only one subscriber at a time to a queue")
27 | }
28 |
29 | this.log("subscribed")
30 |
31 | if (this.queue.length) {
32 | subscriber(this.queue.shift()!)
33 | } else {
34 | this.subscription = subscriber
35 | }
36 |
37 | return this
38 | }
39 |
40 | unsubscribe = (): this => {
41 | this.log("unsubscribed")
42 |
43 | this.subscription = undefined
44 | return this
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/js/Author.ts:
--------------------------------------------------------------------------------
1 | export function recordAuthor(author: string, authors: string[]) {
2 | authors = authors || []
3 | if (author && !authors.includes(author)) {
4 | authors.push(author)
5 | }
6 | return authors
7 | }
--------------------------------------------------------------------------------
/src/js/Bot.ts:
--------------------------------------------------------------------------------
1 | import Repo from "./Repo"
2 | import { Handle } from "hypermerge/dist/Handle"
3 | import { whenChanged } from "./Subscription"
4 | import Compiler from "./Compiler"
5 | import ElmGizmo from "./ElmGizmo"
6 | import * as Code from "./Code"
7 |
8 | export default class Bot {
9 | static set repo(repo: Repo) {
10 | ElmGizmo.repo = repo
11 | }
12 |
13 | static set compiler(compiler: Compiler) {
14 | ElmGizmo.compiler = compiler
15 | }
16 |
17 | static set selfDataUrl(selfDataUrl: string) {
18 | ElmGizmo.selfDataUrl = selfDataUrl
19 | }
20 |
21 | gizmo?: ElmGizmo
22 | source?: Handle
23 | codeUrl: string
24 | dataUrl: string
25 | repo = ElmGizmo.repo
26 |
27 | constructor(codeUrl: string, dataUrl: string) {
28 | this.codeUrl = codeUrl
29 | this.dataUrl = dataUrl
30 | }
31 |
32 | start() {
33 | this.source = this.repo.open(this.codeUrl)
34 | ElmGizmo.compiler.add(this.codeUrl)
35 |
36 | this.source.subscribe(
37 | whenChanged(
38 | doc => doc.outputHash,
39 | async (outputHash, doc) => {
40 | const source = await Code.source(this.repo, doc)
41 | this.remount(toElm(eval(source)), doc)
42 | },
43 | ),
44 | )
45 | }
46 |
47 | stop() {
48 | if (this.source) {
49 | this.source.close()
50 | delete this.source
51 | }
52 | }
53 |
54 | remount(elm: any, codeDoc: any) {
55 | this.unmount()
56 | this.mount(elm, codeDoc)
57 | }
58 |
59 | mount(elm: any, codeDoc: any) {
60 | this.repo.once(this.dataUrl, (doc: any) => {
61 | this.gizmo = new ElmGizmo(null, elm, {
62 | code: this.codeUrl,
63 | data: this.dataUrl,
64 | config: codeDoc.config,
65 | doc,
66 | all: {
67 | code: this.codeUrl,
68 | data: this.dataUrl,
69 | },
70 | })
71 | })
72 | }
73 |
74 | unmount() {
75 | if (this.gizmo) {
76 | this.gizmo.close()
77 | delete this.gizmo
78 | }
79 | }
80 | }
81 |
82 | function toElm(code: string) {
83 | return Object.values(eval(code))[0]
84 | }
85 |
--------------------------------------------------------------------------------
/src/js/Code.ts:
--------------------------------------------------------------------------------
1 | import Repo from "./Repo"
2 |
3 | export async function source(repo: Repo, doc: any): Promise {
4 | if (doc.outputUrl) {
5 | const { text } = await repo.readFile(doc.outputUrl)
6 | return text
7 | } else if (doc["Source.js"]) {
8 | return doc["Source.js"]
9 | } else {
10 | throw new Error("No source available")
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/js/Compiler.ts:
--------------------------------------------------------------------------------
1 | import QueuedWorker from "./QueuedWorker"
2 | import FakeWorker from "./FakeWorker"
3 | import * as Msg from "./Msg"
4 | import Repo from "./Repo"
5 | import { whenChanged } from "./Subscription"
6 | import { sha1 } from "./Digest"
7 | import * as Author from "./Author"
8 | import { TextEncoder } from "text-encoding"
9 |
10 | type CompileWorker = FakeWorker
11 |
12 | const PERSIST = "PERSIST" in process.env
13 | const encoder = new TextEncoder()
14 |
15 | export default class Compiler {
16 | static selfDataUrl: string
17 |
18 | static setSelfDataUrl(url: string) {
19 | Compiler.selfDataUrl = url
20 | }
21 |
22 | worker: CompileWorker
23 | repo: Repo
24 | docUrls: Set = new Set()
25 |
26 | constructor(repo: Repo, url: string) {
27 | this.repo = repo
28 | this.worker = new FakeWorker(url)
29 | //this.worker = new QueuedWorker(url)
30 |
31 | this.worker.subscribe(msg => {
32 | this.repo.change(msg.url, (state: any) => {
33 | switch (msg.t) {
34 | case "Compiled":
35 | delete state.error
36 | delete state.hypermergeFsDiagnostics
37 |
38 | if (state.outputHash === msg.outputHash) {
39 | console.log("Compiled output was identitical. Ignoring.")
40 | return
41 | }
42 |
43 | this.log(msg.url, "Compilation successful. Writing to doc.")
44 |
45 | // XXX: Skip setting the lastEditTimestamp if this is the
46 | // very first compile. We already set it when we bootstrap
47 | // the code doc. Should probably do something smart with
48 | // a `timeCreated` and `timeLastModified`.
49 | if (state.outputHash) {
50 | state.lastEditTimestamp = Date.now()
51 | }
52 | if (Compiler.selfDataUrl) {
53 | state.authors = Author.recordAuthor(Compiler.selfDataUrl,state.authors)
54 | }
55 |
56 | state.sourceHash = msg.sourceHash
57 | state.outputHash = msg.outputHash
58 |
59 | const outputUrl = repo.writeFile(
60 | encoder.encode(msg.output),
61 | "text/plain",
62 | )
63 |
64 | state.outputUrl = outputUrl
65 |
66 | return
67 |
68 | case "CompileError":
69 | if (state.error === msg.error) {
70 | console.log("Compile error is already in doc. Ignoring.")
71 | return
72 | }
73 | state.error = msg.error
74 | state.sourceHash = msg.sourceHash
75 |
76 | this.log(msg.url, "Compile error:", msg.url)
77 |
78 | state.hypermergeFsDiagnostics = produceDiagnosticsFromMessage(
79 | msg.error,
80 | )
81 | return
82 | }
83 | })
84 | })
85 | }
86 |
87 | add(url: string): this {
88 | if (this.docUrls.has(url)) return this
89 |
90 | this.docUrls.add(url)
91 |
92 | this.repo.open(url).subscribe(async doc => {
93 | const source = getElmSource(doc)
94 | if (!source) return
95 |
96 | const sourceHash = await hashSource(doc)
97 | const persist = PERSIST && doc.persist
98 |
99 | if (sourceHash === doc.sourceHash) {
100 | if (persist) {
101 | this.log(url, "Source is unchanged, but PERSIST is set. Compiling...")
102 | } else {
103 | console.log("Source is unchanged, skipping compile.")
104 | return
105 | }
106 | } else {
107 | this.log(url, "Source has changed. Sending to compiler...")
108 | }
109 |
110 | this.worker.send({
111 | t: "Compile",
112 | url,
113 | source,
114 | sourceHash,
115 | outputHash: doc.outputHash,
116 | config: doc.config || {},
117 | debug: doc.debug,
118 | persist,
119 | })
120 | })
121 |
122 | return this
123 | }
124 |
125 | log(url: string, ...args: string[]): void {
126 | const tag = url.replace("hypermerge:/", "").slice(0, 5)
127 | console.log(`[${tag}]`, ...args)
128 | }
129 |
130 | terminate() {
131 | this.worker.terminate()
132 | }
133 | }
134 |
135 | function rootError(filename: string, ...messages: string[]) {
136 | return {
137 | [filename]: messages.map(message => ({
138 | severity: "error",
139 | message,
140 | startLine: 0,
141 | startColumn: 0,
142 | endLine: 0,
143 | endColumn: 1,
144 | })),
145 | }
146 | }
147 |
148 | const getElmSource = (doc: any): string | undefined =>
149 | doc["Source.elm"] || doc["source.elm"]
150 |
151 | async function hashSource(doc: any): Promise {
152 | const extra = JSON.stringify({
153 | debug: doc.debug,
154 | config: doc.config,
155 | })
156 | return sha1(extra + getElmSource(doc))
157 | }
158 |
159 | function produceDiagnosticsFromMessage(error: string) {
160 | // first line is bogus:
161 | const jsonString = error.substring(error.indexOf("\n") + 1)
162 | let json
163 | try {
164 | json = JSON.parse(jsonString)
165 | } catch (e) {
166 | const snippedError = jsonString.slice(0, 500)
167 | console.groupCollapsed("Compiler error is not valid JSON")
168 | console.error(e)
169 | console.log("Attempting to parse this string:")
170 | console.log(snippedError)
171 | console.groupEnd()
172 |
173 | let message = "The compiler threw an error:\n\n" + snippedError
174 |
175 | if (snippedError.includes("elm ENOENT")) {
176 | message =
177 | "It looks like your elm npm package broke.\n" +
178 | "Try running `yarn add elm && yarn remove elm` " +
179 | "in the farm project root.\n\n" +
180 | message
181 | }
182 |
183 | return rootError("Source.elm", message)
184 | }
185 |
186 | const messageReformat = (message: any[]) =>
187 | message
188 | .map(
189 | (message: any) =>
190 | typeof message === "string" ? message : "" + message.string + "", // VSCode still needs to add formatting
191 | )
192 | .join("")
193 |
194 | if (json.type === "error") {
195 | return rootError("Source.elm", messageReformat(json.message))
196 | }
197 |
198 | const nestedProblems = json.errors.map((error: any) =>
199 | error.problems.map((problem: any) => {
200 | return {
201 | severity: "error",
202 | message: messageReformat(problem.message),
203 | startLine: problem.region.start.line - 1,
204 | startColumn: problem.region.start.column - 1,
205 | endLine: problem.region.end.line - 1,
206 | endColumn: problem.region.end.column - 1,
207 | }
208 | }),
209 | )
210 |
211 | console.log(nestedProblems)
212 | return { "Source.elm": [].concat(...nestedProblems) }
213 | }
214 |
--------------------------------------------------------------------------------
/src/js/Diff.ts:
--------------------------------------------------------------------------------
1 | import odiff from "odiff"
2 | import { update } from "lodash"
3 |
4 | // not exported by odiff:
5 | export interface Change {
6 | type: "set" | "unset" | "add" | "rm"
7 | path: Array
8 | val: any
9 | index: number
10 | vals: any[]
11 | num: number
12 | }
13 |
14 | export function apply(lhs: any, rhs: any) {
15 | return applyChanges(lhs, getChanges(lhs, rhs))
16 | }
17 |
18 | export function getChanges(lhs: any, rhs: any): Change[] {
19 | return odiff(lhs, rhs)
20 | }
21 |
22 | export function applyChanges(v: any, changes: Change[]) {
23 | for (let i = 0, l = changes.length; i < l; i++) {
24 | applyChange(v, changes[i])
25 | }
26 | }
27 |
28 | export function applyChange(root: any, ch: Change) {
29 | const key: any = ch.path.pop()
30 | let obj: any = root
31 |
32 | // handles empty keypath:
33 | if (key == null && ch.type === "set") {
34 | Object.assign(root, ch.val)
35 | return
36 | }
37 |
38 | // get the obj at the keypath (minus the key popped above)
39 | for (let i = 0; i < ch.path.length; i++) {
40 | const k = ch.path[i]
41 | obj = obj[k]
42 | }
43 |
44 | switch (ch.type) {
45 | case "set":
46 | if (key != null) obj[key] = ch.val
47 | break
48 |
49 | case "unset":
50 | if (key != null) delete obj[key]
51 | break
52 |
53 | case "add":
54 | obj[key].splice(ch.index, 0, ...ch.vals)
55 | break
56 |
57 | case "rm":
58 | obj[key].splice(ch.index, ch.num)
59 |
60 | break
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/js/Digest.ts:
--------------------------------------------------------------------------------
1 | import { TextEncoder } from "text-encoding"
2 |
3 | const encoder = new TextEncoder()
4 |
5 | export async function sha1(input: string) {
6 | return digest("SHA-1", input)
7 | }
8 |
9 | export async function digest(algo: string, input: string): Promise {
10 | const buffer = encoder.encode(input)
11 | const result = await crypto.subtle.digest(algo, buffer)
12 | return toHex(result)
13 | }
14 |
15 | export function toHex(buffer: ArrayBuffer) {
16 | return [...new Uint8Array(buffer)]
17 | .map(b => b.toString(16).padStart(2, "0"))
18 | .join("")
19 | }
20 |
--------------------------------------------------------------------------------
/src/js/Draggable.ts:
--------------------------------------------------------------------------------
1 | export function constructorForWindow(window: Window) {
2 | class Draggable extends (window as any).HTMLElement {
3 | static get observedAttributes() {
4 | return ["dragtype", "dragdata"]
5 | }
6 |
7 | constructor() {
8 | super()
9 | }
10 |
11 | get dragDataType(): string | null {
12 | return this.getAttribute("dragtype") || null
13 | }
14 |
15 | get dragData(): string | null {
16 | return this.getAttribute("dragdata") || null
17 | }
18 |
19 | get attrs(): { [k: string]: string } {
20 | const out = {} as { [k: string]: string }
21 | for (let i = 0; i < this.attributes.length; i++) {
22 | const attr = this.attributes[i]
23 | out[attr.name] = attr.value
24 | }
25 | return out
26 | }
27 |
28 | connectedCallback() {
29 | const { dragData, dragDataType } = this
30 | if (!dragData || !dragDataType) {
31 | return
32 | }
33 |
34 | this.style.display = "block"
35 | this.draggable = true
36 | this.addEventListener("dragstart", function(event: any) {
37 | event.dataTransfer.setData(dragDataType, dragData)
38 | })
39 | }
40 |
41 | disconnectedCallback() {
42 | }
43 |
44 | attributeChangedCallback(
45 | name: string,
46 | _oldValue: string,
47 | _newValue: string,
48 | ) {
49 | }
50 |
51 | }
52 | return Draggable
53 | }
54 |
--------------------------------------------------------------------------------
/src/js/FakeWorker.ts:
--------------------------------------------------------------------------------
1 | import QueuedPort from "./QueuedPort"
2 |
3 | import PseudoWorker from "pseudo-worker"
4 | import xhr from "xmlhttprequest"
5 |
6 | if (typeof XMLHttpRequest === "undefined") {
7 | ;(global).XMLHttpRequest = xhr.XMLHttpRequest
8 | }
9 |
10 | export default class FakeWorker extends QueuedPort {
11 | worker: Worker
12 |
13 | constructor(url: string, name?: string) {
14 | const worker = new PseudoWorker(url)
15 | super(worker, name || "PseudoWorker")
16 | this.worker = worker
17 |
18 | if (process && process.on) {
19 | // Ensure the worker is terminated in node
20 | process.on("SIGTERM", () => this.close())
21 | process.on("SIGINT", () => this.close())
22 | }
23 | }
24 |
25 | close() {
26 | this.terminate()
27 | }
28 |
29 | terminate() {
30 | super.close()
31 | this.worker.terminate()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/js/FarmUrl.ts:
--------------------------------------------------------------------------------
1 | export interface Pair {
2 | code: string
3 | data: string
4 | }
5 |
6 | export function create({ code, data }: Pair) {
7 | const codeId = new URL(code).pathname.slice(1)
8 | const dataId = new URL(data).pathname.slice(1)
9 | return `farm://${codeId}/${dataId}`
10 | }
11 |
--------------------------------------------------------------------------------
/src/js/Gizmo.ts:
--------------------------------------------------------------------------------
1 | import Repo from "./Repo"
2 | import { Handle } from "hypermerge/dist/Handle"
3 | import { whenChanged } from "./Subscription"
4 | import Compiler from "./Compiler"
5 | import ElmGizmo from "./ElmGizmo"
6 | import * as Code from "./Code"
7 | import * as Link from "./Link"
8 |
9 | export function setRepo(repo: Repo) {
10 | ElmGizmo.repo = repo
11 | }
12 |
13 | export function setCompiler(compiler: Compiler) {
14 | ElmGizmo.compiler = compiler
15 | }
16 |
17 | export function setSelfDataUrl(selfDataUrl: string) {
18 | ElmGizmo.selfDataUrl = selfDataUrl
19 | }
20 |
21 | export function constructorForWindow(window: Window) {
22 | class GizmoElement extends (window as any).HTMLElement {
23 | static get observedAttributes() {
24 | return ["code", "data", "portaltarget"]
25 | }
26 |
27 | gizmo?: ElmGizmo
28 | source?: Handle
29 | repo = ElmGizmo.repo
30 | portal: HTMLElement | null
31 |
32 | constructor() {
33 | super()
34 | this.portal = null
35 | }
36 |
37 | get dataUrl(): string | null {
38 | return this.getAttribute("data") || null
39 | }
40 |
41 | get codeUrl(): string | null {
42 | return this.getAttribute("code") || null
43 | }
44 |
45 | get portalTarget(): string | null {
46 | return this.getAttribute("portaltarget") || null
47 | }
48 |
49 | get attrs(): { [k: string]: string } {
50 | const out = {} as { [k: string]: string }
51 | for (let i = 0; i < this.attributes.length; i++) {
52 | const attr = this.attributes[i]
53 | out[attr.name] = attr.value
54 | }
55 | return out
56 | }
57 |
58 | connectedCallback() {
59 | const { codeUrl } = this
60 | if (!codeUrl) return
61 |
62 | this.source = this.repo.open(codeUrl)
63 | ElmGizmo.compiler.add(codeUrl)
64 |
65 | this.source.subscribe(
66 | whenChanged(
67 | doc => doc.outputHash,
68 | async (outputHash, doc) => {
69 | const source = await Code.source(this.repo, doc)
70 | this.mount(this.toElm(source, outputHash), doc)
71 | },
72 | ),
73 | )
74 | }
75 |
76 | disconnectedCallback() {
77 | this.unmount()
78 | if (this.source) {
79 | this.source.close()
80 | delete this.source
81 | }
82 | }
83 |
84 | attributeChangedCallback(
85 | name: string,
86 | _oldValue: string,
87 | _newValue: string,
88 | ) {
89 | this.disconnectedCallback()
90 | this.connectedCallback()
91 | }
92 |
93 | navigateTo(url: string) {
94 | this.gizmo && this.gizmo.navigateTo(url)
95 | }
96 |
97 | mount(elm: any, codeDoc: any) {
98 | this.unmount()
99 |
100 | const { codeUrl, dataUrl, portalTarget } = this
101 | if (!codeUrl || !dataUrl) return
102 |
103 | let elmNode: any
104 | if (portalTarget) {
105 | const portalTargetNode = (this as any).ownerDocument.querySelector(portalTarget)
106 | if (!portalTargetNode) return
107 | this.portal = this.getPortal(codeUrl, dataUrl)
108 | elmNode = (this as any).ownerDocument.createElement("div")
109 |
110 | if (!this.portal) return
111 | this.portal.appendChild(elmNode)
112 | portalTargetNode.appendChild(this.portal)
113 | } else {
114 | elmNode = (this as any).ownerDocument.createElement("div")
115 | this.appendChild(elmNode)
116 | }
117 |
118 | this.repo.once(dataUrl, (doc: any) => {
119 | this.gizmo = new ElmGizmo(elmNode, elm, {
120 | code: codeUrl,
121 | data: dataUrl,
122 | config: codeDoc.config,
123 | doc,
124 | all: this.attrs,
125 | })
126 |
127 | this.gizmo.dispatchEvent = e => this.dispatchEvent(e)
128 |
129 | if (doc.config) {
130 | Object.values(doc.config).forEach(url => {
131 | if (typeof url === "string" && Link.isValidLink(url)) {
132 | this.repo.preload(url)
133 | }
134 | })
135 | }
136 | })
137 | }
138 |
139 | unmount() {
140 | if (this.gizmo) {
141 | this.gizmo.close()
142 | delete this.gizmo
143 | }
144 |
145 | if (this.portal) {
146 | this.portal.innerHTML = ""
147 | this.portal.remove()
148 | } else {
149 | this.innerHTML = ""
150 | }
151 | }
152 |
153 | getPortal(code: string, data: string) {
154 | const portal = (this as any).ownerDocument.createElement("div")
155 | portal.setAttribute("portal-code", code)
156 | portal.setAttribute("portal-data", data)
157 | return portal
158 | }
159 |
160 | toElm(code: string, outputHash: string) {
161 | // TODO: explore using vm.runInNewContext
162 | // Get a reference to this element's `window` (which may be different than
163 | // the global `window` if the gizmo was launched into its own window) to
164 | // ensure Elm javascript type checks are correct.
165 | // e.g. When evaluated in window A: `arrayFromWindowB instanceof Array == false`
166 | const ourWindow = (this as any).ownerDocument.defaultView
167 | if (!ourWindow.elmCache) ourWindow.elmCache = new Map()
168 |
169 | if (outputHash && ourWindow.elmCache.has(outputHash)) {
170 | return ourWindow.elmCache.get(outputHash)
171 | }
172 | // Elm logs warnings when being evaled, so temporarily noop `console.warn`
173 | const { warn } = ourWindow.console
174 | ourWindow.console.warn = () => {}
175 | const app = ourWindow.eval(code)
176 | ourWindow.console.warn = warn
177 | const result = Object.values(app)[0]
178 | ourWindow.elmCache.set(outputHash, result)
179 | return result
180 | }
181 | }
182 | return GizmoElement
183 | }
184 |
--------------------------------------------------------------------------------
/src/js/GizmoWindow.ts:
--------------------------------------------------------------------------------
1 | import * as Gizmo from "./Gizmo"
2 | import * as FarmUrl from "./FarmUrl"
3 |
4 | export function constructorForWindow(window: Window) {
5 | class GizmoWindowElement extends (window as any).HTMLElement {
6 | openedWindow: Window | null = null
7 |
8 | static get observedAttributes() {
9 | return ["code", "data"]
10 | }
11 |
12 | constructor() {
13 | super()
14 | }
15 |
16 | get dataUrl(): string | null {
17 | return this.getAttribute("data") || null
18 | }
19 |
20 | get codeUrl(): string | null {
21 | return this.getAttribute("code") || null
22 | }
23 |
24 | get attrs(): { [k: string]: string } {
25 | const out = {} as { [k: string]: string }
26 | for (let i = 0; i < this.attributes.length; i++) {
27 | const attr = this.attributes[i]
28 | out[attr.name] = attr.value
29 | }
30 | return out
31 | }
32 |
33 | connectedCallback() {
34 | const { codeUrl } = this
35 | if (!codeUrl) return
36 |
37 | this.mount()
38 | }
39 |
40 | disconnectedCallback() {
41 | if (this.openedWindow) {
42 | this.openedWindow.close()
43 | this.openedWindow = null
44 | }
45 | }
46 |
47 | attributeChangedCallback(
48 | name: string,
49 | _oldValue: string,
50 | _newValue: string,
51 | ) {
52 | this.disconnectedCallback()
53 | this.connectedCallback()
54 | }
55 |
56 | // TODO: a lot going on here
57 | mount() {
58 | if (this.openedWindow) return
59 |
60 | const { codeUrl, dataUrl } = this
61 | if (!codeUrl || !dataUrl) return
62 |
63 | const currentWindow = this.ownerDocument.defaultView
64 |
65 | const windowName = FarmUrl.create({ code: codeUrl, data: dataUrl })
66 | this.openedWindow = open("", windowName)
67 | if (!this.openedWindow) return
68 |
69 | if (!this.openedWindow.customElements.get("farm-ui")) {
70 | this.openedWindow.customElements.define(
71 | "farm-ui",
72 | Gizmo.constructorForWindow(this.openedWindow),
73 | )
74 | }
75 | if (!this.openedWindow.customElements.get("farm-window")) {
76 | this.openedWindow.customElements.define(
77 | "farm-window",
78 | constructorForWindow(this.openedWindow),
79 | )
80 | }
81 | // TODO: focus window when opened.
82 | // Currently doesn't work due to this bug: https://github.com/electron/electron/issues/8969
83 | //this.openedWindow.focus()
84 |
85 | this.openedWindow.onbeforeunload = () => {
86 | console.log("on before unload")
87 | this.dispatchEvent(
88 | new CustomEvent("windowclose", {
89 | bubbles: true,
90 | composed: true,
91 | }),
92 | )
93 | }
94 |
95 | const root = this.openedWindow.document.createElement("farm-ui")
96 | root.setAttribute("code", codeUrl)
97 | root.setAttribute("data", dataUrl)
98 |
99 | const body = this.openedWindow.document.body
100 | const styleNode = currentWindow.document.getElementsByTagName("style")[0]
101 | styleNode && body.appendChild(styleNode.cloneNode(true))
102 | body.appendChild(root)
103 | this.openedWindow.addEventListener(
104 | "beforeunload",
105 | this.onBeforeWindowUnload,
106 | )
107 | }
108 |
109 | onBeforeWindowUnload = () => {
110 | this.openedWindow = null
111 | // if (this.parentElement) {
112 | // this.parentElement.removeChild(this)
113 | // }
114 | }
115 | }
116 | return GizmoWindowElement
117 | }
118 |
--------------------------------------------------------------------------------
/src/js/Link.ts:
--------------------------------------------------------------------------------
1 | import * as Base58 from "bs58"
2 |
3 | export const SCHEME = "hypermerge"
4 |
5 | export type Link = string
6 |
7 | export interface Params {
8 | readonly height?: number
9 | readonly width?: number
10 | }
11 |
12 | export interface Spec {
13 | readonly url: string
14 | readonly id: string
15 | readonly scheme: string
16 | readonly params: Params
17 | }
18 |
19 | export interface LinkArgs extends Pick {
20 | readonly params?: Params
21 | }
22 |
23 | export const create = ({ id, params }: LinkArgs): string => {
24 | return `${SCHEME}:/${id}${params ? createParams(params) : ""}`
25 | }
26 |
27 | export function fromId(id: string): string {
28 | return create({ id })
29 | }
30 |
31 | export function toId(link: string): string {
32 | return parse(link).id
33 | }
34 |
35 | export const createParams = (params: Params): string => {
36 | const keys = Object.keys(params) as Array
37 | if (keys.length === 0) return ""
38 |
39 | return "?" + keys.map(k => `${k}=${params[k]}`).join("&")
40 | }
41 |
42 | export const parse = (url: string): Spec => {
43 | const { scheme, id, params = {} } = parts(url)
44 |
45 | if (!id) throw new Error(`Url missing id in ${url}.`)
46 |
47 | if (scheme !== SCHEME) {
48 | throw new Error(`Invalid url scheme: ${scheme} (expected ${SCHEME})`)
49 | }
50 |
51 | return { url, scheme, id, params }
52 | }
53 |
54 | export const set = (url: string, opts: Partial) => {
55 | const { id, params } = parse(url)
56 | return create({ id, params, ...opts })
57 | }
58 |
59 | export const setType = (url: string) => {
60 | const { id } = parse(url)
61 | return create({ id })
62 | }
63 |
64 | export const parts = (url: string): Partial => {
65 | const [, /* url */ scheme, id, query = ""]: Array =
66 | url.match(/^(\w+):\/\/?(\w+)\/?(?:\?([&.\w=-]*))?$/) || []
67 | const params = parseParams(query)
68 | return { scheme, id, params }
69 | }
70 |
71 | export const parseParams = (query: string): Params => {
72 | return query
73 | .split("&")
74 | .map(q => q.split("="))
75 | .reduce(
76 | (params, [k, v]) => {
77 | params[k] = parseParam(k, v)
78 | return params
79 | },
80 | {} as any,
81 | )
82 | }
83 |
84 | export function parseParam(k: "height", v: string): number
85 | export function parseParam(k: "width", v: string): number
86 | export function parseParam(k: string, v: string): string
87 | export function parseParam(k: string, v: string): string | number {
88 | switch (k) {
89 | case "height":
90 | case "width":
91 | return parseFloat(v)
92 |
93 | default:
94 | return v
95 | }
96 | }
97 |
98 | export const isValidLink = (val: string): boolean => {
99 | try {
100 | parse(val)
101 | } catch {
102 | return false
103 | }
104 | return true
105 | }
106 |
107 | export const hexTo58 = (str: string): string =>
108 | Base58.encode(Buffer.from(str, "hex"))
109 |
--------------------------------------------------------------------------------
/src/js/Msg.ts:
--------------------------------------------------------------------------------
1 | export interface Compile {
2 | t: "Compile"
3 | url: string
4 | source: string
5 | sourceHash: string
6 | outputHash: string | null
7 | config: { [k: string]: any }
8 | debug?: boolean
9 | persist?: boolean
10 | }
11 |
12 | export interface Compiled {
13 | t: "Compiled"
14 | url: string
15 | persist?: boolean
16 | sourceHash: string
17 | outputHash: string
18 | output: string
19 | }
20 |
21 | export interface CompileError {
22 | t: "CompileError"
23 | url: string
24 | sourceHash: string
25 | error: string
26 | }
27 |
28 | export type ToCompiler = Compile
29 | export type FromCompiler = Compiled | CompileError
30 |
--------------------------------------------------------------------------------
/src/js/Queue.ts:
--------------------------------------------------------------------------------
1 | import Debug, { IDebugger } from "debug"
2 | export default class Queue {
3 | queue: T[] = []
4 | subscription?: (item: T) => void
5 | log: IDebugger
6 |
7 | constructor(name: string = "unknown") {
8 | this.log = Debug(`queue:${name}`)
9 | }
10 |
11 | push = (item: T): this => {
12 | if (this.subscription) {
13 | this.log("push subbed", item)
14 | this.subscription(item)
15 | } else {
16 | this.log("push queued", item)
17 | this.queue.push(item)
18 | }
19 | return this
20 | }
21 |
22 | subscribe = (subscriber: (item: T) => void): this => {
23 | if (this.subscription) {
24 | throw new Error("only one subscriber at a time to a queue")
25 | }
26 |
27 | this.log("subscribed")
28 |
29 | this.subscription = subscriber
30 |
31 | for (const item of this.queue) {
32 | subscriber(item)
33 | }
34 | this.queue = []
35 | return this
36 | }
37 |
38 | unsubscribe = (): this => {
39 | this.log("unsubscribed")
40 |
41 | this.subscription = undefined
42 | return this
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/js/QueuedPort.ts:
--------------------------------------------------------------------------------
1 | import QueuedResource from "./QueuedResource"
2 |
3 | export interface Port {
4 | postMessage(msg: any): void
5 | onmessage: null | ((msg: any) => void)
6 | }
7 |
8 | export default class QueuedPort extends QueuedResource {
9 | port: Port
10 |
11 | constructor(port: Port, name?: string) {
12 | super(name || "Port")
13 | this.port = port
14 |
15 | this.sendQ.subscribe(msg => {
16 | port.postMessage(msg)
17 | })
18 |
19 | port.onmessage = event => {
20 | const msg: R = event.data
21 | this.receiveQ.push(msg)
22 | }
23 | }
24 |
25 | close() {
26 | super.unsubscribe()
27 | this.sendQ.unsubscribe()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/js/QueuedResource.ts:
--------------------------------------------------------------------------------
1 | import Queue from "./Queue"
2 |
3 | export default abstract class QueuedResource {
4 | sendQ: Queue
5 | receiveQ: Queue
6 |
7 | constructor(name: string) {
8 | this.sendQ = new Queue(`${name}:sendQ`)
9 | this.receiveQ = new Queue(`${name}:receiveQ`)
10 | }
11 |
12 | send = (item: SendMsg): this => {
13 | this.sendQ.push(item)
14 | return this
15 | }
16 |
17 | subscribe = (subscriber: (item: ReceiveMsg) => void): this => {
18 | this.receiveQ.subscribe(subscriber)
19 | return this
20 | }
21 |
22 | unsubscribe(): this {
23 | this.receiveQ.unsubscribe()
24 | return this
25 | }
26 |
27 | abstract close(): void
28 | }
29 |
--------------------------------------------------------------------------------
/src/js/QueuedWorker.ts:
--------------------------------------------------------------------------------
1 | import QueuedPort from "./QueuedPort"
2 |
3 | export default class QueuedWorker extends QueuedPort {
4 | worker: Worker
5 |
6 | constructor(url: string, name?: string) {
7 | const worker = new Worker(url)
8 | super(worker, name || "Worker")
9 | this.worker = worker
10 |
11 | if (process && process.on) {
12 | // Ensure the worker is terminated in node
13 | process.on("SIGTERM", () => this.close())
14 | process.on("SIGINT", () => this.close())
15 | }
16 | }
17 |
18 | close() {
19 | this.terminate()
20 | }
21 |
22 | terminate() {
23 | super.close()
24 | this.worker.terminate()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/js/Repo.ts:
--------------------------------------------------------------------------------
1 | import { RepoFrontend } from "hypermerge/dist/RepoFrontend"
2 | import {ChangeFn} from "hypermerge"
3 | import { Handle } from "hypermerge/dist/Handle"
4 | import QueuedWorker from "./QueuedWorker"
5 | import { validateDocURL } from "hypermerge/dist/Metadata"
6 | import { TextDecoder } from "text-encoding"
7 | import * as Base58 from "bs58"
8 | import * as URL from "url"
9 | //import FakeWorker from "./FakeWorker"
10 |
11 | const decoder = new TextDecoder()
12 |
13 | export interface HyperFile {
14 | data: Uint8Array
15 | mimeType: string
16 | text: string
17 | }
18 |
19 | export default class Repo {
20 | worker: QueuedWorker
21 | front: RepoFrontend
22 | fileCache: Map
23 | registry?: object
24 | registryHandle?: Handle
25 |
26 | constructor(url: string) {
27 | this.front = new RepoFrontend()
28 | this.fileCache = new Map()
29 |
30 | // Swap to allow utp-native usage:
31 | //this.worker = new QueuedWorker(url)
32 | this.worker = new QueuedWorker(url)
33 |
34 | this.worker.subscribe(this.front.receive)
35 | this.front.subscribe(this.worker.send)
36 | }
37 |
38 | async setRegistry(url: string): Promise {
39 | if (this.registryHandle) {
40 | this.registryHandle.close()
41 | delete this.registryHandle
42 | }
43 |
44 | const registry = await this.read(url)
45 | this.registry = registry
46 |
47 | this.registryHandle = this.open(url).subscribe(newRegistry => {
48 | this.registry = newRegistry
49 | })
50 | return registry
51 | }
52 |
53 | async readFile(origUrl: string): Promise {
54 | const url = origUrl.replace(":///", ":/")
55 | return (
56 | this.fileCache.get(url) ||
57 | new Promise(res => {
58 | this.front.readFile(this.resolveUrl(url), (data, mimeType) => {
59 | const file: HyperFile = { data, mimeType, text: decoder.decode(data) }
60 | this.fileCache.set(url, file)
61 | res(file)
62 | })
63 | })
64 | )
65 | }
66 |
67 | writeFile(data: Uint8Array, mimeType: string): string {
68 | return this.front.writeFile(data, mimeType).replace(":/", ":///")
69 | }
70 |
71 | create = (props: object = {}): string => {
72 | return this.front.create(props)
73 | }
74 |
75 | open = (url: string): Handle => {
76 | return this.front.open(this.resolveUrl(url))
77 | }
78 |
79 | read(url: string): Promise {
80 | return new Promise(res => this.once(url, res))
81 | }
82 |
83 | preload(url: string): this {
84 | this.once(url, () => {})
85 | return this
86 | }
87 |
88 | once = (url: string, fn: Function): this => {
89 | const handle = this.open(url)
90 | handle.subscribe(doc => {
91 | fn(doc)
92 | handle.close()
93 | })
94 | return this
95 | }
96 |
97 | change = (url: string, fn: ChangeFn): this => {
98 | this.front.change(this.resolveUrl(url), fn)
99 | return this
100 | }
101 |
102 | clone = (url: string): string => {
103 | const newUrl = this.create()
104 |
105 | this.once(url, (doc: any) => {
106 | this.change(newUrl, (state: any) => {
107 | Object.assign(state, doc)
108 | })
109 | })
110 |
111 | return newUrl
112 | }
113 |
114 | fork = (url: string): string => {
115 | const newUrl = this.front.fork(this.resolveUrl(url))
116 | this.change(newUrl, (state: any) => {
117 | // Set title for all forked docs:
118 | state.title = `Fork of ${state.title}`
119 | })
120 | return newUrl
121 | }
122 |
123 | // hypermerge:/registry/key -> hypermerge:/abc123
124 | resolveUrl(url: string): string {
125 | // console.log("Resolving url:", url)
126 | const { path } = URL.parse(url)
127 | if (!path) throw new Error("No path in this url")
128 |
129 | const keys = path
130 | .slice(1)
131 | .split("/")
132 | .filter(key => key)
133 |
134 | const [id] = keys
135 |
136 | if (isValidId(id)) {
137 | // console.log("No resolution needed:", url)
138 | return url
139 | }
140 |
141 | if (!this.registry) throw new Error("Registry has not loaded")
142 |
143 | let content: any = this.registry
144 |
145 | keys.forEach(key => {
146 | if (typeof content !== "object" || !(key in content))
147 | throw new Error(`Registry could not resolve ${url}`)
148 | content = content[key]
149 | })
150 | // console.log("Resolved", url, "to", content)
151 | return content
152 | }
153 |
154 | terminate() {
155 | if (this.registryHandle) this.registryHandle.close()
156 | this.worker.terminate()
157 | }
158 | }
159 |
160 | function isValidId(id: string): boolean {
161 | try {
162 | const buffer = Base58.decode(id)
163 | return buffer.length === 32
164 | } catch {
165 | return false
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/js/Subscription.ts:
--------------------------------------------------------------------------------
1 | export function whenChanged(
2 | get: (t: T) => V | undefined,
3 | fn: (v: V, t: T) => void,
4 | ) {
5 | let v: V | undefined
6 |
7 | return (t: T) => {
8 | const newV = get(t)
9 | if (newV === undefined) return
10 | if (v === newV) return
11 | v = newV
12 | fn(v, t)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/js/bootstrap/index.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from "fs"
2 | import path from "path"
3 | import Repo from "../Repo"
4 | import mime from "mime-types"
5 | import * as Diff from "../Diff"
6 |
7 | export interface Opts {
8 | [k: string]: any
9 | }
10 |
11 | export const cache = new Map()
12 |
13 | export function code(identity: string, repo: Repo, file: string, opts: Opts = {}): string {
14 | const cached = cache.get(file)
15 | if (cached) {
16 | repo.change(cached, (state: any) => {
17 | const changes = Diff.getChanges(state, opts).filter(
18 | ch => !["rm", "unset"].includes(ch.type),
19 | )
20 | Diff.applyChanges(state, changes)
21 | })
22 | return cached
23 | } else {
24 | const created = createCode(identity, repo, file, opts)
25 | cache.set(file, created)
26 | return created
27 | }
28 | }
29 |
30 | export function createCode(identity: string, repo: Repo, file: string, opts: Opts = {}): string {
31 | const name = file.replace(/\.elm$/, "")
32 | opts.title = opts.title || `${name} source`
33 | opts.lastEditTimestamp = Date.now()
34 | if (identity) {
35 | opts.authors = [identity]
36 | }
37 | return repo.create({ ...opts, "Source.elm": sourceFor(file) })
38 | }
39 |
40 | export function sourceFor(name: string) {
41 | return readFileSync(path.resolve(`src/elm/examples/${name}`)).toString()
42 | }
43 |
44 | export function asset(repo: Repo, filename: string) {
45 | const mimeType = mime.lookup(filename) || "application/octet-stream"
46 | const data = readFileSync(path.resolve(`assets/${filename}`))
47 | return repo.writeFile(data, mimeType)
48 | }
49 |
--------------------------------------------------------------------------------
/src/js/cli/farm.ts:
--------------------------------------------------------------------------------
1 | ;(global).Worker = require("tiny-worker")
2 |
3 | import program from "commander"
4 |
5 | import Repo from "../Repo"
6 | import Compiler from "../Compiler"
7 | import Bot from "../Bot"
8 | import * as Bs from "../bootstrap"
9 | import * as FarmUrl from "../FarmUrl"
10 |
11 | program.version("0.1.0")
12 |
13 | const repo = new Repo("dist/repo.worker.js")
14 |
15 | program
16 | .command("help")
17 | .description("displays this usage information")
18 | .action(() => {
19 | program.help()
20 | })
21 |
22 | program
23 | .command("bot ")
24 | .description("run a farm bot")
25 | .action((codeUrl, dataUrl) => {
26 | const compiler = new Compiler(repo, "dist/compile.worker.js")
27 |
28 | Bot.repo = repo
29 | Bot.compiler = compiler
30 |
31 | const bot = new Bot(codeUrl, dataUrl)
32 | bot.start()
33 | })
34 |
35 | program
36 | .command("bootstrap ")
37 | .description("Bootstrap a gizmo from src/js/bootstrap.")
38 | .action((gizmo: string) => {
39 | const bs = require("../bootstrap/" + gizmo)
40 | const code = bs.code("", repo)
41 | const data = bs.data("", repo)
42 | const farmUrl = FarmUrl.create({ code, data })
43 | console.log(`Successfully bootstrapped ${gizmo}!\n\nurl:`, farmUrl)
44 | repo.terminate()
45 | //setTimeout(() => {}, 99999999) // HACK: without a worker, node exits
46 | })
47 |
48 | program
49 | .command("create ")
50 | .description("Create a farm gizmo from an elm file")
51 | .action(filename => {
52 | const url = Bs.code("", repo, filename)
53 | console.log("\n\ngizmo code url:", url, "\n\n")
54 | setTimeout(() => {}, 99999999) // HACK: without a worker, node exits
55 | })
56 |
57 | program.on("command:*", () => {
58 | console.error("Invalid command: %s\n", program.args.join(" "))
59 | program.help()
60 | })
61 |
62 | // Start the cli:
63 | program.parse(process.argv)
64 |
65 | if (!process.argv.slice(2).length) {
66 | program.help()
67 | }
68 |
--------------------------------------------------------------------------------
/src/js/compile.worker.ts:
--------------------------------------------------------------------------------
1 | declare const self: DedicatedWorkerGlobalScope
2 | import { resolve } from "path"
3 | import elmFormat from "elm-format"
4 |
5 | if ((self as any).module) {
6 | ;(self as any).module.paths.push(resolve("./node_modules"))
7 | }
8 |
9 | import QueuedPort from "./QueuedPort"
10 | import { ToCompiler, FromCompiler } from "./Msg"
11 | import fs from "fs"
12 | import elm from "node-elm-compiler"
13 | import AsyncQueue from "./AsyncQueue"
14 | import { lock } from "proper-lockfile"
15 | import { promisify } from "util"
16 | import { sha1 } from "./Digest"
17 | import { spawn } from "child_process"
18 |
19 | const writeFile = promisify(fs.writeFile)
20 |
21 | const port = new QueuedPort(self)
22 | ;(self as any).port = port
23 |
24 | const workQ = new AsyncQueue("compiler:workQ")
25 |
26 | workQ.take(work)
27 |
28 | port.subscribe(workQ.push)
29 |
30 | if (!fs.existsSync(".tmp")) {
31 | fs.mkdirSync(".tmp")
32 | }
33 |
34 | async function work(msg: ToCompiler) {
35 | const { url } = msg
36 | switch (msg.t) {
37 | case "Compile":
38 | const { sourceHash } = msg
39 | const source = msg.source.replace(/^module +\w+/, "module Source")
40 |
41 | const sourceFile = "./.tmp/Source.elm"
42 | const lockOpts = { stale: 5000, retries: 5, realpath: false }
43 |
44 | const release = await lock(sourceFile, lockOpts)
45 |
46 | function done() {
47 | release()
48 | workQ.take(work)
49 | }
50 |
51 | try {
52 | await writeFile(sourceFile, source)
53 |
54 | await writeFile("./.tmp/Config.elm", configContents(msg.config))
55 |
56 | const filename = getFilename(source)
57 | const out = await elm.compileToString([filename], {
58 | output: ".js",
59 | report: "json",
60 | debug: msg.debug,
61 | })
62 |
63 | if (msg.persist) {
64 | const [, name = "Unknown"] = msg.source.match(/^module (\w+)/) || []
65 | await saveElmCode(`./src/elm/examples/${name}.elm`, msg.source)
66 | }
67 |
68 | const output = `
69 | (new function Wrapper() {
70 | ${out}
71 | }).Elm
72 | `
73 |
74 | const outputHash = await sha1(output)
75 |
76 | port.send({ t: "Compiled", url, output, sourceHash, outputHash })
77 | return done()
78 | } catch (err) {
79 | port.send({ t: "CompileError", url, sourceHash, error: err.message })
80 | return done()
81 | }
82 | break
83 | }
84 | }
85 |
86 | function getFilename(source: string): string {
87 | return /^main /m.test(source)
88 | ? "./.tmp/Source.elm" // Compile directly if `main` function exists
89 | : /^gizmo /m.test(source)
90 | ? "./src/elm/Harness.elm" // Compile via Harness if `gizmo` function exists
91 | : "./src/elm/BotHarness.elm" // Otherwise, compile via BotHarness
92 | }
93 |
94 | function saveElmCode(filename: string, source: string): Promise {
95 | return new Promise((res, rej) => {
96 | const format = spawn(elmFormat.paths["elm-format"], [
97 | "--stdin",
98 | "--yes",
99 | "--output",
100 | filename,
101 | ])
102 | format.stdout.on("end", () => res())
103 | format.stdin.write(source)
104 | format.stdin.end()
105 | })
106 | }
107 |
108 | function configContents(config: { [key: string]: any }): string {
109 | const keys = Object.keys(config)
110 | const reserved = ["getString", "map", "keys"]
111 |
112 | return [
113 | "module Config exposing (..)",
114 | "",
115 | `keys = ${configValue(keys)}`,
116 | "",
117 | ...keys
118 | .filter(k => !reserved.includes(k))
119 | .map(k => `${k} = ${configValue(config[k])}`),
120 | "",
121 | `getString : String -> Maybe String`,
122 | `getString key =`,
123 | ` case key of`,
124 | ...keys
125 | .filter(k => typeof config[k] === "string")
126 | .map(k => ` "${k}" -> Just ${configValue(config[k])}`),
127 | " _ -> Nothing",
128 | "",
129 | ].join("\n")
130 | }
131 |
132 | function configValue(value: any): string {
133 | switch (value != null ? typeof value : null) {
134 | case "string":
135 | case "number":
136 | return JSON.stringify(value)
137 | case "boolean":
138 | return value ? "True" : "False"
139 |
140 | case "object":
141 | if (Array.isArray(value)) {
142 | return `[ ${value.map(configValue).join(", ")} ]`
143 | } else {
144 | return `{ ${Object.keys(value)
145 | .map(k => `${k} = ${configValue(value[k])}`)
146 | .join(", ")} }`
147 | }
148 |
149 | default:
150 | return "Nothing"
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/js/index.ts:
--------------------------------------------------------------------------------
1 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "1"
2 |
3 | import App from "./App"
4 | import Debug from "debug"
5 | import URL from "url"
6 | import { ipcRenderer, remote } from "electron"
7 |
8 | const { protocol } = remote
9 |
10 | const app = new App()
11 | ;(self).app = app
12 |
13 | Object.assign(self, {
14 | Debug,
15 | app,
16 | })
17 |
18 | protocol.registerBufferProtocol("hyperfile", async ({ url }, callback) => {
19 | console.log("Getting", url)
20 | const { mimeType, data } = await app.repo.readFile(url)
21 | console.log("Found", mimeType)
22 | callback({ mimeType, data: Buffer.from(data) })
23 | })
24 |
25 | addEventListener("beforeunload", () => {
26 | protocol.unregisterProtocol("hyperfile")
27 | })
28 |
29 | ipcRenderer.on("open-url", (_event: any, url: string) => {
30 | console.log("Opening url", url)
31 | app.handleUrl(url)
32 | })
33 |
--------------------------------------------------------------------------------
/src/js/repo.worker.ts:
--------------------------------------------------------------------------------
1 | declare const self: DedicatedWorkerGlobalScope
2 | import { resolve } from "path"
3 |
4 | if ((self as any).module) {
5 | ;(self as any).module.paths.push(resolve("./node_modules"))
6 | }
7 |
8 | import raf from "random-access-file"
9 | import { RepoBackend } from "hypermerge"
10 | //import discoverySwarm from "discovery-swarm"
11 | //import datDefaults from "dat-swarm-defaults"
12 | import discoveryCloud from "discovery-cloud-client"
13 |
14 | const storagePath = process.env.REPO_ROOT || "./.data"
15 |
16 | const repo = new RepoBackend({ storage: raf, path: storagePath })
17 | ;(self as any).repo = repo
18 |
19 | self.onmessage = msg => {
20 | repo.receive(msg.data)
21 | }
22 |
23 | repo.subscribe(msg => {
24 | self.postMessage(msg)
25 | })
26 |
27 | repo.replicate(
28 | // discoverySwarm(
29 | // datDefaults({
30 | // port: 0,
31 | // id: repo.id,
32 | // stream: repo.stream,
33 | // }),
34 | // )
35 | new discoveryCloud({url: "wss://discovery-cloud.herokuapp.com", id: repo.id, stream: repo.stream})
36 | )
37 |
38 | //console.log('repo worker loaded', repo)
--------------------------------------------------------------------------------
/src/node_modules/@types/dat-swarm-defaults/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "dat-swarm-defaults"
2 |
--------------------------------------------------------------------------------
/src/node_modules/@types/discovery-swarm/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "discovery-swarm"
2 |
--------------------------------------------------------------------------------
/src/node_modules/@types/electron-context-menu/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "electron-context-menu"
2 |
--------------------------------------------------------------------------------
/src/node_modules/@types/elm-format/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "elm-format"
2 |
--------------------------------------------------------------------------------
/src/node_modules/@types/hypercore/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "hypercore/lib/crypto"
2 |
--------------------------------------------------------------------------------
/src/node_modules/@types/node-elm-compiler/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "node-elm-compiler" {
2 | export interface Options {
3 | output?: string
4 | report?: "json"
5 | debug?: boolean
6 | optimize?: boolean
7 | verbose?: boolean
8 | }
9 |
10 | /**
11 | * Compiles a list of elm files into an output file
12 | */
13 | export function compile(inputFiles: string[], options: Options): Promise
14 |
15 | /**
16 | * Compiles input files to a string, and optionally to an output file
17 | */
18 | export function compileToString(
19 | inputFiles: string[],
20 | options: Options,
21 | ): Promise
22 |
23 | // export function compileSync()
24 | // export function compileToStringSync()
25 | // export function compileWorker()
26 | // export function findAllDependencies()
27 | // export function _prepareProcessArgs()
28 | }
29 |
--------------------------------------------------------------------------------
/src/node_modules/@types/pseudo-worker/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "pseudo-worker"
2 |
--------------------------------------------------------------------------------
/src/node_modules/@types/random-access-file/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "random-access-file"
2 |
--------------------------------------------------------------------------------
/src/node_modules/@types/xmlhttprequest/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "xmlhttprequest"
2 |
--------------------------------------------------------------------------------
/src/server/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import devServer from "webpack-dev-middleware"
3 | import webpack from "webpack"
4 | import config from "../../webpack.config"
5 |
6 | const PORT = 4000
7 | const webpackCompiler = webpack(config)
8 | const app = express()
9 |
10 | app.use(
11 | devServer(webpackCompiler, {
12 | publicPath: "/",
13 | logLevel: "warn",
14 | writeToDisk(filename: string): boolean {
15 | return /(farm|\.worker)\.js$/.test(filename)
16 | },
17 | }),
18 | )
19 |
20 | app.listen(PORT, () => console.log(`App listening at http://localhost:${PORT}`))
21 |
--------------------------------------------------------------------------------
/src/vscode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "farm",
3 | "version": "0.0.1",
4 | "displayName": "Farm",
5 | "publisher": "inkandswitch",
6 | "description": "An extension into Farm.",
7 | "scripts": {
8 | "local": "ln -s `realpath .` ~/.vscode/extensions/"
9 | },
10 | "extensionDependencies": [
11 | "inkandswitch.hypermerge"
12 | ],
13 | "engines": {
14 | "vscode": "^1.29.1"
15 | },
16 | "main": "dist/index.js",
17 | "repository": "https://github.com/inkandswitch/farm.git",
18 | "license": "MIT",
19 | "private": false,
20 | "devDependencies": {
21 | "@types/node": "^10.12.12",
22 | "typescript": "^3.2.2",
23 | "vscode": "^1.1.26"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist", // just in case
4 | "typeRoots": ["./node_modules/@types", "./src/node_modules/@types"],
5 | "sourceMap": true,
6 | "lib": ["es2015", "es2017", "dom", "webworker"],
7 | "module": "commonjs",
8 | "target": "es6",
9 | "downlevelIteration": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "skipLibCheck": true, // prevents type errors in third-party libs, but also no type checking for our .d.ts files
13 | "strict": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "pretty": true
17 | },
18 | "include": ["src/**/*", "src/node_modules/**/*"]
19 | }
20 |
--------------------------------------------------------------------------------
/webpack.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path"
2 | import webpack from "webpack"
3 | import HtmlPlugin from "html-webpack-plugin"
4 | import CopyPlugin from "copy-webpack-plugin"
5 | import nodeExternals from "webpack-node-externals"
6 | import HardSourcePlugin from "hard-source-webpack-plugin"
7 |
8 | const cacheDirectory = undefined //path.resolve(__dirname, ".cache")
9 |
10 | const shared: webpack.Configuration = {
11 | mode: "development",
12 | context: path.resolve(__dirname),
13 | devtool: "inline-source-map",
14 | resolve: {
15 | extensions: [".js", ".ts", ".elm"],
16 | },
17 | externals: [
18 | nodeExternals({
19 | whitelist: [/webpack/],
20 | }),
21 | ],
22 | module: {
23 | rules: [
24 | {
25 | test: /\.ts$/,
26 | loader: "ts-loader",
27 | exclude: [/farm\/node_modules/],
28 | },
29 |
30 | {
31 | test: /\.elm$/,
32 | exclude: [/elm-stuff/, /node_modules/],
33 | use: [
34 | {
35 | loader: "elm-webpack-loader",
36 | options: {},
37 | },
38 | ],
39 | },
40 | ],
41 | },
42 | }
43 |
44 | function config(opts: webpack.Configuration) {
45 | return Object.assign(
46 | {},
47 | shared,
48 | {
49 | output: {
50 | path: path.resolve(__dirname, "dist"),
51 | filename: `${opts.name}.js`,
52 | publicPath: "./",
53 | globalObject: "this",
54 | },
55 | },
56 | opts,
57 | )
58 | }
59 |
60 | export default [
61 | config({
62 | name: "electron",
63 | entry: ["./src/electron"],
64 | target: "electron-main",
65 | plugins: [new HardSourcePlugin({ cacheDirectory })],
66 | }),
67 |
68 | config({
69 | name: "farm",
70 | entry: ["./src/js/cli/farm"],
71 | target: "node",
72 | plugins: [new HardSourcePlugin({ cacheDirectory })],
73 | }),
74 |
75 | config({
76 | name: "renderer",
77 | entry: ["./src/js"],
78 | target: "electron-renderer",
79 | plugins: [
80 | new HtmlPlugin({ title: "Farm" }),
81 | new HardSourcePlugin({ cacheDirectory }),
82 | ],
83 | }),
84 |
85 | config({
86 | name: "repo.worker",
87 | entry: ["./src/js/repo.worker"],
88 | target: "electron-renderer",
89 | plugins: [new HardSourcePlugin({ cacheDirectory })],
90 | }),
91 |
92 | config({
93 | name: "compile.worker",
94 | entry: ["./src/js/compile.worker"],
95 | target: "electron-renderer",
96 | plugins: [new HardSourcePlugin({ cacheDirectory })],
97 | }),
98 |
99 | config({
100 | name: "hypermerge-devtools/main",
101 | entry: ["./src/hypermerge-devtools/main"],
102 | target: "web",
103 | plugins: [
104 | new HtmlPlugin({
105 | title: "Hypermerge 1",
106 | filename: "hypermerge-devtools/main.html",
107 | }),
108 | new CopyPlugin([
109 | {
110 | from: "./src/hypermerge-devtools/manifest.json",
111 | to: "hypermerge-devtools/",
112 | },
113 | ]),
114 | new HardSourcePlugin({ cacheDirectory }),
115 | ],
116 | }),
117 |
118 | config({
119 | name: "hypermerge-devtools/panel",
120 | entry: ["./src/hypermerge-devtools/panel"],
121 | target: "web",
122 | plugins: [
123 | new HtmlPlugin({
124 | title: "Hypermerge 2",
125 | filename: "hypermerge-devtools/panel.html",
126 | }),
127 | new HardSourcePlugin({ cacheDirectory }),
128 | ],
129 | }),
130 | ]
131 |
--------------------------------------------------------------------------------