├── .editorconfig ├── .github ├── FUNDING.yml ├── settings.yml └── workflows │ └── build-docker.yml ├── .gitignore ├── .gitpod.yml ├── .jshintrc ├── .travis.yml ├── CNAME ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── __tests__ ├── CommandLineStore.spec.js ├── GlobalStateStore.spec.js ├── LevelStore.spec.js ├── LocaleStore.spec.js ├── animation.spec.js ├── base.js ├── create.js ├── git.spec.js ├── levels.spec.js ├── mercurial.spec.js ├── remote.spec.js ├── simpleRemote.spec.js ├── treeCompare.spec.js └── vcs.spec.js ├── assets ├── favicon.ico ├── font │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── learnGitBranching.png └── webfonts │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff2 │ ├── fa-v4compatibility.ttf │ └── fa-v4compatibility.woff2 ├── checkgit.sh ├── generatedDocs ├── github-markdown.css └── levels.html ├── gulpfile.js ├── package.json ├── src ├── js │ ├── actions │ │ ├── CommandLineActions.js │ │ ├── GlobalStateActions.js │ │ ├── LevelActions.js │ │ └── LocaleActions.js │ ├── app │ │ └── index.js │ ├── commands │ │ └── index.js │ ├── constants │ │ └── AppConstants.js │ ├── dialogs │ │ ├── confirmShowSolution.js │ │ ├── levelBuilder.js │ │ ├── nextLevel.js │ │ └── sandbox.js │ ├── dispatcher │ │ └── AppDispatcher.js │ ├── git │ │ ├── commands.js │ │ ├── gitShim.js │ │ ├── headless.js │ │ └── index.js │ ├── graph │ │ ├── index.js │ │ └── treeCompare.js │ ├── intl │ │ ├── checkStrings.js │ │ ├── index.js │ │ └── strings.js │ ├── level │ │ ├── builder.js │ │ ├── disabledMap.js │ │ ├── index.js │ │ └── parseWaterfall.js │ ├── log │ │ └── index.js │ ├── mercurial │ │ └── commands.js │ ├── models │ │ ├── collections.js │ │ └── commandModel.js │ ├── react_views │ │ ├── CommandHistoryView.jsx │ │ ├── CommandView.jsx │ │ ├── CommandsHelperBarView.jsx │ │ ├── HelperBarView.jsx │ │ ├── IntlHelperBarView.jsx │ │ ├── LevelToolbarView.jsx │ │ └── MainHelperBarView.jsx │ ├── sandbox │ │ ├── commands.js │ │ └── index.js │ ├── stores │ │ ├── CommandLineStore.js │ │ ├── GlobalStateStore.js │ │ ├── LevelStore.js │ │ └── LocaleStore.js │ ├── util │ │ ├── constants.js │ │ ├── debounce.js │ │ ├── debug.js │ │ ├── errors.js │ │ ├── escapeString.js │ │ ├── eventBaton.js │ │ ├── index.js │ │ ├── keyMirror.js │ │ ├── keyboard.js │ │ ├── mock.js │ │ ├── reactUtil.js │ │ ├── throttle.js │ │ └── zoomLevel.js │ ├── views │ │ ├── builderViews.js │ │ ├── commandViews.js │ │ ├── gitDemonstrationView.js │ │ ├── index.js │ │ ├── levelDropdownView.js │ │ ├── multiView.js │ │ └── rebaseView.js │ └── visuals │ │ ├── animation │ │ ├── animationFactory.js │ │ └── index.js │ │ ├── index.js │ │ ├── tree.js │ │ ├── visBase.js │ │ ├── visBranch.js │ │ ├── visEdge.js │ │ ├── visNode.js │ │ ├── visTag.js │ │ └── visualization.js ├── levels │ ├── advanced │ │ └── multipleParents.js │ ├── index.js │ ├── intro │ │ ├── branching.js │ │ ├── commits.js │ │ ├── merging.js │ │ └── rebasing.js │ ├── mixed │ │ ├── describe.js │ │ ├── grabbingOneCommit.js │ │ ├── jugglingCommits.js │ │ ├── jugglingCommits2.js │ │ └── tags.js │ ├── rampup │ │ ├── cherryPick.js │ │ ├── detachedHead.js │ │ ├── interactiveRebase.js │ │ ├── relativeRefs.js │ │ ├── relativeRefs2.js │ │ └── reversingChanges.js │ ├── rebase │ │ ├── manyRebases.js │ │ └── selectiveRebase.js │ └── remote │ │ ├── clone.js │ │ ├── fakeTeamwork.js │ │ ├── fetch.js │ │ ├── fetchArgs.js │ │ ├── fetchRebase.js │ │ ├── lockedMain.js │ │ ├── mergeManyFeatures.js │ │ ├── pull.js │ │ ├── pullArgs.js │ │ ├── push.js │ │ ├── pushArgs.js │ │ ├── pushArgs2.js │ │ ├── pushManyFeatures.js │ │ ├── remoteBranches.js │ │ ├── sourceNothing.js │ │ └── tracking.js ├── style │ ├── all.min.css │ ├── font-awesome.css │ ├── main.css │ └── rainbows.css └── template.index.html ├── vite.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true # Top-most editorconfig file 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: [pcottle] 3 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | description: An interactive git visualization and tutorial. Aspiring students of git can use this app to educate and challenge themselves towards mastery of git! 3 | homepage: https://pcottle.github.io/learnGitBranching/ 4 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker - learnGitBranching image 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main # Trigger CI on main branch 8 | - bmcclure/main 9 | paths: 10 | - '**/*' 11 | - '.github/workflows/build-docker.yml' 12 | pull_request: 13 | branches: 14 | - main # Trigger gated pipeline on PR to main 15 | - bmcclure/main 16 | paths: 17 | - '**/*' 18 | - '.github/workflows/build-docker.yml' 19 | 20 | env: 21 | REGISTRY: ghcr.io 22 | IMAGE_NAME: ${{ github.repository }} 23 | 24 | jobs: 25 | build-and-push-image: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: read 29 | packages: write 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v3 34 | 35 | - name: Log in to the Container registry 36 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 37 | with: 38 | registry: ${{ env.REGISTRY }} 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Extract metadata (tags, labels) for Docker 43 | id: meta 44 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 45 | with: 46 | tags: | 47 | type=ref,event=pr 48 | type=ref,event=branch 49 | type=sha,format=long 50 | type=raw,value=latest 51 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 52 | 53 | - name: Build Docker image (non main branch) 54 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 55 | if: github.ref != 'refs/heads/bmcclure/main' 56 | with: 57 | context: . 58 | load: true 59 | push: false 60 | tags: ${{ steps.meta.outputs.tags }} 61 | labels: ${{ steps.meta.outputs.labels }} 62 | - name: Build and push Docker image (main branch) 63 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 64 | if: github.ref == 'refs/heads/bmcclure/main' 65 | with: 66 | context: . 67 | push: true 68 | tags: ${{ steps.meta.outputs.tags }} 69 | labels: ${{ steps.meta.outputs.labels }} 70 | - id: lowercaseImageName 71 | uses: ASzc/change-string-case-action@v2 72 | with: 73 | string: ${{ env.IMAGE_NAME }} 74 | - name: Save Docker Image archive to local filesystem 75 | run: "docker save --output learnGitBranching.tar ${{env.REGISTRY}}/${{ steps.lowercaseImageName.outputs.lowercase }}" 76 | - name: Upload application's Docker Image as pipeline artifact 77 | uses: actions/upload-artifact@v4 78 | with: 79 | path: learnGitBranching.tar 80 | name: learnGitBranching.tar 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM / Yarn / Pnpm 2 | npm-debug.log 3 | yarn-error.log 4 | pnpm-lock.yaml 5 | node_modules/ 6 | 7 | # Build artifacts and asset stuff 8 | build/* 9 | screens 10 | FontAwesome-Vectors.pdf 11 | index.html 12 | 13 | # Vim swaps 14 | *.sw* 15 | 16 | # sed backups 17 | *.bak 18 | 19 | # Annoying mac stuff 20 | .DS_STORE 21 | 22 | # Xcode 23 | *.xcuserstate 24 | *.xcworkspace 25 | xcuserdata 26 | UserInterfaceState.xcuserstate 27 | 28 | *.pbxuser 29 | *.perspective 30 | *.perspectivev3 31 | 32 | *.mode1v3 33 | *.mode2v3 34 | 35 | *.xcodeproj/xcuserdata/*.xcuserdatad 36 | 37 | *.xccheckout 38 | *.xcuserdatad 39 | 40 | *.pyc 41 | 42 | .User.xcconfig 43 | 44 | Tools/xctool/build 45 | Tools/clang/analyzer/build 46 | Tools/clang/libtooling/build 47 | Tools/clang/clang-ocaml/build 48 | Tools/clang/xcode 49 | VendorLib/Breakpad/src/tools/mac/dump_syms/build 50 | DerivedData 51 | VendorLib/clang/lib/arc 52 | VendorLib/clang/lib/c++ 53 | 54 | .idea 55 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | ports: 2 | - port: 8000 3 | onOpen: open-preview 4 | tasks: 5 | - init: yarn install 6 | command: > 7 | yarn gulp fastBuild && 8 | printf "\nWelcome to Learn Git Branching\nTo rebuild the app, simply run 'yarn gulp fastBuild' and reload index.html.\n\n" && 9 | python3 -m http.server 8000 2>/dev/null 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "curly": true, 4 | "eqeqeq": false, 5 | "regexp": false, 6 | "nonew": false, 7 | "latedef": false, 8 | "forin": false, 9 | "globalstrict": false, 10 | "node": true, 11 | "immed": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "bitwise": true, 15 | "sub": true, 16 | "undef": true, 17 | "unused": true, 18 | "trailing": true, 19 | "devel": true, 20 | "jquery": true, 21 | "nonstandard": true, 22 | "boss": true, 23 | "eqnull": true, 24 | "browser": true, 25 | "debug": true, 26 | "globals": { 27 | "casper": true, 28 | "Raphael": true, 29 | "require": true, 30 | "console": true, 31 | "describe": true, 32 | "expect": true, 33 | "it": true, 34 | "runs": true, 35 | "waitsFor": true, 36 | "exports": true, 37 | "module": true, 38 | "prompt": true, 39 | "process": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "10" 5 | - "8" 6 | before_install: 7 | - curl -o- -L https://yarnpkg.com/install.sh | bash 8 | - export PATH="$HOME/.yarn/bin:$PATH" 9 | cache: 10 | yarn: true 11 | directories: 12 | - "node_modules" 13 | script: 14 | - ./checkgit.sh "Source files were modified before build; is yarn.lock out of sync with package.json?" || travis_terminate $? 15 | - yarn gulp 16 | - ./checkgit.sh "Source files were modified by the build" || travis_terminate $? 17 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | learnGitBranching.js.org 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome to the Learn Git Branching contributing guide! 2 | 3 | We have a pretty relaxed environment in this project so there's no formal template to submit Pull Requests for. Contributions generally fall into two buckets: 4 | 5 | ### Translations 6 | 7 | I welcome all translation improvements or additions! The levels are stored in giant JSON blobs, keyed by locale for each string. This means its somewhat awkward to add new translations, since you have to edit the JSON manually. 8 | 9 | ### Bug Fixes, New Features, etc 10 | 11 | These are great too! If you are adding new functionality to the git engine, I would try to add some tests in the `__tests__` folder. It's pretty simple, you can just input a bunch of git commands and show the expected tree state afterwards. 12 | 13 | For bug fixes or CSS/style layout issues, simply attach screenshots of the before and after. For more obscure browsers, targeted CSS rules by browser is a bit more preferred. 14 | 15 | Thanks for stopping by! 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.20.0-alpine3.16 as build 2 | 3 | RUN apk add git --no-cache 4 | WORKDIR "/src" 5 | 6 | COPY . /src 7 | RUN yarn install && \ 8 | yarn cache clean 9 | RUN yarn gulp build 10 | 11 | FROM scratch AS export 12 | WORKDIR / 13 | COPY --from=build /src/index.html . 14 | COPY --from=build /src/build ./build 15 | 16 | 17 | 18 | FROM nginx:stable-alpine 19 | WORKDIR /usr/share/nginx/html/ 20 | COPY . . 21 | # Override the local source with the built artifacts 22 | COPY --from=export . . 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright (c) 2012-2025 Peter Cottle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) 2 | SHELL := pwsh.exe 3 | else 4 | SHELL := pwsh 5 | endif 6 | 7 | .SHELLFLAGS := -NoProfile -Command 8 | 9 | REGISTRY_NAME := ghcr.io/ 10 | REPOSITORY_NAME := pcottle/ 11 | IMAGE_NAME := learngitbranching 12 | TAG := :latest 13 | 14 | .PHONY: all clean test 15 | all: build 16 | 17 | getcommitid: 18 | $(eval COMMITID = $(shell git log -1 --pretty=format:'%H')) 19 | 20 | getbranchname: 21 | $(eval BRANCH_NAME = $(shell (git branch --show-current ) -replace '/','.')) 22 | 23 | build: 24 | docker run --rm -v $${PWD}:/mnt --workdir /mnt node:14.20.0-alpine3.16 yarn install 25 | docker run --rm -v $${PWD}:/mnt --workdir /mnt node:14.20.0-alpine3.16 yarn gulp fastBuild 26 | build_docker: getcommitid getbranchname 27 | docker build -t $(REGISTRY_NAME)$(REPOSITORY_NAME)$(IMAGE_NAME)$(TAG) -t $(REGISTRY_NAME)$(REPOSITORY_NAME)$(IMAGE_NAME):$(BRANCH_NAME) -t $(REGISTRY_NAME)$(REPOSITORY_NAME)$(IMAGE_NAME):$(BRANCH_NAME)_$(COMMITID) . 28 | docker build --target export --output type=local,dest=learnGitBranching_$(BRANCH_NAME)_$(COMMITID) . 29 | 30 | run: 31 | docker run -p 8080:80 $(REGISTRY_NAME)$(REPOSITORY_NAME)$(IMAGE_NAME)$(TAG) 32 | 33 | clean: 34 | echo 'not implemented' 35 | test: 36 | echo 'not implemented' -------------------------------------------------------------------------------- /__tests__/CommandLineStore.spec.js: -------------------------------------------------------------------------------- 1 | var CommandLineActions = require('../src/js/actions/CommandLineActions'); 2 | var CommandLineStore = require('../src/js/stores/CommandLineStore'); 3 | 4 | describe('this store', function() { 5 | 6 | it('starts with no entries', function() { 7 | expect(CommandLineStore.getCommandHistoryLength()) 8 | .toEqual(0); 9 | }); 10 | 11 | it('receives new commands', function() { 12 | var command = 'git commit; git checkout HEAD'; 13 | CommandLineActions.submitCommand(command); 14 | 15 | expect(CommandLineStore.getCommandHistoryLength()) 16 | .toEqual(1); 17 | 18 | expect(CommandLineStore.getCommandHistory()[0]) 19 | .toEqual(command); 20 | var newCommand = 'echo "yo dude";'; 21 | CommandLineActions.submitCommand(newCommand); 22 | 23 | expect(CommandLineStore.getCommandHistoryLength()) 24 | .toEqual(2); 25 | 26 | expect(CommandLineStore.getCommandHistory()[0]) 27 | .toEqual(newCommand); 28 | expect(CommandLineStore.getCommandHistory()[1]) 29 | .toEqual(command); 30 | }); 31 | 32 | it('slices after max length', function() { 33 | var maxLength = CommandLineStore.getMaxHistoryLength(); 34 | var numOver = 10; 35 | for (var i = 0; i < maxLength + numOver; i++) { 36 | CommandLineActions.submitCommand('commandNum' + i); 37 | } 38 | var numNow = 11 + numOver; 39 | expect( 40 | CommandLineStore.getCommandHistoryLength() 41 | ).toEqual(numNow); 42 | 43 | expect( 44 | CommandLineStore.getCommandHistory()[0] 45 | ).toEqual('commandNum109'); 46 | 47 | expect( 48 | CommandLineStore.getCommandHistory()[numNow - 1] 49 | ).toEqual('commandNum89'); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/GlobalStateStore.spec.js: -------------------------------------------------------------------------------- 1 | var GlobalStateActions = require('../src/js/actions/GlobalStateActions'); 2 | var GlobalStateStore = require('../src/js/stores/GlobalStateStore'); 3 | 4 | describe('this store', function() { 5 | it('is can change animating', function() { 6 | expect(GlobalStateStore.getIsAnimating()).toEqual(false); 7 | GlobalStateActions.changeIsAnimating(true); 8 | expect(GlobalStateStore.getIsAnimating()).toEqual(true); 9 | GlobalStateActions.changeIsAnimating(false); 10 | expect(GlobalStateStore.getIsAnimating()).toEqual(false); 11 | }); 12 | 13 | it('can change flip treey', function() { 14 | expect(GlobalStateStore.getFlipTreeY()).toEqual(false); 15 | GlobalStateActions.changeFlipTreeY(true); 16 | expect(GlobalStateStore.getFlipTreeY()).toEqual(true); 17 | GlobalStateActions.changeFlipTreeY(false); 18 | expect(GlobalStateStore.getFlipTreeY()).toEqual(false); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/LevelStore.spec.js: -------------------------------------------------------------------------------- 1 | var LevelActions = require('../src/js/actions/LevelActions'); 2 | var LevelStore = require('../src/js/stores/LevelStore'); 3 | 4 | describe('this store', function() { 5 | 6 | it('has sequences and levels', function() { 7 | var sequenceMap = LevelStore.getSequenceToLevels(); 8 | Object.keys(sequenceMap).forEach(function(levelSequence) { 9 | expect(LevelStore.getSequences().indexOf(levelSequence) >= 0) 10 | .toEqual(true); 11 | 12 | sequenceMap[levelSequence].forEach(function(level) { 13 | expect(LevelStore.getLevel(level.id)).toEqual(level); 14 | }.bind(this)); 15 | }.bind(this)); 16 | }); 17 | 18 | it('can solve a level and then reset', function() { 19 | var sequenceMap = LevelStore.getSequenceToLevels(); 20 | var firstLevel = sequenceMap[ 21 | Object.keys(sequenceMap)[0] 22 | ][0]; 23 | 24 | expect(LevelStore.isLevelSolved(firstLevel.id)) 25 | .toEqual(false); 26 | LevelActions.setLevelSolved(firstLevel.id, false); 27 | expect(LevelStore.isLevelSolved(firstLevel.id)) 28 | .toEqual(true); 29 | LevelActions.resetLevelsSolved(); 30 | expect(LevelStore.isLevelSolved(firstLevel.id)) 31 | .toEqual(false); 32 | }); 33 | 34 | it('can solve a level with best status and then reset', function() { 35 | var sequenceMap = LevelStore.getSequenceToLevels(); 36 | var firstLevel = sequenceMap[ 37 | Object.keys(sequenceMap)[0] 38 | ][0]; 39 | 40 | expect(LevelStore.isLevelBest(firstLevel.id)) 41 | .toEqual(false); 42 | LevelActions.setLevelSolved(firstLevel.id, true); 43 | expect(LevelStore.isLevelBest(firstLevel.id)) 44 | .toEqual(true); 45 | LevelActions.resetLevelsSolved(); 46 | expect(LevelStore.isLevelBest(firstLevel.id)) 47 | .toEqual(false); 48 | }); 49 | 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/LocaleStore.spec.js: -------------------------------------------------------------------------------- 1 | var LocaleActions = require('../src/js/actions/LocaleActions'); 2 | var LocaleStore = require('../src/js/stores/LocaleStore'); 3 | 4 | describe('LocaleStore', function() { 5 | 6 | it('has default locale', function() { 7 | expect(LocaleStore.getLocale()) 8 | .toEqual(LocaleStore.getDefaultLocale()); 9 | }); 10 | 11 | it('changes locales', function() { 12 | expect(LocaleStore.getLocale()).toEqual('en_US'); 13 | LocaleActions.changeLocale('ja_JP'); 14 | expect(LocaleStore.getLocale()).toEqual('ja_JP'); 15 | }); 16 | 17 | it('changes locales from headers', function() { 18 | var headerLocaleMap = LocaleStore.getHeaderLocaleMap(); 19 | Object.keys(headerLocaleMap).forEach(function(header) { 20 | LocaleActions.changeLocaleFromHeader(header); 21 | expect(LocaleStore.getLocale()).toEqual( 22 | headerLocaleMap[header] 23 | ); 24 | }); 25 | }); 26 | 27 | it('changes locales from languages', function() { 28 | var langLocaleMap = LocaleStore.getLangLocaleMap(); 29 | Object.keys(langLocaleMap).forEach(function(lang) { 30 | LocaleActions.changeLocaleFromHeader(lang); 31 | expect(LocaleStore.getLocale()).toEqual( 32 | langLocaleMap[lang] 33 | ); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/animation.spec.js: -------------------------------------------------------------------------------- 1 | var AnimationModule = require('../src/js/visuals/animation/index'); 2 | var PromiseAnimation = AnimationModule.PromiseAnimation; 3 | var Animation = AnimationModule.Animation; 4 | var Q = require('q'); 5 | 6 | describe('Promise animation', function() { 7 | it('Will execute the closure', function() { 8 | var value = 0; 9 | var closure = function() { 10 | value++; 11 | }; 12 | 13 | var animation = new PromiseAnimation({ 14 | deferred: Q.defer(), 15 | closure: closure 16 | }); 17 | animation.play(); 18 | expect(value).toBe(1); 19 | }); 20 | 21 | it('also takes animation packs', function() { 22 | var value = 0; 23 | var animation = new PromiseAnimation({ 24 | animation: function() { value++; } 25 | }); 26 | animation.play(); 27 | expect(value).toBe(1); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/base.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | 3 | var HeadlessGit = require('../src/js/git/headless').HeadlessGit; 4 | var TreeCompare = require('../src/js/graph/treeCompare.js'); 5 | 6 | var loadTree = function(json) { 7 | return JSON.parse(unescape(json)); 8 | }; 9 | 10 | var compareLevelTree = function(headless, levelBlob) { 11 | var actualTree = headless.gitEngine.printTree(); 12 | return TreeCompare.dispatchFromLevel(levelBlob, actualTree); 13 | }; 14 | 15 | var compareAnswer = function(headless, expectedJSON) { 16 | var expectedTree = loadTree(expectedJSON); 17 | var actualTree = headless.gitEngine.exportTree(); 18 | 19 | return TreeCompare.compareTrees(expectedTree, actualTree); 20 | }; 21 | 22 | var getHeadlessSummary = function(headless) { 23 | var tree = headless.gitEngine.exportTree(); 24 | TreeCompare.reduceTreeFields([tree]); 25 | return tree; 26 | }; 27 | 28 | var expectLevelAsync = function(headless, levelBlob) { 29 | var command = levelBlob.solutionCommand; 30 | if (command.indexOf('git rebase -i') !== -1) { 31 | // don't do interactive rebase levels 32 | return; 33 | } 34 | 35 | return headless.sendCommand(command).then(function() { 36 | expect(compareLevelTree(headless, levelBlob)).toBeTruthy( 37 | 'Level "' + levelBlob['name']['en_US'] + '" should get solved' 38 | ); 39 | }); 40 | }; 41 | 42 | var expectTreeAsync = function(command, expectedJSON, startJSON) { 43 | var headless = new HeadlessGit(); 44 | 45 | if (startJSON) { 46 | headless.gitEngine.loadTreeFromString(startJSON); 47 | } 48 | 49 | return headless.sendCommand(command).then(function() { 50 | expect(compareAnswer(headless, expectedJSON)).toBeTruthy(); 51 | }); 52 | }; 53 | 54 | var expectLevelSolved = function(levelBlob) { 55 | var headless = new HeadlessGit(); 56 | if (levelBlob.startTree) { 57 | headless.gitEngine.loadTreeFromString(levelBlob.startTree); 58 | } 59 | expectLevelAsync(headless, levelBlob); 60 | }; 61 | 62 | var runCommand = function(command, resultHandler) { 63 | var headless = new HeadlessGit(); 64 | var deferred = Q.defer(); 65 | var msg = null; 66 | 67 | return headless.sendCommand(command, deferred).then(function() { 68 | return deferred.promise.then(function(commands) { 69 | msg = commands[commands.length - 1].get('error').get('msg'); 70 | resultHandler(msg); 71 | }); 72 | }); 73 | }; 74 | 75 | var TIME = 150; 76 | // useful for throwing garbage and then expecting one commit 77 | var ONE_COMMIT_TREE = '{"branches":{"main":{"target":"C2","id":"main"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"main","id":"HEAD"}}'; 78 | 79 | module.exports = { 80 | loadTree: loadTree, 81 | compareAnswer: compareAnswer, 82 | TIME: TIME, 83 | expectTreeAsync: expectTreeAsync, 84 | expectLevelSolved: expectLevelSolved, 85 | ONE_COMMIT_TREE: ONE_COMMIT_TREE, 86 | runCommand: runCommand 87 | }; 88 | -------------------------------------------------------------------------------- /__tests__/create.js: -------------------------------------------------------------------------------- 1 | var TreeCompare = require('../src/js/graph/treeCompare'); 2 | var HeadlessGit = require('../src/js/git/headless').HeadlessGit; 3 | 4 | var fs = require('fs'); 5 | prompt = require('prompt'); 6 | 7 | function getFile(truthy) { 8 | var filename = (truthy) ? 9 | './git.spec.js' : 10 | './remote.spec.js'; 11 | return fs.readFileSync(filename, 'utf8'); 12 | } 13 | 14 | function writeFile(truthy, content) { 15 | var filename = (truthy) ? 16 | './git.spec.js' : 17 | './remote.spec.js'; 18 | 19 | fs.writeFileSync(filename, content); 20 | } 21 | 22 | prompt.start(); 23 | 24 | prompt.get( 25 | ['command', 'whatItDoes', 'intoGitSpec'], 26 | function(err, result) { 27 | var headless = new HeadlessGit(); 28 | headless.sendCommand(result.command); 29 | setTimeout(function() { 30 | var testCase = '\t\texpectTreeAsync(\n' + 31 | "\t\t\t'" + result.command + "',\n" + 32 | "\t\t\t'" + headless.gitEngine.printTree() + "'\n" + 33 | "\t\t);\n"; 34 | 35 | console.log(testCase); 36 | // now add it 37 | var testFile = getFile(result.intoGitSpec); 38 | // insert after the last }) 39 | var toSlice = testFile.lastIndexOf('})'); 40 | var partOne = testFile.slice(0, toSlice); 41 | var partTwo = testFile.slice(toSlice); 42 | 43 | var funcCall = "\tit('" + result.whatItDoes + "', function() {\n" + 44 | testCase + "\t});\n\n"; 45 | writeFile(result.intoGitSpec, partOne + funcCall + partTwo); 46 | }, 100); 47 | } 48 | ); 49 | 50 | -------------------------------------------------------------------------------- /__tests__/levels.spec.js: -------------------------------------------------------------------------------- 1 | var base = require('./base'); 2 | 3 | describe('GitEngine Levels', function() { 4 | var sequences = require('../src/levels/index').levelSequences; 5 | Object.keys(sequences).forEach(function(sequenceKey) { 6 | var levels = sequences[sequenceKey]; 7 | Object.keys(levels).forEach(function(index) { 8 | var levelBlob = levels[index]; 9 | it('solves level ' + levelBlob['name']['en_US'] + ' in sequence ' + sequenceKey, function() { 10 | base.expectLevelSolved(levelBlob); 11 | }); 12 | }.bind(this)); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/mercurial.spec.js: -------------------------------------------------------------------------------- 1 | var base = require('./base'); 2 | var expectTreeAsync = base.expectTreeAsync; 3 | 4 | describe('Mercurial', function() { 5 | var assert = function(msg, command, tree) { 6 | it(msg, function() { 7 | return expectTreeAsync(command, tree); 8 | }); 9 | }; 10 | 11 | assert( 12 | 'Commits', 13 | 'hg commit', 14 | base.ONE_COMMIT_TREE 15 | ); 16 | 17 | assert( 18 | 'Makes a bookmark', 19 | 'hg book;hg book foo;hg ci;hg book -r C0 asd;', 20 | '{"branches":{"main":{"target":"C1","id":"main"},"foo":{"target":"C2","id":"foo"},"asd":{"target":"C0","id":"asd"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"foo","id":"HEAD"}}' 21 | ); 22 | 23 | assert( 24 | 'updates to bookmarks', 25 | 'hg book;hg book foo;hg ci;hg book -r C0 asd; hg update asd', 26 | '{"branches":{"main":{"target":"C1","id":"main"},"foo":{"target":"C2","id":"foo"},"asd":{"target":"C0","id":"asd"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"asd","id":"HEAD"}}' 27 | ); 28 | 29 | assert( 30 | 'updates to revisions', 31 | 'hg update -r C0', 32 | '{"branches":{"main":{"target":"C1","id":"main"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"C0","id":"HEAD"}}' 33 | ); 34 | 35 | assert( 36 | 'backs out revisions and bookmarks', 37 | 'hg book -r C0 foo;hg ci;hg backout foo;hg backout -r C1 C2;', 38 | '%7B%22branches%22%3A%7B%22main%22%3A%7B%22target%22%3A%22C2%27%22%2C%22id%22%3A%22main%22%7D%2C%22foo%22%3A%7B%22target%22%3A%22C0%22%2C%22id%22%3A%22foo%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C0%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C0%27%22%7D%2C%22C1%27%22%3A%7B%22parents%22%3A%5B%22C0%27%22%5D%2C%22id%22%3A%22C1%27%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%27%22%5D%2C%22id%22%3A%22C2%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22main%22%2C%22id%22%3A%22HEAD%22%7D%7D' 39 | ); 40 | 41 | assert( 42 | 'commits and amends', 43 | 'hg commit -A; hg commit --amend', 44 | '%7B%22branches%22%3A%7B%22main%22%3A%7B%22target%22%3A%22C2%27%22%2C%22id%22%3A%22main%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22main%22%2C%22id%22%3A%22HEAD%22%7D%7D' 45 | ); 46 | 47 | assert( 48 | 'rebases with -d', 49 | 'hg ci; hg book -r C1 feature; hg update feature; hg ci;hg book debug;hg ci;hg rebase -d main;', 50 | '%7B%22branches%22%3A%7B%22main%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22main%22%7D%2C%22feature%22%3A%7B%22target%22%3A%22C3%27%22%2C%22id%22%3A%22feature%22%7D%2C%22debug%22%3A%7B%22target%22%3A%22C4%27%22%2C%22id%22%3A%22debug%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C4%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22debug%22%2C%22id%22%3A%22HEAD%22%7D%7D' 51 | ); 52 | 53 | assert( 54 | 'rebases with -d below stuff', 55 | 'hg ci; hg book -r C1 feature; hg update feature; hg ci;hg book -r C3 debug;hg ci;hg up debug;hg rebase -d main -b .;', 56 | '%7B%22branches%22%3A%7B%22main%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22main%22%7D%2C%22feature%22%3A%7B%22target%22%3A%22C4%22%2C%22id%22%3A%22feature%22%7D%2C%22debug%22%3A%7B%22target%22%3A%22C3%27%22%2C%22id%22%3A%22debug%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22debug%22%2C%22id%22%3A%22HEAD%22%7D%7D' 57 | ); 58 | 59 | assert( 60 | 'grafts commits down', 61 | 'hg book foo;hg commit; hg update main; hg commit;hg graft -r C2;hg update foo; hg graft -r C3', 62 | '%7B%22branches%22%3A%7B%22main%22%3A%7B%22target%22%3A%22C2%27%22%2C%22id%22%3A%22main%22%2C%22remoteTrackingBranchID%22%3Anull%7D%2C%22foo%22%3A%7B%22target%22%3A%22C3%27%22%2C%22id%22%3A%22foo%22%2C%22remoteTrackingBranchID%22%3Anull%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%7D%2C%22tags%22%3A%7B%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22foo%22%2C%22id%22%3A%22HEAD%22%7D%7D' 63 | ); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /__tests__/simpleRemote.spec.js: -------------------------------------------------------------------------------- 1 | var base = require('./base'); 2 | var expectTreeAsync = base.expectTreeAsync; 3 | 4 | describe('Git Remote simple', function() { 5 | it('clones', function() { 6 | return expectTreeAsync( 7 | 'git clone', 8 | '{"branches":{"main":{"target":"C1","id":"main","remoteTrackingBranchID":"o/main"},"o/main":{"target":"C1","id":"o/main","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"main","id":"HEAD"},"originTree":{"branches":{"main":{"target":"C1","id":"main","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"main","id":"HEAD"}}}' 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /__tests__/vcs.spec.js: -------------------------------------------------------------------------------- 1 | var Command = require('../src/js/models/commandModel').Command; 2 | 3 | describe('commands', function() { 4 | it('replaces . with HEAD correctly', function() { 5 | var testCases = { 6 | '.^': 'HEAD^', 7 | '.': 'HEAD', 8 | '.~4': 'HEAD~4' 9 | }; 10 | 11 | var c = new Command({rawStr: 'foo'}); 12 | Object.keys(testCases).forEach(function(input) { 13 | var expected = testCases[input]; 14 | var actual = c.replaceDotWithHead(input); 15 | expect(actual).toBe(expected); 16 | }.bind(this)); 17 | }); 18 | 19 | it('maps options and general args', function() { 20 | var testCases = [{ 21 | args: ['.~4', 'HEAD^'], 22 | options: { 23 | '--amend': ['.'], 24 | '-m': ['"oh hai"'] 25 | }, 26 | gitArgs: ['HEAD~4', 'HEAD^'], 27 | gitOptions: { 28 | '--amend': ['HEAD'], 29 | '-m': ['"oh hai"'] 30 | } 31 | }]; 32 | 33 | var c = new Command({rawStr: 'foo'}); 34 | testCases.forEach(function(tCase) { 35 | c.setOptionsMap(tCase.options); 36 | c.setGeneralArgs(tCase.args); 37 | c.mapDotToHead(); 38 | 39 | var j = JSON.stringify; 40 | expect( 41 | j(c.getGeneralArgs()) 42 | ).toBe( 43 | j(tCase.gitArgs) 44 | ); 45 | 46 | expect( 47 | j(c.getOptionsMap()) 48 | ).toBe( 49 | j(tCase.gitOptions) 50 | ); 51 | }.bind(this)); 52 | }); 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/favicon.ico -------------------------------------------------------------------------------- /assets/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/learnGitBranching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/learnGitBranching.png -------------------------------------------------------------------------------- /assets/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /assets/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /assets/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /assets/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /assets/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /assets/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /assets/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /assets/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcottle/learnGitBranching/bd908010d9e7ccdbe8299b8b20dba227a6566d27/assets/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /checkgit.sh: -------------------------------------------------------------------------------- 1 | GIT_STATUS=$(git status --porcelain | wc -l ) 2 | if [[ GIT_STATUS -ne 0 ]]; then 3 | echo "${1:-Source files were modified}" 4 | git status 5 | exit $GIT_STATUS 6 | fi; 7 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var { execSync } = require('child_process'); 2 | var { 3 | writeFileSync, readdirSync, readFileSync, 4 | existsSync, statSync, mkdirSync, copyFileSync, 5 | } = require('fs'); 6 | var path = require('path'); 7 | 8 | var { marked } = require('marked'); 9 | var glob = require('glob'); 10 | var _ = require('underscore'); 11 | 12 | var { src, dest, series, watch } = require('gulp'); 13 | var log = require('fancy-log'); 14 | var gHash = require('gulp-hash'); 15 | var gClean = require('gulp-clean'); 16 | var concat = require('gulp-concat'); 17 | var cleanCSS = require('gulp-clean-css'); 18 | var gTerser = require('gulp-terser'); 19 | var gJasmine = require('gulp-jasmine'); 20 | var { minify } = require('html-minifier'); 21 | var { SpecReporter } = require('jasmine-spec-reporter'); 22 | var gJshint = require('gulp-jshint'); 23 | 24 | var source = require('vinyl-source-stream'); 25 | var buffer = require('vinyl-buffer'); 26 | var browserify = require('browserify'); 27 | var babelify = require('babelify'); 28 | 29 | _.templateSettings.interpolate = /\{\{(.+?)\}\}/g; 30 | _.templateSettings.escape = /\{\{\{(.*?)\}\}\}/g; 31 | _.templateSettings.evaluate = /\{\{-(.*?)\}\}/g; 32 | 33 | // precompile for speed 34 | var indexFile = readFileSync('src/template.index.html').toString(); 35 | var indexTemplate = _.template(indexFile); 36 | 37 | var compliments = [ 38 | 'Thanks to Hong4rc for the modern and amazing gulp workflow!', 39 | 'I hope you all have a great day :)' 40 | ]; 41 | var compliment = (done) => { 42 | var index = Math.floor(Math.random() * compliments.length); 43 | 44 | log(compliments[index]); 45 | done(); 46 | }; 47 | 48 | const lintStrings = (done) => { 49 | execSync('node src/js/intl/checkStrings'); 50 | done(); 51 | }; 52 | 53 | 54 | var destDir = './build/'; 55 | 56 | var copyRecursiveSync = (src, dest) => { 57 | var exists = existsSync(src); 58 | var stats = exists && statSync(src); 59 | var isDirectory = exists && stats.isDirectory(); 60 | if (isDirectory) { 61 | mkdirSync(dest); 62 | readdirSync(src).forEach((childItemName) => { 63 | copyRecursiveSync( 64 | path.join(src, childItemName), 65 | path.join(dest, childItemName)); 66 | }); 67 | } else { 68 | copyFileSync(src, dest); 69 | } 70 | }; 71 | 72 | var buildIndex = function(done) { 73 | log('Building index...'); 74 | 75 | // first find the one in here that we want 76 | var buildFiles = readdirSync(destDir); 77 | 78 | var jsRegex = /bundle-[\.\w]+\.js/; 79 | var jsFile = buildFiles.find(function(name) { 80 | return jsRegex.exec(name); 81 | }); 82 | if (!jsFile) { 83 | throw new Error('no hashed min file found!'); 84 | } 85 | log('Found hashed js file: ' + jsFile); 86 | 87 | var styleRegex = /main-[\.\w]+\.css/; 88 | var styleFile = buildFiles.find(function(name) { 89 | return styleRegex.exec(name); 90 | }); 91 | if (!styleFile) { 92 | throw new Error('no hashed css file found!'); 93 | } 94 | log('Found hashed style file: ' + styleFile); 95 | 96 | var buildDir = process.env.CI ? '.' : 'build'; 97 | 98 | // output these filenames to our index template 99 | var outputIndex = indexTemplate({ 100 | buildDir, 101 | jsFile, 102 | styleFile, 103 | }); 104 | 105 | if (process.env.NODE_ENV === 'production') { 106 | outputIndex = minify(outputIndex, { 107 | minifyJS: true, 108 | collapseWhitespace: true, 109 | processScripts: ['text/html'], 110 | removeComments: true, 111 | }); 112 | } 113 | 114 | if (process.env.CI) { 115 | writeFileSync('build/index.html', outputIndex); 116 | copyRecursiveSync('assets', 'build/assets'); 117 | } else { 118 | writeFileSync('index.html', outputIndex); 119 | } 120 | done(); 121 | }; 122 | 123 | var getBundle = function() { 124 | return browserify({ 125 | entries: [...glob.sync('src/**/*.js'), ...glob.sync('src/**/*.jsx')], 126 | debug: true, 127 | }) 128 | .transform(babelify, { presets: ['@babel/preset-react'] }) 129 | .bundle() 130 | .pipe(source('bundle.js')) 131 | .pipe(buffer()) 132 | .pipe(gHash()); 133 | }; 134 | 135 | var clean = function () { 136 | return src(destDir, { read: false, allowEmpty: true }) 137 | .pipe(gClean()); 138 | }; 139 | 140 | var convertMarkdownStringsToHTML = function(markdowns) { 141 | return marked(markdowns.join('\n')); 142 | }; 143 | 144 | 145 | var jshint = function() { 146 | return src([ 147 | 'gulpfile.js', 148 | '__tests__/', 149 | 'src/' 150 | ]) 151 | .pipe(gJshint()) 152 | .pipe(gJshint.reporter('default')); 153 | }; 154 | 155 | var ifyBuild = function() { 156 | return getBundle() 157 | .pipe(dest(destDir)); 158 | }; 159 | 160 | var miniBuild = function() { 161 | process.env.NODE_ENV = 'production'; 162 | return getBundle() 163 | .pipe(gTerser()) 164 | .pipe(dest(destDir)); 165 | }; 166 | 167 | var style = function() { 168 | var chain = src('src/style/*.css') 169 | .pipe(concat('main.css')); 170 | 171 | if (process.env.NODE_ENV === 'production') { 172 | chain = chain.pipe(cleanCSS()); 173 | } 174 | 175 | return chain.pipe(gHash()) 176 | .pipe(dest(destDir)); 177 | }; 178 | 179 | var jasmine = function() { 180 | return src('__tests__/*.spec.js') 181 | .pipe(gJasmine({ 182 | config: { 183 | verbose: true, 184 | random: false, 185 | }, 186 | reporter: new SpecReporter(), 187 | })); 188 | }; 189 | 190 | var gitAdd = function(done) { 191 | execSync('git add build/'); 192 | done(); 193 | }; 194 | 195 | var gitDeployMergeMain = function(done) { 196 | execSync('git checkout gh-pages && git merge main -m "merge main"'); 197 | done(); 198 | }; 199 | 200 | var gitDeployPushOrigin = function(done) { 201 | execSync('git commit -am "rebuild for prod"; ' + 202 | 'git push origin gh-pages --force && ' + 203 | 'git branch -f trunk gh-pages && ' + 204 | 'git checkout main' 205 | ); 206 | done(); 207 | }; 208 | 209 | var generateLevelDocs = function(done) { 210 | log('Generating level documentation...'); 211 | 212 | // Get all level files 213 | const allLevels= require('./src/levels/index'); 214 | const cssContent = readFileSync('./generatedDocs/github-markdown.css'); 215 | 216 | let htmlContent = ` 217 | 218 | 219 | 220 | Learn Git Branching - Level Documentation 221 | 222 | 230 | 231 | 232 |
233 |

