├── .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 | 2 | 3 | 4 | 5 | 6 | 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 | --------------------------------------------------------------------------------