├── .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 |
62 | {this.state.isGoalExpanded ?
63 | intl.str('hide-goal-button') :
64 | intl.str('show-goal-button')
65 | }
66 |
67 |
68 |
69 |
72 | {intl.str('objective-button')}
73 |
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 |
--------------------------------------------------------------------------------