Learn Git Branching - All Levels Documentation

234 | `; 235 | 236 | Object.keys(allLevels.sequenceInfo).forEach(sequenceKey => { 237 | log('Processing sequence: ', sequenceKey); 238 | 239 | const sequenceInfo = allLevels.sequenceInfo[sequenceKey]; 240 | htmlContent += ` 241 |

Level Sequence: ${sequenceInfo.displayName.en_US}

242 |
${sequenceInfo.about.en_US}
243 | `; 244 | 245 | const levels = allLevels.levelSequences[sequenceKey]; 246 | for (const level of levels) { 247 | htmlContent += `

Level: ${level.name.en_US}

`; 248 | 249 | const startDialog = level.startDialog.en_US; 250 | for (const dialog of startDialog.childViews) { 251 | const childViewType = dialog.type; 252 | if (childViewType === 'ModalAlert') { 253 | htmlContent += convertMarkdownStringsToHTML(dialog.options.markdowns); 254 | } else if (childViewType === 'GitDemonstrationView') { 255 | htmlContent += convertMarkdownStringsToHTML(dialog.options.beforeMarkdowns); 256 | htmlContent += `
${dialog.options.command}
`; 257 | htmlContent += convertMarkdownStringsToHTML(dialog.options.afterMarkdowns); 258 | } else { 259 | throw new Error(`Unknown child view type: ${childViewType}`); 260 | } 261 | } 262 | } 263 | 264 | }); 265 | 266 | htmlContent += ` 267 |
268 | 269 | 270 | `; 271 | 272 | // Write the file 273 | writeFileSync('generatedDocs/levels.html', htmlContent); 274 | log('Level documentation generated at build/levels.html'); 275 | done(); 276 | }; 277 | 278 | var fastBuild = series(clean, ifyBuild, style, buildIndex, jshint); 279 | 280 | var build = series( 281 | clean, 282 | miniBuild, style, buildIndex, 283 | gitAdd, jasmine, jshint, 284 | lintStrings, compliment 285 | ); 286 | 287 | var deploy = series( 288 | clean, 289 | jasmine, 290 | jshint, 291 | gitDeployMergeMain, 292 | build, 293 | gitDeployPushOrigin, 294 | compliment 295 | ); 296 | 297 | var lint = series(jshint, compliment); 298 | 299 | var watching = function() { 300 | return watch([ 301 | 'gulpfile.js', 302 | '__tests__/git.spec.js', 303 | 'src/js/**/*.js', 304 | 'src/js/**/**/*.js', 305 | 'src/js/**/*.jsx', 306 | 'src/levels/**/*.js' 307 | ], series([fastBuild , jasmine, jshint, lintStrings])); 308 | }; 309 | 310 | module.exports = { 311 | default: build, 312 | lint, 313 | fastBuild, 314 | watching, 315 | build, 316 | test: jasmine, 317 | deploy, 318 | generateLevelDocs, 319 | }; 320 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LearnGitBranching", 3 | "version": "0.8.0", 4 | "description": "An interactive git visualization to challenge and educate!", 5 | "homepage": "https://learngitbranching.js.org", 6 | "author": "Peter Cottle ", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "vite", 10 | "prepare": "gulp fastBuild", 11 | "test": "gulp test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/pcottle/learnGitBranching" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.17.5", 19 | "@babel/preset-react": "^7.16.7", 20 | "babelify": "^10.0.0", 21 | "browserify": "^17.0.0", 22 | "fancy-log": "^1.3.3", 23 | "glob": "^7.2.0", 24 | "gulp": "^4.0.2", 25 | "gulp-clean": "^0.4.0", 26 | "gulp-clean-css": "^4.3.0", 27 | "gulp-concat": "^2.6.1", 28 | "gulp-hash": "^4.2.2", 29 | "gulp-jasmine": "^4.0.0", 30 | "gulp-jshint": "^2.1.0", 31 | "gulp-terser": "^2.1.0", 32 | "gulp-uglify": "^3.0.2", 33 | "html-minifier": "^4.0.0", 34 | "jasmine-spec-reporter": "^7.0.0", 35 | "jshint": "^2.13.4", 36 | "prompt": "^1.2.2", 37 | "vinyl-buffer": "^1.0.1", 38 | "vinyl-source-stream": "^2.0.0", 39 | "vite": "^4.5.6" 40 | }, 41 | "dependencies": { 42 | "backbone": "^1.4.0", 43 | "flux": "^4.0.3", 44 | "jquery": "^3.4.0", 45 | "jquery-ui": "^1.13.2", 46 | "marked": "^4.0.12", 47 | "prop-types": "^15.8.1", 48 | "q": "^1.5.1", 49 | "raphael": "^2.1.0", 50 | "react": "^17.0.2", 51 | "react-dom": "^17.0.2", 52 | "underscore": "^1.13.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/js/actions/CommandLineActions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | 6 | var ActionTypes = AppConstants.ActionTypes; 7 | 8 | var CommandLineActions = { 9 | 10 | submitCommand: function(text) { 11 | AppDispatcher.handleViewAction({ 12 | type: ActionTypes.SUBMIT_COMMAND, 13 | text: text 14 | }); 15 | } 16 | 17 | }; 18 | 19 | module.exports = CommandLineActions; 20 | -------------------------------------------------------------------------------- /src/js/actions/GlobalStateActions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | 6 | var ActionTypes = AppConstants.ActionTypes; 7 | 8 | var GlobalStateActions = { 9 | 10 | changeIsAnimating: function(isAnimating) { 11 | AppDispatcher.handleViewAction({ 12 | type: ActionTypes.CHANGE_IS_ANIMATING, 13 | isAnimating: isAnimating 14 | }); 15 | }, 16 | 17 | levelSolved: function() { 18 | AppDispatcher.handleViewAction({ 19 | type: ActionTypes.LEVEL_SOLVED, 20 | }); 21 | }, 22 | 23 | disableLevelInstructions: function() { 24 | AppDispatcher.handleViewAction({ 25 | type: ActionTypes.DISABLE_LEVEL_INSTRUCTIONS, 26 | }); 27 | }, 28 | 29 | changeFlipTreeY: function(flipTreeY) { 30 | AppDispatcher.handleViewAction({ 31 | type: ActionTypes.CHANGE_FLIP_TREE_Y, 32 | flipTreeY: flipTreeY 33 | }); 34 | } 35 | 36 | }; 37 | 38 | module.exports = GlobalStateActions; 39 | -------------------------------------------------------------------------------- /src/js/actions/LevelActions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | 6 | var ActionTypes = AppConstants.ActionTypes; 7 | 8 | var LevelActions = { 9 | 10 | setLevelSolved: function(levelID, best) { 11 | AppDispatcher.handleViewAction({ 12 | type: ActionTypes.SET_LEVEL_SOLVED, 13 | levelID: levelID, 14 | best: best 15 | }); 16 | }, 17 | 18 | resetLevelsSolved: function() { 19 | AppDispatcher.handleViewAction({ 20 | type: ActionTypes.RESET_LEVELS_SOLVED 21 | }); 22 | }, 23 | 24 | setIsSolvingLevel: function(isSolvingLevel) { 25 | AppDispatcher.handleViewAction({ 26 | type: ActionTypes.SET_IS_SOLVING_LEVEL, 27 | isSolvingLevel: isSolvingLevel, 28 | }); 29 | }, 30 | 31 | }; 32 | 33 | module.exports = LevelActions; -------------------------------------------------------------------------------- /src/js/actions/LocaleActions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | 6 | var ActionTypes = AppConstants.ActionTypes; 7 | 8 | var LocaleActions = { 9 | 10 | changeLocale: function(newLocale) { 11 | AppDispatcher.handleViewAction({ 12 | type: ActionTypes.CHANGE_LOCALE, 13 | locale: newLocale 14 | }); 15 | }, 16 | 17 | changeLocaleFromURI: function(newLocale) { 18 | AppDispatcher.handleURIAction({ 19 | type: ActionTypes.CHANGE_LOCALE, 20 | locale: newLocale 21 | }); 22 | }, 23 | 24 | changeLocaleFromHeader: function(header) { 25 | AppDispatcher.handleViewAction({ 26 | type: ActionTypes.CHANGE_LOCALE_FROM_HEADER, 27 | header: header 28 | }); 29 | } 30 | }; 31 | 32 | module.exports = LocaleActions; 33 | -------------------------------------------------------------------------------- /src/js/commands/index.js: -------------------------------------------------------------------------------- 1 | var intl = require('../intl'); 2 | 3 | var Errors = require('../util/errors'); 4 | var GitCommands = require('../git/commands'); 5 | var MercurialCommands = require('../mercurial/commands'); 6 | 7 | var CommandProcessError = Errors.CommandProcessError; 8 | var CommandResult = Errors.CommandResult; 9 | 10 | var commandConfigs = { 11 | 'git': GitCommands.commandConfig, 12 | 'hg': MercurialCommands.commandConfig 13 | }; 14 | 15 | var commands = { 16 | execute: function(vcs, name, engine, commandObj) { 17 | if (!commandConfigs[vcs][name]) { 18 | throw new Error('i don\'t have a command for ' + name); 19 | } 20 | var config = commandConfigs[vcs][name]; 21 | if (config.delegate) { 22 | return this.delegateExecute(config, engine, commandObj); 23 | } 24 | 25 | config.execute.call(this, engine, commandObj); 26 | }, 27 | 28 | delegateExecute: function(config, engine, commandObj) { 29 | // we have delegated to another vcs command, so lets 30 | // execute that and get the result 31 | var result = config.delegate.call(this, engine, commandObj); 32 | 33 | if (result.multiDelegate) { 34 | // we need to do multiple delegations with 35 | // a different command at each step 36 | result.multiDelegate.forEach(function(delConfig) { 37 | // copy command, and then set opts 38 | commandObj.setOptionsMap(delConfig.options || {}); 39 | commandObj.setGeneralArgs(delConfig.args || []); 40 | 41 | commandConfigs[delConfig.vcs][delConfig.name].execute.call(this, engine, commandObj); 42 | }, this); 43 | } else { 44 | config = commandConfigs[result.vcs][result.name]; 45 | // commandObj is PASSED BY REFERENCE 46 | // and modified in the function 47 | commandConfigs[result.vcs][result.name].execute.call(this, engine, commandObj); 48 | } 49 | }, 50 | 51 | blankMap: function() { 52 | return {git: {}, hg: {}}; 53 | }, 54 | 55 | getShortcutMap: function() { 56 | var map = this.blankMap(); 57 | this.loop(function(config, name, vcs) { 58 | if (!config.sc) { 59 | return; 60 | } 61 | map[vcs][name] = config.sc; 62 | }, this); 63 | return map; 64 | }, 65 | 66 | getOptionMap: function() { 67 | var optionMap = this.blankMap(); 68 | this.loop(function(config, name, vcs) { 69 | var displayName = config.displayName || name; 70 | var thisMap = {}; 71 | // start all options off as disabled 72 | (config.options || []).forEach(function(option) { 73 | thisMap[option] = false; 74 | }); 75 | optionMap[vcs][displayName] = thisMap; 76 | }); 77 | return optionMap; 78 | }, 79 | 80 | getRegexMap: function() { 81 | var map = this.blankMap(); 82 | this.loop(function(config, name, vcs) { 83 | var displayName = config.displayName || name; 84 | map[vcs][displayName] = config.regex; 85 | }); 86 | return map; 87 | }, 88 | 89 | /** 90 | * which commands count for the git golf game 91 | */ 92 | getCommandsThatCount: function() { 93 | var counted = this.blankMap(); 94 | this.loop(function(config, name, vcs) { 95 | if (config.dontCountForGolf) { 96 | return; 97 | } 98 | counted[vcs][name] = config.regex; 99 | }); 100 | return counted; 101 | }, 102 | 103 | loop: function(callback, context) { 104 | Object.keys(commandConfigs).forEach(function(vcs) { 105 | var commandConfig = commandConfigs[vcs]; 106 | Object.keys(commandConfig).forEach(function(name) { 107 | var config = commandConfig[name]; 108 | callback(config, name, vcs); 109 | }); 110 | }); 111 | } 112 | }; 113 | 114 | var parse = function(str) { 115 | var vcs; 116 | var method; 117 | var options; 118 | 119 | // see if we support this particular command 120 | var regexMap = commands.getRegexMap(); 121 | Object.keys(regexMap).forEach(function (thisVCS) { 122 | var map = regexMap[thisVCS]; 123 | Object.keys(map).forEach(function(thisMethod) { 124 | var regex = map[thisMethod]; 125 | if (regex.exec(str)) { 126 | vcs = thisVCS; 127 | method = thisMethod; 128 | // every valid regex has to have the parts of 129 | // 130 | // because there are always two space-groups 131 | // before our "stuff" we can simply 132 | // split on space-groups and grab everything after 133 | // the second: 134 | options = str.match(/('.*?'|".*?"|\S+)/g).slice(2); 135 | } 136 | }); 137 | }); 138 | 139 | if (!method) { 140 | return false; 141 | } 142 | 143 | // we support this command! 144 | // parse off the options and assemble the map / general args 145 | var parsedOptions = new CommandOptionParser(vcs, method, options); 146 | var error = parsedOptions.explodeAndSet(); 147 | return { 148 | toSet: { 149 | generalArgs: parsedOptions.generalArgs, 150 | supportedMap: parsedOptions.supportedMap, 151 | error: error, 152 | vcs: vcs, 153 | method: method, 154 | options: options, 155 | eventName: 'processGitCommand' 156 | } 157 | }; 158 | }; 159 | 160 | /** 161 | * CommandOptionParser 162 | */ 163 | function CommandOptionParser(vcs, method, options) { 164 | this.vcs = vcs; 165 | this.method = method; 166 | this.rawOptions = options; 167 | 168 | this.supportedMap = commands.getOptionMap()[vcs][method]; 169 | if (this.supportedMap === undefined) { 170 | throw new Error('No option map for ' + method); 171 | } 172 | 173 | this.generalArgs = []; 174 | } 175 | 176 | CommandOptionParser.prototype.explodeAndSet = function() { 177 | for (var i = 0; i < this.rawOptions.length; i++) { 178 | var part = this.rawOptions[i]; 179 | 180 | if (part.slice(0,1) == '-') { 181 | // it's an option, check supportedMap 182 | if (this.supportedMap[part] === undefined) { 183 | return new CommandProcessError({ 184 | msg: intl.str( 185 | 'option-not-supported', 186 | { option: part } 187 | ) 188 | }); 189 | } 190 | 191 | var next = this.rawOptions[i + 1]; 192 | var optionArgs = []; 193 | if (next && next.slice(0,1) !== '-') { 194 | // only store the next argument as this 195 | // option value if its not another option 196 | i++; 197 | optionArgs = [next]; 198 | } 199 | this.supportedMap[part] = optionArgs; 200 | } else { 201 | // must be a general arg 202 | this.generalArgs.push(part); 203 | } 204 | } 205 | }; 206 | 207 | exports.commands = commands; 208 | exports.parse = parse; 209 | -------------------------------------------------------------------------------- /src/js/constants/AppConstants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var keyMirror = require('../util/keyMirror'); 4 | 5 | var CHANGE_EVENT = 'change'; 6 | 7 | module.exports = { 8 | 9 | CHANGE_EVENT: CHANGE_EVENT, 10 | 11 | StoreSubscribePrototype: { 12 | subscribe: function(cb) { 13 | this.on(CHANGE_EVENT, cb); 14 | }, 15 | 16 | unsubscribe: function(cb) { 17 | this.removeListener(CHANGE_EVENT, cb); 18 | } 19 | }, 20 | 21 | ActionTypes: keyMirror({ 22 | SET_LEVEL_SOLVED: null, 23 | RESET_LEVELS_SOLVED: null, 24 | CHANGE_IS_ANIMATING: null, 25 | CHANGE_FLIP_TREE_Y: null, 26 | SUBMIT_COMMAND: null, 27 | CHANGE_LOCALE: null, 28 | CHANGE_LOCALE_FROM_HEADER: null, 29 | DISABLE_LEVEL_INSTRUCTIONS: null, 30 | /** 31 | * only dispatched when you actually 32 | * solve the level, not ask for solution 33 | * or solve it again. 34 | */ 35 | SOLVE_LEVEL: null, 36 | SET_IS_SOLVING_LEVEL: null, 37 | }), 38 | 39 | PayloadSources: keyMirror({ 40 | VIEW_ACTION: null, 41 | URI_ACTION: null 42 | }) 43 | }; 44 | -------------------------------------------------------------------------------- /src/js/dialogs/confirmShowSolution.js: -------------------------------------------------------------------------------- 1 | exports.dialog = { 2 | 'en_US': [{ 3 | type: 'ModalAlert', 4 | options: { 5 | markdowns: [ 6 | '## Are you sure you want to see the solution?', 7 | '', 8 | 'I believe in you! You can do it' 9 | ] 10 | } 11 | }], 12 | 'de_DE': [{ 13 | type: 'ModalAlert', 14 | options: { 15 | markdowns: [ 16 | '## Bist du sicher, dass du die Lösung sehen willst?', 17 | '', 18 | 'Ich glaube an dich! Du schaffst das schon!' 19 | ] 20 | } 21 | }], 22 | 'zh_CN': [{ 23 | type: 'ModalAlert', 24 | options: { 25 | markdowns: [ 26 | '## 确定要看答案吗?', 27 | '', 28 | '相信自己,你可以的!' 29 | ] 30 | } 31 | }], 32 | 'zh_TW': [{ 33 | type: 'ModalAlert', 34 | options: { 35 | markdowns: [ 36 | '## 確定偷看解答嗎?', 37 | '', 38 | '我相信你!你可以的' 39 | ] 40 | } 41 | }], 42 | 'es_AR': [{ 43 | type: 'ModalAlert', 44 | options: { 45 | markdowns: [ 46 | '## ¿Realmente querés ver la solución?', 47 | '', 48 | '¡Creo en vos! ¡Dale que podés!' 49 | ] 50 | } 51 | }], 52 | 'es_MX': [{ 53 | type: 'ModalAlert', 54 | options: { 55 | markdowns: [ 56 | '## ¿Estás seguro de que quieres ver la solución?', 57 | '', 58 | '¡Creo en ti! ¡Yo sé que puedes!' 59 | ] 60 | } 61 | }], 62 | 'es_ES': [{ 63 | type: 'ModalAlert', 64 | options: { 65 | markdowns: [ 66 | '## ¿Estás seguro de que quieres ver la solución?', 67 | '', 68 | '¡Creo en ti! ¡Ánimo!' 69 | ] 70 | } 71 | }], 72 | 'pt_BR': [{ 73 | type: 'ModalAlert', 74 | options: { 75 | markdowns: [ 76 | '## Tem certeza que quer ver a solução?', 77 | '', 78 | 'Vamos lá, acredito que você consegue!' 79 | ] 80 | } 81 | }], 82 | 'gl': [{ 83 | type: 'ModalAlert', 84 | options: { 85 | markdowns: [ 86 | '## ¿Queres ver a solución?', 87 | '', 88 | 'Seguro que podes, ¡inténtao unha vez máis!' 89 | ] 90 | } 91 | }], 92 | 'fr_FR': [{ 93 | type: 'ModalAlert', 94 | options: { 95 | markdowns: [ 96 | '## Êtes-vous sûr de vouloir voir la solution ?', 97 | '', 98 | 'Je crois en vous ! Vous pouvez le faire !' 99 | ] 100 | } 101 | }], 102 | 'ja': [{ 103 | type: 'ModalAlert', 104 | options: { 105 | markdowns: [ 106 | '## どうしても正解がみたいですか?', 107 | '', 108 | 'あなたならきっとできるって信じてます!' 109 | ] 110 | } 111 | }], 112 | 'ro': [ 113 | { 114 | type: "ModalAlert", 115 | options: { 116 | markdowns: [ 117 | "## Ești sigur că vrei să vezi soluția?", 118 | "", 119 | "Am încredere în tine! Poți să o faci!", 120 | ], 121 | }, 122 | }, 123 | ], 124 | 'ru_RU': [{ 125 | type: 'ModalAlert', 126 | options: { 127 | markdowns: [ 128 | '## Уверен, что хочешь посмотреть решение?', 129 | '', 130 | 'Мы верим в тебя! Не прыгай! Ты сможешь!' 131 | ] 132 | } 133 | }], 134 | 'uk': [{ 135 | type: 'ModalAlert', 136 | options: { 137 | markdowns: [ 138 | '## Впевнений, що хочеш побачити розв’язок?', 139 | '', 140 | 'Я вірю в тебе! Ти впораєшся!' 141 | ] 142 | } 143 | }], 144 | 'vi': [{ 145 | type: 'ModalAlert', 146 | options: { 147 | markdowns: [ 148 | '## Bạn chắc là muốn xem đáp án chứ?', 149 | '', 150 | 'Tôi tin ở bạn! Bạn có thể làm được!' 151 | ] 152 | } 153 | }], 154 | 'sl_SI': [{ 155 | type: 'ModalAlert', 156 | options: { 157 | markdowns: [ 158 | '## Si prepričan, da hočeš videti rešitev?', 159 | '', 160 | 'Verjamem vate! Maš ti to! Ali pač ne?' 161 | ] 162 | } 163 | }], 164 | 'pl': [{ 165 | type: 'ModalAlert', 166 | options: { 167 | markdowns: [ 168 | '## Czy na pewno chcesz zobaczyć rozwiązanie?', 169 | '', 170 | 'Wierzę w Ciebie! Możesz to zrobić' 171 | ] 172 | } 173 | }], 174 | 'ta_IN': [{ 175 | type: 'ModalAlert', 176 | options: { 177 | markdowns: [ 178 | '## நீங்கள் நிச்சயமாக தீர்வை காண விரும்புகிறீர்களா?', 179 | '', 180 | 'நான் உங்களால் அதை செய்ய முடியும் என நினைக்கிறேன்!' 181 | ] 182 | } 183 | }], 184 | "it_IT": [ 185 | { 186 | type: "ModalAlert", 187 | options: { 188 | markdowns: [ 189 | "## Sicuro di voler sbirciare la soluzione?", 190 | "", 191 | "Io credo in te, dai che ce la fai!", 192 | ], 193 | }, 194 | }, 195 | ], 196 | 'tr_TR': [{ 197 | type: 'ModalAlert', 198 | options: { 199 | markdowns: [ 200 | '## Çözümü görmek istediğine emin misin?', 201 | '', 202 | 'Sana inanıyorum bunu yapabilirsin!' 203 | ] 204 | } 205 | }], 206 | }; 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/js/dialogs/nextLevel.js: -------------------------------------------------------------------------------- 1 | exports.dialog = { 2 | 'en_US': [{ 3 | type: 'ModalAlert', 4 | options: { 5 | markdowns: [ 6 | '## Great Job!!!', 7 | '', 8 | 'You solved the level in *{numCommands}* command(s); ', 9 | 'our solution uses {best}.' 10 | ] 11 | } 12 | }], 13 | 'de_DE': [{ 14 | type: 'ModalAlert', 15 | options: { 16 | markdowns: [ 17 | '## Super gemacht!!!', 18 | '', 19 | 'Du hast das Level mit *{numCommands}* Befehl(en) gelöst;', 20 | 'unsere Lösung nutzt {best}.' 21 | ] 22 | } 23 | }], 24 | 'ja': [{ 25 | type: 'ModalAlert', 26 | options: { 27 | markdowns: [ 28 | '## 完成!!!', 29 | '', 30 | 'あなたは*{numCommands}*回のコマンドでこの課題をクリアしました; ', 31 | '模範解答では{best}回です。', 32 | '', 33 | '模範解答は、右下の`?`メニューの`Solution`から見ることができます。' 34 | ] 35 | } 36 | }], 37 | 'zh_CN': [{ 38 | type: 'ModalAlert', 39 | options: { 40 | markdowns: [ 41 | '## 好样的!!!', 42 | '', 43 | '你用 *{numCommands}* 条命令通过了这一关;', 44 | '我们的答案要用 {best} 条命令。' 45 | ] 46 | } 47 | }], 48 | 'zh_TW': [{ 49 | type: 'ModalAlert', 50 | options: { 51 | markdowns: [ 52 | '## 太棒了!!!', 53 | '', 54 | '您用了 *{numCommands}* 個指令完成這一關,', 55 | '我們的解答用了 {best} 個。' 56 | ] 57 | } 58 | }], 59 | 'es_AR': [{ 60 | type: 'ModalAlert', 61 | options: { 62 | markdowns: [ 63 | '## ¡Buen trabajo!!!', 64 | '', 65 | 'Resolviste el nivel en *{numCommands}* comandos; ', 66 | 'nuestra mejor solución usa {best}.' 67 | ] 68 | } 69 | }], 70 | 'es_MX': [{ 71 | type: 'ModalAlert', 72 | options: { 73 | markdowns: [ 74 | '## ¡Buen trabajo!!!', 75 | '', 76 | 'Resolviste el nivel en *{numCommands}* comandos; ', 77 | 'nuestra mejor solución usa: {best}.' 78 | ] 79 | } 80 | }], 81 | 'es_ES': [{ 82 | type: 'ModalAlert', 83 | options: { 84 | markdowns: [ 85 | '## ¡Buen trabajo!!!', 86 | '', 87 | 'Resolviste el nivel en *{numCommands}* comandos; ', 88 | 'nuestra mejor solución usa {best}.' 89 | ] 90 | } 91 | }], 92 | 'pt_BR': [{ 93 | type: 'ModalAlert', 94 | options: { 95 | markdowns: [ 96 | '## Bom trabalho!!!', 97 | '', 98 | 'Você resolveu o nível usando *{numCommands}* comandos; ', 99 | 'nossa melhor solução usa {best}.' 100 | ] 101 | } 102 | }], 103 | 'gl': [{ 104 | type: 'ModalAlert', 105 | options: { 106 | markdowns: [ 107 | '## Bo traballo!!!', 108 | '', 109 | 'Resolviches o nivel empregando *{numCommands}* comandos; ', 110 | 'a nosa mellor solución é en {best}.' 111 | ] 112 | } 113 | }], 114 | 'fr_FR': [{ 115 | type: 'ModalAlert', 116 | options: { 117 | markdowns: [ 118 | '## Beau Travail!!!', 119 | '', 120 | 'Vous avez résolu le niveau en *{numCommands}* commande(s); ', 121 | 'notre solution le fait en {best}.' 122 | ] 123 | } 124 | }], 125 | 'ro': [ 126 | { 127 | type: "ModalAlert", 128 | options: { 129 | markdowns: [ 130 | "## Bravo!!!", 131 | "", 132 | "Ai rezolvat nivelul în *{numCommands}* comenzi; ", 133 | "soluția noastră utilizează {best}.", 134 | ], 135 | }, 136 | }, 137 | ], 138 | 'ru_RU': [{ 139 | type: 'ModalAlert', 140 | options: { 141 | markdowns: [ 142 | '## Супер!!!', 143 | '', 144 | 'Ты прошёл уровень. Количество использованных команд - *{numCommands}* ; ', 145 | 'а наше решение состоит из {best}.' 146 | ] 147 | } 148 | }], 149 | 'uk': [{ 150 | type: 'ModalAlert', 151 | options: { 152 | markdowns: [ 153 | '## Молодець!!!', 154 | '', 155 | 'Ти пройшов рівень. Кількість використаних команд \u2014 *{numCommands}*; ', 156 | 'наш розв’язок складається з {best}.' 157 | ] 158 | } 159 | }], 160 | 'ko': [{ 161 | type: 'ModalAlert', 162 | options: { 163 | markdowns: [ 164 | '## 훌륭합니다!!!', 165 | '', 166 | '*{numCommands}*개의 명령으로 레벨을 통과했습니다; ', 167 | '모범 답안은 {best}개를 사용합니다.' 168 | ] 169 | } 170 | }], 171 | 'vi': [{ 172 | type: 'ModalAlert', 173 | options: { 174 | markdowns: [ 175 | '## Làm tốt lắm!!!', 176 | '', 177 | 'Bạn hoàn thành cấp độ này với *{numCommands}* câu lệnh; ', 178 | 'Đáp án của chúng tôi sử dụng {best}.' 179 | ] 180 | } 181 | }], 182 | 'sl_SI': [{ 183 | type: 'ModalAlert', 184 | options: { 185 | markdowns: [ 186 | '## Dobro opravljeno!!!', 187 | '', 188 | 'Rešil si stopnjo z *{numCommands}* ukazi; ', 189 | 'naša rešitev uporabi {best}.' 190 | ] 191 | } 192 | }], 193 | 'pl': [{ 194 | type: 'ModalAlert', 195 | options: { 196 | markdowns: [ 197 | '## Dobra robota!!!', 198 | '', 199 | 'Rozwiązałeś poziom używając *{numCommands}* poleceń/ia; ', 200 | 'nasze rozwiązanie składa się z {best}.' 201 | ] 202 | } 203 | }], 204 | 'ta_IN': [{ 205 | type: 'ModalAlert', 206 | options: { 207 | markdowns: [ 208 | '## ஆக சிரந்த செயல்!!!', 209 | '', 210 | 'நீங்கள் *{numCommands}* நிலைக்கான கட்டளை(கள்) கொண்டு தீர்வை அடிந்து விட்டீர்கள்; ', 211 | 'நமது தீர்வு {best}-ஐ பயன்படுத்து கின்றது.' 212 | ] 213 | } 214 | }], 215 | "it_IT": [ 216 | { 217 | type: "ModalAlert", 218 | options: { 219 | markdowns: [ 220 | "## Ben fatto!!!", 221 | "", 222 | "Hai risolto il livello con *{numCommands}* comando(i); ", 223 | "noi l'abbiamo risolto con {best}.", 224 | ], 225 | }, 226 | }, 227 | ], 228 | 'tr_TR': [{ 229 | type: 'ModalAlert', 230 | options: { 231 | markdowns: [ 232 | '## Muhteşem!!!', 233 | '', 234 | 'Bu seviyeyi *{numCommands}* komut ile çözdünüz.', 235 | 'İdeal çözüm {best} komuttan oluşuyor.' 236 | ] 237 | } 238 | }], 239 | }; 240 | 241 | -------------------------------------------------------------------------------- /src/js/dispatcher/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var Dispatcher = require('flux').Dispatcher; 5 | 6 | var PayloadSources = AppConstants.PayloadSources; 7 | 8 | var AppDispatcher = new Dispatcher(); 9 | 10 | AppDispatcher.handleViewAction = function(action) { 11 | this.dispatch({ 12 | source: PayloadSources.VIEW_ACTION, 13 | action: action 14 | }); 15 | }; 16 | 17 | AppDispatcher.handleURIAction = function(action) { 18 | this.dispatch({ 19 | source: PayloadSources.URI_ACTION, 20 | action: action 21 | }); 22 | }; 23 | 24 | module.exports = AppDispatcher; 25 | -------------------------------------------------------------------------------- /src/js/git/gitShim.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | 3 | var Main = require('../app'); 4 | var MultiView = require('../views/multiView').MultiView; 5 | 6 | function GitShim(options) { 7 | options = options || {}; 8 | 9 | // these variables are just functions called before / after for 10 | // simple things (like incrementing a counter) 11 | this.beforeCB = options.beforeCB || function() {}; 12 | this.afterCB = options.afterCB || function() {}; 13 | 14 | // these guys handle an optional async process before the git 15 | // command executes or afterwards. If there is none, 16 | // it just resolves the deferred immediately 17 | var resolveImmediately = function(deferred) { 18 | deferred.resolve(); 19 | }; 20 | this.beforeDeferHandler = options.beforeDeferHandler || resolveImmediately; 21 | this.afterDeferHandler = options.afterDeferHandler || resolveImmediately; 22 | this.eventBaton = options.eventBaton || Main.getEventBaton(); 23 | } 24 | 25 | GitShim.prototype.insertShim = function() { 26 | this.eventBaton.stealBaton('processGitCommand', this.processGitCommand, this); 27 | }; 28 | 29 | GitShim.prototype.removeShim = function() { 30 | this.eventBaton.releaseBaton('processGitCommand', this.processGitCommand, this); 31 | }; 32 | 33 | GitShim.prototype.processGitCommand = function(command, deferred) { 34 | this.beforeCB(command); 35 | 36 | // ok we make a NEW deferred that will, upon resolution, 37 | // call our afterGitCommandProcessed. This inserts the 'after' shim 38 | // functionality. we give this new deferred to the eventBaton handler 39 | var newDeferred = Q.defer(); 40 | newDeferred.promise 41 | .then(function() { 42 | // give this method the original defer so it can resolve it 43 | this.afterGitCommandProcessed(command, deferred); 44 | }.bind(this)) 45 | .done(); 46 | 47 | // now our shim owner might want to launch some kind of deferred beforehand, like 48 | // a modal or something. in order to do this, we need to defer the passing 49 | // of the event baton backwards, and either resolve that promise immediately or 50 | // give it to our shim owner. 51 | var passBaton = function() { 52 | // punt to the previous listener 53 | this.eventBaton.passBatonBack('processGitCommand', this.processGitCommand, this, [command, newDeferred]); 54 | }.bind(this); 55 | 56 | var beforeDefer = Q.defer(); 57 | beforeDefer.promise 58 | .then(passBaton) 59 | .done(); 60 | 61 | // if we didnt receive a defer handler in the options, this just 62 | // resolves immediately 63 | this.beforeDeferHandler(beforeDefer, command); 64 | }; 65 | 66 | GitShim.prototype.afterGitCommandProcessed = function(command, deferred) { 67 | this.afterCB(command); 68 | 69 | // again we can't just resolve this deferred right away... our shim owner might 70 | // want to insert some promise functionality before that happens. so again 71 | // we make a defer 72 | var afterDefer = Q.defer(); 73 | afterDefer.promise 74 | .then(function() { 75 | deferred.resolve(); 76 | }) 77 | .done(); 78 | 79 | this.afterDeferHandler(afterDefer, command); 80 | }; 81 | 82 | exports.GitShim = GitShim; 83 | 84 | -------------------------------------------------------------------------------- /src/js/git/headless.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var Q = require('q'); 3 | 4 | var GitEngine = require('../git').GitEngine; 5 | var AnimationFactory = require('../visuals/animation/animationFactory').AnimationFactory; 6 | var GitVisuals = require('../visuals').GitVisuals; 7 | var TreeCompare = require('../graph/treeCompare'); 8 | var EventBaton = require('../util/eventBaton').EventBaton; 9 | 10 | var Collections = require('../models/collections'); 11 | var CommitCollection = Collections.CommitCollection; 12 | var BranchCollection = Collections.BranchCollection; 13 | var TagCollection = Collections.TagCollection; 14 | var Command = require('../models/commandModel').Command; 15 | 16 | var mock = require('../util/mock').mock; 17 | var util = require('../util'); 18 | 19 | function getMockFactory() { 20 | var mockFactory = {}; 21 | var mockReturn = function() { 22 | var d = Q.defer(); 23 | // fall through! 24 | d.resolve(); 25 | return d.promise; 26 | }; 27 | for (var key in AnimationFactory) { 28 | mockFactory[key] = mockReturn; 29 | } 30 | 31 | mockFactory.playRefreshAnimationAndFinish = function(gitVisuals, aQueue) { 32 | aQueue.finish(); 33 | }; 34 | mockFactory.refreshTree = function(aQueue, gitVisuals) { 35 | aQueue.finish(); 36 | }; 37 | 38 | mockFactory.highlightEachWithPromise = function(chain, toRebase, destBranch) { 39 | // don't add any steps 40 | return chain; 41 | }; 42 | 43 | return mockFactory; 44 | } 45 | 46 | function getMockVisualization() { 47 | return { 48 | makeOrigin: function(options) { 49 | var localRepo = options.localRepo; 50 | var treeString = options.treeString; 51 | 52 | var headless = new HeadlessGit(); 53 | headless.gitEngine.loadTreeFromString(treeString); 54 | return { 55 | customEvents: { 56 | on: function(key, cb, context) { 57 | cb.apply(context, []); 58 | } 59 | }, 60 | gitEngine: headless.gitEngine 61 | }; 62 | } 63 | }; 64 | } 65 | 66 | var HeadlessGit = function() { 67 | this.init(); 68 | }; 69 | 70 | HeadlessGit.prototype.init = function() { 71 | this.commitCollection = new CommitCollection(); 72 | this.branchCollection = new BranchCollection(); 73 | this.tagCollection = new TagCollection(); 74 | 75 | // here we mock visuals and animation factory so the git engine 76 | // is headless 77 | var animationFactory = getMockFactory(); 78 | var gitVisuals = mock(GitVisuals); 79 | // add some stuff for origin making 80 | var mockVis = getMockVisualization(); 81 | gitVisuals.getVisualization = function() { 82 | return mockVis; 83 | }; 84 | 85 | this.gitEngine = new GitEngine({ 86 | collection: this.commitCollection, 87 | branches: this.branchCollection, 88 | tags: this.tagCollection, 89 | gitVisuals: gitVisuals, 90 | animationFactory: animationFactory, 91 | eventBaton: new EventBaton() 92 | }); 93 | this.gitEngine.init(); 94 | }; 95 | 96 | // horrible hack so we can just quickly get a tree string for async git 97 | // operations, aka for git demonstration views 98 | var getTreeQuick = function(commandStr, getTreePromise) { 99 | var deferred = Q.defer(); 100 | var headless = new HeadlessGit(); 101 | headless.sendCommand(commandStr, deferred); 102 | deferred.promise.then(function() { 103 | getTreePromise.resolve(headless.gitEngine.exportTree()); 104 | }); 105 | }; 106 | 107 | HeadlessGit.prototype.sendCommand = function(value, entireCommandPromise) { 108 | var deferred = Q.defer(); 109 | var chain = deferred.promise; 110 | var startTime = new Date().getTime(); 111 | 112 | var commands = []; 113 | 114 | util.splitTextCommand(value, function(commandStr) { 115 | chain = chain.then(function() { 116 | var commandObj = new Command({ 117 | rawStr: commandStr 118 | }); 119 | 120 | var thisDeferred = Q.defer(); 121 | this.gitEngine.dispatch(commandObj, thisDeferred); 122 | commands.push(commandObj); 123 | return thisDeferred.promise; 124 | }.bind(this)); 125 | }, this); 126 | 127 | chain.then(function() { 128 | var nowTime = new Date().getTime(); 129 | if (entireCommandPromise) { 130 | entireCommandPromise.resolve(commands); 131 | } 132 | }); 133 | 134 | chain.fail(function(err) { 135 | console.log('!!!!!!!! error !!!!!!!'); 136 | console.log(err); 137 | console.log(err.stack); 138 | console.log('!!!!!!!!!!!!!!!!!!!!!!'); 139 | }); 140 | deferred.resolve(); 141 | return chain; 142 | }; 143 | 144 | exports.HeadlessGit = HeadlessGit; 145 | exports.getTreeQuick = getTreeQuick; 146 | -------------------------------------------------------------------------------- /src/js/graph/index.js: -------------------------------------------------------------------------------- 1 | function invariant(truthy, reason) { 2 | if (!truthy) { 3 | throw new Error(reason); 4 | } 5 | } 6 | 7 | var Graph = { 8 | 9 | getOrMakeRecursive: function( 10 | tree, 11 | createdSoFar, 12 | objID, 13 | gitVisuals 14 | ) { 15 | // circular dependency, should move these base models OUT of 16 | // the git class to resolve this 17 | var Git = require('../git'); 18 | var Commit = Git.Commit; 19 | var Ref = Git.Ref; 20 | var Branch = Git.Branch; 21 | var Tag = Git.Tag; 22 | if (createdSoFar[objID]) { 23 | // base case 24 | return createdSoFar[objID]; 25 | } 26 | 27 | var getType = function(tree, id) { 28 | if (tree.commits[id]) { 29 | return 'commit'; 30 | } else if (tree.branches[id]) { 31 | return 'branch'; 32 | } else if (id == 'HEAD') { 33 | return 'HEAD'; 34 | } else if (tree.tags[id]) { 35 | return 'tag'; 36 | } 37 | throw new Error("bad type for " + id); 38 | }; 39 | 40 | // figure out what type 41 | var type = getType(tree, objID); 42 | 43 | if (type == 'HEAD') { 44 | var headJSON = tree.HEAD; 45 | var HEAD = new Ref(Object.assign( 46 | tree.HEAD, 47 | { 48 | target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target) 49 | } 50 | )); 51 | createdSoFar[objID] = HEAD; 52 | return HEAD; 53 | } 54 | 55 | if (type == 'branch') { 56 | var branchJSON = tree.branches[objID]; 57 | 58 | var branch = new Branch(Object.assign( 59 | tree.branches[objID], 60 | { 61 | target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target) 62 | } 63 | )); 64 | createdSoFar[objID] = branch; 65 | return branch; 66 | } 67 | 68 | if (type == 'tag') { 69 | var tagJSON = tree.tags[objID]; 70 | 71 | var tag = new Tag(Object.assign( 72 | tree.tags[objID], 73 | { 74 | target: this.getOrMakeRecursive(tree, createdSoFar, tagJSON.target) 75 | } 76 | )); 77 | createdSoFar[objID] = tag; 78 | return tag; 79 | } 80 | 81 | if (type == 'commit') { 82 | // for commits, we need to grab all the parents 83 | var commitJSON = tree.commits[objID]; 84 | 85 | var parentObjs = []; 86 | commitJSON.parents.forEach(function(parentID) { 87 | parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID)); 88 | }, this); 89 | 90 | var commit = new Commit(Object.assign( 91 | commitJSON, 92 | { 93 | parents: parentObjs, 94 | gitVisuals: this.gitVisuals 95 | } 96 | )); 97 | createdSoFar[objID] = commit; 98 | return commit; 99 | } 100 | 101 | throw new Error('ruh rho!! unsupported type for ' + objID); 102 | }, 103 | 104 | descendSortDepth: function(objects) { 105 | return objects.sort(function(oA, oB) { 106 | return oB.depth - oA.depth; 107 | }); 108 | }, 109 | 110 | bfsFromLocationWithSet: function(engine, location, set) { 111 | var result = []; 112 | var pQueue = [engine.getCommitFromRef(location)]; 113 | 114 | while (pQueue.length) { 115 | var popped = pQueue.pop(); 116 | if (set[popped.get('id')]) { 117 | continue; 118 | } 119 | 120 | result.push(popped); 121 | // keep searching 122 | pQueue = pQueue.concat(popped.get('parents')); 123 | } 124 | return result; 125 | }, 126 | 127 | getUpstreamSet: function(engine, ancestor) { 128 | var commit = engine.getCommitFromRef(ancestor); 129 | var ancestorID = commit.get('id'); 130 | var queue = [commit]; 131 | 132 | var exploredSet = {}; 133 | exploredSet[ancestorID] = true; 134 | 135 | var addToExplored = function(rent) { 136 | exploredSet[rent.get('id')] = true; 137 | queue.push(rent); 138 | }; 139 | 140 | while (queue.length) { 141 | var here = queue.pop(); 142 | var rents = here.get('parents'); 143 | 144 | (rents || []).forEach(addToExplored); 145 | } 146 | return exploredSet; 147 | }, 148 | 149 | getUniqueObjects: function(objects) { 150 | var unique = {}; 151 | var result = []; 152 | objects.forEach(function(object) { 153 | if (unique[object.id]) { 154 | return; 155 | } 156 | unique[object.id] = true; 157 | result.push(object); 158 | }); 159 | return result; 160 | }, 161 | 162 | getDefaultTree: function() { 163 | return JSON.parse(unescape("%7B%22branches%22%3A%7B%22main%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22main%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22main%22%2C%22type%22%3A%22general%20ref%22%7D%7D")); 164 | } 165 | }; 166 | 167 | module.exports = Graph; 168 | -------------------------------------------------------------------------------- /src/js/intl/checkStrings.js: -------------------------------------------------------------------------------- 1 | var { join } = require('path'); 2 | var { readFileSync } = require('fs'); 3 | 4 | var util = require('../util'); 5 | var { strings } = require('../intl/strings'); 6 | 7 | var easyRegex = /intl\.str\(\s*'([a-zA-Z\-]+)'/g; 8 | 9 | var allKetSet = new Set(Object.keys(strings)); 10 | allKetSet.delete('error-untranslated'); // used in ./index.js 11 | 12 | var goodKeySet = new Set(); 13 | var validateKey = function(key) { 14 | if (!strings[key]) { 15 | console.log('NO KEY for: "', key, '"'); 16 | } else { 17 | goodKeySet.add(key); 18 | allKetSet.delete(key); 19 | } 20 | }; 21 | 22 | if (!util.isBrowser()) { 23 | util.readDirDeep(join(__dirname, '../../')).forEach(function(path) { 24 | var content = readFileSync(path); 25 | var match; 26 | while (match = easyRegex.exec(content)) { 27 | validateKey(match[1]); 28 | } 29 | }); 30 | console.log(goodKeySet.size, ' good keys found!'); 31 | console.log(allKetSet.size, ' keys did not use!'); 32 | console.log(allKetSet); 33 | } 34 | -------------------------------------------------------------------------------- /src/js/intl/index.js: -------------------------------------------------------------------------------- 1 | var LocaleStore = require('../stores/LocaleStore'); 2 | 3 | var _ = require('underscore'); 4 | var strings = require('../intl/strings').strings; 5 | 6 | var getDefaultLocale = LocaleStore.getDefaultLocale; 7 | 8 | var fallbackMap = { 9 | 'zh_TW': 'zh_CN', 10 | 'es_AR': 'es_ES', 11 | 'es_MX': 'es_ES' 12 | }; 13 | 14 | // lets change underscores template settings so it interpolates 15 | // things like "{branchName} does not exist". 16 | var templateSettings = Object.assign({}, _.templateSettings); 17 | templateSettings.interpolate = /\{(.+?)\}/g; 18 | var template = exports.template = function(str, params) { 19 | return _.template(str, templateSettings)(params); 20 | }; 21 | 22 | var str = exports.str = function(key, params) { 23 | params = params || {}; 24 | // this function takes a key like "error-branch-delete" 25 | // and parameters like {branchName: 'bugFix', num: 3}. 26 | // 27 | // it sticks those into a translation string like: 28 | // 'en': 'You can not delete the branch {branchName} because' + 29 | // 'you are currently on that branch! This is error number + {num}' 30 | // 31 | // to produce: 32 | // 33 | // 'You can not delete the branch bugFix because you are currently on that branch! 34 | // This is error number 3' 35 | 36 | var locale = LocaleStore.getLocale(); 37 | if (!strings[key]) { 38 | console.warn('NO INTL support for key ' + key); 39 | return 'NO INTL support for key ' + key + '. this is probably a dev error'; 40 | } 41 | 42 | if (!strings[key][locale]) { 43 | // try falling back to another locale if in the map 44 | locale = fallbackMap[locale] || getDefaultLocale(); 45 | } 46 | 47 | if (!strings[key][locale]) { 48 | if (key !== 'error-untranslated') { 49 | return str('error-untranslated'); 50 | } 51 | return 'No translation for the key "' + key + '"'; 52 | } 53 | 54 | return template( 55 | strings[key][locale], 56 | params 57 | ); 58 | }; 59 | 60 | var getIntlKey = exports.getIntlKey = function(obj, key, overrideLocale) { 61 | if (!obj || !obj[key]) { 62 | throw new Error('that key ' + key + ' doesn\'t exist in this blob ' + obj); 63 | } 64 | if (!obj[key][getDefaultLocale()]) { 65 | console.warn( 66 | 'WARNING!! This blob does not have intl support:', 67 | obj, 68 | 'for this key', 69 | key 70 | ); 71 | } 72 | 73 | var locale = overrideLocale || LocaleStore.getLocale(); 74 | return obj[key][locale]; 75 | }; 76 | 77 | exports.todo = function(str) { 78 | return str; 79 | }; 80 | 81 | exports.getDialog = function(obj) { 82 | return getIntlKey(obj, 'dialog') || obj.dialog[getDefaultLocale()]; 83 | }; 84 | 85 | exports.getHint = function(level) { 86 | if (!getIntlKey(level, 'hint')) { 87 | return getIntlKey(level, 'hint', getDefaultLocale()) + ' -- ' + str('error-untranslated'); 88 | } 89 | return getIntlKey(level, 'hint'); 90 | }; 91 | 92 | exports.getName = function(level) { 93 | if (!getIntlKey(level, 'name')) { 94 | return getIntlKey(level, 'name', getDefaultLocale()) + ' -- ' + str('error-untranslated'); 95 | } 96 | return getIntlKey(level, 'name'); 97 | }; 98 | 99 | exports.getStartDialog = function(level) { 100 | var startDialog = getIntlKey(level, 'startDialog'); 101 | if (startDialog) { return startDialog; } 102 | 103 | // this level translation isn't supported yet, so lets add 104 | // an alert to the front and give the english version. 105 | var errorAlert = { 106 | type: 'ModalAlert', 107 | options: { 108 | markdown: str('error-untranslated') 109 | } 110 | }; 111 | var startCopy = Object.assign( 112 | {}, 113 | level.startDialog[getDefaultLocale()] || level.startDialog 114 | ); 115 | startCopy.childViews.unshift(errorAlert); 116 | 117 | return startCopy; 118 | }; 119 | -------------------------------------------------------------------------------- /src/js/level/disabledMap.js: -------------------------------------------------------------------------------- 1 | var intl = require('../intl'); 2 | 3 | var Commands = require('../commands'); 4 | 5 | var Errors = require('../util/errors'); 6 | var GitError = Errors.GitError; 7 | 8 | function DisabledMap(options) { 9 | options = options || {}; 10 | this.disabledMap = options.disabledMap || { 11 | 'git cherry-pick': true, 12 | 'git rebase': true 13 | }; 14 | } 15 | 16 | DisabledMap.prototype.getInstantCommands = function() { 17 | // this produces an array of regex / function pairs that can be 18 | // piped into a parse waterfall to disable certain git commands 19 | // :D 20 | var instants = []; 21 | var onMatch = function() { 22 | throw new GitError({ 23 | msg: intl.str('command-disabled') 24 | }); 25 | }; 26 | 27 | Object.keys(this.disabledMap).forEach(function(disabledCommand) { 28 | // XXX get hold of vcs from disabledMap 29 | var vcs = 'git'; 30 | disabledCommand = disabledCommand.slice(vcs.length + 1); 31 | var gitRegex = Commands.commands.getRegexMap()[vcs][disabledCommand]; 32 | if (!gitRegex) { 33 | throw new Error('wuttttt this disbaled command' + disabledCommand + 34 | ' has no regex matching'); 35 | } 36 | instants.push([gitRegex, onMatch]); 37 | }.bind(this)); 38 | return instants; 39 | }; 40 | 41 | exports.DisabledMap = DisabledMap; 42 | 43 | -------------------------------------------------------------------------------- /src/js/level/parseWaterfall.js: -------------------------------------------------------------------------------- 1 | var GitCommands = require('../git/commands'); 2 | var Commands = require('../commands'); 3 | var SandboxCommands = require('../sandbox/commands'); 4 | 5 | // more or less a static class 6 | var ParseWaterfall = function(options) { 7 | options = options || {}; 8 | this.options = options; 9 | this.shortcutWaterfall = options.shortcutWaterfall || [ 10 | Commands.commands.getShortcutMap() 11 | ]; 12 | 13 | this.instantWaterfall = options.instantWaterfall || [ 14 | GitCommands.instantCommands, 15 | SandboxCommands.instantCommands 16 | ]; 17 | 18 | // defer the parse waterfall until later... 19 | }; 20 | 21 | ParseWaterfall.prototype.initParseWaterfall = function() { 22 | // check for node when testing 23 | if (!require('../util').isBrowser()) { 24 | this.parseWaterfall = [Commands.parse]; 25 | return; 26 | } 27 | 28 | // by deferring the initialization here, we don't require() 29 | // level too early (which barfs our init) 30 | this.parseWaterfall = this.options.parseWaterfall || [ 31 | Commands.parse, 32 | SandboxCommands.parse, 33 | SandboxCommands.getOptimisticLevelParse(), 34 | SandboxCommands.getOptimisticLevelBuilderParse() 35 | ]; 36 | }; 37 | 38 | ParseWaterfall.prototype.clone = function() { 39 | return new ParseWaterfall({ 40 | shortcutWaterfall: this.shortcutWaterfall.slice(), 41 | instantWaterfall: this.instantWaterfall.slice(), 42 | parseWaterfall: this.parseWaterfall.slice() 43 | }); 44 | }; 45 | 46 | ParseWaterfall.prototype.getWaterfallMap = function() { 47 | if (!this.parseWaterfall) { 48 | this.initParseWaterfall(); 49 | } 50 | return { 51 | shortcutWaterfall: this.shortcutWaterfall, 52 | instantWaterfall: this.instantWaterfall, 53 | parseWaterfall: this.parseWaterfall 54 | }; 55 | }; 56 | 57 | ParseWaterfall.prototype.addFirst = function(which, value) { 58 | if (!which || !value) { 59 | throw new Error('need to know which!!!'); 60 | } 61 | this.getWaterfallMap()[which].unshift(value); 62 | }; 63 | 64 | ParseWaterfall.prototype.addLast = function(which, value) { 65 | this.getWaterfallMap()[which].push(value); 66 | }; 67 | 68 | ParseWaterfall.prototype.expandAllShortcuts = function(commandStr) { 69 | this.shortcutWaterfall.forEach(function(shortcutMap) { 70 | commandStr = this.expandShortcut(commandStr, shortcutMap); 71 | }, this); 72 | return commandStr; 73 | }; 74 | 75 | ParseWaterfall.prototype.expandShortcut = function(commandStr, shortcutMap) { 76 | Object.keys(shortcutMap).forEach(function(vcs) { 77 | var map = shortcutMap[vcs]; 78 | Object.keys(map).forEach(function(method) { 79 | var regex = map[method]; 80 | var results = regex.exec(commandStr); 81 | if (results) { 82 | commandStr = vcs + ' ' + method + ' ' + commandStr.slice(results[0].length); 83 | } 84 | }); 85 | }); 86 | return commandStr; 87 | }; 88 | 89 | ParseWaterfall.prototype.processAllInstants = function(commandStr) { 90 | this.instantWaterfall.forEach(function(instantCommands) { 91 | this.processInstant(commandStr, instantCommands); 92 | }, this); 93 | }; 94 | 95 | ParseWaterfall.prototype.processInstant = function(commandStr, instantCommands) { 96 | instantCommands.forEach(function(tuple) { 97 | var regex = tuple[0]; 98 | var results = regex.exec(commandStr); 99 | if (results) { 100 | // this will throw a result because it's an instant 101 | tuple[1](results); 102 | } 103 | }); 104 | }; 105 | 106 | ParseWaterfall.prototype.parseAll = function(commandStr) { 107 | if (!this.parseWaterfall) { 108 | this.initParseWaterfall(); 109 | } 110 | 111 | var toReturn = false; 112 | this.parseWaterfall.forEach(function(parseFunc) { 113 | var results = parseFunc(commandStr); 114 | if (results) { 115 | toReturn = results; 116 | } 117 | }, this); 118 | 119 | return toReturn; 120 | }; 121 | 122 | exports.ParseWaterfall = ParseWaterfall; 123 | -------------------------------------------------------------------------------- /src/js/log/index.js: -------------------------------------------------------------------------------- 1 | 2 | var log = function(category, action, label) { 3 | window._gaq = window._gaq || []; 4 | window._gaq.push(['_trackEvent', category, action, label]); 5 | //console.log('just logged ', [category, action, label].join('|')); 6 | }; 7 | 8 | exports.viewInteracted = function(viewName) { 9 | log('views', 'interacted', viewName); 10 | }; 11 | 12 | exports.showLevelSolution = function(levelName) { 13 | log('levels', 'showedLevelSolution', levelName); 14 | }; 15 | 16 | exports.choseNextLevel = function(levelID) { 17 | log('levels', 'nextLevelChosen', levelID); 18 | }; 19 | 20 | exports.levelSelected = function(levelName) { 21 | log('levels', 'levelSelected', levelName); 22 | }; 23 | 24 | exports.levelSolved = function(levelName) { 25 | log('levels', 'levelSolved', levelName); 26 | }; 27 | 28 | exports.commandEntered = function(value) { 29 | log('commands', 'commandEntered', value); 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /src/js/mercurial/commands.js: -------------------------------------------------------------------------------- 1 | var intl = require('../intl'); 2 | 3 | var GitCommands = require('../git/commands'); 4 | var Errors = require('../util/errors'); 5 | 6 | var CommandProcessError = Errors.CommandProcessError; 7 | var GitError = Errors.GitError; 8 | var Warning = Errors.Warning; 9 | var CommandResult = Errors.CommandResult; 10 | 11 | var commandConfig = { 12 | commit: { 13 | regex: /^hg +(commit|ci)($|\s)/, 14 | options: [ 15 | '--amend', 16 | '-A', 17 | '-m' 18 | ], 19 | delegate: function(engine, command) { 20 | var options = command.getOptionsMap(); 21 | if (options['-A']) { 22 | command.addWarning(intl.str('hg-a-option')); 23 | } 24 | 25 | return { 26 | vcs: 'git', 27 | name: 'commit' 28 | }; 29 | } 30 | }, 31 | 32 | status: { 33 | regex: /^hg +(status|st) *$/, 34 | dontCountForGolf: true, 35 | execute: function(engine, command) { 36 | throw new GitError({ 37 | msg: intl.str('hg-error-no-status') 38 | }); 39 | } 40 | }, 41 | 42 | 'export': { 43 | regex: /^hg +export($|\s)/, 44 | dontCountForGolf: true, 45 | delegate: function(engine, command) { 46 | command.mapDotToHead(); 47 | return { 48 | vcs: 'git', 49 | name: 'show' 50 | }; 51 | } 52 | }, 53 | 54 | graft: { 55 | regex: /^hg +graft($|\s)/, 56 | options: [ 57 | '-r' 58 | ], 59 | delegate: function(engine, command) { 60 | command.acceptNoGeneralArgs(); 61 | command.prependOptionR(); 62 | return { 63 | vcs: 'git', 64 | name: 'cherrypick' 65 | }; 66 | } 67 | }, 68 | 69 | log: { 70 | regex: /^hg +log($|\s)/, 71 | options: [ 72 | '-f' 73 | ], 74 | dontCountForGolf: true, 75 | delegate: function(engine, command) { 76 | var options = command.getOptionsMap(); 77 | command.acceptNoGeneralArgs(); 78 | 79 | if (!options['-f']) { 80 | throw new GitError({ 81 | msg: intl.str('hg-error-log-no-follow') 82 | }); 83 | } 84 | command.mapDotToHead(); 85 | return { 86 | vcs: 'git', 87 | name: 'log' 88 | }; 89 | } 90 | }, 91 | 92 | bookmark: { 93 | regex: /^hg (bookmarks|bookmark|book)($|\s)/, 94 | options: [ 95 | '-r', 96 | '-f', 97 | '-d' 98 | ], 99 | delegate: function(engine, command) { 100 | var options = command.getOptionsMap(); 101 | var generalArgs = command.getGeneralArgs(); 102 | var branchName; 103 | var rev; 104 | 105 | var delegate = {vcs: 'git'}; 106 | 107 | if (options['-m'] && options['-d']) { 108 | throw new GitError({ 109 | msg: intl.todo('-m and -d are incompatible') 110 | }); 111 | } 112 | if (options['-d'] && options['-r']) { 113 | throw new GitError({ 114 | msg: intl.todo('-r is incompatible with -d') 115 | }); 116 | } 117 | if (options['-m'] && options['-r']) { 118 | throw new GitError({ 119 | msg: intl.todo('-r is incompatible with -m') 120 | }); 121 | } 122 | if (generalArgs.length + (options['-r'] ? options['-r'].length : 0) + 123 | (options['-d'] ? options['-d'].length : 0) === 0) { 124 | delegate.name = 'branch'; 125 | return delegate; 126 | } 127 | 128 | if (options['-d']) { 129 | options['-D'] = options['-d']; 130 | delete options['-d']; 131 | delegate.name = 'branch'; 132 | } else { 133 | if (options['-r']) { 134 | // we specified a revision with -r but 135 | // need to flip the order 136 | generalArgs = command.getGeneralArgs(); 137 | branchName = generalArgs[0]; 138 | rev = options['-r'][0]; 139 | delegate.name = 'branch'; 140 | 141 | // transform to what git wants 142 | command.setGeneralArgs([branchName, rev]); 143 | } else if (generalArgs.length > 0) { 144 | command.setOptionsMap({'-b': [generalArgs[0]]}); 145 | delegate.name = 'checkout'; 146 | command.setGeneralArgs([]); 147 | } else { 148 | delegate.name = 'branch'; 149 | } 150 | } 151 | 152 | return delegate; 153 | } 154 | }, 155 | 156 | rebase: { 157 | regex: /^hg +rebase($|\s+)/, 158 | options: [ 159 | '-d', 160 | '-s', 161 | '-b' 162 | ], 163 | execute: function(engine, command) { 164 | var throwE = function() { 165 | throw new GitError({ 166 | msg: intl.str('git-error-options') 167 | }); 168 | }; 169 | 170 | var options = command.getOptionsMap(); 171 | // if we have both OR if we have neither 172 | if ((options['-d'] && options['-s']) || 173 | (!options['-d'] && !options['-s'])) { 174 | } 175 | 176 | if (!options['-b']) { 177 | options['-b'] = ['.']; 178 | } 179 | 180 | command.setOptionsMap(options); 181 | command.mapDotToHead(); 182 | options = command.getOptionsMap(); 183 | 184 | if (options['-d']) { 185 | var dest = options['-d'][0] || throwE(); 186 | var base = options['-b'][0]; 187 | 188 | engine.hgRebase(dest, base); 189 | } else { 190 | // TODO!!! 191 | throwE(); 192 | } 193 | } 194 | }, 195 | 196 | update: { 197 | regex: /^hg +(update|up)($|\s+)/, 198 | options: [ 199 | '-r' 200 | ], 201 | delegate: function(engine, command) { 202 | command.appendOptionR(); 203 | return { 204 | vcs: 'git', 205 | name: 'checkout' 206 | }; 207 | } 208 | }, 209 | 210 | backout: { 211 | regex: /^hg +backout($|\s+)/, 212 | options: [ 213 | '-r' 214 | ], 215 | delegate: function(engine, command) { 216 | command.prependOptionR(); 217 | return { 218 | vcs: 'git', 219 | name: 'revert' 220 | }; 221 | } 222 | }, 223 | 224 | histedit: { 225 | regex: /^hg +histedit($|\s+)/, 226 | delegate: function(engine, command) { 227 | var args = command.getGeneralArgs(); 228 | command.validateArgBounds(args, 1, 1); 229 | command.setOptionsMap({ 230 | '-i': args 231 | }); 232 | command.setGeneralArgs([]); 233 | return { 234 | vcs: 'git', 235 | name: 'rebase' 236 | }; 237 | } 238 | }, 239 | 240 | pull: { 241 | regex: /^hg +pull($|\s+)/, 242 | delegate: function(engine, command) { 243 | return { 244 | vcs: 'git', 245 | name: 'pull' 246 | }; 247 | } 248 | }, 249 | 250 | summary: { 251 | regex: /^hg +(summary|sum) *$/, 252 | delegate: function(engine, command) { 253 | return { 254 | vcs: 'git', 255 | name: 'branch' 256 | }; 257 | } 258 | } 259 | }; 260 | 261 | exports.commandConfig = commandConfig; 262 | -------------------------------------------------------------------------------- /src/js/models/collections.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var Backbone = require('backbone'); 3 | 4 | var Commit = require('../git').Commit; 5 | var Branch = require('../git').Branch; 6 | var Tag = require('../git').Tag; 7 | 8 | var Command = require('../models/commandModel').Command; 9 | var TIME = require('../util/constants').TIME; 10 | 11 | var intl = require('../intl'); 12 | 13 | var CommitCollection = Backbone.Collection.extend({ 14 | model: Commit 15 | }); 16 | 17 | var CommandCollection = Backbone.Collection.extend({ 18 | model: Command 19 | }); 20 | 21 | var BranchCollection = Backbone.Collection.extend({ 22 | model: Branch 23 | }); 24 | 25 | var TagCollection = Backbone.Collection.extend({ 26 | model: Tag 27 | }); 28 | 29 | var CommandBuffer = Backbone.Model.extend({ 30 | defaults: { 31 | collection: null 32 | }, 33 | 34 | initialize: function(options) { 35 | options.collection.bind('add', this.addCommand, this); 36 | 37 | this.buffer = []; 38 | this.timeout = null; 39 | }, 40 | 41 | addCommand: function(command) { 42 | this.buffer.push(command); 43 | this.touchBuffer(); 44 | }, 45 | 46 | touchBuffer: function() { 47 | // touch buffer just essentially means we just check if our buffer is being 48 | // processed. if it's not, we immediately process the first item 49 | // and then set the timeout. 50 | if (this.timeout) { 51 | // timeout existence implies its being processed 52 | return; 53 | } 54 | this.setTimeout(); 55 | }, 56 | 57 | 58 | setTimeout: function() { 59 | this.timeout = setTimeout(function() { 60 | this.sipFromBuffer(); 61 | }.bind(this), TIME.betweenCommandsDelay); 62 | }, 63 | 64 | popAndProcess: function() { 65 | var popped = this.buffer.shift(0); 66 | 67 | // find a command with no error (aka unprocessed) 68 | while (popped.get('error') && this.buffer.length) { 69 | popped = this.buffer.shift(0); 70 | } 71 | if (!popped.get('error')) { 72 | this.processCommand(popped); 73 | } else { 74 | // no more commands to process 75 | this.clear(); 76 | } 77 | }, 78 | 79 | processCommand: function(command) { 80 | command.set('status', 'processing'); 81 | 82 | var deferred = Q.defer(); 83 | deferred.promise.then(function() { 84 | this.setTimeout(); 85 | }.bind(this)); 86 | 87 | var eventName = command.get('eventName'); 88 | if (!eventName) { 89 | throw new Error('I need an event to trigger when this guy is parsed and ready'); 90 | } 91 | 92 | var Main = require('../app'); 93 | var eventBaton = Main.getEventBaton(); 94 | 95 | var numListeners = eventBaton.getNumListeners(eventName); 96 | if (!numListeners) { 97 | var Errors = require('../util/errors'); 98 | command.set('error', new Errors.GitError({ 99 | msg: intl.str('error-command-currently-not-supported') 100 | })); 101 | deferred.resolve(); 102 | return; 103 | } 104 | 105 | Main.getEventBaton().trigger(eventName, command, deferred); 106 | }, 107 | 108 | clear: function() { 109 | clearTimeout(this.timeout); 110 | this.timeout = null; 111 | }, 112 | 113 | sipFromBuffer: function() { 114 | if (!this.buffer.length) { 115 | this.clear(); 116 | return; 117 | } 118 | 119 | this.popAndProcess(); 120 | } 121 | }); 122 | 123 | exports.CommitCollection = CommitCollection; 124 | exports.CommandCollection = CommandCollection; 125 | exports.BranchCollection = BranchCollection; 126 | exports.TagCollection = TagCollection; 127 | exports.CommandBuffer = CommandBuffer; 128 | 129 | -------------------------------------------------------------------------------- /src/js/models/commandModel.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | 3 | var Errors = require('../util/errors'); 4 | 5 | var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall; 6 | var LevelStore = require('../stores/LevelStore'); 7 | var intl = require('../intl'); 8 | 9 | var CommandProcessError = Errors.CommandProcessError; 10 | var GitError = Errors.GitError; 11 | var Warning = Errors.Warning; 12 | var CommandResult = Errors.CommandResult; 13 | 14 | var Command = Backbone.Model.extend({ 15 | defaults: { 16 | status: 'inqueue', 17 | rawStr: null, 18 | result: '', 19 | createTime: null, 20 | 21 | error: null, 22 | warnings: null, 23 | parseWaterfall: new ParseWaterfall(), 24 | 25 | generalArgs: null, 26 | supportedMap: null, 27 | options: null, 28 | method: null 29 | 30 | }, 31 | 32 | initialize: function() { 33 | this.initDefaults(); 34 | this.validateAtInit(); 35 | 36 | this.on('change:error', this.errorChanged, this); 37 | // catch errors on init 38 | if (this.get('error')) { 39 | this.errorChanged(); 40 | } 41 | 42 | this.parseOrCatch(); 43 | }, 44 | 45 | initDefaults: function() { 46 | // weird things happen with defaults if you don't 47 | // make new objects 48 | this.set('generalArgs', []); 49 | this.set('supportedMap', {}); 50 | this.set('warnings', []); 51 | }, 52 | 53 | replaceDotWithHead: function(string) { 54 | return string.replace(/\./g, 'HEAD'); 55 | }, 56 | 57 | /** 58 | * Since mercurial always wants revisions with 59 | * -r, we want to just make these general 60 | * args for git 61 | */ 62 | appendOptionR: function() { 63 | var rOptions = this.getOptionsMap()['-r'] || []; 64 | this.setGeneralArgs( 65 | this.getGeneralArgs().concat(rOptions) 66 | ); 67 | }, 68 | 69 | // if order is important 70 | prependOptionR: function() { 71 | var rOptions = this.getOptionsMap()['-r'] || []; 72 | this.setGeneralArgs( 73 | rOptions.concat(this.getGeneralArgs()) 74 | ); 75 | }, 76 | 77 | mapDotToHead: function() { 78 | var generalArgs = this.getGeneralArgs(); 79 | var options = this.getOptionsMap(); 80 | 81 | generalArgs = generalArgs.map(function(arg) { 82 | return this.replaceDotWithHead(arg); 83 | }, this); 84 | var newMap = {}; 85 | Object.keys(options).forEach(function(key) { 86 | var args = options[key]; 87 | newMap[key] = Object.values(args).map(function (arg) { 88 | return this.replaceDotWithHead(arg); 89 | }, this); 90 | }, this); 91 | this.setGeneralArgs(generalArgs); 92 | this.setOptionsMap(newMap); 93 | }, 94 | 95 | deleteOptions: function(options) { 96 | var map = this.getOptionsMap(); 97 | options.forEach(function(option) { 98 | delete map[option]; 99 | }, this); 100 | this.setOptionsMap(map); 101 | }, 102 | 103 | getGeneralArgs: function() { 104 | return this.get('generalArgs'); 105 | }, 106 | 107 | setGeneralArgs: function(args) { 108 | this.set('generalArgs', args); 109 | }, 110 | 111 | setOptionsMap: function(map) { 112 | this.set('supportedMap', map); 113 | }, 114 | 115 | getOptionsMap: function() { 116 | return this.get('supportedMap'); 117 | }, 118 | 119 | acceptNoGeneralArgs: function() { 120 | if (this.getGeneralArgs().length) { 121 | throw new GitError({ 122 | msg: intl.str('git-error-no-general-args') 123 | }); 124 | } 125 | }, 126 | 127 | argImpliedHead: function (args, lower, upper, option) { 128 | // our args we expect to be between {lower} and {upper} 129 | this.validateArgBounds(args, lower, upper, option); 130 | // and if it's one, add a HEAD to the back 131 | this.impliedHead(args, lower); 132 | }, 133 | 134 | oneArgImpliedHead: function(args, option) { 135 | this.argImpliedHead(args, 0, 1, option); 136 | }, 137 | 138 | twoArgsImpliedHead: function(args, option) { 139 | this.argImpliedHead(args, 1, 2, option); 140 | }, 141 | 142 | threeArgsImpliedHead: function(args, option) { 143 | this.argImpliedHead(args, 2, 3, option); 144 | }, 145 | 146 | oneArgImpliedOrigin: function(args) { 147 | this.validateArgBounds(args, 0, 1); 148 | if (!args.length) { 149 | args.unshift('origin'); 150 | } 151 | }, 152 | 153 | twoArgsForOrigin: function(args) { 154 | this.validateArgBounds(args, 0, 2); 155 | }, 156 | 157 | impliedHead: function(args, min) { 158 | if(args.length == min) { 159 | args.push('HEAD'); 160 | } 161 | }, 162 | 163 | // this is a little utility class to help arg validation that happens over and over again 164 | validateArgBounds: function(args, lower, upper, option) { 165 | var what = (option === undefined) ? 166 | 'git ' + this.get('method') : 167 | this.get('method') + ' ' + option + ' '; 168 | what = 'with ' + what; 169 | 170 | if (args.length < lower) { 171 | throw new GitError({ 172 | msg: intl.str( 173 | 'git-error-args-few', 174 | { 175 | lower: String(lower), 176 | what: what 177 | } 178 | ) 179 | }); 180 | } 181 | if (args.length > upper) { 182 | throw new GitError({ 183 | msg: intl.str( 184 | 'git-error-args-many', 185 | { 186 | upper: String(upper), 187 | what: what 188 | } 189 | ) 190 | }); 191 | } 192 | }, 193 | 194 | validateAtInit: function() { 195 | if (this.get('rawStr') === null) { 196 | throw new Error('Give me a string!'); 197 | } 198 | if (!this.get('createTime')) { 199 | this.set('createTime', new Date().toString()); 200 | } 201 | }, 202 | 203 | setResult: function(msg) { 204 | this.set('result', msg); 205 | }, 206 | 207 | finishWith: function(deferred) { 208 | this.set('status', 'finished'); 209 | deferred.resolve(); 210 | }, 211 | 212 | addWarning: function(msg) { 213 | this.get('warnings').push(msg); 214 | // change numWarnings so the change event fires. This is bizarre -- Backbone can't 215 | // detect if an array changes, so adding an element does nothing 216 | this.set('numWarnings', this.get('numWarnings') ? this.get('numWarnings') + 1 : 1); 217 | }, 218 | 219 | parseOrCatch: function() { 220 | this.expandShortcuts(this.get('rawStr')); 221 | try { 222 | this.processInstants(); 223 | } catch (err) { 224 | Errors.filterError(err); 225 | // errorChanged() will handle status and all of that 226 | this.set('error', err); 227 | return; 228 | } 229 | 230 | if (this.parseAll()) { 231 | // something in our parse waterfall succeeded 232 | return; 233 | } 234 | 235 | // if we reach here, this command is not supported :-/ 236 | this.set('error', new CommandProcessError({ 237 | msg: intl.str( 238 | 'git-error-command-not-supported', 239 | { 240 | command: this.get('rawStr') 241 | }) 242 | }) 243 | ); 244 | }, 245 | 246 | errorChanged: function() { 247 | var err = this.get('error'); 248 | if (!err) { return; } 249 | if (err instanceof CommandProcessError || 250 | err instanceof GitError) { 251 | this.set('status', 'error'); 252 | } else if (err instanceof CommandResult) { 253 | this.set('status', 'finished'); 254 | } else if (err instanceof Warning) { 255 | this.set('status', 'warning'); 256 | } 257 | this.formatError(); 258 | }, 259 | 260 | formatError: function() { 261 | this.set('result', this.get('error').getMsg()); 262 | }, 263 | 264 | expandShortcuts: function(str) { 265 | str = this.get('parseWaterfall').expandAllShortcuts(str); 266 | this.set('rawStr', str); 267 | }, 268 | 269 | processInstants: function() { 270 | var str = this.get('rawStr'); 271 | // first if the string is empty, they just want a blank line 272 | if (!str.length) { 273 | throw new CommandResult({msg: ""}); 274 | } 275 | 276 | // then instant commands that will throw 277 | this.get('parseWaterfall').processAllInstants(str); 278 | }, 279 | 280 | parseAll: function() { 281 | var rawInput = this.get('rawStr'); 282 | const aliasMap = LevelStore.getAliasMap(); 283 | for (var i = 0; i this.clearOldCommands(), this); 28 | } 29 | 30 | componentWillUnmount() { 31 | for (var i = 0; i < _subscribeEvents.length; i++) { 32 | this.props.commandCollection.off( 33 | _subscribeEvents[i], 34 | this.updateFromCollection, 35 | this 36 | ); 37 | } 38 | } 39 | 40 | updateFromCollection() { 41 | this.forceUpdate(); 42 | } 43 | 44 | render() { 45 | var allCommands = []; 46 | this.props.commandCollection.each(function(command, index) { 47 | allCommands.push( 48 | 53 | ); 54 | }, this); 55 | return ( 56 |
57 | {allCommands} 58 |
59 | ); 60 | } 61 | 62 | scrollDown() { 63 | var cD = document.getElementById('commandDisplay'); 64 | var t = document.getElementById('terminal'); 65 | 66 | // firefox hack 67 | var shouldScroll = (cD.clientHeight > t.clientHeight) || 68 | (window.innerHeight < cD.clientHeight); 69 | 70 | // ugh sometimes i wish i had toggle class 71 | var hasScroll = t.className.match(/scrolling/g); 72 | if (shouldScroll && !hasScroll) { 73 | t.className += ' scrolling'; 74 | } else if (!shouldScroll && hasScroll) { 75 | t.className = t.className.replace(/shouldScroll/g, ''); 76 | } 77 | 78 | if (shouldScroll) { 79 | t.scrollTop = t.scrollHeight; 80 | } 81 | } 82 | 83 | clearOldCommands() { 84 | // go through and get rid of every command that is "processed" or done 85 | var toDestroy = []; 86 | 87 | this.props.commandCollection.each(function(command) { 88 | if (command.get('status') !== 'inqueue' && 89 | command.get('status') !== 'processing') { 90 | toDestroy.push(command); 91 | } 92 | }, this); 93 | for (var i = 0; i < toDestroy.length; i++) { 94 | toDestroy[i].destroy(); 95 | } 96 | this.updateFromCollection(); 97 | this.scrollDown(); 98 | } 99 | 100 | } 101 | 102 | CommandHistoryView.propTypes = { 103 | // the backbone command model collection 104 | commandCollection: PropTypes.object.isRequired 105 | }; 106 | 107 | module.exports = CommandHistoryView; 108 | -------------------------------------------------------------------------------- /src/js/react_views/CommandView.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ReactDOM = require('react-dom'); 3 | var PropTypes = require('prop-types'); 4 | 5 | var reactUtil = require('../util/reactUtil'); 6 | var keyMirror = require('../util/keyMirror'); 7 | 8 | var STATUSES = keyMirror({ 9 | inqueue: null, 10 | processing: null, 11 | finished: null 12 | }); 13 | 14 | class CommandView extends React.Component{ 15 | 16 | componentDidMount() { 17 | this.props.command.on('change', this.updateStateFromModel, this); 18 | this.updateStateFromModel(); 19 | } 20 | 21 | componentWillUnmount() { 22 | this.props.command.off('change', this.updateStateFromModel, this); 23 | } 24 | 25 | updateStateFromModel() { 26 | var commandJSON = this.props.command.toJSON(); 27 | this.setState({ 28 | status: commandJSON.status, 29 | rawStr: commandJSON.rawStr, 30 | warnings: commandJSON.warnings, 31 | result: commandJSON.result 32 | }); 33 | } 34 | 35 | constructor(props, context) { 36 | super(props, context); 37 | this.state = { 38 | status: STATUSES.inqueue, 39 | rawStr: 'git commit', 40 | warnings: [], 41 | result: '' 42 | }; 43 | } 44 | 45 | render() { 46 | var commandClass = reactUtil.joinClasses([ 47 | this.state.status, 48 | 'commandLine', 49 | 'transitionBackground' 50 | ]); 51 | 52 | return ( 53 |
54 |

55 | {'$'} 56 | {' '} 57 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |

68 | {this.renderResult()} 69 |
70 | {this.renderFormattedWarnings()} 71 |
72 |
73 | ); 74 | } 75 | 76 | renderResult() { 77 | if (!this.state.result) { 78 | return null; 79 | } 80 | // We are going to get a ton of raw markup here 81 | // so lets split into paragraphs ourselves 82 | var paragraphs = this.state.result.split("\n"); 83 | var result = []; 84 | for (var i = 0; i < paragraphs.length; i++) { 85 | if (paragraphs[i].startsWith('https://')) { 86 | result.push( 87 | 94 | ); 95 | } else { 96 | result.push( 97 |

103 | ); 104 | } 105 | } 106 | return ( 107 |

108 | {result} 109 |
110 | ); 111 | } 112 | 113 | renderFormattedWarnings() { 114 | var warnings = this.state.warnings; 115 | var result = []; 116 | for (var i = 0; i < warnings.length; i++) { 117 | result.push( 118 |

119 | 120 | {warnings[i]} 121 |

122 | ); 123 | } 124 | return result; 125 | } 126 | }; 127 | 128 | CommandView.propTypes = { 129 | // the backbone command model 130 | command: PropTypes.object.isRequired, 131 | id: PropTypes.string, 132 | }; 133 | 134 | module.exports = CommandView; 135 | -------------------------------------------------------------------------------- /src/js/react_views/CommandsHelperBarView.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PropTypes = require('prop-types'); 3 | var HelperBarView = require('../react_views/HelperBarView.jsx'); 4 | var Main = require('../app'); 5 | 6 | var log = require('../log'); 7 | var intl = require('../intl'); 8 | 9 | class CommandsHelperBarView extends React.Component { 10 | 11 | render() { 12 | return ( 13 | 17 | ); 18 | } 19 | 20 | fireCommand(command) { 21 | log.viewInteracted('commandHelperBar'); 22 | Main.getEventBaton().trigger('commandSubmitted', command); 23 | } 24 | 25 | getItems() { 26 | return [{ 27 | text: intl.str('command-helper-bar-levels'), 28 | onClick: function() { 29 | this.fireCommand('levels'); 30 | }.bind(this), 31 | }, { 32 | text: intl.str('command-helper-bar-solution'), 33 | onClick: function() { 34 | this.fireCommand('show solution'); 35 | }.bind(this), 36 | }, { 37 | text: intl.str('command-helper-bar-reset'), 38 | onClick: function() { 39 | this.fireCommand('reset'); 40 | }.bind(this), 41 | }, { 42 | text: intl.str('command-helper-bar-undo'), 43 | onClick: function() { 44 | this.fireCommand('undo'); 45 | }.bind(this), 46 | }, { 47 | text: intl.str('command-helper-bar-objective'), 48 | onClick: function() { 49 | this.fireCommand('objective'); 50 | }.bind(this), 51 | }, { 52 | text: intl.str('command-helper-bar-help'), 53 | onClick: function() { 54 | this.fireCommand('help general; git help'); 55 | }.bind(this) 56 | }, { 57 | icon: 'fa-solid fa-right-from-bracket', 58 | onClick: function() { 59 | this.props.onExit(); 60 | }.bind(this) 61 | }]; 62 | } 63 | 64 | }; 65 | 66 | CommandsHelperBarView.propTypes = { 67 | shown: PropTypes.bool.isRequired, 68 | onExit: PropTypes.func.isRequired 69 | }; 70 | 71 | module.exports = CommandsHelperBarView; 72 | -------------------------------------------------------------------------------- /src/js/react_views/HelperBarView.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PropTypes = require('prop-types'); 3 | 4 | var reactUtil = require('../util/reactUtil'); 5 | 6 | class HelperBarView extends React.Component { 7 | 8 | render() { 9 | var topClassName = reactUtil.joinClasses([ 10 | 'helperBar', 11 | 'transitionAll', 12 | this.props.shown ? 'show' : '', 13 | this.props.className ? this.props.className : '' 14 | ]); 15 | 16 | return ( 17 |
18 | {this.props.items.map(function(item, index) { 19 | return [ 20 | this.renderItem(item, index), 21 | // ugh -- we need this spacer at the end only 22 | // if we are not the last element 23 | index === this.props.items.length - 1 ? 24 | null : 25 | {' '} 26 | ]; 27 | }.bind(this))} 28 |
29 | ); 30 | } 31 | 32 | renderItem(item, index) { 33 | var testID = item.icon || item.testID || 34 | item.text.toLowerCase(); 35 | if (item.newPageLink) { 36 | return ( 37 |
44 | 45 | {' '} 46 | 47 | ); 48 | } 49 | return ( 50 | 55 | {item.text ? item.text : 56 | 57 | } 58 | {' '} 59 | 60 | ); 61 | } 62 | 63 | }; 64 | 65 | HelperBarView.propTypes = { 66 | className: PropTypes.string, 67 | shown: PropTypes.bool.isRequired, 68 | items: PropTypes.array.isRequired 69 | }; 70 | 71 | 72 | module.exports = HelperBarView; 73 | -------------------------------------------------------------------------------- /src/js/react_views/IntlHelperBarView.jsx: -------------------------------------------------------------------------------- 1 | var PropTypes = require('prop-types'); 2 | 3 | var HelperBarView = require('../react_views/HelperBarView.jsx'); 4 | var Main = require('../app'); 5 | var React = require('react'); 6 | 7 | var log = require('../log'); 8 | 9 | class IntlHelperBarView extends React.Component{ 10 | 11 | render() { 12 | return ( 13 | 17 | ); 18 | } 19 | 20 | fireCommand(command) { 21 | log.viewInteracted('intlSelect'); 22 | Main.getEventBaton().trigger('commandSubmitted', command); 23 | this.props.onExit(); 24 | } 25 | 26 | getItems() { 27 | return [{ 28 | text: 'English', 29 | testID: 'english', 30 | onClick: function() { 31 | this.fireCommand('locale en_US; levels'); 32 | }.bind(this) 33 | }, { 34 | text: '日本語', 35 | testID: 'japanese', 36 | onClick: function() { 37 | this.fireCommand('locale ja; levels'); 38 | }.bind(this) 39 | }, { 40 | text: '한국어 배우기', 41 | testID: 'korean', 42 | onClick: function() { 43 | this.fireCommand('locale ko; levels'); 44 | }.bind(this) 45 | }, { 46 | text: '学习分支', 47 | testID: 'simplifiedChinese', 48 | onClick: function() { 49 | this.fireCommand('locale zh_CN; levels'); 50 | }.bind(this) 51 | }, { 52 | text: '學習分支', 53 | testID: 'traditionalChinese', 54 | onClick: function() { 55 | this.fireCommand('locale zh_TW; levels'); 56 | }.bind(this) 57 | }, { 58 | text: 'Español', 59 | testID: 'spanish', 60 | onClick: function() { 61 | this.fireCommand('locale es_ES; levels'); 62 | }.bind(this) 63 | }, { 64 | text: 'Español (Argentina)', 65 | testID: 'argentinian', 66 | onClick: function() { 67 | this.fireCommand('locale es_AR; levels'); 68 | }.bind(this) 69 | }, { 70 | text: 'Español (México)', 71 | testID: 'mexican', 72 | onClick: function() { 73 | this.fireCommand('locale es_MX; levels'); 74 | }.bind(this) 75 | }, { 76 | text: 'Português', 77 | testID: 'portuguese', 78 | onClick: function() { 79 | this.fireCommand('locale pt_BR; levels'); 80 | }.bind(this) 81 | }, { 82 | text: 'Français', 83 | testID: 'french', 84 | onClick: function() { 85 | this.fireCommand('locale fr_FR; levels'); 86 | }.bind(this) 87 | }, { 88 | text: 'Deutsch', 89 | testID: 'german', 90 | onClick: function() { 91 | this.fireCommand('locale de_DE; levels'); 92 | }.bind(this) 93 | }, { 94 | text: "Română", 95 | testID: "romanian", 96 | onClick: function () { 97 | this.fireCommand("locale ro; levels"); 98 | }.bind(this), 99 | }, { 100 | text: 'Русский', 101 | testID: 'russian', 102 | onClick: function() { 103 | this.fireCommand('locale ru_RU; levels'); 104 | }.bind(this) 105 | }, { 106 | text: 'Українська', 107 | testID: 'ukrainian', 108 | onClick: function() { 109 | this.fireCommand('locale uk; levels'); 110 | }.bind(this) 111 | }, { 112 | text: 'Tiếng Việt', 113 | testID: 'vietnamese', 114 | onClick: function() { 115 | this.fireCommand('locale vi; levels'); 116 | }.bind(this) 117 | }, { 118 | text: 'Türkçe', 119 | testID: 'turkish', 120 | onClick: function() { 121 | this.fireCommand('locale tr_TR; levels'); 122 | }.bind(this) 123 | }, { 124 | text: 'Galego', 125 | testID: 'galician', 126 | onClick: function() { 127 | this.fireCommand('locale gl; levels'); 128 | }.bind(this) 129 | }, { 130 | text: 'Slovenščina', 131 | testID: 'slovenian', 132 | onClick: function() { 133 | this.fireCommand('locale sl_SI; levels'); 134 | }.bind(this) 135 | }, { 136 | text: 'Polski', 137 | testID: 'polish', 138 | onClick: function() { 139 | this.fireCommand('locale pl; levels'); 140 | }.bind(this) 141 | }, { 142 | text: 'தமிழ்', 143 | testID: 'tamil', 144 | onClick: function() { 145 | this.fireCommand('locale ta_IN; levels'); 146 | }.bind(this) 147 | }, { 148 | text: "Italiano", 149 | testID: "italian", 150 | onClick: function () { 151 | this.fireCommand("locale it_IT; levels"); 152 | }.bind(this), 153 | },{ 154 | icon: 'fa-solid fa-right-from-bracket', 155 | onClick: function() { 156 | this.props.onExit(); 157 | }.bind(this) 158 | } 159 | ]; 160 | } 161 | 162 | }; 163 | 164 | IntlHelperBarView.propTypes = { 165 | shown: PropTypes.bool.isRequired, 166 | onExit: PropTypes.func.isRequired 167 | } 168 | 169 | module.exports = IntlHelperBarView; 170 | -------------------------------------------------------------------------------- /src/js/react_views/LevelToolbarView.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PropTypes = require('prop-types'); 3 | 4 | var intl = require('../intl'); 5 | var reactUtil = require('../util/reactUtil'); 6 | 7 | class LevelToolbarView extends React.Component { 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | this.state = { 12 | isHidden: true, 13 | isGoalExpanded: this.props.parent.getIsGoalExpanded() 14 | }; 15 | } 16 | 17 | componentWillUnmount() { 18 | this._isMounted = false; 19 | } 20 | componentDidMount() { 21 | this._isMounted = true; 22 | this.setState({ 23 | isHidden: this.props.parent.getIsGoalExpanded(), 24 | isGoalExpanded: this.props.parent.getIsGoalExpanded() 25 | }); 26 | this.props.parent.on('goalToggled', function() { 27 | if (!this._isMounted) { 28 | return; 29 | } 30 | 31 | this.setState({ 32 | isGoalExpanded: this.props.parent.getIsGoalExpanded() 33 | }); 34 | }.bind(this)); 35 | } 36 | 37 | render() { 38 | return ( 39 |
48 |
49 |
50 | 51 | { intl.str('level-label') } 52 | 53 | {this.props.name} 54 | 55 |
56 |
57 |
58 |
59 | 67 |
68 |
69 | 74 |
75 |
76 |
77 | ); 78 | } 79 | 80 | }; 81 | 82 | LevelToolbarView.propTypes = { 83 | name: PropTypes.string.isRequired, 84 | onGoalClick: PropTypes.func.isRequired, 85 | onObjectiveClick: PropTypes.func.isRequired, 86 | parent: PropTypes.object.isRequired 87 | } 88 | 89 | module.exports = LevelToolbarView; 90 | -------------------------------------------------------------------------------- /src/js/react_views/MainHelperBarView.jsx: -------------------------------------------------------------------------------- 1 | var HelperBarView = require('../react_views/HelperBarView.jsx'); 2 | var IntlHelperBarView = 3 | require('../react_views/IntlHelperBarView.jsx'); 4 | var CommandsHelperBarView = 5 | require('../react_views/CommandsHelperBarView.jsx'); 6 | var React = require('react'); 7 | 8 | var keyMirror = require('../util/keyMirror'); 9 | var log = require('../log'); 10 | 11 | var BARS = keyMirror({ 12 | SELF: null, 13 | INTL: null, 14 | COMMANDS: null 15 | }); 16 | 17 | class MainHelperBarView extends React.Component { 18 | 19 | constructor(props, context) { 20 | super(props, context); 21 | this.state = { 22 | shownBar: BARS.SELF 23 | }; 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 | 34 | 38 | 42 |
43 | ); 44 | } 45 | 46 | showSelf() { 47 | this.setState({ 48 | shownBar: BARS.SELF 49 | }); 50 | } 51 | 52 | getItems() { 53 | return [{ 54 | icon: 'fa-solid fa-question', 55 | onClick: function() { 56 | this.setState({ 57 | shownBar: BARS.COMMANDS 58 | }); 59 | }.bind(this), 60 | title: 'Show commands' 61 | }, { 62 | icon: 'fa-solid fa-language', 63 | onClick: function() { 64 | this.setState({ 65 | shownBar: BARS.INTL 66 | }); 67 | }.bind(this), 68 | title: 'Show available languages' 69 | }, { 70 | newPageLink: true, 71 | icon: 'fa-brands fa-threads', 72 | href: 'https://www.threads.net/@pcottle', 73 | title: 'Follow me on Threads' 74 | }]; 75 | } 76 | 77 | }; 78 | 79 | module.exports = MainHelperBarView; 80 | -------------------------------------------------------------------------------- /src/js/sandbox/commands.js: -------------------------------------------------------------------------------- 1 | var util = require('../util'); 2 | 3 | var constants = require('../util/constants'); 4 | var intl = require('../intl'); 5 | 6 | var Commands = require('../commands'); 7 | var Errors = require('../util/errors'); 8 | var CommandProcessError = Errors.CommandProcessError; 9 | var LocaleStore = require('../stores/LocaleStore'); 10 | var LocaleActions = require('../actions/LocaleActions'); 11 | var LevelStore = require('../stores/LevelStore'); 12 | var GlobalStateStore = require('../stores/GlobalStateStore'); 13 | var GlobalStateActions = require('../actions/GlobalStateActions'); 14 | var GitError = Errors.GitError; 15 | var Warning = Errors.Warning; 16 | var CommandResult = Errors.CommandResult; 17 | 18 | var instantCommands = [ 19 | // Add a third and fourth item in the tuple if you want this to show 20 | // up in the `show commands` function 21 | [/^ls( |$)/, function() { 22 | throw new CommandResult({ 23 | msg: intl.str('ls-command') 24 | }); 25 | }], 26 | [/^cd( |$)/, function() { 27 | throw new CommandResult({ 28 | msg: intl.str('cd-command') 29 | }); 30 | }], 31 | [/^(locale|locale reset)$/, function(bits) { 32 | LocaleActions.changeLocale( 33 | LocaleStore.getDefaultLocale() 34 | ); 35 | 36 | throw new CommandResult({ 37 | msg: intl.str( 38 | 'locale-reset-command', 39 | { locale: LocaleStore.getDefaultLocale() } 40 | ) 41 | }); 42 | }, 'locale', 'change locale from the command line, or reset with `locale reset`'], 43 | [/^show$/, function(bits) { 44 | var lines = [ 45 | intl.str('show-command'), 46 | '
', 47 | 'show commands', 48 | 'show solution', 49 | 'show goal' 50 | ]; 51 | 52 | throw new CommandResult({ 53 | msg: lines.join('\n') 54 | }); 55 | }, 'show', 'Run `show commands|solution|goal` to see the available commands or aspects of the current level'], 56 | [/^alias (\w+)="(.+)"$/, function(bits) { 57 | const alias = bits[1]; 58 | const expansion = bits[2]; 59 | LevelStore.addToAliasMap(alias, expansion); 60 | throw new CommandResult({ 61 | msg: 'Set alias "'+alias+'" to "'+expansion+'"', 62 | }); 63 | }, 'alias', 'Run `alias` to map a certain shortcut to an expansion'], 64 | [/^unalias (\w+)$/, function(bits) { 65 | const alias = bits[1]; 66 | LevelStore.removeFromAliasMap(alias); 67 | throw new CommandResult({ 68 | msg: 'Removed alias "'+alias+'"', 69 | }); 70 | }, 'unalias', 'Opposite of `alias`'], 71 | [/^locale (\w+)$/, function(bits) { 72 | LocaleActions.changeLocale(bits[1]); 73 | throw new CommandResult({ 74 | msg: intl.str( 75 | 'locale-command', 76 | { locale: bits[1] } 77 | ) 78 | }); 79 | }], 80 | [/^flip$/, function() { 81 | GlobalStateActions.changeFlipTreeY( 82 | !GlobalStateStore.getFlipTreeY() 83 | ); 84 | require('../app').getEvents().trigger('refreshTree'); 85 | throw new CommandResult({ 86 | msg: intl.str('flip-tree-command') 87 | }); 88 | }, 'flip', 'flip the direction of the tree (and commit arrows)'], 89 | [/^disableLevelInstructions$/, function() { 90 | GlobalStateActions.disableLevelInstructions(); 91 | throw new CommandResult({ 92 | msg: intl.todo('Level instructions disabled'), 93 | }); 94 | }, 'disableLevelInstructions', 'Disable the level instructions'], 95 | [/^refresh$/, function() { 96 | var events = require('../app').getEvents(); 97 | 98 | events.trigger('refreshTree'); 99 | throw new CommandResult({ 100 | msg: intl.str('refresh-tree-command') 101 | }); 102 | }], 103 | [/^rollup (\d+)$/, function(bits) { 104 | var events = require('../app').getEvents(); 105 | 106 | // go roll up these commands by joining them with semicolons 107 | events.trigger('rollupCommands', bits[1]); 108 | throw new CommandResult({ 109 | msg: 'Commands combined!' 110 | }); 111 | }], 112 | [/^echo "(.*?)"$|^echo (.*?)$/, function(bits) { 113 | var msg = bits[1] || bits[2]; 114 | throw new CommandResult({ 115 | msg: msg 116 | }); 117 | }, 'echo', 'echo out a string to the terminal output'], 118 | [/^show +commands$/, function(bits) { 119 | var allCommands = Object.assign( 120 | {}, 121 | getAllCommands() 122 | ); 123 | var allOptions = Commands.commands.getOptionMap(); 124 | var commandToOptions = {}; 125 | 126 | Object.keys(allOptions).forEach(function(vcs) { 127 | var vcsMap = allOptions[vcs]; 128 | Object.keys(vcsMap).forEach(function(method) { 129 | var options = vcsMap[method]; 130 | if (options) { 131 | commandToOptions[vcs + ' ' + method] = Object.keys(options).filter(option => option.length > 1); 132 | } 133 | }); 134 | }); 135 | 136 | var selectedInstantCommands = {}; 137 | instantCommands.map( 138 | tuple => { 139 | var commandName = tuple[2]; 140 | if (!commandName) { 141 | return; 142 | } 143 | commandToOptions[commandName] = [tuple[3]]; 144 | // add this as a key so we map over it 145 | allCommands[commandName] = tuple[3]; 146 | // and save it in another map so we can add extra whitespace 147 | selectedInstantCommands[commandName] = tuple[3]; 148 | }, 149 | ); 150 | 151 | var lines = [ 152 | intl.str('show-all-commands'), 153 | '
' 154 | ]; 155 | Object.keys(allCommands) 156 | .forEach(function(command) { 157 | if (selectedInstantCommands[command]) { 158 | lines.push('
'); 159 | } 160 | lines.push(command); 161 | if (commandToOptions[command]) { 162 | commandToOptions[command].forEach(option => lines.push('    ' + option)); 163 | } 164 | 165 | if (selectedInstantCommands[command]) { 166 | lines.push('
'); 167 | } 168 | }); 169 | 170 | throw new CommandResult({ 171 | msg: lines.join('\n') 172 | }); 173 | }] 174 | ]; 175 | 176 | var regexMap = { 177 | 'reset solved': /^reset solved($|\s)/, 178 | 'help': /^help( +general)?$|^\?$/, 179 | 'reset': /^reset( +--forSolution)?$/, 180 | 'delay': /^delay (\d+)$/, 181 | 'clear': /^clear($|\s)/, 182 | 'exit level': /^exit level($|\s)/, 183 | 'sandbox': /^sandbox($|\s)/, 184 | 'level': /^level\s?([a-zA-Z0-9]*)/, 185 | 'levels': /^levels($|\s)/, 186 | 'mobileAlert': /^mobile alert($|\s)/, 187 | 'build level': /^build +level\s?([a-zA-Z0-9]*)( +--skipIntro)?$/, 188 | 'export tree': /^export +tree$/, 189 | 'importTreeNow': /^importTreeNow($|\s)/, 190 | 'importLevelNow': /^importLevelNow($|\s)/, 191 | 'import tree': /^import +tree$/, 192 | 'import level': /^import +level$/, 193 | 'undo': /^undo($|\s)/, 194 | 'share permalink': /^share( +permalink)?$/ 195 | }; 196 | 197 | var getAllCommands = function() { 198 | var toDelete = [ 199 | 'mobileAlert' 200 | ]; 201 | 202 | var allCommands = Object.assign( 203 | {}, 204 | require('../level').regexMap, 205 | regexMap 206 | ); 207 | var mRegexMap = Commands.commands.getRegexMap(); 208 | Object.keys(mRegexMap).forEach(function(vcs) { 209 | var map = mRegexMap[vcs]; 210 | Object.keys(map).forEach(function(method) { 211 | var regex = map[method]; 212 | allCommands[vcs + ' ' + method] = regex; 213 | }); 214 | }); 215 | toDelete.forEach(function(key) { 216 | delete allCommands[key]; 217 | }); 218 | 219 | return allCommands; 220 | }; 221 | 222 | exports.getAllCommands = getAllCommands; 223 | exports.instantCommands = instantCommands; 224 | exports.parse = util.genParseCommand(regexMap, 'processSandboxCommand'); 225 | 226 | // optimistically parse some level and level builder commands; we do this 227 | // so you can enter things like "level intro1; show goal" and not 228 | // have it barf. when the 229 | // command fires the event, it will check if there is a listener and if not throw 230 | // an error 231 | 232 | // note: these are getters / setters because the require kills us 233 | exports.getOptimisticLevelParse = function() { 234 | return util.genParseCommand( 235 | require('../level').regexMap, 236 | 'processLevelCommand' 237 | ); 238 | }; 239 | 240 | exports.getOptimisticLevelBuilderParse = function() { 241 | return util.genParseCommand( 242 | require('../level/builder').regexMap, 243 | 'processLevelBuilderCommand' 244 | ); 245 | }; 246 | -------------------------------------------------------------------------------- /src/js/stores/CommandLineStore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | var ActionTypes = AppConstants.ActionTypes; 8 | var COMMAND_HISTORY_KEY = 'lgb_CommandHistory'; 9 | var COMMAND_HISTORY_MAX_LENGTH = 100; 10 | var COMMAND_HISTORY_TO_KEEP = 10; 11 | 12 | var _commandHistory = []; 13 | try { 14 | _commandHistory = JSON.parse( 15 | localStorage.getItem(COMMAND_HISTORY_KEY) || '[]' 16 | ) || []; 17 | } catch (e) { 18 | } 19 | 20 | function _checkForSize() { 21 | // if our command line history is too big... 22 | if (_commandHistory.length > COMMAND_HISTORY_MAX_LENGTH) { 23 | // grab the last 10 24 | _commandHistory = 25 | _commandHistory.slice(0, COMMAND_HISTORY_TO_KEEP); 26 | } 27 | } 28 | 29 | function _saveToLocalStorage() { 30 | try { 31 | localStorage.setItem( 32 | COMMAND_HISTORY_KEY, 33 | JSON.stringify(_commandHistory) 34 | ); 35 | } catch (e) { 36 | } 37 | } 38 | 39 | var CommandLineStore = Object.assign( 40 | {}, 41 | EventEmitter.prototype, 42 | AppConstants.StoreSubscribePrototype, 43 | { 44 | 45 | getMaxHistoryLength: function() { 46 | return COMMAND_HISTORY_MAX_LENGTH; 47 | }, 48 | 49 | getCommandHistoryLength: function() { 50 | return _commandHistory.length; 51 | }, 52 | 53 | getCommandHistory: function() { 54 | return _commandHistory.slice(0); 55 | }, 56 | 57 | dispatchToken: AppDispatcher.register(function(payload) { 58 | var action = payload.action; 59 | var shouldInform = false; 60 | 61 | switch (action.type) { 62 | case ActionTypes.SUBMIT_COMMAND: 63 | _commandHistory.unshift(String(action.text)); 64 | _checkForSize(); 65 | _saveToLocalStorage(); 66 | shouldInform = true; 67 | break; 68 | case ActionTypes.CHANGE_FLIP_TREE_Y: 69 | break; 70 | } 71 | 72 | if (shouldInform) { 73 | CommandLineStore.emit(AppConstants.CHANGE_EVENT); 74 | } 75 | }) 76 | 77 | }); 78 | 79 | module.exports = CommandLineStore; 80 | -------------------------------------------------------------------------------- /src/js/stores/GlobalStateStore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | var ActionTypes = AppConstants.ActionTypes; 8 | 9 | var _isAnimating = false; 10 | var _flipTreeY = false; 11 | var _numLevelsSolved = 0; 12 | var _disableLevelInstructions = false; 13 | var _isSolvingLevel = false; 14 | 15 | var GlobalStateStore = Object.assign( 16 | {}, 17 | EventEmitter.prototype, 18 | AppConstants.StoreSubscribePrototype, 19 | { 20 | getIsAnimating: function() { 21 | return _isAnimating; 22 | }, 23 | 24 | getIsSolvingLevel: function() { 25 | return _isSolvingLevel; 26 | }, 27 | 28 | getFlipTreeY: function() { 29 | return _flipTreeY; 30 | }, 31 | 32 | getNumLevelsSolved: function() { 33 | return _numLevelsSolved; 34 | }, 35 | 36 | getShouldDisableLevelInstructions: function() { 37 | return _disableLevelInstructions; 38 | }, 39 | 40 | dispatchToken: AppDispatcher.register(function(payload) { 41 | var action = payload.action; 42 | var shouldInform = false; 43 | 44 | switch (action.type) { 45 | case ActionTypes.SET_IS_SOLVING_LEVEL: 46 | _isSolvingLevel = action.isSolvingLevel; 47 | shouldInform = true; 48 | break; 49 | case ActionTypes.CHANGE_IS_ANIMATING: 50 | _isAnimating = action.isAnimating; 51 | shouldInform = true; 52 | break; 53 | case ActionTypes.CHANGE_FLIP_TREE_Y: 54 | _flipTreeY = action.flipTreeY; 55 | shouldInform = true; 56 | break; 57 | case ActionTypes.LEVEL_SOLVED: 58 | _numLevelsSolved++; 59 | shouldInform = true; 60 | break; 61 | case ActionTypes.DISABLE_LEVEL_INSTRUCTIONS: 62 | _disableLevelInstructions = true; 63 | shouldInform = true; 64 | break; 65 | } 66 | 67 | if (shouldInform) { 68 | GlobalStateStore.emit(AppConstants.CHANGE_EVENT); 69 | } 70 | }) 71 | 72 | }); 73 | 74 | module.exports = GlobalStateStore; 75 | -------------------------------------------------------------------------------- /src/js/stores/LevelStore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | var EventEmitter = require('events').EventEmitter; 6 | var levelSequences = require('../../levels').levelSequences; 7 | var sequenceInfo = require('../../levels').sequenceInfo; 8 | var util = require('../util'); 9 | 10 | var ActionTypes = AppConstants.ActionTypes; 11 | var SOLVED_MAP_STORAGE_KEY = 'solvedMap'; 12 | var ALIAS_STORAGE_KEY = 'aliasMap'; 13 | 14 | var _levelMap = {}; 15 | var _solvedMap = {}; 16 | var _sequences = []; 17 | 18 | if (!util.isBrowser()) { 19 | // https://stackoverflow.com/a/26177872/6250402 20 | var storage = {}; 21 | var localStorage = { 22 | setItem: function(key, value) { 23 | storage[key] = value || ''; 24 | }, 25 | getItem: function(key) { 26 | return key in storage ? storage[key] : null; 27 | }, 28 | removeItem: function(key) { 29 | delete storage[key]; 30 | }, 31 | get length() { 32 | return Object.keys(storage).length; 33 | }, 34 | key: function(i) { 35 | const keys = Object.keys(storage); 36 | return keys[i] || null; 37 | } 38 | }; 39 | } else { 40 | var localStorage = window.localStorage; 41 | } 42 | 43 | try { 44 | _solvedMap = JSON.parse( 45 | localStorage.getItem(SOLVED_MAP_STORAGE_KEY) || '{}' 46 | ) || {}; 47 | } catch (e) { 48 | console.warn('local storage failed', e); 49 | } 50 | 51 | function _syncToStorage() { 52 | try { 53 | localStorage.setItem(SOLVED_MAP_STORAGE_KEY, JSON.stringify(_solvedMap)); 54 | } catch (e) { 55 | console.warn('local storage failed on set', e); 56 | } 57 | } 58 | 59 | function getAliasMap() { 60 | try { 61 | return JSON.parse(localStorage.getItem(ALIAS_STORAGE_KEY) || '{}') || {}; 62 | } catch (e) { 63 | return {}; 64 | } 65 | } 66 | 67 | function addToAliasMap(alias, expansion) { 68 | const aliasMap = getAliasMap(); 69 | aliasMap[alias] = expansion; 70 | localStorage.setItem(ALIAS_STORAGE_KEY, JSON.stringify(aliasMap)); 71 | } 72 | 73 | function removeFromAliasMap(alias) { 74 | const aliasMap = getAliasMap(); 75 | delete aliasMap[alias]; 76 | localStorage.setItem(ALIAS_STORAGE_KEY, JSON.stringify(aliasMap)); 77 | } 78 | 79 | var validateLevel = function(level) { 80 | level = level || {}; 81 | var requiredFields = [ 82 | 'name', 83 | 'goalTreeString', 84 | //'description', 85 | 'solutionCommand' 86 | ]; 87 | 88 | requiredFields.forEach(function(field) { 89 | if (level[field] === undefined) { 90 | console.log(level); 91 | throw new Error('I need this field for a level: ' + field); 92 | } 93 | }); 94 | }; 95 | 96 | /** 97 | * Unpack the level sequences. 98 | */ 99 | Object.keys(levelSequences).forEach(function(levelSequenceName) { 100 | var levels = levelSequences[levelSequenceName]; 101 | _sequences.push(levelSequenceName); 102 | if (!levels || !levels.length) { 103 | throw new Error('no empty sequences allowed'); 104 | } 105 | 106 | // for this particular sequence... 107 | levels.forEach(function(level, index) { 108 | validateLevel(level); 109 | 110 | var id = levelSequenceName + String(index + 1); 111 | var compiledLevel = Object.assign( 112 | {}, 113 | level, 114 | { 115 | index: index, 116 | id: id, 117 | sequenceName: levelSequenceName 118 | } 119 | ); 120 | 121 | // update our internal data 122 | _levelMap[id] = compiledLevel; 123 | levelSequences[levelSequenceName][index] = compiledLevel; 124 | }); 125 | }); 126 | 127 | var LevelStore = Object.assign( 128 | {}, 129 | EventEmitter.prototype, 130 | AppConstants.StoreSubscribePrototype, 131 | { 132 | getAliasMap: getAliasMap, 133 | addToAliasMap: addToAliasMap, 134 | removeFromAliasMap: removeFromAliasMap, 135 | 136 | getSequenceToLevels: function() { 137 | return levelSequences; 138 | }, 139 | 140 | getSequences: function() { 141 | return Object.keys(levelSequences); 142 | }, 143 | 144 | getLevelsInSequence: function(sequenceName) { 145 | if (!levelSequences[sequenceName]) { 146 | throw new Error('that sequence name ' + sequenceName + ' does not exist'); 147 | } 148 | return levelSequences[sequenceName]; 149 | }, 150 | 151 | getSequenceInfo: function(sequenceName) { 152 | return sequenceInfo[sequenceName]; 153 | }, 154 | 155 | getLevel: function(id) { 156 | return _levelMap[id]; 157 | }, 158 | 159 | getNextLevel: function(id) { 160 | if (!_levelMap[id]) { 161 | console.warn('that level doesn\'t exist!!!'); 162 | return null; 163 | } 164 | 165 | // meh, this method could be better. It's a trade-off between 166 | // having the sequence structure be really simple JSON 167 | // and having no connectivity information between levels, which means 168 | // you have to build that up yourself on every query 169 | var level = _levelMap[id]; 170 | var sequenceName = level.sequenceName; 171 | var sequence = levelSequences[sequenceName]; 172 | 173 | var nextIndex = level.index + 1; 174 | if (nextIndex < sequence.length) { 175 | return sequence[nextIndex]; 176 | } 177 | 178 | var nextSequenceIndex = _sequences.indexOf(sequenceName) + 1; 179 | if (nextSequenceIndex < _sequences.length) { 180 | var nextSequenceName = _sequences[nextSequenceIndex]; 181 | return levelSequences[nextSequenceName][0]; 182 | } 183 | 184 | // they finished the last level! 185 | return null; 186 | }, 187 | 188 | isLevelSolved: function(levelID) { 189 | var levelData = _solvedMap[levelID]; 190 | if (levelData === true) { 191 | return true; 192 | } 193 | return levelData ? levelData.solved === true : false; 194 | }, 195 | 196 | 197 | isLevelBest: function(levelID) { 198 | var levelData = _solvedMap[levelID]; 199 | return levelData ? levelData.best === true : false; 200 | }, 201 | 202 | 203 | 204 | dispatchToken: AppDispatcher.register(function(payload) { 205 | var action = payload.action; 206 | var shouldInform = false; 207 | 208 | switch (action.type) { 209 | case ActionTypes.RESET_LEVELS_SOLVED: 210 | _solvedMap = {}; 211 | _syncToStorage(); 212 | shouldInform = true; 213 | break; 214 | case ActionTypes.SET_LEVEL_SOLVED: 215 | _solvedMap[action.levelID] = { solved: true, best: action.best || false }; 216 | _syncToStorage(); 217 | shouldInform = true; 218 | break; 219 | } 220 | 221 | if (shouldInform) { 222 | LevelStore.emit(AppConstants.CHANGE_EVENT); 223 | } 224 | }) 225 | 226 | }); 227 | 228 | module.exports = LevelStore; 229 | -------------------------------------------------------------------------------- /src/js/stores/LocaleStore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppConstants = require('../constants/AppConstants'); 4 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | var util = require('../util'); 6 | var EventEmitter = require('events').EventEmitter; 7 | 8 | var ActionTypes = AppConstants.ActionTypes; 9 | var DEFAULT_LOCALE = 'en_US'; 10 | 11 | // resolve the messy mapping between browser language 12 | // and our supported locales 13 | var langLocaleMap = { 14 | en: 'en_US', 15 | zh: 'zh_CN', 16 | ja: 'ja', 17 | ko: 'ko', 18 | es: 'es_AR', 19 | fr: 'fr_FR', 20 | de: 'de_DE', 21 | pt: 'pt_BR', 22 | ro: 'ro', 23 | ru: 'ru_RU', 24 | uk: 'uk', 25 | vi: 'vi', 26 | sl: 'sl_SI', 27 | pl: 'pl', 28 | it: 'it_IT', 29 | ta: 'ta_IN', 30 | tr: 'tr_TR', 31 | }; 32 | 33 | var headerLocaleMap = { 34 | 'zh-CN': 'zh_CN', 35 | 'zh-TW': 'zh_TW', 36 | 'pt-BR': 'pt_BR', 37 | 'es-MX': 'es_MX', 38 | 'es-ES': 'es_ES', 39 | 'it-IT': 'it_IT', 40 | 'sl-SI': 'sl_SI', 41 | 'tr-TR': 'tr_TR', 42 | }; 43 | 44 | var supportedLocalesList = Object.values(langLocaleMap) 45 | .concat(Object.values(headerLocaleMap)) 46 | .filter(function (value, index, self) { return self.indexOf(value) === index;}); 47 | 48 | function _getLocaleFromHeader(langString) { 49 | var languages = langString.split(','); 50 | var desiredLocale; 51 | for (var i = 0; i < languages.length; i++) { 52 | var header = languages[i].split(';')[0]; 53 | // first check the full string raw 54 | if (headerLocaleMap[header]) { 55 | desiredLocale = headerLocaleMap[header]; 56 | break; 57 | } 58 | 59 | var lang = header.slice(0, 2); 60 | if (langLocaleMap[lang]) { 61 | desiredLocale = langLocaleMap[lang]; 62 | break; 63 | } 64 | } 65 | return desiredLocale; 66 | } 67 | 68 | var _locale = DEFAULT_LOCALE; 69 | var LocaleStore = Object.assign( 70 | {}, 71 | EventEmitter.prototype, 72 | AppConstants.StoreSubscribePrototype, 73 | { 74 | 75 | getDefaultLocale: function() { 76 | return DEFAULT_LOCALE; 77 | }, 78 | 79 | getLangLocaleMap: function() { 80 | return Object.assign({}, langLocaleMap); 81 | }, 82 | 83 | getHeaderLocaleMap: function() { 84 | return Object.assign({}, headerLocaleMap); 85 | }, 86 | 87 | getLocale: function() { 88 | return _locale; 89 | }, 90 | 91 | getSupportedLocales: function() { 92 | return supportedLocalesList.slice(); 93 | }, 94 | 95 | dispatchToken: AppDispatcher.register(function(payload) { 96 | var action = payload.action; 97 | var shouldInform = false; 98 | var oldLocale = _locale; 99 | 100 | switch (action.type) { 101 | case ActionTypes.CHANGE_LOCALE: 102 | _locale = action.locale; 103 | shouldInform = true; 104 | break; 105 | case ActionTypes.CHANGE_LOCALE_FROM_HEADER: 106 | var value = _getLocaleFromHeader(action.header); 107 | if (value) { 108 | _locale = value; 109 | shouldInform = true; 110 | } 111 | break; 112 | } 113 | 114 | if (util.isBrowser() && oldLocale !== _locale) { 115 | var url = new URL(document.location.href); 116 | url.searchParams.set('locale', _locale); 117 | window.history.replaceState({}, '', url.href); 118 | } 119 | 120 | if (shouldInform) { 121 | LocaleStore.emit(AppConstants.CHANGE_EVENT); 122 | } 123 | }) 124 | 125 | }); 126 | 127 | module.exports = LocaleStore; 128 | -------------------------------------------------------------------------------- /src/js/util/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants....!!! 3 | */ 4 | var TIME = { 5 | betweenCommandsDelay: 400 6 | }; 7 | 8 | var VIEWPORT = { 9 | minZoom: 0.55, 10 | maxZoom: 1.25, 11 | minWidth: 600, 12 | minHeight: 600 13 | }; 14 | 15 | var GRAPHICS = { 16 | arrowHeadSize: 8, 17 | 18 | nodeRadius: 17, 19 | curveControlPointOffset: 50, 20 | defaultEasing: 'easeInOut', 21 | defaultAnimationTime: 400, 22 | 23 | rectFill: 'hsb(0.8816909813322127,0.6,1)', 24 | headRectFill: '#7278FF', 25 | rectStroke: '#FFF', 26 | rectStrokeWidth: '3', 27 | 28 | originDash: '- ', 29 | 30 | multiBranchY: 20, 31 | multiTagY: 15, 32 | upstreamHeadOpacity: 0.5, 33 | upstreamNoneOpacity: 0.2, 34 | edgeUpstreamHeadOpacity: 0.4, 35 | edgeUpstreamNoneOpacity: 0.15, 36 | 37 | visBranchStrokeWidth: 2, 38 | visBranchStrokeColorNone: '#333', 39 | 40 | defaultNodeFill: 'hsba(0.5,0.6,0.7,1)', 41 | defaultNodeStrokeWidth: 2, 42 | defaultNodeStroke: '#FFF', 43 | 44 | tagFill: 'hsb(0,0,0.9)', 45 | tagStroke: '#FFF', 46 | tagStrokeWidth: '2', 47 | 48 | orphanNodeFill: 'hsb(0.5,0.8,0.7)' 49 | }; 50 | 51 | exports.TIME = TIME; 52 | exports.GRAPHICS = GRAPHICS; 53 | exports.VIEWPORT = VIEWPORT; 54 | -------------------------------------------------------------------------------- /src/js/util/debounce.js: -------------------------------------------------------------------------------- 1 | module.exports = function(func, time, immediate) { 2 | var timeout; 3 | return function() { 4 | var later = function() { 5 | timeout = null; 6 | if (!immediate) { 7 | func.apply(this, arguments); 8 | } 9 | }; 10 | var callNow = immediate && !timeout; 11 | clearTimeout(timeout); 12 | timeout = setTimeout(later, time); 13 | if (callNow) { 14 | func.apply(this, arguments); 15 | } 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/js/util/debug.js: -------------------------------------------------------------------------------- 1 | var toGlobalize = { 2 | App: require('../app/index.js'), 3 | Tree: require('../visuals/tree'), 4 | Visuals: require('../visuals'), 5 | Git: require('../git'), 6 | CommandModel: require('../models/commandModel'), 7 | CommandLineStore: require('../stores/CommandLineStore'), 8 | CommandLineActions: require('../actions/CommandLineActions'), 9 | LevelActions: require('../actions/LevelActions'), 10 | LevelStore: require('../stores/LevelStore'), 11 | LocaleActions: require('../actions/LocaleActions'), 12 | GlobalStateActions: require('../actions/GlobalStateActions'), 13 | GlobalStateStore: require('../stores/GlobalStateStore'), 14 | LocaleStore: require('../stores/LocaleStore'), 15 | Levels: require('../graph/treeCompare'), 16 | Constants: require('../util/constants'), 17 | Commands: require('../commands'), 18 | Collections: require('../models/collections'), 19 | Async: require('../visuals/animation'), 20 | AnimationFactory: require('../visuals/animation/animationFactory'), 21 | Main: require('../app'), 22 | HeadLess: require('../git/headless'), 23 | Q: { Q: require('q') }, 24 | RebaseView: require('../views/rebaseView'), 25 | Views: require('../views'), 26 | MultiView: require('../views/multiView'), 27 | ZoomLevel: require('../util/zoomLevel'), 28 | VisBranch: require('../visuals/visBranch'), 29 | TreeCompare: require('../graph/treeCompare'), 30 | Level: require('../level'), 31 | Sandbox: require('../sandbox/'), 32 | SandboxCommands: require('../sandbox/commands'), 33 | GitDemonstrationView: require('../views/gitDemonstrationView'), 34 | Markdown: require('marked').marked, 35 | LevelDropdownView: require('../views/levelDropdownView'), 36 | BuilderViews: require('../views/builderViews'), 37 | Util: require('../util/index'), 38 | Intl: require('../intl') 39 | }; 40 | 41 | Object.keys(toGlobalize).forEach(function(moduleName) { 42 | var module = toGlobalize[moduleName]; 43 | 44 | for (var key in module) { 45 | var value = module[key]; 46 | if (value instanceof Function) { 47 | value = value.bind(module); 48 | } 49 | window['debug_' + moduleName + '_' + key] = value; 50 | } 51 | }); 52 | 53 | $(document).ready(function() { 54 | window.debug_events = toGlobalize.Main.getEvents(); 55 | window.debug_eventBaton = toGlobalize.Main.getEventBaton(); 56 | window.debug_sandbox = toGlobalize.Main.getSandbox(); 57 | window.debug_modules = toGlobalize; 58 | window.debug_levelDropdown = toGlobalize.Main.getLevelDropdown(); 59 | window.debug_copyTree = function() { 60 | return toGlobalize.Main.getSandbox().mainVis.gitEngine.printAndCopyTree(); 61 | }; 62 | }); 63 | -------------------------------------------------------------------------------- /src/js/util/errors.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | 3 | var MyError = Backbone.Model.extend({ 4 | defaults: { 5 | type: 'MyError' 6 | }, 7 | toString: function() { 8 | return this.get('type') + ': ' + this.get('msg'); 9 | }, 10 | 11 | getMsg: function() { 12 | if (!this.get('msg')) { 13 | debugger; 14 | console.warn('my error without message'); 15 | } 16 | return this.get('msg'); 17 | } 18 | }); 19 | 20 | var CommandProcessError = exports.CommandProcessError = MyError.extend({ 21 | defaults: { 22 | type: 'Command Process Error' 23 | } 24 | }); 25 | 26 | var CommandResult = exports.CommandResult = MyError.extend({ 27 | defaults: { 28 | type: 'Command Result' 29 | } 30 | }); 31 | 32 | var Warning = exports.Warning = MyError.extend({ 33 | defaults: { 34 | type: 'Warning' 35 | } 36 | }); 37 | 38 | var GitError = exports.GitError = MyError.extend({ 39 | defaults: { 40 | type: 'Git Error' 41 | } 42 | }); 43 | 44 | var filterError = function(err) { 45 | if (err instanceof CommandProcessError || 46 | err instanceof GitError || 47 | err instanceof CommandResult || 48 | err instanceof Warning) { 49 | // yay! one of ours 50 | return; 51 | } else { 52 | throw err; 53 | } 54 | }; 55 | 56 | exports.filterError = filterError; 57 | -------------------------------------------------------------------------------- /src/js/util/escapeString.js: -------------------------------------------------------------------------------- 1 | var mapping = { 2 | '&': '&', 3 | '<': '<', 4 | '>': '>', 5 | '"': '"', 6 | "'": ''', 7 | '/': '/' 8 | }; 9 | 10 | module.exports = function(string) { 11 | return ('' + string).replace(/[&<>"'\/]/g, function(match) { 12 | return mapping[match]; 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/js/util/eventBaton.js: -------------------------------------------------------------------------------- 1 | function EventBaton(options) { 2 | this.eventMap = {}; 3 | this.options = options || {}; 4 | } 5 | 6 | // this method steals the "baton" -- aka, only this method will now 7 | // get called. analogous to events.on 8 | // EventBaton.prototype.on = function(name, func, context) { 9 | EventBaton.prototype.stealBaton = function(name, func, context) { 10 | if (!name) { throw new Error('need name'); } 11 | if (!func) { throw new Error('need func!'); } 12 | 13 | var listeners = this.eventMap[name] || []; 14 | listeners.push({ 15 | func: func, 16 | context: context 17 | }); 18 | this.eventMap[name] = listeners; 19 | }; 20 | 21 | EventBaton.prototype.sliceOffArgs = function(num, args) { 22 | var newArgs = []; 23 | for (var i = num; i < args.length; i++) { 24 | newArgs.push(args[i]); 25 | } 26 | return newArgs; 27 | }; 28 | 29 | EventBaton.prototype.trigger = function(name) { 30 | // arguments is weird and doesn't do slice right 31 | var argsToApply = this.sliceOffArgs(1, arguments); 32 | 33 | var listeners = this.eventMap[name]; 34 | if (!listeners || !listeners.length) { 35 | console.warn('no listeners for', name); 36 | return; 37 | } 38 | 39 | // call the top most listener with context and such 40 | var toCall = listeners.slice(-1)[0]; 41 | toCall.func.apply(toCall.context, argsToApply); 42 | }; 43 | 44 | EventBaton.prototype.getNumListeners = function(name) { 45 | var listeners = this.eventMap[name] || []; 46 | return listeners.length; 47 | }; 48 | 49 | EventBaton.prototype.getListenersThrow = function(name) { 50 | var listeners = this.eventMap[name]; 51 | if (!listeners || !listeners.length) { 52 | throw new Error('no one has that baton!' + name); 53 | } 54 | return listeners; 55 | }; 56 | 57 | EventBaton.prototype.passBatonBackSoft = function(name, func, context, args) { 58 | try { 59 | return this.passBatonBack(name, func, context, args); 60 | } catch (e) { 61 | } 62 | }; 63 | 64 | EventBaton.prototype.passBatonBack = function(name, func, context, args) { 65 | // this method will call the listener BEFORE the name/func pair. this 66 | // basically allows you to put in shims, where you steal batons but pass 67 | // them back if they don't meet certain conditions 68 | var listeners = this.getListenersThrow(name); 69 | 70 | var indexBefore; 71 | listeners.forEach(function(listenerObj, index) { 72 | // skip the first 73 | if (index === 0) { return; } 74 | if (listenerObj.func === func && listenerObj.context === context) { 75 | indexBefore = index - 1; 76 | } 77 | }); 78 | if (indexBefore === undefined) { 79 | throw new Error('you are the last baton holder! or i didn\'t find you'); 80 | } 81 | var toCallObj = listeners[indexBefore]; 82 | 83 | toCallObj.func.apply(toCallObj.context, args); 84 | }; 85 | 86 | EventBaton.prototype.releaseBaton = function(name, func, context) { 87 | // might be in the middle of the stack, so we have to loop instead of 88 | // just popping blindly 89 | var listeners = this.getListenersThrow(name); 90 | 91 | var newListeners = []; 92 | var found = false; 93 | listeners.forEach(function(listenerObj) { 94 | if (listenerObj.func === func && listenerObj.context === context) { 95 | if (found) { 96 | console.warn('woah duplicates!!!'); 97 | console.log(listeners); 98 | } 99 | found = true; 100 | } else { 101 | newListeners.push(listenerObj); 102 | } 103 | }); 104 | 105 | if (!found) { 106 | console.log('did not find that function', func, context, name, arguments); 107 | console.log(this.eventMap); 108 | throw new Error('can\'t releasebaton if you don\'t have it'); 109 | } 110 | this.eventMap[name] = newListeners; 111 | }; 112 | 113 | exports.EventBaton = EventBaton; 114 | -------------------------------------------------------------------------------- /src/js/util/index.js: -------------------------------------------------------------------------------- 1 | var { readdirSync, lstatSync } = require('fs'); 2 | var { join } = require('path'); 3 | 4 | var escapeString = require('../util/escapeString'); 5 | var constants = require('../util/constants'); 6 | 7 | exports.parseQueryString = function(uri) { 8 | // from http://stevenbenner.com/2010/03/javascript-regex-trick-parse-a-query-string-into-an-object/ 9 | var params = {}; 10 | uri.replace( 11 | new RegExp("([^?=&]+)(=([^&]*))?", "g"), 12 | function($0, $1, $2, $3) { params[$1] = $3; } 13 | ); 14 | return params; 15 | }; 16 | 17 | exports.isBrowser = function() { 18 | var inBrowser = String(typeof window) !== 'undefined'; 19 | return inBrowser; 20 | }; 21 | 22 | exports.splitTextCommand = function(value, func, context) { 23 | func = func.bind(context); 24 | value.split(';').forEach(function(command, index) { 25 | command = escapeString(command); 26 | command = command 27 | .replace(/^(\s+)/, '') 28 | .replace(/(\s+)$/, '') 29 | .replace(/"/g, '"') 30 | .replace(/'/g, "'") 31 | .replace(///g, "/"); 32 | 33 | if (index > 0 && !command.length) { 34 | return; 35 | } 36 | func(command); 37 | }); 38 | }; 39 | 40 | exports.genParseCommand = function(regexMap, eventName) { 41 | return function(str) { 42 | var method; 43 | var regexResults; 44 | 45 | Object.keys(regexMap).forEach(function(_method) { 46 | var results = regexMap[_method].exec(str); 47 | if (results) { 48 | method = _method; 49 | regexResults = results; 50 | } 51 | }); 52 | 53 | return (!method) ? false : { 54 | toSet: { 55 | eventName: eventName, 56 | method: method, 57 | regexResults: regexResults 58 | } 59 | }; 60 | }; 61 | }; 62 | 63 | exports.readDirDeep = function(dir) { 64 | var paths = []; 65 | readdirSync(dir).forEach(function(path) { 66 | var aPath = join(dir, path); 67 | if (lstatSync(aPath).isDirectory()) { 68 | paths.push(...exports.readDirDeep(aPath)); 69 | } else { 70 | paths.push(aPath); 71 | } 72 | }); 73 | return paths; 74 | }; 75 | -------------------------------------------------------------------------------- /src/js/util/keyMirror.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Our own flavor of keyMirror since I get some weird 5 | * obscure error when trying to import the react lib one. 6 | */ 7 | var keyMirror = function(obj) { 8 | var result = {}; 9 | for (var key in obj) { 10 | if (!obj.hasOwnProperty(key)) { 11 | continue; 12 | } 13 | result[key] = key; 14 | } 15 | return result; 16 | }; 17 | 18 | module.exports = keyMirror; 19 | -------------------------------------------------------------------------------- /src/js/util/keyboard.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | 3 | var Main = require('../app'); 4 | 5 | var mapKeycodeToKey = function(keycode) { 6 | // HELP WANTED -- internationalize? Dvorak? I have no idea 7 | var keyMap = { 8 | 37: 'left', 9 | 38: 'up', 10 | 39: 'right', 11 | 40: 'down', 12 | 27: 'esc', 13 | 13: 'enter' 14 | }; 15 | return keyMap[keycode]; 16 | }; 17 | 18 | function KeyboardListener(options) { 19 | this.events = options.events; 20 | this.aliasMap = options.aliasMap || {}; 21 | 22 | if (!options.wait) { 23 | this.listen(); 24 | } 25 | } 26 | 27 | KeyboardListener.prototype.listen = function() { 28 | if (this.listening) { 29 | return; 30 | } 31 | this.listening = true; 32 | Main.getEventBaton().stealBaton('docKeydown', this.keydown, this); 33 | Main.getEventBaton().stealBaton('onCloseButtonClick', this.onCloseButtonClick, this); 34 | 35 | }; 36 | 37 | KeyboardListener.prototype.mute = function() { 38 | this.listening = false; 39 | Main.getEventBaton().releaseBaton('docKeydown', this.keydown, this); 40 | Main.getEventBaton().releaseBaton('onCloseButtonClick', this.onCloseButtonClick, this); 41 | }; 42 | 43 | KeyboardListener.prototype.onCloseButtonClick = function(e) { 44 | this.fireEvent('esc', e); 45 | }; 46 | 47 | KeyboardListener.prototype.keydown = function(e) { 48 | var which = e.which || e.keyCode; 49 | 50 | var key = mapKeycodeToKey(which); 51 | if (key === undefined) { 52 | return; 53 | } 54 | 55 | this.fireEvent(key, e); 56 | }; 57 | 58 | KeyboardListener.prototype.fireEvent = function(eventName, e) { 59 | eventName = this.aliasMap[eventName] || eventName; 60 | this.events.trigger(eventName, e); 61 | }; 62 | 63 | KeyboardListener.prototype.passEventBack = function(e) { 64 | Main.getEventBaton().passBatonBackSoft('docKeydown', this.keydown, this, [e]); 65 | }; 66 | 67 | exports.KeyboardListener = KeyboardListener; 68 | exports.mapKeycodeToKey = mapKeycodeToKey; 69 | -------------------------------------------------------------------------------- /src/js/util/mock.js: -------------------------------------------------------------------------------- 1 | exports.mock = function(Constructor) { 2 | var dummy = {}; 3 | var stub = function() {}; 4 | 5 | for (var key in Constructor.prototype) { 6 | dummy[key] = stub; 7 | } 8 | return dummy; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/js/util/reactUtil.js: -------------------------------------------------------------------------------- 1 | var joinClasses = function(classes) { 2 | return classes.join(' '); 3 | }; 4 | 5 | exports.joinClasses = joinClasses; 6 | -------------------------------------------------------------------------------- /src/js/util/throttle.js: -------------------------------------------------------------------------------- 1 | module.exports = function(func, time) { 2 | var wait = false; 3 | return function() { 4 | if (!wait) { 5 | func.apply(this, arguments); 6 | wait = true; 7 | 8 | setTimeout(function() { 9 | wait = false; 10 | }, time); 11 | } 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/js/util/zoomLevel.js: -------------------------------------------------------------------------------- 1 | var _warnOnce = true; 2 | function detectZoom() { 3 | /** 4 | * Note: this method has only been tested on Chrome 5 | * but seems to work. A much more elaborate library is available here: 6 | * https://github.com/yonran/detect-zoom 7 | * but seems to return a "2" zoom level for my computer (who knows) 8 | * so I can't use it. The ecosystem for zoom level detection is a mess 9 | */ 10 | if (!window.outerWidth || !window.innerWidth) { 11 | if (_warnOnce) { 12 | console.warn("Can't detect zoom level correctly :-/"); 13 | _warnOnce = false; 14 | } 15 | return 1; 16 | } 17 | 18 | return window.outerWidth / window.innerWidth; 19 | } 20 | 21 | exports.detectZoom = detectZoom; 22 | 23 | -------------------------------------------------------------------------------- /src/js/views/gitDemonstrationView.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var Q = require('q'); 3 | var Backbone = require('backbone'); 4 | var { marked } = require('marked'); 5 | 6 | var util = require('../util'); 7 | var intl = require('../intl'); 8 | var KeyboardListener = require('../util/keyboard').KeyboardListener; 9 | var Command = require('../models/commandModel').Command; 10 | 11 | var ModalTerminal = require('../views').ModalTerminal; 12 | var ContainedBase = require('../views').ContainedBase; 13 | 14 | var Visualization = require('../visuals/visualization').Visualization; 15 | var HeadlessGit = require('../git/headless'); 16 | 17 | var GitDemonstrationView = ContainedBase.extend({ 18 | tagName: 'div', 19 | className: 'gitDemonstrationView box horizontal', 20 | template: _.template($('#git-demonstration-view').html()), 21 | 22 | events: { 23 | 'click div.command > p.uiButton:not([target="reset"])': 'positive', 24 | 'click div.command > p[target="reset"]': 'onResetButtonClick', 25 | }, 26 | 27 | initialize: function(options) { 28 | options = options || {}; 29 | this.options = options; 30 | this.JSON = Object.assign( 31 | { 32 | beforeMarkdowns: [ 33 | '## Git Commits', 34 | '', 35 | 'Awesome!' 36 | ], 37 | command: 'git commit', 38 | afterMarkdowns: [ 39 | 'Now you have seen it in action', 40 | '', 41 | 'Go ahead and try the level!' 42 | ] 43 | }, 44 | options 45 | ); 46 | 47 | var convert = function(markdowns) { 48 | return marked(markdowns.join('\n')); 49 | }; 50 | 51 | this.JSON.beforeHTML = convert(this.JSON.beforeMarkdowns); 52 | this.JSON.afterHTML = convert(this.JSON.afterMarkdowns); 53 | 54 | this.container = new ModalTerminal({ 55 | title: options.title || intl.str('git-demonstration-title') 56 | }); 57 | this.render(); 58 | this.checkScroll(); 59 | 60 | this.navEvents = Object.assign({}, Backbone.Events); 61 | this.navEvents.on('positive', this.positive, this); 62 | this.navEvents.on('negative', this.negative, this); 63 | this.navEvents.on('exit', this.exit, this); 64 | this.navEvents.on('onResetButtonClick', this.onResetButtonClick, this); 65 | this.keyboardListener = new KeyboardListener({ 66 | events: this.navEvents, 67 | aliasMap: { 68 | enter: 'positive', 69 | right: 'positive', 70 | left: 'negative' 71 | }, 72 | wait: true 73 | }); 74 | 75 | this.visFinished = false; 76 | this.initVis(); 77 | 78 | if (!options.wait) { 79 | this.show(); 80 | } 81 | }, 82 | 83 | exit: function() { 84 | alert('exittt'); 85 | }, 86 | 87 | receiveMetaNav: function(navView, metaContainerView) { 88 | var _this = this; 89 | navView.navEvents.on('positive', this.positive, this); 90 | navView.navEvents.on('exit', this.exit, this); 91 | this.metaContainerView = metaContainerView; 92 | }, 93 | 94 | checkScroll: function() { 95 | var children = this.$('div.demonstrationText').children().toArray(); 96 | var heights = children.map(function(child) { return child.clientHeight; }); 97 | var totalHeight = heights.reduce(function(a, b) { return a + b; }); 98 | if (totalHeight < this.$('div.demonstrationText').height()) { 99 | this.$('div.demonstrationText').addClass('noLongText'); 100 | } 101 | }, 102 | 103 | dispatchBeforeCommand: function() { 104 | if (!this.options.beforeCommand) { 105 | return; 106 | } 107 | 108 | var whenHaveTree = Q.defer(); 109 | HeadlessGit.getTreeQuick(this.options.beforeCommand, whenHaveTree); 110 | whenHaveTree.promise.then(function(tree) { 111 | this.mainVis.gitEngine.loadTree(tree); 112 | this.mainVis.gitVisuals.refreshTreeHarsh(); 113 | }.bind(this)); 114 | }, 115 | 116 | takeControl: function() { 117 | this.hasControl = true; 118 | this.keyboardListener.listen(); 119 | 120 | if (this.metaContainerView) { this.metaContainerView.lock(); } 121 | }, 122 | 123 | releaseControl: function() { 124 | if (!this.hasControl) { return; } 125 | this.hasControl = false; 126 | this.keyboardListener.mute(); 127 | 128 | if (this.metaContainerView) { this.metaContainerView.unlock(); } 129 | }, 130 | 131 | reset: function() { 132 | this.mainVis.reset(); 133 | this.dispatchBeforeCommand(); 134 | this.demonstrated = false; 135 | this.$el.toggleClass('demonstrated', false); 136 | this.$el.toggleClass('demonstrating', false); 137 | }, 138 | 139 | positive: function() { 140 | if (this.demonstrated || !this.hasControl) { 141 | // don't do anything if we are demonstrating, and if 142 | // we receive a meta nav event and we aren't listening, 143 | // then don't do anything either 144 | return; 145 | } 146 | this.demonstrated = true; 147 | this.demonstrate(); 148 | }, 149 | 150 | onResetButtonClick: function() { 151 | this.takeControl(); 152 | this.reset(); 153 | }, 154 | 155 | demonstrate: function() { 156 | this.$el.toggleClass('demonstrating', true); 157 | 158 | var whenDone = Q.defer(); 159 | this.dispatchCommand(this.JSON.command, whenDone); 160 | whenDone.promise.then(function() { 161 | this.$el.toggleClass('demonstrating', false); 162 | this.$el.toggleClass('demonstrated', true); 163 | this.releaseControl(); 164 | }.bind(this)); 165 | }, 166 | 167 | negative: function(e) { 168 | if (this.$el.hasClass('demonstrating')) { 169 | return; 170 | } 171 | this.keyboardListener.passEventBack(e); 172 | }, 173 | 174 | dispatchCommand: function(value, whenDone) { 175 | var commands = []; 176 | util.splitTextCommand(value, function(commandStr) { 177 | commands.push(new Command({ 178 | rawStr: commandStr 179 | })); 180 | }, this); 181 | 182 | var chainDeferred = Q.defer(); 183 | var chainPromise = chainDeferred.promise; 184 | 185 | commands.forEach(function(command, index) { 186 | chainPromise = chainPromise.then(function() { 187 | var myDefer = Q.defer(); 188 | this.mainVis.gitEngine.dispatch(command, myDefer); 189 | return myDefer.promise; 190 | }.bind(this)); 191 | chainPromise = chainPromise.then(function() { 192 | return Q.delay(300); 193 | }); 194 | }, this); 195 | 196 | chainPromise = chainPromise.then(function() { 197 | whenDone.resolve(); 198 | }); 199 | 200 | chainDeferred.resolve(); 201 | }, 202 | 203 | tearDown: function() { 204 | this.mainVis.tearDown(); 205 | GitDemonstrationView.__super__.tearDown.apply(this); 206 | }, 207 | 208 | hide: function() { 209 | this.releaseControl(); 210 | this.reset(); 211 | if (this.visFinished) { 212 | this.mainVis.setTreeIndex(-1); 213 | this.mainVis.setTreeOpacity(0); 214 | } 215 | 216 | this.shown = false; 217 | GitDemonstrationView.__super__.hide.apply(this); 218 | }, 219 | 220 | show: function() { 221 | this.takeControl(); 222 | if (this.visFinished) { 223 | setTimeout(function() { 224 | if (this.shown) { 225 | this.mainVis.setTreeIndex(300); 226 | this.mainVis.showHarsh(); 227 | } 228 | }.bind(this), this.getAnimationTime() * 1.5); 229 | } 230 | 231 | this.shown = true; 232 | GitDemonstrationView.__super__.show.apply(this); 233 | }, 234 | 235 | die: function() { 236 | if (!this.visFinished) { return; } 237 | 238 | GitDemonstrationView.__super__.die.apply(this); 239 | }, 240 | 241 | initVis: function() { 242 | this.mainVis = new Visualization({ 243 | el: this.$('div.visHolder div.visHolderInside')[0], 244 | noKeyboardInput: true, 245 | noClick: true, 246 | smallCanvas: true, 247 | zIndex: -1 248 | }); 249 | this.mainVis.customEvents.on('paperReady', function() { 250 | this.visFinished = true; 251 | this.dispatchBeforeCommand(); 252 | if (this.shown) { 253 | // show the canvas once its done if we are shown 254 | this.show(); 255 | } 256 | }.bind(this)); 257 | } 258 | }); 259 | 260 | exports.GitDemonstrationView = GitDemonstrationView; 261 | -------------------------------------------------------------------------------- /src/js/views/multiView.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var Backbone = require('backbone'); 3 | 4 | var LeftRightView = require('../views').LeftRightView; 5 | var ModalAlert = require('../views').ModalAlert; 6 | var GitDemonstrationView = require('../views/gitDemonstrationView').GitDemonstrationView; 7 | 8 | var BuilderViews = require('../views/builderViews'); 9 | var MarkdownPresenter = BuilderViews.MarkdownPresenter; 10 | 11 | var KeyboardListener = require('../util/keyboard').KeyboardListener; 12 | var debounce = require('../util/debounce'); 13 | 14 | var MultiView = Backbone.View.extend({ 15 | tagName: 'div', 16 | className: 'multiView', 17 | // ms to debounce the nav functions 18 | navEventDebounce: 550, 19 | deathTime: 700, 20 | 21 | // a simple mapping of what childViews we support 22 | typeToConstructor: { 23 | ModalAlert: ModalAlert, 24 | GitDemonstrationView: GitDemonstrationView, 25 | MarkdownPresenter: MarkdownPresenter 26 | }, 27 | 28 | initialize: function(options) { 29 | options = options || {}; 30 | this.childViewJSONs = options.childViews || [{ 31 | type: 'ModalAlert', 32 | options: { 33 | markdown: 'Woah wtf!!' 34 | } 35 | }, { 36 | type: 'GitDemonstrationView', 37 | options: { 38 | command: 'git checkout -b side; git commit; git commit' 39 | } 40 | }, { 41 | type: 'ModalAlert', 42 | options: { 43 | markdown: 'Im second' 44 | } 45 | }]; 46 | this.deferred = options.deferred || Q.defer(); 47 | 48 | this.childViews = []; 49 | this.currentIndex = 0; 50 | 51 | this.navEvents = Object.assign({}, Backbone.Events); 52 | this.navEvents.on('negative', this.getNegFunc(), this); 53 | this.navEvents.on('positive', this.getPosFunc(), this); 54 | this.navEvents.on('quit', this.finish, this); 55 | this.navEvents.on('exit', this.finish, this); 56 | 57 | this.keyboardListener = new KeyboardListener({ 58 | events: this.navEvents, 59 | aliasMap: { 60 | left: 'negative', 61 | right: 'positive', 62 | enter: 'positive', 63 | esc: 'quit' 64 | } 65 | }); 66 | 67 | this.render(); 68 | if (!options.wait) { 69 | this.start(); 70 | } 71 | }, 72 | 73 | onWindowFocus: function() { 74 | // nothing here for now... 75 | // TODO -- add a cool glow effect? 76 | }, 77 | 78 | getAnimationTime: function() { 79 | return 700; 80 | }, 81 | 82 | getPromise: function() { 83 | return this.deferred.promise; 84 | }, 85 | 86 | getPosFunc: function() { 87 | return debounce(function() { 88 | this.navForward(); 89 | }.bind(this), this.navEventDebounce, true); 90 | }, 91 | 92 | getNegFunc: function() { 93 | return debounce(function() { 94 | this.navBackward(); 95 | }.bind(this), this.navEventDebounce, true); 96 | }, 97 | 98 | lock: function() { 99 | this.locked = true; 100 | }, 101 | 102 | unlock: function() { 103 | this.locked = false; 104 | }, 105 | 106 | navForward: function() { 107 | // we need to prevent nav changes when a git demonstration view hasnt finished 108 | if (this.locked) { return; } 109 | if (this.currentIndex === this.childViews.length - 1) { 110 | this.hideViewIndex(this.currentIndex); 111 | this.finish(); 112 | return; 113 | } 114 | 115 | this.navIndexChange(1); 116 | }, 117 | 118 | navBackward: function() { 119 | if (this.currentIndex === 0) { 120 | return; 121 | } 122 | 123 | this.navIndexChange(-1); 124 | }, 125 | 126 | navIndexChange: function(delta) { 127 | this.hideViewIndex(this.currentIndex); 128 | this.currentIndex += delta; 129 | this.showViewIndex(this.currentIndex); 130 | }, 131 | 132 | hideViewIndex: function(index) { 133 | this.childViews[index].hide(); 134 | }, 135 | 136 | showViewIndex: function(index) { 137 | this.childViews[index].show(); 138 | }, 139 | 140 | finish: function() { 141 | // first we stop listening to keyboard and give that back to UI, which 142 | // other views will take if they need to 143 | this.keyboardListener.mute(); 144 | 145 | this.childViews.forEach(function(childView) { 146 | childView.die(); 147 | }); 148 | 149 | this.deferred.resolve(); 150 | }, 151 | 152 | start: function() { 153 | // steal the window focus baton 154 | this.showViewIndex(this.currentIndex); 155 | }, 156 | 157 | createChildView: function(viewJSON) { 158 | var type = viewJSON.type; 159 | if (!this.typeToConstructor[type]) { 160 | throw new Error('no constructor for type "' + type + '"'); 161 | } 162 | var view = new this.typeToConstructor[type](Object.assign( 163 | {}, 164 | viewJSON.options, 165 | { wait: true } 166 | )); 167 | return view; 168 | }, 169 | 170 | addNavToView: function(view, index) { 171 | var leftRight = new LeftRightView({ 172 | events: this.navEvents, 173 | // we want the arrows to be on the same level as the content (not 174 | // beneath), so we go one level up with getDestination() 175 | destination: view.getDestination(), 176 | showLeft: (index !== 0), 177 | lastNav: (index === this.childViewJSONs.length - 1) 178 | }); 179 | if (view.receiveMetaNav) { 180 | view.receiveMetaNav(leftRight, this); 181 | } 182 | }, 183 | 184 | render: function() { 185 | // go through each and render... show the first 186 | this.childViewJSONs.forEach(function(childViewJSON, index) { 187 | var childView = this.createChildView(childViewJSON); 188 | this.childViews.push(childView); 189 | this.addNavToView(childView, index); 190 | }, this); 191 | } 192 | }); 193 | 194 | exports.MultiView = MultiView; 195 | -------------------------------------------------------------------------------- /src/js/views/rebaseView.js: -------------------------------------------------------------------------------- 1 | var GitError = require('../util/errors').GitError; 2 | var _ = require('underscore'); 3 | var Q = require('q'); 4 | var Backbone = require('backbone'); 5 | 6 | var ModalTerminal = require('../views').ModalTerminal; 7 | var ContainedBase = require('../views').ContainedBase; 8 | var ConfirmCancelView = require('../views').ConfirmCancelView; 9 | 10 | var intl = require('../intl'); 11 | 12 | require('jquery-ui/ui/widget'); 13 | require('jquery-ui/ui/scroll-parent'); 14 | require('jquery-ui/ui/data'); 15 | require('jquery-ui/ui/widgets/mouse'); 16 | require('jquery-ui/ui/ie'); 17 | require('jquery-ui/ui/widgets/sortable'); 18 | require('jquery-ui/ui/plugin'); 19 | require('jquery-ui/ui/safe-active-element'); 20 | require('jquery-ui/ui/safe-blur'); 21 | require('jquery-ui/ui/widgets/draggable'); 22 | 23 | var InteractiveRebaseView = ContainedBase.extend({ 24 | tagName: 'div', 25 | template: _.template($('#interactive-rebase-template').html()), 26 | 27 | initialize: function(options) { 28 | this.deferred = options.deferred; 29 | this.rebaseMap = {}; 30 | this.entryObjMap = {}; 31 | this.options = options; 32 | 33 | this.rebaseEntries = new RebaseEntryCollection(); 34 | options.toRebase.reverse(); 35 | options.toRebase.forEach(function(commit) { 36 | var id = commit.get('id'); 37 | this.rebaseMap[id] = commit; 38 | 39 | // make basic models for each commit 40 | this.entryObjMap[id] = new RebaseEntry({ 41 | id: id 42 | }); 43 | this.rebaseEntries.add(this.entryObjMap[id]); 44 | }, this); 45 | 46 | this.container = new ModalTerminal({ 47 | title: intl.str('interactive-rebase-title') 48 | }); 49 | this.render(); 50 | 51 | // show the dialog holder 52 | this.show(); 53 | 54 | if (options.aboveAll) { 55 | // TODO fix this :( 56 | $('#canvasHolder').css('display', 'none'); 57 | } 58 | }, 59 | 60 | restoreVis: function() { 61 | // restore the absolute position canvases 62 | $('#canvasHolder').css('display', 'inherit'); 63 | }, 64 | 65 | confirm: function() { 66 | this.die(); 67 | if (this.options.aboveAll) { 68 | this.restoreVis(); 69 | } 70 | 71 | // get our ordering 72 | var uiOrder = []; 73 | this.$('ul.rebaseEntries li').each(function(i, obj) { 74 | uiOrder.push(obj.id); 75 | }); 76 | 77 | // now get the real array 78 | var toRebase = []; 79 | uiOrder.forEach(function(id) { 80 | // the model pick check 81 | if (this.entryObjMap[id].get('pick')) { 82 | toRebase.unshift(this.rebaseMap[id]); 83 | } 84 | }, this); 85 | toRebase.reverse(); 86 | 87 | this.deferred.resolve(toRebase); 88 | // garbage collection will get us 89 | this.$el.html(''); 90 | }, 91 | 92 | render: function() { 93 | var json = { 94 | num: Object.keys(this.rebaseMap).length, 95 | solutionOrder: this.options.initialCommitOrdering 96 | }; 97 | 98 | var destination = this.container.getInsideElement(); 99 | this.$el.html(this.template(json)); 100 | $(destination).append(this.el); 101 | 102 | // also render each entry 103 | var listHolder = this.$('ul.rebaseEntries'); 104 | this.rebaseEntries.each(function(entry) { 105 | new RebaseEntryView({ 106 | el: listHolder, 107 | model: entry 108 | }); 109 | }, this); 110 | 111 | // then make it reorderable.. 112 | listHolder.sortable({ 113 | axis: 'y', 114 | placeholder: 'rebaseEntry transitionOpacity ui-state-highlight', 115 | appendTo: 'parent' 116 | }); 117 | 118 | this.makeButtons(); 119 | }, 120 | 121 | cancel: function() { 122 | // empty array does nothing, just like in git 123 | this.hide(); 124 | if (this.options.aboveAll) { 125 | this.restoreVis(); 126 | } 127 | this.deferred.resolve([]); 128 | }, 129 | 130 | makeButtons: function() { 131 | // control for button 132 | var deferred = Q.defer(); 133 | deferred.promise 134 | .then(function() { 135 | this.confirm(); 136 | }.bind(this)) 137 | .fail(function() { 138 | this.cancel(); 139 | }.bind(this)) 140 | .done(); 141 | 142 | // finally get our buttons 143 | new ConfirmCancelView({ 144 | destination: this.$('.confirmCancel'), 145 | deferred: deferred, 146 | disableCancelButton: !!this.options.aboveAll, 147 | }); 148 | } 149 | }); 150 | 151 | var RebaseEntry = Backbone.Model.extend({ 152 | defaults: { 153 | pick: true 154 | }, 155 | 156 | toggle: function() { 157 | this.set('pick', !this.get('pick')); 158 | } 159 | }); 160 | 161 | var RebaseEntryCollection = Backbone.Collection.extend({ 162 | model: RebaseEntry 163 | }); 164 | 165 | var RebaseEntryView = Backbone.View.extend({ 166 | tagName: 'li', 167 | template: _.template($('#interactive-rebase-entry-template').html()), 168 | 169 | toggle: function() { 170 | this.model.toggle(); 171 | 172 | // toggle a class also 173 | this.listEntry.toggleClass('notPicked', !this.model.get('pick')); 174 | }, 175 | 176 | initialize: function(options) { 177 | this.render(); 178 | }, 179 | 180 | render: function() { 181 | this.$el.append(this.template(this.model.toJSON())); 182 | 183 | // hacky :( who would have known jquery barfs on ids with %'s and quotes 184 | this.listEntry = this.$el.children(':last'); 185 | 186 | this.listEntry.delegate('#toggleButton', 'click', function() { 187 | this.toggle(); 188 | }.bind(this)); 189 | } 190 | }); 191 | 192 | exports.InteractiveRebaseView = InteractiveRebaseView; 193 | -------------------------------------------------------------------------------- /src/js/visuals/animation/animationFactory.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var Q = require('q'); 3 | 4 | var Animation = require('./index').Animation; 5 | var PromiseAnimation = require('./index').PromiseAnimation; 6 | var GRAPHICS = require('../../util/constants').GRAPHICS; 7 | 8 | /****************** 9 | * This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time. 10 | * The tricky thing is that when a new commit has to be "born," say in the middle of a rebase 11 | * or something, it must animate out from the parent position to it's birth position. 12 | 13 | * These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree, 14 | * store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots. 15 | * and then essentially animate the entire tree too. 16 | */ 17 | 18 | // static class 19 | var AnimationFactory = {}; 20 | 21 | var makeCommitBirthAnimation = function(gitVisuals, visNode) { 22 | var time = GRAPHICS.defaultAnimationTime * 1.0; 23 | var bounceTime = time * 2; 24 | 25 | var animation = function() { 26 | // essentially refresh the entire tree, but do a special thing for the commit 27 | gitVisuals.refreshTree(time); 28 | 29 | visNode.setBirth(); 30 | visNode.parentInFront(); 31 | gitVisuals.visBranchesFront(); 32 | 33 | visNode.animateUpdatedPosition(bounceTime, 'bounce'); 34 | visNode.animateOutgoingEdges(time); 35 | }; 36 | return { 37 | animation: animation, 38 | duration: Math.max(time, bounceTime) 39 | }; 40 | }; 41 | 42 | var makeHighlightAnimation = function(visNode, visBranch) { 43 | var fullTime = GRAPHICS.defaultAnimationTime * 0.66; 44 | var slowTime = fullTime * 2.0; 45 | 46 | return { 47 | animation: function() { 48 | visNode.highlightTo(visBranch, slowTime, 'easeInOut'); 49 | }, 50 | duration: slowTime * 1.5 51 | }; 52 | }; 53 | 54 | AnimationFactory.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) { 55 | if (!animationQueue) { 56 | throw new Error("Need animation queue to add closure to!"); 57 | } 58 | 59 | var visNode = commit.get('visNode'); 60 | var anPack = makeCommitBirthAnimation(gitVisuals, visNode); 61 | 62 | animationQueue.add(new Animation({ 63 | closure: anPack.animation, 64 | duration: anPack.duration 65 | })); 66 | }; 67 | 68 | AnimationFactory.genCommitBirthPromiseAnimation = function(commit, gitVisuals) { 69 | var visNode = commit.get('visNode'); 70 | return new PromiseAnimation(makeCommitBirthAnimation(gitVisuals, visNode)); 71 | }; 72 | 73 | AnimationFactory.highlightEachWithPromise = function( 74 | chain, 75 | toHighlight, 76 | destObj 77 | ) { 78 | toHighlight.forEach(function(commit) { 79 | chain = chain.then(function() { 80 | return this.playHighlightPromiseAnimation( 81 | commit, 82 | destObj 83 | ); 84 | }.bind(this)); 85 | }.bind(this)); 86 | return chain; 87 | }; 88 | 89 | AnimationFactory.playCommitBirthPromiseAnimation = function(commit, gitVisuals) { 90 | var animation = this.genCommitBirthPromiseAnimation(commit, gitVisuals); 91 | animation.play(); 92 | return animation.getPromise(); 93 | }; 94 | 95 | AnimationFactory.playRefreshAnimationAndFinish = function(gitVisuals, animationQueue) { 96 | var animation = new PromiseAnimation({ 97 | closure: function() { 98 | gitVisuals.refreshTree(); 99 | } 100 | }); 101 | animation.play(); 102 | animationQueue.thenFinish(animation.getPromise()); 103 | }; 104 | 105 | AnimationFactory.genRefreshPromiseAnimation = function(gitVisuals) { 106 | return new PromiseAnimation({ 107 | closure: function() { 108 | gitVisuals.refreshTree(); 109 | } 110 | }); 111 | }; 112 | 113 | AnimationFactory.playRefreshAnimationSlow = function(gitVisuals) { 114 | var time = GRAPHICS.defaultAnimationTime; 115 | return this.playRefreshAnimation(gitVisuals, time * 2); 116 | }; 117 | 118 | AnimationFactory.playRefreshAnimation = function(gitVisuals, speed) { 119 | var animation = new PromiseAnimation({ 120 | duration: speed, 121 | closure: function() { 122 | gitVisuals.refreshTree(speed); 123 | } 124 | }); 125 | animation.play(); 126 | return animation.getPromise(); 127 | }; 128 | 129 | AnimationFactory.refreshTree = function(animationQueue, gitVisuals) { 130 | animationQueue.add(new Animation({ 131 | closure: function() { 132 | gitVisuals.refreshTree(); 133 | } 134 | })); 135 | }; 136 | 137 | AnimationFactory.genHighlightPromiseAnimation = function(commit, destObj) { 138 | // could be branch or node 139 | var visObj = destObj.get('visBranch') || destObj.get('visNode') || 140 | destObj.get('visTag'); 141 | if (!visObj) { 142 | console.log(destObj); 143 | throw new Error('could not find vis object for dest obj'); 144 | } 145 | var visNode = commit.get('visNode'); 146 | return new PromiseAnimation(makeHighlightAnimation(visNode, visObj)); 147 | }; 148 | 149 | AnimationFactory.playHighlightPromiseAnimation = function(commit, destObj) { 150 | var animation = this.genHighlightPromiseAnimation(commit, destObj); 151 | animation.play(); 152 | return animation.getPromise(); 153 | }; 154 | 155 | AnimationFactory.getDelayedPromise = function(amount) { 156 | var deferred = Q.defer(); 157 | setTimeout(deferred.resolve, amount || 1000); 158 | return deferred.promise; 159 | }; 160 | 161 | AnimationFactory.delay = function(animationQueue, time) { 162 | time = time || GRAPHICS.defaultAnimationTime; 163 | animationQueue.add(new Animation({ 164 | closure: function() { }, 165 | duration: time 166 | })); 167 | }; 168 | 169 | exports.AnimationFactory = AnimationFactory; 170 | 171 | -------------------------------------------------------------------------------- /src/js/visuals/animation/index.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var Backbone = require('backbone'); 3 | var GlobalStateActions = require('../../actions/GlobalStateActions'); 4 | var GRAPHICS = require('../../util/constants').GRAPHICS; 5 | 6 | var Animation = Backbone.Model.extend({ 7 | defaults: { 8 | duration: GRAPHICS.defaultAnimationTime, 9 | closure: null 10 | }, 11 | 12 | validateAtInit: function() { 13 | if (!this.get('closure')) { 14 | throw new Error('give me a closure!'); 15 | } 16 | }, 17 | 18 | initialize: function(options) { 19 | this.validateAtInit(); 20 | }, 21 | 22 | run: function() { 23 | this.get('closure')(); 24 | } 25 | }); 26 | 27 | var AnimationQueue = Backbone.Model.extend({ 28 | defaults: { 29 | animations: null, 30 | index: 0, 31 | callback: null, 32 | defer: false, 33 | promiseBased: false 34 | }, 35 | 36 | initialize: function(options) { 37 | this.set('animations', []); 38 | if (!options.callback) { 39 | console.warn('no callback'); 40 | } 41 | }, 42 | 43 | thenFinish: function(promise, deferred) { 44 | promise.then(function() { 45 | this.finish(); 46 | }.bind(this)); 47 | promise.fail(function(e) { 48 | console.log('uncaught error', e); 49 | throw e; 50 | }); 51 | this.set('promiseBased', true); 52 | if (deferred) { 53 | deferred.resolve(); 54 | } 55 | }, 56 | 57 | add: function(animation) { 58 | if (!(animation instanceof Animation)) { 59 | throw new Error("Need animation not something else"); 60 | } 61 | 62 | this.get('animations').push(animation); 63 | }, 64 | 65 | start: function() { 66 | this.set('index', 0); 67 | 68 | // set the global lock that we are animating 69 | GlobalStateActions.changeIsAnimating(true); 70 | this.next(); 71 | }, 72 | 73 | finish: function() { 74 | // release lock here 75 | GlobalStateActions.changeIsAnimating(false); 76 | this.get('callback')(); 77 | }, 78 | 79 | next: function() { 80 | // ok so call the first animation, and then set a timeout to call the next. 81 | // since an animation is defined as taking a specific amount of time, 82 | // we can simply just use timeouts rather than promises / deferreds. 83 | 84 | // for graphical displays that require an unknown amount of time, use deferreds 85 | // but not animation queue (see the finishAnimation for that) 86 | var animations = this.get('animations'); 87 | var index = this.get('index'); 88 | if (index >= animations.length) { 89 | this.finish(); 90 | return; 91 | } 92 | 93 | var next = animations[index]; 94 | var duration = next.get('duration'); 95 | 96 | next.run(); 97 | 98 | this.set('index', index + 1); 99 | setTimeout(function() { 100 | this.next(); 101 | }.bind(this), duration); 102 | } 103 | }); 104 | 105 | var PromiseAnimation = Backbone.Model.extend({ 106 | defaults: { 107 | deferred: null, 108 | closure: null, 109 | duration: GRAPHICS.defaultAnimationTime 110 | }, 111 | 112 | initialize: function(options) { 113 | if (!options.closure && !options.animation) { 114 | throw new Error('need closure or animation'); 115 | } 116 | this.set('closure', options.closure || options.animation); 117 | this.set('duration', options.duration || this.get('duration')); 118 | this.set('deferred', options.deferred || Q.defer()); 119 | }, 120 | 121 | getPromise: function() { 122 | return this.get('deferred').promise; 123 | }, 124 | 125 | play: function() { 126 | // a single animation is just something with a timeout, but now 127 | // we want to resolve a deferred when the animation finishes 128 | this.get('closure')(); 129 | setTimeout(function() { 130 | this.get('deferred').resolve(); 131 | }.bind(this), this.get('duration')); 132 | }, 133 | 134 | then: function(func) { 135 | return this.get('deferred').promise.then(func); 136 | } 137 | }); 138 | 139 | PromiseAnimation.fromAnimation = function(animation) { 140 | return new PromiseAnimation({ 141 | closure: animation.get('closure'), 142 | duration: animation.get('duration') 143 | }); 144 | }; 145 | 146 | exports.Animation = Animation; 147 | exports.PromiseAnimation = PromiseAnimation; 148 | exports.AnimationQueue = AnimationQueue; 149 | -------------------------------------------------------------------------------- /src/js/visuals/tree.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | 3 | var VisBase = Backbone.Model.extend({ 4 | removeKeys: function(keys) { 5 | keys.forEach(function(key) { 6 | if (this.get(key)) { 7 | this.get(key).remove(); 8 | } 9 | }, this); 10 | }, 11 | 12 | animateAttrKeys: function(keys, attrObj, speed, easing) { 13 | // either we animate a specific subset of keys or all 14 | // possible things we could animate 15 | keys = Object.assign( 16 | {}, 17 | { 18 | include: ['circle', 'arrow', 'rect', 'path', 'text'], 19 | exclude: [] 20 | }, 21 | keys || {} 22 | ); 23 | 24 | var attr = this.getAttributes(); 25 | 26 | // safely insert this attribute into all the keys we want 27 | keys.include.forEach(function(key) { 28 | attr[key] = Object.assign( 29 | {}, 30 | attr[key], 31 | attrObj 32 | ); 33 | }); 34 | 35 | keys.exclude.forEach(function(key) { 36 | delete attr[key]; 37 | }); 38 | 39 | this.animateToAttr(attr, speed, easing); 40 | } 41 | }); 42 | 43 | exports.VisBase = VisBase; 44 | -------------------------------------------------------------------------------- /src/js/visuals/visBase.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | 3 | var VisBase = Backbone.Model.extend({ 4 | removeKeys: function(keys) { 5 | keys.forEach(function(key) { 6 | if (this.get(key)) { 7 | this.get(key).remove(); 8 | } 9 | }, this); 10 | }, 11 | 12 | getNonAnimateKeys: function() { 13 | return [ 14 | 'stroke-dasharray' 15 | ]; 16 | }, 17 | 18 | getIsInOrigin: function() { 19 | if (!this.get('gitEngine')) { 20 | return false; 21 | } 22 | return this.get('gitEngine').isOrigin(); 23 | }, 24 | 25 | animateToAttr: function(attr, speed, easing) { 26 | if (speed === 0) { 27 | this.setAttr(attr, /* instant */ true); 28 | return; 29 | } 30 | 31 | var s = speed !== undefined ? speed : this.get('animationSpeed'); 32 | var e = easing || this.get('animationEasing'); 33 | this.setAttr(attr, /* instance */ false, s, e); 34 | }, 35 | 36 | setAttrBase: function(keys, attr, instant, speed, easing) { 37 | keys.forEach(function(key) { 38 | if (instant) { 39 | this.get(key).attr(attr[key]); 40 | } else { 41 | this.get(key).stop(); 42 | this.get(key).animate(attr[key], speed, easing); 43 | // some keys don't support animating too, so set those instantly here 44 | this.getNonAnimateKeys().forEach(function(nonAnimateKey) { 45 | if (attr[key] && attr[key][nonAnimateKey] !== undefined) { 46 | this.get(key).attr(nonAnimateKey, attr[key][nonAnimateKey]); 47 | } 48 | }, this); 49 | } 50 | 51 | if (attr.css) { 52 | $(this.get(key).node).css(attr.css); 53 | } 54 | }, this); 55 | }, 56 | 57 | animateAttrKeys: function(keys, attrObj, speed, easing) { 58 | // either we animate a specific subset of keys or all 59 | // possible things we could animate 60 | keys = Object.assign( 61 | {}, 62 | { 63 | include: ['circle', 'arrow', 'rect', 'path', 'text'], 64 | exclude: [] 65 | }, 66 | keys || {} 67 | ); 68 | 69 | var attr = this.getAttributes(); 70 | 71 | // safely insert this attribute into all the keys we want 72 | keys.include.forEach(function(key) { 73 | attr[key] = Object.assign( 74 | {}, 75 | attr[key], 76 | attrObj 77 | ); 78 | }); 79 | 80 | keys.exclude.forEach(function(key) { 81 | delete attr[key]; 82 | }); 83 | 84 | this.animateToAttr(attr, speed, easing); 85 | } 86 | }); 87 | 88 | exports.VisBase = VisBase; 89 | -------------------------------------------------------------------------------- /src/js/visuals/visEdge.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var GRAPHICS = require('../util/constants').GRAPHICS; 3 | 4 | var VisBase = require('../visuals/visBase').VisBase; 5 | var GlobalStateStore = require('../stores/GlobalStateStore'); 6 | 7 | var VisEdge = VisBase.extend({ 8 | defaults: { 9 | tail: null, 10 | head: null, 11 | animationSpeed: GRAPHICS.defaultAnimationTime, 12 | animationEasing: GRAPHICS.defaultEasing 13 | }, 14 | 15 | validateAtInit: function() { 16 | var required = ['tail', 'head']; 17 | required.forEach(function(key) { 18 | if (!this.get(key)) { 19 | throw new Error(key + ' is required!'); 20 | } 21 | }, this); 22 | }, 23 | 24 | getID: function() { 25 | return this.get('tail').get('id') + '.' + this.get('head').get('id'); 26 | }, 27 | 28 | initialize: function() { 29 | this.validateAtInit(); 30 | 31 | // shorthand for the main objects 32 | this.gitVisuals = this.get('gitVisuals'); 33 | this.gitEngine = this.get('gitEngine'); 34 | 35 | this.get('tail').get('outgoingEdges').push(this); 36 | }, 37 | 38 | remove: function() { 39 | this.removeKeys(['path']); 40 | this.gitVisuals.removeVisEdge(this); 41 | }, 42 | 43 | genSmoothBezierPathString: function(tail, head) { 44 | var tailPos = tail.getScreenCoords(); 45 | var headPos = head.getScreenCoords(); 46 | return this.genSmoothBezierPathStringFromCoords(tailPos, headPos); 47 | }, 48 | 49 | genSmoothBezierPathStringFromCoords: function(tailPos, headPos) { 50 | // we need to generate the path and control points for the bezier. format 51 | // is M(move abs) C (curve to) (control point 1) (control point 2) (final point) 52 | // the control points have to be __below__ to get the curve starting off straight. 53 | 54 | var flipFactor = (GlobalStateStore.getFlipTreeY()) ? -1 : 1; 55 | var coords = function(pos) { 56 | return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); 57 | }; 58 | var offset = function(pos, dir, delta) { 59 | delta = delta || GRAPHICS.curveControlPointOffset; 60 | return { 61 | x: pos.x, 62 | y: pos.y + flipFactor * delta * dir 63 | }; 64 | }; 65 | var offset2d = function(pos, x, y) { 66 | return { 67 | x: pos.x + x, 68 | y: pos.y + flipFactor * y 69 | }; 70 | }; 71 | 72 | // first offset tail and head by radii 73 | tailPos = offset(tailPos, -1, this.get('tail').getRadius()); 74 | headPos = offset(headPos, 1, this.get('head').getRadius() * 1.15); 75 | 76 | var str = ''; 77 | // first move to bottom of tail 78 | str += 'M' + coords(tailPos) + ' '; 79 | // start bezier 80 | str += 'C'; 81 | // then control points above tail and below head 82 | str += coords(offset(tailPos, -1)) + ' '; 83 | str += coords(offset(headPos, 1)) + ' '; 84 | // now finish 85 | str += coords(headPos); 86 | 87 | // arrow head 88 | var delta = GRAPHICS.arrowHeadSize || 10; 89 | str += ' L' + coords(offset2d(headPos, -delta, delta)); 90 | str += ' L' + coords(offset2d(headPos, delta, delta)); 91 | str += ' L' + coords(headPos); 92 | 93 | // then go back, so we can fill correctly 94 | str += 'C'; 95 | str += coords(offset(headPos, 1)) + ' '; 96 | str += coords(offset(tailPos, -1)) + ' '; 97 | str += coords(tailPos); 98 | 99 | return str; 100 | }, 101 | 102 | getBezierCurve: function() { 103 | return this.genSmoothBezierPathString(this.get('tail'), this.get('head')); 104 | }, 105 | 106 | getStrokeColor: function() { 107 | return GRAPHICS.visBranchStrokeColorNone; 108 | }, 109 | 110 | setOpacity: function(opacity) { 111 | opacity = (opacity === undefined) ? 1 : opacity; 112 | 113 | this.get('path').attr({opacity: opacity}); 114 | }, 115 | 116 | genGraphics: function(paper) { 117 | var pathString = this.getBezierCurve(); 118 | 119 | var path = paper.path(pathString).attr({ 120 | 'stroke-width': GRAPHICS.visBranchStrokeWidth, 121 | 'stroke': this.getStrokeColor(), 122 | 'stroke-linecap': 'round', 123 | 'stroke-linejoin': 'round', 124 | 'fill': this.getStrokeColor() 125 | }); 126 | path.toBack(); 127 | this.set('path', path); 128 | }, 129 | 130 | getOpacity: function() { 131 | var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail')); 132 | var map = { 133 | 'branch': 1, 134 | 'tag': 1, 135 | 'head': GRAPHICS.edgeUpstreamHeadOpacity, 136 | 'none': GRAPHICS.edgeUpstreamNoneOpacity 137 | }; 138 | 139 | if (map[stat] === undefined) { throw new Error('bad stat'); } 140 | return map[stat]; 141 | }, 142 | 143 | getAttributes: function() { 144 | var newPath = this.getBezierCurve(); 145 | var opacity = this.getOpacity(); 146 | return { 147 | path: { 148 | path: newPath, 149 | opacity: opacity 150 | } 151 | }; 152 | }, 153 | 154 | animateUpdatedPath: function(speed, easing) { 155 | var attr = this.getAttributes(); 156 | this.animateToAttr(attr, speed, easing); 157 | }, 158 | 159 | animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { 160 | // an animation of 0 is essentially setting the attribute directly 161 | this.animateToAttr(fromAttr, 0); 162 | this.animateToAttr(toAttr, speed, easing); 163 | }, 164 | 165 | animateToAttr: function(attr, speed, easing) { 166 | if (speed === 0) { 167 | this.get('path').attr(attr.path); 168 | return; 169 | } 170 | 171 | this.get('path').toBack(); 172 | this.get('path').stop(); 173 | this.get('path').animate( 174 | attr.path, 175 | speed !== undefined ? speed : this.get('animationSpeed'), 176 | easing || this.get('animationEasing') 177 | ); 178 | } 179 | }); 180 | 181 | var VisEdgeCollection = Backbone.Collection.extend({ 182 | model: VisEdge 183 | }); 184 | 185 | exports.VisEdgeCollection = VisEdgeCollection; 186 | exports.VisEdge = VisEdge; 187 | -------------------------------------------------------------------------------- /src/js/visuals/visualization.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | 3 | var Collections = require('../models/collections'); 4 | var CommitCollection = Collections.CommitCollection; 5 | var BranchCollection = Collections.BranchCollection; 6 | var TagCollection = Collections.TagCollection; 7 | var EventBaton = require('../util/eventBaton').EventBaton; 8 | 9 | var GitVisuals = require('../visuals').GitVisuals; 10 | 11 | var Visualization = Backbone.View.extend({ 12 | initialize: function(options) { 13 | options = options || {}; 14 | this.options = options; 15 | this.customEvents = Object.assign({}, Backbone.Events); 16 | this.containerElement = options.containerElement; 17 | 18 | var _this = this; 19 | // we want to add our canvas somewhere 20 | var container = options.containerElement || $('#canvasHolder')[0]; 21 | new Raphael(container, 200, 200, function() { 22 | // raphael calls with paper as this for some inane reason... 23 | var paper = this; 24 | // use process.nextTick to go from sync to async 25 | process.nextTick(function() { 26 | _this.paperInitialize(paper, options); 27 | }); 28 | }); 29 | }, 30 | 31 | paperInitialize: function(paper, options) { 32 | this.treeString = options.treeString; 33 | this.paper = paper; 34 | 35 | var Main = require('../app'); 36 | // if we don't want to receive keyboard input (directly), 37 | // make a new event baton so git engine steals something that no one 38 | // is broadcasting to 39 | this.eventBaton = (options.noKeyboardInput) ? 40 | new EventBaton({noInput: true}) : 41 | Main.getEventBaton(); 42 | 43 | this.commitCollection = new CommitCollection(); 44 | this.branchCollection = new BranchCollection(); 45 | this.tagCollection = new TagCollection(); 46 | 47 | this.gitVisuals = new GitVisuals({ 48 | commitCollection: this.commitCollection, 49 | branchCollection: this.branchCollection, 50 | tagCollection: this.tagCollection, 51 | paper: this.paper, 52 | noClick: this.options.noClick, 53 | isGoalVis: this.options.isGoalVis, 54 | smallCanvas: this.options.smallCanvas, 55 | visualization: this 56 | }); 57 | 58 | var GitEngine = require('../git').GitEngine; 59 | this.gitEngine = new GitEngine({ 60 | collection: this.commitCollection, 61 | branches: this.branchCollection, 62 | tags: this.tagCollection, 63 | gitVisuals: this.gitVisuals, 64 | eventBaton: this.eventBaton 65 | }); 66 | this.gitEngine.init(); 67 | this.gitVisuals.assignGitEngine(this.gitEngine); 68 | 69 | this.myResize(); 70 | 71 | $(window).on('resize', () => this.myResize()); 72 | 73 | // If the visualization is within a draggable container, we need to update the 74 | // position whenever the container is moved. 75 | this.$el.parents('.ui-draggable').on('drag', function(event, ui) { 76 | this.customEvents.trigger('drag', event, ui); 77 | this.myResize(); 78 | }.bind(this)); 79 | 80 | this.gitVisuals.drawTreeFirstTime(); 81 | if (this.treeString) { 82 | this.gitEngine.loadTreeFromString(this.treeString); 83 | } 84 | if (this.options.zIndex) { 85 | this.setTreeIndex(this.options.zIndex); 86 | } 87 | 88 | this.shown = false; 89 | this.setTreeOpacity(0); 90 | // reflow needed 91 | process.nextTick(this.fadeTreeIn.bind(this)); 92 | 93 | this.customEvents.trigger('gitEngineReady'); 94 | this.customEvents.trigger('paperReady'); 95 | }, 96 | 97 | clearOrigin: function() { 98 | delete this.originVis; 99 | }, 100 | 101 | makeOrigin: function(options) { 102 | // oh god, here we go. We basically do a bizarre form of composition here, 103 | // where this visualization actually contains another one of itself. 104 | this.originVis = new Visualization(Object.assign( 105 | {}, 106 | // copy all of our options over, except... 107 | this.options, 108 | { 109 | // never accept keyboard input or clicks 110 | noKeyboardInput: true, 111 | noClick: true, 112 | treeString: options.treeString 113 | } 114 | )); 115 | // if the z index is set on ours, carry that over 116 | this.originVis.customEvents.on('paperReady', function() { 117 | var value = $(this.paper.canvas).css('z-index'); 118 | this.originVis.setTreeIndex(value); 119 | }.bind(this)); 120 | 121 | // return the newly created visualization which will soon have a git engine 122 | return this.originVis; 123 | }, 124 | 125 | originToo: function(methodToCall, args) { 126 | if (!this.originVis) { 127 | return; 128 | } 129 | var callMethod = function() { 130 | this.originVis[methodToCall].apply(this.originVis, args); 131 | }.bind(this); 132 | 133 | if (this.originVis.paper) { 134 | callMethod(); 135 | return; 136 | } 137 | // this is tricky -- sometimes we already have paper initialized but 138 | // our origin vis does not (since we kill that on every reset). 139 | // in this case lets bind to the custom event on paper ready 140 | this.originVis.customEvents.on('paperReady', callMethod); 141 | }, 142 | 143 | setTreeIndex: function(level) { 144 | $(this.paper.canvas).css('z-index', level); 145 | this.originToo('setTreeIndex', arguments); 146 | }, 147 | 148 | setTreeOpacity: function(level) { 149 | if (level === 0) { 150 | this.shown = false; 151 | } 152 | 153 | $(this.paper.canvas).css('opacity', level); 154 | this.originToo('setTreeOpacity', arguments); 155 | }, 156 | 157 | getAnimationTime: function() { return 300; }, 158 | 159 | fadeTreeIn: function() { 160 | this.shown = true; 161 | if (!this.paper) { 162 | return; 163 | } 164 | $(this.paper.canvas).animate({opacity: 1}, this.getAnimationTime()); 165 | 166 | this.originToo('fadeTreeIn', arguments); 167 | }, 168 | 169 | fadeTreeOut: function() { 170 | this.shown = false; 171 | if (this.paper && this.paper.canvas) { 172 | $(this.paper.canvas).animate({opacity: 0}, this.getAnimationTime()); 173 | } 174 | this.originToo('fadeTreeOut', arguments); 175 | }, 176 | 177 | hide: function() { 178 | this.fadeTreeOut(); 179 | // remove click handlers by toggling visibility 180 | setTimeout(function() { 181 | $(this.paper.canvas).css('visibility', 'hidden'); 182 | }.bind(this), this.getAnimationTime()); 183 | this.originToo('hide', arguments); 184 | }, 185 | 186 | show: function() { 187 | $(this.paper.canvas).css('visibility', 'visible'); 188 | setTimeout(this.fadeTreeIn.bind(this), 10); 189 | this.originToo('show', arguments); 190 | this.myResize(); 191 | }, 192 | 193 | showHarsh: function() { 194 | $(this.paper.canvas).css('visibility', 'visible'); 195 | this.setTreeOpacity(1); 196 | this.originToo('showHarsh', arguments); 197 | this.myResize(); 198 | }, 199 | 200 | resetFromThisTreeNow: function(treeString) { 201 | this.treeString = treeString; 202 | // do the same but for origin tree string 203 | var oTree = this.getOriginInTreeString(treeString); 204 | if (oTree) { 205 | var oTreeString = this.gitEngine.printTree(oTree); 206 | this.originToo('resetFromThisTreeNow', [oTreeString]); 207 | } 208 | }, 209 | 210 | getOriginInTreeString: function(treeString) { 211 | var tree = JSON.parse(unescape(treeString)); 212 | return tree.originTree; 213 | }, 214 | 215 | reset: function(tree) { 216 | var treeString = tree || this.treeString; 217 | this.setTreeOpacity(0); 218 | if (treeString) { 219 | this.gitEngine.loadTreeFromString(treeString); 220 | } else { 221 | this.gitEngine.defaultInit(); 222 | } 223 | this.fadeTreeIn(); 224 | 225 | if (this.originVis) { 226 | if (treeString) { 227 | var oTree = this.getOriginInTreeString(treeString); 228 | this.originToo('reset', [JSON.stringify(oTree)]); 229 | } else { 230 | // easy 231 | this.originToo('reset', arguments); 232 | } 233 | } 234 | }, 235 | 236 | tearDown: function(options) { 237 | options = options || {}; 238 | 239 | this.gitEngine.tearDown(); 240 | this.gitVisuals.tearDown(); 241 | delete this.paper; 242 | this.originToo('tearDown', arguments); 243 | }, 244 | 245 | die: function() { 246 | this.fadeTreeOut(); 247 | setTimeout(function() { 248 | if (!this.shown) { 249 | this.tearDown({fromDie: true}); 250 | } 251 | }.bind(this), this.getAnimationTime()); 252 | this.originToo('die', arguments); 253 | }, 254 | 255 | myResize: function() { 256 | if (!this.paper) { return; } 257 | 258 | var el = this.el; 259 | 260 | var elSize = el.getBoundingClientRect(); 261 | var width = elSize.width; 262 | var height = elSize.height; 263 | 264 | // if we don't have a container, we need to set our 265 | // position absolutely to whatever we are tracking 266 | if (!this.containerElement) { 267 | var left = this.$el.offset().left; 268 | var top = this.$el.offset().top; 269 | 270 | $(this.paper.canvas).css({ 271 | position: 'absolute', 272 | left: left + 'px', 273 | top: top + 'px' 274 | }); 275 | } else { 276 | // set position to absolute so we all stack nicely 277 | $(this.paper.canvas).css({ 278 | position: 'absolute' 279 | }); 280 | } 281 | 282 | this.paper.setSize(width, height); 283 | this.gitVisuals.canvasResize(width, height); 284 | this.originToo('myResize', arguments); 285 | } 286 | }); 287 | 288 | exports.Visualization = Visualization; 289 | -------------------------------------------------------------------------------- /src/style/rainbows.css: -------------------------------------------------------------------------------- 1 | /* 2 | * CSS animated rainbow dividers of awesome 3 | * by Chris Heilmann @codepo8 and Lea Verou @leaverou 4 | **/ 5 | 6 | /** 7 | * From CSS3-Rainbow-Dividers on Github :) 8 | */ 9 | 10 | @-moz-keyframes rainbowanim { 11 | from { background-position:top left; } 12 | to {background-position:top right; } 13 | } 14 | @-webkit-keyframes rainbowanim { 15 | from { background-position:top left; } 16 | to { background-position:top right; } 17 | } 18 | @-o-keyframes rainbowanim { 19 | from { background-position:top left; } 20 | to { background-position:top right; } 21 | } 22 | @-ms-keyframes rainbowanim { 23 | from { background-position:top left; } 24 | to { background-position:top right; } 25 | } 26 | @-khtml-keyframes rainbowanim { 27 | from { background-position:top left; } 28 | to { background-position:top right; } 29 | } 30 | @keyframes rainbowanim { 31 | from { background-position:top left; } 32 | to { background-position:top right; } 33 | } 34 | 35 | .catchadream{ 36 | background-image:-webkit-linear-gradient( left, red, orange, yellow, green, 37 | blue, indigo, violet, indigo, blue, 38 | green, yellow, orange, red ); 39 | background-image:-moz-linear-gradient( left, red, orange, yellow, green, 40 | blue,indigo, violet, indigo, blue, 41 | green, yellow, orange,red ); 42 | background-image:-o-linear-gradient( left, red, orange, yellow, green, 43 | blue,indigo, violet, indigo, blue, 44 | green, yellow, orange,red ); 45 | background-image:-ms-linear-gradient( left, red, orange, yellow, green, 46 | blue,indigo, violet, indigo, blue, 47 | green, yellow, orange,red ); 48 | background-image:-khtml-linear-gradient( left, red, orange, yellow, green, 49 | blue,indigo, violet, indigo, blue, 50 | green, yellow, orange,red ); 51 | background-image:linear-gradient( left, red, orange, yellow, green, 52 | blue,indigo, violet, indigo, blue, 53 | green, yellow, orange,red ); 54 | -moz-animation:rainbowanim 3.5s forwards linear infinite; 55 | -webkit-animation:rainbowanim 2.5s forwards linear infinite; 56 | -o-animation:rainbowanim 2.5s forwards linear infinite; 57 | -khtml-animation:rainbowanim 2.5s forwards linear infinite; 58 | -ms-animation:rainbowanim 2.5s forwards linear infinite; 59 | -lynx-animation:rainbowanim 2.5s forwards linear infinite; 60 | animation:rainbowanim 2.5s forwards linear infinite; 61 | background-size:50% auto; 62 | 63 | height: 20px; 64 | padding: 12px; 65 | } 66 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { 4 | if (command === 'serve') { 5 | return { 6 | // dev specific config 7 | } 8 | } else { 9 | // command === 'build' 10 | return { 11 | // build specific config 12 | } 13 | } 14 | }) 15 | 16 | 17 | --------------------------------------------------------------------------------