├── .gitignore ├── README.md ├── background.js ├── constants.js ├── crossdart.css ├── crossdart.js ├── crossdart ├── pull.js └── tree.js ├── errors.js ├── github.js ├── github ├── pull_path.js └── tree_path.js ├── icon128.png ├── icon38.png ├── location_change_detector.js ├── manifest.json ├── path.js ├── popup.css ├── popup.html ├── popup.js ├── request.js ├── status.js ├── tooltip.css ├── tooltip.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin-debug/ 3 | bin-release/ 4 | out/ 5 | dist/ 6 | 7 | # Dart files and folders 8 | packages 9 | .packages 10 | *.dart.js 11 | *.js.deps 12 | *.js.map 13 | 14 | # Other files and folders 15 | .settings/ 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .DS_Store 20 | .idea/ 21 | html-template/ 22 | .bundle 23 | /vendor 24 | 25 | # Dart Editor files 26 | .buildlog 27 | .project 28 | 29 | # Generated assets files 30 | /public 31 | .sass-cache 32 | 33 | # Phabricator files 34 | .phutil_module_cache 35 | 36 | .rvmrc 37 | 38 | s3_deployer_config.rb 39 | 40 | /html 41 | 42 | # Ruby version for RVM or rbenv 43 | .ruby-version 44 | 45 | # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` 46 | # should NOT be excluded as they contain compiler settings and other important 47 | # information for Eclipse / Flash Builder. 48 | 49 | config.yaml 50 | credentials.yaml 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crossdart Chrome Extension 2 | 3 | This extension adds "Go to definition" and "Find usages" functionality to Dart projects on Github, 4 | to the tree views and pull requests. 5 | You can take it there: 6 | 7 | [https://chrome.google.com/webstore/detail/crossdart-chrome-extensio/jmdjoliiaibifkklhipgmnciiealomhd](https://chrome.google.com/webstore/detail/crossdart-chrome-extensio/jmdjoliiaibifkklhipgmnciiealomhd) 8 | 9 | ## Demo 10 | 11 | [Here](https://www.crossdart.info/demo.html) (1.7MB) 12 | 13 | ## Some examples 14 | 15 | Make sure you have the [extension](https://chrome.google.com/webstore/detail/crossdart-chrome-extensio/jmdjoliiaibifkklhipgmnciiealomhd) installed, then go to one of the following links, click on 'XD' icon in the toolbar, and check "Enable Crossdart for this project", and click "Apply" 16 | 17 | * [Pull Request for the 'logging' package](https://github.com/dart-lang/logging/pull/28/files#diff-d084143be045800c1c34fffff152e585R166) 18 | * [IOClient class in the 'http' package](https://github.com/dart-lang/http/blob/15d1b6b139a71f6aa85fa34236324db92b97a49f/lib/src/io_client.dart) 19 | * [Pull Request for the 'sqljocky' package](https://github.com/jamesots/sqljocky/pull/67/files#diff-e4c95e6ae84ea58b5c9b7be275d62f80L41) 20 | 21 | ## Installation 22 | 23 | ### Simple way 24 | 25 | If you have a public project, or a private project without other private dependencies, you can just install 26 | the extension into Chrome, go to a repo on Github, click on "XD" icon in the Chrome toolbar, and check the checkbox 27 | "Enable Crossdart for this project". That's it, the extension will send a request to analyze the source code 28 | to https://metadata.crossdart.com/analyze, and it will analyze the source code and upload the analysis data to 29 | Google Cloud Storage, where the extension will take it from. The extension will show the progress of the operations 30 | in a popup in the right top corner. 31 | 32 | You can preventively send the JSON request to POST https://metadata.crossdart.com/analyze, to speed up the analyzing process 33 | (e.g. on commit or pull request hook). It accepts JSON payload, which look like this: 34 | 35 | * url - required, String, the url of the project, looks like https://github.com/johnsmith/my-dart-project 36 | * sha - required, String, full SHA of the commit 37 | * token - optional, String, personal access token in case your project is private 38 | 39 | Example: 40 | 41 | ```json 42 | {"url":"https://github.com/johnsmith/my-dart-project","sha":"62e3956d59878f24dd0bdb042e2f3bc320bf159f"} 43 | ``` 44 | 45 | ### More complicated, but private and secure way (for super private projects) 46 | 47 | We destroy the cloned repo on metadata.crossdart.com as soon as possible right 48 | after finishing analyzing, but in case you don't want to give access to your 49 | code to anything at all, or you have private dependencies in your project, 50 | you'll have to build the analysis data and upload them to some publicly 51 | available place (e.g. S3 or GCS) by yourself. 52 | 53 | Unfortunately, for now this is not just one-click installation, you have to do plenty of steps to make it work. 54 | I'll try to document them here in details, to simplify the ramp up process. 55 | 56 | Install it globally: 57 | 58 | ```bash 59 | $ pub global activate crossdart 60 | ``` 61 | 62 | and then run as 63 | 64 | ```bash 65 | $ pub global run crossdart --input=/path/to/your/project --dart-sdk=/path/to/dart-sdk 66 | ``` 67 | 68 | for example: 69 | 70 | ```bash 71 | $ pub global run crossdart --input=/home/john/my_dart_project --dart-sdk=/usr/lib/dart 72 | ``` 73 | 74 | It will generate the crossdart.json file in the `--input` directory, which you will need to put somewhere, for example, to S3 (see below). 75 | 76 | #### Uploading metadata 77 | 78 | You need some publicly available place to store metadatas for every single commit for your project. You can use S3 for that. It's cheap and relatively easy to configure. 79 | 80 | You probably may want to create a separate bucket on S3 for crossdart metadata files, and then set correct CORS configuration for it. For that, click to the bucket in AWS S3 console, and in the "Properties" tab find "Add CORS Configuration". You can add something like this there: 81 | 82 | ```xml 83 | 84 | 85 | 86 | * 87 | GET 88 | 89 | 90 | ``` 91 | 92 | To deliver your metadata files to S3, you can use s3cmd tool. Create a file `.s3cfg` with the contents: 93 | 94 | ``` 95 | [default] 96 | access_key = YourAccessKey 97 | secret_key = YourSecretKey 98 | use_https = True 99 | ``` 100 | 101 | and then run `s3cmd` to put newly created file. Something like: 102 | 103 | ```bash 104 | $ s3cmd -P -c /path/to/.s3cfg put /path/to/crossdart.json s3://my-bucket/my-github-name/my-project/32c139a7775736e96e476b1e0c89dd20e6588155/crossdart.json 105 | ``` 106 | 107 | The structure of the URL on S3 is important. It should always end with the github name, project name, git sha and `crossdart.json`. Like above, the URL ends with `my-github-name/my-project/32c139a7775736e96e476b1e0c89dd20e6588155/crossdart.json` 108 | 109 | #### Integrating with Travis CI 110 | 111 | Doing all the uploads to S3 manually is very cumbersome, so better to use some machinery, like CI or build server, to do that stuff for you, for example Travis CI. Here's how the configuration could look like: 112 | 113 | `.travis.yml` file: 114 | 115 | ```yaml 116 | language: dart 117 | dart: 118 | - stable 119 | install: 120 | # Here are other stuff to install 121 | - travis_retry sudo apt-get install --yes s3cmd 122 | # ... 123 | # Other sections if needed 124 | # ... 125 | after_success: 126 | - tool/crossdart_runner 127 | ``` 128 | 129 | `tool/crossdart_runner` file: 130 | 131 | ```bash 132 | #!/bin/bash 133 | # 134 | # This script is invoked by Travis CI to generate Crossdart metadata for the Crossdart Chrome extension 135 | if [ "$TRAVIS_PULL_REQUEST" != "false" ] 136 | then 137 | CROSSDART_HASH="${TRAVIS_COMMIT_RANGE#*...}" 138 | else 139 | CROSSDART_HASH="${TRAVIS_COMMIT}" 140 | fi 141 | echo "Installing crossdart" 142 | pub global activate crossdart 143 | echo "Generating metadata for crossdart" 144 | pub global run crossdart --input=. --dart-sdk=$DART_SDK 145 | echo "Copying the crossdart json file to S3 ($CROSSDART_HASH)" 146 | s3cmd -P -c ./.s3cfg put ./crossdart.json s3://my-bucket/crossdart/my-github-name/my-project/$CROSSDART_HASH/crossdart.json 147 | ``` 148 | 149 | Now, every time somebody pushes to 'master', after Travis run, I'll have hyperlinked code of my project on Github. 150 | And every time somebody creates a pull request for me on Github, it's code also going to be hyperlinked. 151 | 152 | How cool is that! :) 153 | 154 | ## Setting up the Crossdart Chrome extension: 155 | 156 | After installing [Crossdart Chrome Extension](https://chrome.google.com/webstore/detail/crossdart-chrome-extensio/jmdjoliiaibifkklhipgmnciiealomhd), you'll see a little "XD" icon in Chrome's URL bar on Github pages. 157 | If you click to it, you'll see a little popup, where you can turn Crossdart on for the current project, and also 158 | specify the URL where it should get the analysis data from (in case you generated and uploaded it by yourself. If you don't - just leave it empty). 159 | You only should provide a base for this URL, the extension will later append git sha and 'crossdart.json' to it. I.e. if you specify URL in this field like: 160 | 161 | ``` 162 | https://my-bucket.s3.amazonaws.com/crossdart 163 | ``` 164 | 165 | then the extension will try to find crossdart.json files by URLs, which will look like: 166 | 167 | ``` 168 | https://my-bucket.s3.amazonaws.com/crossdart/my-github-name/my-project/4a9f8b41d042183116bbfaba31bdea109cc3080d/crossdart.json 169 | ``` 170 | 171 | If your project is private, you also will need to create access token, and paste it into the field in the popup as well. 172 | You can do that there: https://github.com/settings/tokens/new. 173 | 174 | ## Contributing 175 | 176 | Please use Github's bug tracker for bugs. Pull Requests are welcome. 177 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | chrome.extension.onMessage.addListener(function(message, sender) { 2 | if (message && message.crossdart) { 3 | if (message.crossdart.action === 'initPopup') { 4 | chrome.pageAction.show(sender.tab.id); 5 | var pathname = decodeURIComponent(sender.tab.url.replace(/https?:\/\/(www.)?github.com\//, "")); 6 | var basePath = pathname.split("/").slice(0, 2).join("/"); 7 | var token = localStorage.getItem(basePath + "/crossdartToken"); 8 | var jsonUrl = localStorage.getItem(basePath + "/crossdartUrl"); 9 | var enabled = localStorage.getItem(basePath + "/crossdartEnabled").toString() === "true"; 10 | chrome.tabs.sendMessage( 11 | sender.tab.id, {crossdart: {action: 'popupInitialized', jsonUrl: jsonUrl, token: token, enabled: enabled}}); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | var PREFIX = 'crossdart'; 2 | var EVENT = { 3 | LOCATION_CHANGE: 'crossdart:location' 4 | }; 5 | -------------------------------------------------------------------------------- /crossdart.css: -------------------------------------------------------------------------------- 1 | .crossdart-link { 2 | color: inherit; 3 | text-decoration: none; 4 | border-bottom: 1px solid #e4e4e4; 5 | } 6 | 7 | .crossdart-link__external { 8 | position: relative; 9 | } 10 | 11 | .crossdart-link__external:hover::before { 12 | content: '⇗'; 13 | position: absolute; 14 | top: -9px; 15 | right: -13px; 16 | font-size: 14px; 17 | width: 14px; 18 | height: 14px; 19 | } 20 | 21 | .crossdart-link:hover { 22 | text-decoration: none; 23 | border-bottom: 1px solid #c4c4c4; 24 | } 25 | 26 | .crossdart-declarations-popup { 27 | position: absolute; 28 | display: none; 29 | } 30 | 31 | .crossdart-declarations-popup.is-visible { 32 | display: block; 33 | } 34 | 35 | .crossdart-declaration { 36 | border-bottom: 1px dashed silver; 37 | cursor: help; 38 | } 39 | 40 | .crossdart-declaration--contents { 41 | padding: 1em; 42 | max-height: 20em; 43 | margin-right: 0.5em; 44 | overflow: auto; 45 | } 46 | 47 | .crossdart-declaration--contents--label { 48 | padding-bottom: 0.5em; 49 | } 50 | 51 | .crossdart-declaration--contents ul { 52 | margin: 0; 53 | padding: 0; 54 | list-style: none; 55 | } 56 | 57 | .crossdart-declaration--contents li { 58 | margin: 0; 59 | padding: 0; 60 | } 61 | 62 | .crossdart-loader { 63 | position: absolute; 64 | width: 1.2em; 65 | height: 1.2em; 66 | right: 10px; 67 | bottom: 10px; 68 | font-size: 10px; 69 | text-indent: -9999em; 70 | border-radius: 50%; 71 | background: #ffffff; 72 | background: linear-gradient(to right, #000 10%, rgba(255, 255, 255, 0) 42%); 73 | animation: crossdart-loader 1s infinite linear; 74 | transform: translateZ(0); 75 | } 76 | 77 | .crossdart-loader:before { 78 | width: 50%; 79 | height: 50%; 80 | background: #000; 81 | border-radius: 100% 0 0 0; 82 | position: absolute; 83 | top: 0; 84 | left: 0; 85 | content: ''; 86 | } 87 | 88 | .crossdart-loader:after { 89 | background: white; 90 | width: 75%; 91 | height: 75%; 92 | border-radius: 50%; 93 | content: ''; 94 | margin: auto; 95 | position: absolute; 96 | top: 0; 97 | left: 0; 98 | bottom: 0; 99 | right: 0; 100 | } 101 | 102 | @-webkit-keyframes crossdart-loader { 103 | 0% { 104 | -webkit-transform: rotate(0deg); 105 | transform: rotate(0deg); 106 | } 107 | 100% { 108 | -webkit-transform: rotate(360deg); 109 | transform: rotate(360deg); 110 | } 111 | } 112 | @keyframes crossdart-loader { 113 | 0% { 114 | -webkit-transform: rotate(0deg); 115 | transform: rotate(0deg); 116 | } 117 | 100% { 118 | -webkit-transform: rotate(360deg); 119 | transform: rotate(360deg); 120 | } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /crossdart.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function applyTreeCrossdart(github, crossdartBaseUrl, crossdart) { 3 | github.path.getRealRef(function (ref) { 4 | var crossdartUrl = Path.join([crossdartBaseUrl, github.basePath, ref, "crossdart.json"]); 5 | Request.getJson(crossdartUrl, function (json) { 6 | crossdart.applyJson(json); 7 | }, Errors.showUrlError); 8 | }, Errors.showTokenError); 9 | } 10 | 11 | function applyPullSplitCrossdart(github, crossdartBaseUrl, crossdart) { 12 | github.path.getRealRefs(function (refs, repoNames) { 13 | var oldRef = refs[0]; 14 | var newRef = refs[1]; 15 | var oldRepoName = repoNames[0]; 16 | var newRepoName = repoNames[1]; 17 | var crossdartUrlOld = Path.join([crossdartBaseUrl, oldRepoName, oldRef, "crossdart.json"]); 18 | Request.getJson(crossdartUrlOld, function (oldJson) { 19 | crossdart.applyJson(CROSSDART_PULL_OLD, oldJson, oldRef); 20 | }, Errors.showUrlError); 21 | var crossdartUrlNew = Path.join([crossdartBaseUrl, newRepoName, newRef, "crossdart.json"]); 22 | Request.getJson(crossdartUrlNew, function (newJson) { 23 | crossdart.applyJson(CROSSDART_PULL_NEW, newJson, newRef); 24 | }, Errors.showUrlError); 25 | }, Errors.showTokenError); 26 | } 27 | 28 | function applyPullUnifiedCrossdart(github, crossdartBaseUrl, crossdart) { 29 | github.path.getRealRefs(function (refs, repoNames) { 30 | var oldRef = refs[0]; 31 | var newRef = refs[1]; 32 | var oldRepoName = repoNames[0]; 33 | var newRepoName = repoNames[1]; 34 | var crossdartUrlOld = Path.join([crossdartBaseUrl, oldRepoName, oldRef, "crossdart.json"]); 35 | Request.getJson(crossdartUrlOld, function (oldJson) { 36 | crossdart.applyJson(CROSSDART_PULL_OLD, oldJson, oldRef); 37 | }, Errors.showUrlError); 38 | var crossdartUrlNew = Path.join([crossdartBaseUrl, newRepoName, newRef, "crossdart.json"]); 39 | Request.getJson(crossdartUrlNew, function (newJson) { 40 | crossdart.applyJson(CROSSDART_PULL_NEW, newJson, newRef); 41 | }, Errors.showUrlError); 42 | }, Errors.showTokenError); 43 | } 44 | 45 | function checkRef(index, github, baseUrl, ref, repoName, callback) { 46 | var url = baseUrl + repoName + "/" + ref + "/crossdart.json"; 47 | Request.head(url, function () { 48 | Status.show(index, ref, "done"); 49 | callback(); 50 | }, 51 | function () { 52 | Request.get(baseUrl + repoName + "/" + ref + "/status.txt", function (status) { 53 | Status.show(index, ref, status); 54 | if (status !== "error") { 55 | setTimeout(function () { 56 | checkRef(index, github, baseUrl, ref, repoName, callback); 57 | }, 1000); 58 | } 59 | }, function () { 60 | Request.post("https://metadata.crossdart.info/analyze", { 61 | url: "https://github.com/" + repoName, 62 | sha: ref, 63 | token: Github.token 64 | }, function () { 65 | setTimeout(function () { 66 | checkRef(index, github, baseUrl, ref, repoName, callback); 67 | }, 1000); 68 | }); 69 | }); 70 | }); 71 | } 72 | 73 | function fetchMetadataUrl(shouldReuseCrossdart) { 74 | var github = new Github(); 75 | var baseUrl = "https://www.crossdart.info/metadata/"; 76 | if (github.type === Github.PULL_REQUEST) { 77 | github.path.getRealRefs(function (refs, repoNames) { 78 | var isOneDone = false; 79 | checkRef(0, github, baseUrl, refs[0], repoNames[0], function () { 80 | if (isOneDone) { 81 | applyCrossdart(baseUrl, shouldReuseCrossdart); 82 | } 83 | isOneDone = true; 84 | }); 85 | checkRef(1, github, baseUrl, refs[1], repoNames[1], function () { 86 | if (isOneDone) { 87 | applyCrossdart(baseUrl, shouldReuseCrossdart); 88 | } 89 | isOneDone = true; 90 | }); 91 | }); 92 | } else { 93 | github.path.getRealRef(function (ref) { 94 | checkRef(0, github, baseUrl, ref, github.basePath, function () { 95 | applyCrossdart(baseUrl, shouldReuseCrossdart); 96 | }); 97 | }); 98 | } 99 | } 100 | 101 | var crossdart; 102 | function applyCrossdart(crossdartBaseUrl, shouldReuseCrossdart) { 103 | if (enabled) { 104 | if (!crossdartBaseUrl || crossdartBaseUrl.toString().trim() === "") { 105 | fetchMetadataUrl(); 106 | } else { 107 | var github = new Github(); 108 | if (Github.isTree()) { 109 | if (!shouldReuseCrossdart || !crossdart) { 110 | crossdart = new CrossdartTree(github); 111 | } 112 | applyTreeCrossdart(github, crossdartBaseUrl, crossdart); 113 | } else if (Github.isPullSplit()) { 114 | if (!shouldReuseCrossdart || !crossdart) { 115 | crossdart = new CrossdartPullSplit(github); 116 | } 117 | applyPullSplitCrossdart(github, crossdartBaseUrl, crossdart); 118 | } else if (Github.isPullUnified()) { 119 | if (!shouldReuseCrossdart || !crossdart) { 120 | crossdart = new CrossdartPullUnified(github); 121 | } 122 | applyPullUnifiedCrossdart(github, crossdartBaseUrl, crossdart); 123 | } 124 | } 125 | } 126 | } 127 | 128 | chrome.extension.sendMessage({crossdart: {action: 'initPopup'}}); 129 | 130 | var jsonUrl; 131 | var enabled; 132 | chrome.runtime.onMessage.addListener(function (request) { 133 | if (request.crossdart) { 134 | if (request.crossdart.action === 'popupInitialized' || request.crossdart.action === 'apply') { 135 | window.Github.token = request.crossdart.token; 136 | jsonUrl = request.crossdart.jsonUrl; 137 | enabled = request.crossdart.enabled; 138 | if (enabled) { 139 | applyCrossdart(jsonUrl, request.crossdart.action === 'apply'); 140 | } 141 | } else if (request.crossdart.action === 'tokenLink') { 142 | location.href = request.crossdart.url; 143 | } 144 | } 145 | }); 146 | 147 | document.addEventListener(EVENT.LOCATION_CHANGE, function (e) { 148 | var oldPath = e.detail.before.pathname; 149 | var newPath = e.detail.now.pathname; 150 | var condition = false; 151 | condition = condition || (oldPath !== newPath && Path.isTree(newPath)); 152 | condition = condition || (!Path.isPull(oldPath) && Path.isPull(newPath)); 153 | if (condition) { 154 | applyCrossdart(jsonUrl); 155 | } 156 | }); 157 | 158 | document.body.addEventListener("click", function (e) { 159 | var className = e.target.className; 160 | if (className.includes("octicon-unfold") || className.includes("diff-expander")) { 161 | setTimeout(function () { 162 | applyCrossdart(jsonUrl, true); 163 | }, 500); 164 | } 165 | }); 166 | 167 | var tooltip; 168 | document.addEventListener("click", function (e) { 169 | if (e.target.className == "crossdart-declaration") { 170 | var content = declarationTooltipContent(e.target); 171 | if (tooltip) { 172 | tooltip.destroy(); 173 | } 174 | tooltip = new Tooltip(e.target, {tooltipOffset: {x: 0, y: 8}}); 175 | 176 | var documentBodyListener = function (e) { 177 | tooltip.hide(); 178 | document.body.removeEventListener("click", documentBodyListener); 179 | }; 180 | document.body.addEventListener("click", documentBodyListener); 181 | 182 | tooltip.setContent(content); 183 | tooltip.show(); 184 | } 185 | }); 186 | 187 | function declarationTooltipContent(element) { 188 | var references = JSON.parse(element.attributes["data-references"].value); 189 | var ref = element.attributes["data-ref"].value; 190 | var div = document.createElement("div"); 191 | div.className = "crossdart-declaration--contents"; 192 | var label = document.createElement("div"); 193 | label.className = "crossdart-declaration--contents--label"; 194 | label.appendChild(document.createTextNode("Usages:")); 195 | div.appendChild(label); 196 | var ul = document.createElement("ul"); 197 | references.forEach(function (reference) { 198 | var a = document.createElement("a"); 199 | var href = new TreePath(new Github(), ref, reference.remotePath).absolutePath(); 200 | a.setAttribute("href", href); 201 | a.appendChild(document.createTextNode(reference.remotePath)); 202 | var li = document.createElement("li"); 203 | li.appendChild(a); 204 | ul.appendChild(li); 205 | }); 206 | div.appendChild(ul); 207 | return div; 208 | } 209 | 210 | }()); 211 | -------------------------------------------------------------------------------- /crossdart/pull.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.CROSSDART_PULL_OLD = "old"; 3 | window.CROSSDART_PULL_NEW = "new"; 4 | 5 | var CrossdartPull = function (github) { 6 | this.github = github; 7 | this.handledLinesByFiles = {}; 8 | 9 | this.applyJson = function (type, json, ref) { 10 | this.handledLinesByFiles[type] = this.handledLinesByFiles[type] || {}; 11 | var fileElements = document.querySelectorAll("#files .file"); 12 | for (var index in fileElements) { 13 | if (fileElements.hasOwnProperty(index) && index.match(/\d+/)) { 14 | var fileElement = fileElements[index]; 15 | var file = fileElement.querySelector("#files_bucket .file-header").attributes["data-path"].value; 16 | this.handledLinesByFiles[type][file] = this.handledLinesByFiles[type][file] || []; 17 | var entitiesByLines = groupEntitiesByLinesAndTypes(json[file]); 18 | for (var line in entitiesByLines) { 19 | if (this._doesLineElementExist(type, file, line) && this.handledLinesByFiles[type][file].indexOf(line) === -1) { 20 | var entities = entitiesByLines[line]; 21 | entities.sort(function (a, b) { 22 | return a.offset - b.offset; 23 | }); 24 | var content = this._getLineContent(type, file, line); 25 | var prefix = content[0]; 26 | content = content.substr(1); 27 | var newContent = applyEntities(this.github, ref, content, entities, this._getHrefCallback(ref, type)); 28 | this._setLineContent(type, file, line, prefix + newContent); 29 | this.handledLinesByFiles[type][file].push(line); 30 | } 31 | } 32 | } 33 | } 34 | }; 35 | 36 | this._doesLineElementExist = function (type, file, line) { 37 | return !!this._getLineElement(type, file, line); 38 | }; 39 | 40 | this._getLineContent = function (type, file, line) { 41 | return this._getLineElement(type, file, line).innerHTML; 42 | }; 43 | 44 | this._setLineContent = function (type, file, line, content) { 45 | this._getLineElement(type, file, line).innerHTML = content; 46 | }; 47 | 48 | this._getHrefCallback = function (ref, type) { 49 | var that = this; 50 | var result = function (entity) { 51 | var defaultPath = new TreePath(github, ref, entity.remotePath).absolutePath(); 52 | if (entity.remotePath.match(/^http/)) { 53 | return defaultPath; 54 | } else { 55 | var match = entity.remotePath.match(/^([^h].*)#L(\d+)/); 56 | if (match) { 57 | var file = match[1]; 58 | var line = parseInt(match[2], 10); 59 | var element = that._getLineElement(type, file, line); 60 | if (element) { 61 | var parent = element.parentNode; 62 | var anchor = parent.querySelector("[data-anchor^='diff-'"); 63 | var diff = anchor.attributes["data-anchor"].value; 64 | return "#" + diff + (type === "old" ? "L" : "R") + line; 65 | } else { 66 | return defaultPath; 67 | } 68 | } else { 69 | return defaultPath; 70 | } 71 | } 72 | }; 73 | return result; 74 | }; 75 | }; 76 | 77 | window.CrossdartPullSplit = function (github) { 78 | CrossdartPull.apply(this, [github]); 79 | 80 | this._getLineElement = function (type, file, line) { 81 | var fileHeader = document.querySelector("#files_bucket .file-header[data-path='" + file + "']"); 82 | if (fileHeader) { 83 | var lineElements = fileHeader.parentElement.querySelectorAll("[data-line-number~='" + line + "'] + td"); 84 | var lineElement = Array.prototype.filter.call(lineElements, function (i) { 85 | var index = Array.prototype.indexOf.call(i.parentNode.children, i); 86 | return (type === CROSSDART_PULL_OLD ? index === 1 : index === 3); 87 | })[0]; 88 | if (lineElement) { 89 | if (lineElement.className.includes("blob-code-inner")) { 90 | return lineElement; 91 | } else { 92 | return lineElement.querySelector(".blob-code-inner"); 93 | } 94 | } 95 | } 96 | }; 97 | }; 98 | 99 | window.CrossdartPullUnified = function (github) { 100 | CrossdartPull.apply(this, [github]); 101 | 102 | this._getLineElement = function (type, file, line) { 103 | var fileHeader = document.querySelector("#files_bucket .file-header[data-path='" + file + "']"); 104 | if (fileHeader) { 105 | var elIndex = (type === CROSSDART_PULL_OLD ? 1 : 2); 106 | var lineNumberElement = fileHeader.parentElement.querySelector( 107 | "[data-line-number~='" + line + "']:nth-child(" + elIndex + ")" 108 | ); 109 | if (lineNumberElement) { 110 | var lineContainerChildren = lineNumberElement.parentNode.children; 111 | var lineElement = lineContainerChildren[lineContainerChildren.length - 1]; 112 | if (lineElement) { 113 | if (lineElement.className.includes("blob-code-inner")) { 114 | return lineElement; 115 | } else { 116 | return lineElement.querySelector(".blob-code-inner"); 117 | } 118 | } 119 | } 120 | } 121 | }; 122 | }; 123 | }()); 124 | 125 | -------------------------------------------------------------------------------- /crossdart/tree.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.CrossdartTree = function (github) { 3 | this.github = github; 4 | this.handledLines = []; 5 | }; 6 | 7 | window.CrossdartTree.prototype.applyJson = function (json) { 8 | var path = this.github.path.path; 9 | var allEntities = json[path]; 10 | if (allEntities) { 11 | var entitiesByLines = groupEntitiesByLinesAndTypes(allEntities); 12 | for (var line in entitiesByLines) { 13 | if (this.handledLines.indexOf(line) === -1) { 14 | var entities = entitiesByLines[line]; 15 | entities.sort(function (a, b) { 16 | return a.offset - b.offset; 17 | }); 18 | var that = this; 19 | var newContent = applyEntities(this.github, this.github.path.ref, getLineContent(line), entities, function (entity) { 20 | return new TreePath(that.github, that.github.path.ref, entity.remotePath).absolutePath(); 21 | }); 22 | setLineContent(line, newContent); 23 | this.handledLines.push(line); 24 | } 25 | } 26 | } 27 | }; 28 | 29 | function getLineElement(line) { 30 | return window.document.querySelector("#LC" + line); 31 | } 32 | 33 | function getLineContent(line) { 34 | return getLineElement(line).innerHTML; 35 | } 36 | 37 | function setLineContent(line, content) { 38 | getLineElement(line).innerHTML = content; 39 | } 40 | }()); 41 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | window.Errors = {}; 4 | window.Errors.URL_HELP = "It could look something like: 'https://my-crossdarts.s3.amazonaws.com/my-project', " + 5 | "then the Crossdart Chrome extension " + 6 | "will try to make a call to this url + sha + /crossdart.json, e.g.: " + 7 | "'https://my-crossdarts.s3.amazonaws.com/my-project/36a6c88/crossdart.json'"; 8 | 9 | window.Errors.showUrlError = function (url, status, response) { 10 | var message = "Got error trying to access '" + url + "', HTTP response code: '" + status + "'.
" + 11 | "Make sure you specified the right base in the page action popup (with the XD icon). " + 12 | window.Errors.URL_HELP; 13 | showErrorMessage(message); 14 | }; 15 | 16 | window.Errors.showMissingJsonUrlError = function () { 17 | var message = "You should specify base for the url where to retrieve the JSON file with the Crossdart " + 18 | "project metadata from. " + window.Errors.URL_HELP; 19 | showErrorMessage(message); 20 | }; 21 | 22 | window.Errors.showTokenError = function (url, status, response) { 23 | var message = "Got error trying to access '" + url + "', HTTP response code: '" + status + "'.
"; 24 | if (status.toString() === '404') { 25 | message += " If this is a private project, make sure you added the correct access token in the page " + 26 | "action popup (with the XD icon), and then refresh the page."; 27 | } 28 | showErrorMessage(message); 29 | }; 30 | 31 | function showErrorMessage(message) { 32 | var element = document.querySelector("#crossdart-error"); 33 | if (element) { 34 | element.parentNode.removeChild(element); 35 | } 36 | element = document.createElement("div"); 37 | element.setAttribute("style", 38 | "position: fixed; top: 0; left: 0; width: 100%; padding: 1em 10em; background: #FFD1CA; " + 39 | "border-bottom: #C7786C 1px solid; z-index: 1000; " + 40 | "text-align: center; font-size: 14px;"); 41 | element.setAttribute("id", "crossdart-error"); 42 | element.innerHTML = "Crossdart Chrome Extension error: " + message; 43 | document.querySelector("body").appendChild(element); 44 | var close = document.createElement("button"); 45 | close.setAttribute("style", 46 | "position: absolute; top: 5px; right: 5px; color: red; font-size: 14px; background: none; border: none"); 47 | close.addEventListener("click", function () { 48 | element.parentNode.removeChild(element); 49 | }); 50 | close.textContent = "X"; 51 | element.appendChild(close); 52 | } 53 | 54 | }()); 55 | -------------------------------------------------------------------------------- /github.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.Github = function () { 3 | var pathname = Path.current(); 4 | var splittedPath = Path.split(pathname); 5 | this.user = splittedPath[0]; 6 | this.project = splittedPath[1]; 7 | this.basePath = Path.join([this.user, this.project]); 8 | if (Path.isTree(pathname)) { 9 | this.type = Github.TREE; 10 | this.path = buildTreePath(this); 11 | } else if (Path.isPull(pathname)) { 12 | this.type = Github.PULL_REQUEST; 13 | this.path = new PullPath(this); 14 | } else { 15 | throw "Unknown type for the pathname - " + pathname; 16 | } 17 | }; 18 | 19 | window.Github.TREE = "tree"; 20 | window.Github.PULL_REQUEST = "pull_request"; 21 | var API_HOST = "https://api.github.com"; 22 | window.Github.HOST = "https://github.com"; 23 | 24 | window.Github.isTree = function () { 25 | return Path.isTree(Path.current()); 26 | }; 27 | 28 | window.Github.pullType = function () { 29 | var node = document.querySelector("meta[name='diff-view']"); 30 | if (node) { 31 | return node.attributes.content.value; 32 | } 33 | }; 34 | 35 | window.Github.isPullSplit = function () { 36 | return Path.isPull(Path.current()) && this.pullType() === "split"; 37 | }; 38 | 39 | window.Github.isPullUnified = function () { 40 | return Path.isPull(Path.current()) && this.pullType() === "unified"; 41 | }; 42 | 43 | window.Github.api = function (path, callback, errorCallback) { 44 | var url = Path.join([API_HOST, path]); 45 | if (Github.token) { 46 | url += (url.match(/\?/) ? "&" : "?"); 47 | url += "access_token=" + Github.token; 48 | } 49 | Request.getJson(url, callback, errorCallback); 50 | }; 51 | }()); 52 | -------------------------------------------------------------------------------- /github/pull_path.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.PullPath = function (github, path) { 3 | this.absolutePath = path; 4 | this.github = github; 5 | var splittedPath = Path.split(Path.current()); 6 | this.id = parseInt(splittedPath[3], 10); 7 | }; 8 | 9 | window.PullPath.prototype.getRealRefs = function (callback, errorCallback) { 10 | if (this._getRealRefs === undefined) { 11 | var path = Path.join(["repos", this.github.basePath, "pulls", this.id]); 12 | Github.api(path, function (json) { 13 | this._getRealRefs = [json.base.sha, json.head.sha, json.base.repo.full_name, json.head.repo.full_name]; 14 | callback(this._getRealRefs.slice(0, 2), this._getRealRefs.slice(2, 4)); 15 | }, errorCallback); 16 | } else { 17 | callback(this._getRealRefs); 18 | } 19 | }; 20 | 21 | window.PullPath.prototype.absolutePath = function () { 22 | if (this.path.match(/^http/)) { 23 | return this.path; 24 | } else { 25 | return Path.join([Github.HOST, this.github.basePath, "blob", this.ref, this.path]); 26 | } 27 | }; 28 | 29 | }()); 30 | 31 | -------------------------------------------------------------------------------- /github/tree_path.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.TreePath = function (github, ref, path) { 3 | this.github = github; 4 | this.ref = ref; 5 | this.path = path; 6 | }; 7 | 8 | window.buildTreePath = function (github) { 9 | var splittedPath = Path.split(Path.current()); 10 | return new TreePath(github, splittedPath[3], splittedPath.slice(4).join("/")); 11 | }; 12 | 13 | window.TreePath.prototype.getRealRef = function (callback) { 14 | if (this._getRealRef === undefined) { 15 | if (this.ref.match(/[a-z0-9]{40}/)) { 16 | this._getRealRef = this.ref; 17 | callback(this._getRealRef); 18 | } else { 19 | var path = Path.join(["repos", this.github.basePath, "git", "refs", "heads", this.ref]); 20 | Github.api(path, function (json) { 21 | this._getRealRef = json.object.sha; 22 | callback(this._getRealRef); 23 | }); 24 | } 25 | } else { 26 | callback(this._getRealRef); 27 | } 28 | }; 29 | 30 | window.TreePath.prototype.absolutePath = function () { 31 | if (this.path.match(/^http/)) { 32 | return this.path; 33 | } else { 34 | return Path.join([Github.HOST, this.github.basePath, "blob", this.ref, this.path]); 35 | } 36 | }; 37 | 38 | }()); 39 | -------------------------------------------------------------------------------- /icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astashov/crossdart-chrome-extension/984f44a5cbdcdc6cc16a85e4a3fed5ec864e4c14/icon128.png -------------------------------------------------------------------------------- /icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astashov/crossdart-chrome-extension/984f44a5cbdcdc6cc16a85e4a3fed5ec864e4c14/icon38.png -------------------------------------------------------------------------------- /location_change_detector.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var pathname, hash; 3 | 4 | function detectLocationChange() { 5 | if (Path.current() !== pathname || location.hash !== hash) { 6 | var event = new CustomEvent(EVENT.LOCATION_CHANGE, { 7 | detail: {before: {pathname: pathname, hash: hash}, now: {pathname: Path.current(), hash: location.hash}} 8 | }); 9 | document.dispatchEvent(event); 10 | pathname = Path.current(); 11 | hash = location.hash; 12 | } 13 | setTimeout(detectLocationChange, 1000); 14 | } 15 | 16 | detectLocationChange(); 17 | }()); 18 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Crossdart Chrome Extension", 4 | "version": "0.3.0", 5 | 6 | "description": "Adds 'Go to definition' and 'Find Usages' functionality for Dart packages on Github", 7 | 8 | "author": "Anton Astashov", 9 | "background": { 10 | "scripts": ["background.js"], 11 | "persistent": false 12 | }, 13 | "icons": { 14 | "128": "icon128.png" 15 | }, 16 | "permissions": [ 17 | "activeTab" 18 | ], 19 | "page_action": { 20 | "default_icon": "icon38.png", 21 | "default_popup": "popup.html" 22 | }, 23 | "content_scripts": [{ 24 | "matches": ["https://github.com/*"], 25 | "js": [ 26 | "constants.js", 27 | "utils.js", 28 | "path.js", 29 | "errors.js", 30 | "status.js", 31 | "request.js", 32 | "tooltip.js", 33 | "github.js", 34 | "github/tree_path.js", 35 | "github/pull_path.js", 36 | "crossdart/pull.js", 37 | "crossdart/tree.js", 38 | "location_change_detector.js", 39 | "crossdart.js" 40 | ], 41 | "css": ["crossdart.css", "tooltip.css"] 42 | }] 43 | } 44 | -------------------------------------------------------------------------------- /path.js: -------------------------------------------------------------------------------- 1 | Path = { 2 | current: function () { 3 | return decodeURIComponent(location.pathname); 4 | }, 5 | 6 | normalize: function (part) { 7 | return (part || "").toString().replace(/(^\/|\/$)/, ""); 8 | }, 9 | 10 | join: function (parts) { 11 | return parts.map(Path.normalize).join("/"); 12 | }, 13 | 14 | split: function (path) { 15 | return Path.normalize(path).split("/"); 16 | }, 17 | 18 | isTree: function (path) { 19 | return path.match(/^\/[^\/]+\/[^\/]+\/blob\/[^\/]+\/lib\/(.*)$/); 20 | }, 21 | 22 | isPull: function (path) { 23 | return path.match(/^\/[^\/]+\/[^\/]+\/pull\/\d+\/files/); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | .popup { 2 | padding: 0.5em; 3 | width: 300px; 4 | } 5 | 6 | .crossdart-enabled { 7 | padding: 0.3em 0; 8 | } 9 | 10 | .crossdart-url { 11 | padding: 0.3em 0; 12 | } 13 | 14 | #url, #token { 15 | padding: 0.3em 0.6em; 16 | width: 100%; 17 | display: block; 18 | box-sizing: border-box; 19 | } 20 | 21 | .github-token { 22 | padding: 0.3em 0; 23 | } 24 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | var basePath; 2 | 3 | var urlField = document.querySelector("#url"); 4 | var tokenField = document.querySelector("#token"); 5 | var enabledField = document.querySelector("#enabled"); 6 | 7 | urlField.addEventListener("change", saveChangesInUrl); 8 | tokenField.addEventListener("change", saveChangesInToken); 9 | enabledField.addEventListener("change", saveChangesInEnabled); 10 | 11 | var button = document.querySelector("#button"); 12 | button.addEventListener("click", function () { 13 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 14 | sendApplyMessage(tabs[0].id); 15 | }); 16 | }); 17 | 18 | var tokenLink = document.querySelector("#token-link"); 19 | tokenLink.addEventListener("click", function () { 20 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 21 | chrome.tabs.sendMessage(tabs[0].id, {crossdart: {action: "tokenLink", url: tokenLink.attributes.href.value}}); 22 | }); 23 | return false; 24 | }); 25 | 26 | function setTokenFieldValue() { 27 | tokenField.value = getTokenFromLocalStorage(); 28 | } 29 | 30 | function setUrlFieldValue() { 31 | urlField.value = getUrlFromLocalStorage(); 32 | } 33 | 34 | function setEnabledFieldValue() { 35 | enabledField.checked = getEnabledFromLocalStorage(); 36 | } 37 | 38 | function setTokenToLocalStorage(value) { 39 | localStorage.setItem(getTokenKey(), value); 40 | } 41 | 42 | function setUrlToLocalStorage(value) { 43 | localStorage.setItem(getUrlKey(), value); 44 | } 45 | 46 | function setEnabledToLocalStorage(value) { 47 | localStorage.setItem(getEnabledKey(), value); 48 | } 49 | 50 | function getTokenKey() { 51 | return basePath + '/crossdartToken'; 52 | } 53 | 54 | function getUrlKey() { 55 | return basePath + '/crossdartUrl'; 56 | } 57 | 58 | function getEnabledKey() { 59 | return basePath + '/crossdartEnabled'; 60 | } 61 | 62 | function getTokenFromLocalStorage() { 63 | return localStorage.getItem(getTokenKey()); 64 | } 65 | 66 | function getUrlFromLocalStorage() { 67 | return localStorage.getItem(getUrlKey()); 68 | } 69 | 70 | function getEnabledFromLocalStorage() { 71 | var isEnabled = localStorage.getItem(getEnabledKey()); 72 | return isEnabled && isEnabled.toString() === "true"; 73 | } 74 | 75 | function saveChangesInUrl() { 76 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 77 | setUrlToLocalStorage(urlField.value); 78 | }); 79 | } 80 | 81 | function saveChangesInToken() { 82 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 83 | setTokenToLocalStorage(tokenField.value); 84 | }); 85 | } 86 | 87 | function saveChangesInEnabled() { 88 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 89 | setEnabledToLocalStorage(enabledField.checked); 90 | }); 91 | } 92 | 93 | function sendApplyMessage(id) { 94 | var token = getTokenFromLocalStorage(); 95 | var url = getUrlFromLocalStorage(); 96 | var enabled = getEnabledFromLocalStorage(); 97 | chrome.tabs.sendMessage(id, {crossdart: {action: "apply", jsonUrl: url, token: token, enabled: enabled}}); 98 | } 99 | 100 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 101 | var pathname = decodeURIComponent(tabs[0].url.replace(/https?:\/\/(www.)?github.com\//, "")); 102 | basePath = pathname.split("/").slice(0, 2).join("/"); 103 | setTokenFieldValue(); 104 | setUrlFieldValue(); 105 | setEnabledFieldValue(); 106 | var urlInfo = document.querySelector(".crossdart-url--info"); 107 | urlInfo.innerHTML = urlInfo.innerHTML + " " + window.Errors.URL_HELP; 108 | }); 109 | -------------------------------------------------------------------------------- /request.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function request(method, url, payload, callback, errorCallback) { 3 | var httpRequest = new XMLHttpRequest(); 4 | 5 | httpRequest.open(method, url, true); 6 | if (method === "POST") { 7 | httpRequest.setRequestHeader("Content-Type", "application/json"); 8 | } 9 | httpRequest.send(payload); 10 | 11 | httpRequest.onreadystatechange = function(response) { 12 | if (httpRequest.readyState === 4) { 13 | if (httpRequest.status === 200) { 14 | callback(httpRequest.responseText); 15 | } else { 16 | if (errorCallback) { 17 | errorCallback(url, httpRequest.status, response); 18 | } else { 19 | console.log("Unhandled response " + httpRequest.status + " - " + httpRequest.responseText); 20 | } 21 | } 22 | } 23 | }; 24 | } 25 | 26 | window.Request = { 27 | head: function (url, callback, errorCallback) { 28 | return request("HEAD", url, null, callback, errorCallback); 29 | }, 30 | get: function (url, callback, errorCallback) { 31 | return request("GET", url, null, callback, errorCallback); 32 | }, 33 | getJson: function (url, callback, errorCallback) { 34 | return request("GET", url, null, function (responseText) { 35 | return callback(JSON.parse(responseText)); 36 | }, errorCallback); 37 | }, 38 | post: function (url, payload, callback, errorCallback) { 39 | return request("POST", url, JSON.stringify(payload), callback, errorCallback); 40 | } 41 | } 42 | }()); 43 | -------------------------------------------------------------------------------- /status.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var refStatuses = []; // [{ref: null, status: null}, {ref: null, status: null}]; 3 | 4 | function showStatus(content, showSpinner) { 5 | var element = document.querySelector("#crossdart-status"); 6 | if (!element) { 7 | element = document.createElement("div"); 8 | element.setAttribute("style", 9 | "position: fixed; top: 10px; right: 10px; width: 26em; padding: 1em 1.8em; background: #fefefe; " + 10 | "border: #C7786C 1px solid; z-index: 999; color: #666; " + 11 | "text-align: left; font-size: 12px; min-height: 50px;"); 12 | element.setAttribute("id", "crossdart-status"); 13 | var contents = document.createElement("div"); 14 | contents.setAttribute("class", "crossdart-status-contents"); 15 | element.appendChild(contents); 16 | contents.innerHTML = content; 17 | document.querySelector("body").appendChild(element); 18 | var close = document.createElement("button"); 19 | close.setAttribute("style", 20 | "position: absolute; top: 5px; right: 5px; color: red; font-size: 14px; background: none; border: none"); 21 | close.addEventListener("click", function () { 22 | element.parentNode.removeChild(element); 23 | }); 24 | close.textContent = "X"; 25 | element.appendChild(close); 26 | } else { 27 | element.querySelector(".crossdart-status-contents").innerHTML = content; 28 | } 29 | var spinner = element.querySelector(".crossdart-loader"); 30 | if (showSpinner && !spinner) { 31 | spinner = document.createElement("div"); 32 | spinner.setAttribute("class", "crossdart-loader"); 33 | element.appendChild(spinner); 34 | } else if (!showSpinner && spinner) { 35 | spinner.parentNode.removeChild(spinner); 36 | } 37 | } 38 | 39 | function statusMessage(refStatus) { 40 | var status; 41 | if (refStatus.status === "error") { 42 | var github = new Github(); 43 | var url = "https://www.crossdart.info/metadata/" + github.basePath + "/" + refStatus.ref + "/log.txt"; 44 | status = "" + refStatus.status + ""; 45 | } else { 46 | status = refStatus.status; 47 | } 48 | return "
Getting metadata for " + refStatus.ref.substring(0, 8) + " - " + status + "
"; 49 | } 50 | 51 | window.Status = { 52 | show: function (index, ref, status) { 53 | refStatuses[index] = {ref: ref, status: status}; 54 | var message = []; 55 | if (refStatuses[0] && refStatuses[0].ref) { 56 | message.push(statusMessage(refStatuses[0])); 57 | } 58 | if (refStatuses[1] && refStatuses[1].ref) { 59 | message.push(statusMessage(refStatuses[1])); 60 | } 61 | if (message.length === 0 || ((!refStatuses[0] || refStatuses[0].status === "done") && (!refStatuses[1] || refStatuses[1].status === "done"))) { 62 | var element = document.querySelector("#crossdart-status"); 63 | if (element) { 64 | element.parentNode.removeChild(element); 65 | } 66 | } else { 67 | var html = "
" + message.join("") + "
"; 68 | var showSpinner = (refStatuses[0] && refStatuses[0].status !== "error" && refStatuses[0].status !== "done") || 69 | (refStatuses[1] && refStatuses[1].status !== "error" && refStatuses[1].status !== "done"); 70 | showStatus(html, showSpinner); 71 | } 72 | } 73 | }; 74 | }()); 75 | -------------------------------------------------------------------------------- /tooltip.css: -------------------------------------------------------------------------------- 1 | .crossdart-tooltip { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | z-index: 20000; 6 | opacity: 0; 7 | visibility: hidden; 8 | padding: 1px; 9 | text-align: left; 10 | white-space: normal; 11 | border-radius: 4px; 12 | transition: opacity 0.1s linear, visibility 0.1s linear; 13 | } 14 | 15 | .crossdart-tooltip.is-visible { 16 | visibility: visible; 17 | } 18 | 19 | .crossdart-tooltip { 20 | background-color: #fefbf1; 21 | border: 1px solid #d1d2d3; 22 | } 23 | 24 | .crossdart-tooltip .crossdart-tooltip--arrow-container { 25 | position: absolute; 26 | width: 100%; 27 | height: 6px; 28 | left: 0px; 29 | z-index: -1; 30 | } 31 | 32 | .crossdart-tooltip .crossdart-tooltip--arrow { 33 | position: absolute; 34 | display: block; 35 | height: 6px; 36 | width: 12px; 37 | background: #fefbf1; 38 | } 39 | 40 | .crossdart-tooltip .crossdart-tooltip--arrow:before, 41 | .crossdart-tooltip .crossdart-tooltip--arrow:after { 42 | border: solid transparent; 43 | content: " "; 44 | height: 0; 45 | width: 0; 46 | position: absolute; 47 | pointer-events: none; 48 | } 49 | 50 | .crossdart-tooltip .crossdart-tooltip--arrow:before { 51 | color: #d1d2d3; 52 | border-width: 7px; 53 | } 54 | 55 | .crossdart-tooltip .crossdart-tooltip--arrow:after { 56 | color: #fefbf1; 57 | border-width: 6px; 58 | } 59 | 60 | .crossdart-tooltip.is-up .crossdart-tooltip--arrow, .crossdart-tooltip.is-down .crossdart-tooltip--arrow { 61 | left: 50%; 62 | } 63 | 64 | .crossdart-tooltip.is-up .crossdart-tooltip--arrow:before, .crossdart-tooltip.is-down .crossdart-tooltip--arrow:before { 65 | left: 50%; 66 | margin-left: -7px; 67 | } 68 | 69 | .crossdart-tooltip.is-up .crossdart-tooltip--arrow:after, .crossdart-tooltip.is-down .crossdart-tooltip--arrow:after { 70 | left: 50%; 71 | margin-left: -6px; 72 | } 73 | 74 | .crossdart-tooltip.is-up .crossdart-tooltip--arrow-container { 75 | bottom: -6px; 76 | } 77 | 78 | .crossdart-tooltip.is-up .crossdart-tooltip--arrow { 79 | top: -6px; 80 | } 81 | 82 | .crossdart-tooltip.is-up .crossdart-tooltip--arrow:before { 83 | top: 100%; 84 | border-top-color: #d1d2d3; 85 | } 86 | 87 | .crossdart-tooltip.is-up .crossdart-tooltip--arrow:after { 88 | top: 100%; 89 | border-top-color: #fefbf1; 90 | } 91 | 92 | .crossdart-tooltip.is-down .crossdart-tooltip--arrow-container { 93 | top: -6px; 94 | } 95 | 96 | .crossdart-tooltip.is-down .crossdart-tooltip--arrow { 97 | bottom: -6px; 98 | } 99 | 100 | .crossdart-tooltip.is-down .crossdart-tooltip--arrow:before { 101 | bottom: 100%; 102 | border-bottom-color: #d1d2d3; 103 | } 104 | 105 | .crossdart-tooltip.is-down .crossdart-tooltip--arrow:after { 106 | bottom: 100%; 107 | border-bottom-color: #fefbf1; 108 | } 109 | 110 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow-container, .crossdart-tooltip.is-right .crossdart-tooltip--arrow-container { 111 | top: 0; 112 | bottom: 0; 113 | width: 6px; 114 | height: auto; 115 | } 116 | 117 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow, .crossdart-tooltip.is-right .crossdart-tooltip--arrow { 118 | width: 6px; 119 | height: 12px; 120 | top: 50%; 121 | } 122 | 123 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow:before, .crossdart-tooltip.is-right .crossdart-tooltip--arrow:before { 124 | top: 50%; 125 | margin-top: -7px; 126 | } 127 | 128 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow:after, .crossdart-tooltip.is-right .crossdart-tooltip--arrow:after { 129 | top: 50%; 130 | margin-top: -6px; 131 | } 132 | 133 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow-container { 134 | right: -6px; 135 | left: auto; 136 | } 137 | 138 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow { 139 | left: -6px; 140 | } 141 | 142 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow:before { 143 | left: 100%; 144 | border-left-color: #d1d2d3; 145 | } 146 | 147 | .crossdart-tooltip.is-left .crossdart-tooltip--arrow:after { 148 | left: 100%; 149 | border-left-color: #fefbf1; 150 | } 151 | 152 | .crossdart-tooltip.is-right .crossdart-tooltip--arrow-container { 153 | left: -6px; 154 | right: auto; 155 | } 156 | 157 | .crossdart-tooltip.is-right .crossdart-tooltip--arrow { 158 | right: -6px; 159 | } 160 | 161 | .crossdart-tooltip.is-right .crossdart-tooltip--arrow:before { 162 | right: 100%; 163 | border-right-color: #d1d2d3; 164 | } 165 | 166 | .crossdart-tooltip.is-right .crossdart-tooltip--arrow:after { 167 | right: 100%; 168 | border-right-color: #fefbf1; 169 | } 170 | 171 | .crossdart-tooltip.is-visible { 172 | opacity: 1; 173 | } 174 | 175 | -------------------------------------------------------------------------------- /tooltip.js: -------------------------------------------------------------------------------- 1 | function Tooltip(link, options) { 2 | options = options || {}; 3 | options.tooltipOffset = options.tooltipOffset || {x: 0, y: 0}; 4 | options.shift = options.shift || 0; 5 | options.screenPadding = options.screenPadding || 16; 6 | options.initialDirection = options.initialDirection || TooltipDirection.DOWN; 7 | var self = this; 8 | 9 | self.tooltip = document.createElement("div"); 10 | self.tooltip.className = "crossdart-tooltip"; 11 | self.arrowContainer = document.createElement("div"); 12 | self.arrowContainer.className = "crossdart-tooltip--arrow-container"; 13 | self.arrow = document.createElement("div"); 14 | self.arrow.className = "crossdart-tooltip--arrow"; 15 | self.container = document.createElement("div"); 16 | self.container.className = "crossdart-tooltip--container"; 17 | self.tooltip.appendChild(self.arrowContainer); 18 | self.arrowContainer.appendChild(self.arrow); 19 | self.tooltip.appendChild(self.container); 20 | 21 | self.tooltip.addEventListener("click", function (e) { 22 | e.stopPropagation(); 23 | }); 24 | 25 | this.show = function () { 26 | if (!isAttached()) { 27 | attach(); 28 | } 29 | setTimeout(function () { 30 | self.tooltip.classList.add("is-visible"); 31 | reposition(options.initialDirection, options.tooltipOffset); 32 | var listener = function () { 33 | self.tooltip.removeEventListener("transitionend", listener); 34 | }; 35 | self.tooltip.addEventListener("transitionend", listener); 36 | }, 0); 37 | }; 38 | 39 | this.hide = function () { 40 | if (isAttached()) { 41 | setTimeout(function () { 42 | self.tooltip.classList.remove("is-visible"); 43 | }); 44 | } 45 | }; 46 | 47 | this.isVisible = function () { 48 | return self.tooltip.classList.contains("is-visible"); 49 | }; 50 | 51 | this.destroy = function () { 52 | if (isAttached()) { 53 | self.tooltip.parentNode.removeChild(self.tooltip); 54 | } 55 | }; 56 | 57 | this.setContent = function (content) { 58 | while (self.container.firstChild) { 59 | self.container.removeChild(self.container.firstChild); 60 | } 61 | self.container.appendChild(content); 62 | }; 63 | 64 | function windowBounds() { 65 | return { 66 | top: window.pageXOffset, 67 | left: window.pageYOffset, 68 | right: window.pageXOffset + window.innerWidth, 69 | bottom: window.pageYOffset + window.innerHeight 70 | }; 71 | } 72 | 73 | function tooltipBounds() { 74 | var rect = self.tooltip.getBoundingClientRect(); 75 | return { 76 | left: rect.left, 77 | right: rect.right, 78 | top: rect.top + window.scrollY, 79 | bottom: rect.bottom + window.scrollY 80 | } 81 | } 82 | 83 | function doesExceedTopEdge(direction) { 84 | return tooltipBounds().top < (direction.isVertical() ? 0 : options.screenPadding); 85 | } 86 | 87 | function doesExceedBottomEdge(direction) { 88 | var edge = windowBounds().bottom; 89 | if (direction.isHorizontal()) { 90 | edge -= options.screenPadding; 91 | } 92 | return tooltipBounds().bottom > edge; 93 | } 94 | 95 | function doesExceedLeftEdge(direction) { 96 | return tooltipBounds().left < (direction.isHorizontal() ? 0 : options.screenPadding); 97 | } 98 | 99 | function doesExceedRightEdge(direction) { 100 | var edge = windowBounds().right; 101 | if (direction.isVertical()) { 102 | edge -= options.screenPadding; 103 | } 104 | return tooltipBounds().right > edge; 105 | } 106 | 107 | function isVisibleOnScreenInSameDirection(direction) { 108 | if (direction.isVertical()) { 109 | return !doesExceedTopEdge(direction) && !doesExceedBottomEdge(direction); 110 | } else { 111 | return !doesExceedLeftEdge(direction) && !doesExceedRightEdge(direction); 112 | } 113 | } 114 | 115 | function isVisibleOnScreenInPerpendicularDirection(direction) { 116 | if (direction.isHorizontal()) { 117 | return !doesExceedTopEdge(direction) && !doesExceedBottomEdge(direction); 118 | } else { 119 | return !doesExceedLeftEdge(direction) && !doesExceedRightEdge(direction); 120 | } 121 | } 122 | 123 | function isAttached() { 124 | return self.tooltip.parentNode !== null; 125 | } 126 | 127 | function attach() { 128 | document.body.appendChild(self.tooltip); 129 | } 130 | 131 | function reposition(direction, tooltipOffset, opts) { 132 | opts = opts || {}; 133 | opts.isAdjustedInSameDirection = 134 | (opts.isAdjustedInSameDirection === undefined ? false : opts.isAdjustedInSameDirection); 135 | opts.isAdjustedInPerpendicularDirection = 136 | (opts.isAdjustedInPerpendicularDirection === undefined ? false : opts.isAdjustedInPerpendicularDirection); 137 | TooltipDirection.ALL.forEach(function (d) { 138 | self.tooltip.classList.remove(d.cssClass()); 139 | }); 140 | self.tooltip.classList.add(direction.cssClass()); 141 | var position = calculatePosition(direction, tooltipOffset); 142 | setPosition(position, direction); 143 | 144 | if (!isVisibleOnScreenInPerpendicularDirection(direction) && !opts.isAdjustedInPerpendicularDirection) { 145 | reposition(direction, adjustOffset(direction), {isAdjustedInPerpendicularDirection: true}); 146 | } 147 | if (!isVisibleOnScreenInSameDirection(direction) && !opts.isAdjustedInSameDirection) { 148 | reposition(direction.opposite(), tooltipOffset, {isAdjustedInSameDirection: true}); 149 | } 150 | } 151 | 152 | function setPosition(position, direction) { 153 | self.tooltip.style.left = position.point.x.toString() + "px"; 154 | self.tooltip.style.top = position.point.y.toString() + "px"; 155 | if (direction.isVertical()) { 156 | self.arrow.style.marginLeft = position.arrowMargin.toString() + "px"; 157 | } else { 158 | self.arrow.style.marginTop = position.arrowMargin.toString() + "px"; 159 | } 160 | } 161 | 162 | function calculatePosition(direction, tooltipOffset) { 163 | if (direction.isHorizontal()) { 164 | return calculateHorizontalPosition(direction, tooltipOffset); 165 | } else { 166 | return calculateVerticalPosition(direction, tooltipOffset); 167 | } 168 | } 169 | 170 | function linkPoint() { 171 | var boundingRect = link.getBoundingClientRect(); 172 | return {x: boundingRect.left + window.scrollX, y: boundingRect.top + window.scrollY}; 173 | } 174 | 175 | function parentPoint() { 176 | var boundingRect = self.tooltip.offsetParent.getBoundingClientRect(); 177 | return {x: boundingRect.left + window.scrollX, y: boundingRect.top + window.scrollY}; 178 | } 179 | 180 | function calculateHorizontalPosition(direction, tooltipOffset) { 181 | var base = linkPoint().x - parentPoint().x; 182 | var arrowShift = tooltipOffset.x + self.arrowContainer.offsetWidth; 183 | var top = linkPoint().y - 184 | parentPoint().y - 185 | (self.tooltip.offsetHeight / 2) + 186 | (link.offsetHeight / 2) + 187 | options.shift; 188 | var left; 189 | if (direction === TooltipDirection.RIGHT) { 190 | left = base + arrowShift + link.offsetWidth; 191 | } else { 192 | left = base - arrowShift - self.tooltip.offsetWidth; 193 | } 194 | var point = {x: Math.round(left), y: Math.round(top)}; 195 | var arrowMargin = Math.round(-self.arrow.offsetHeight / 2 + tooltipOffset.y); 196 | return {point: point, arrowMargin: arrowMargin}; 197 | } 198 | 199 | function calculateVerticalPosition(direction, tooltipOffset) { 200 | var base = linkPoint().y - parentPoint().y; 201 | var arrowShift = tooltipOffset.y + self.arrowContainer.offsetHeight; 202 | var left = -self.tooltip.offsetWidth / 2 + 203 | options.shift + 204 | linkPoint().x + 205 | link.offsetWidth / 2 - 206 | tooltipOffset.x - 207 | parentPoint().x; 208 | var top; 209 | if (direction === TooltipDirection.DOWN) { 210 | top = base + arrowShift + link.offsetHeight; 211 | } else { 212 | top = base - arrowShift - self.tooltip.offsetHeight; 213 | } 214 | var point = {x: Math.round(left), y: Math.round(top)}; 215 | var arrowMargin = Math.round(-self.arrow.offsetWidth / 2 + tooltipOffset.x); 216 | return {point: point, arrowMargin: arrowMargin}; 217 | } 218 | 219 | function adjustOffset(direction) { 220 | var overflow; 221 | if (direction.isVertical()) { 222 | if (doesExceedLeftEdge(direction)) { 223 | overflow = options.screenPadding - tooltipBounds().left; 224 | return {x: options.tooltipOffset.x - overflow, y: options.tooltipOffset.y}; 225 | } else if (doesExceedRightEdge(direction)) { 226 | overflow = tooltipBounds().left + tooltipBounds().width - 227 | windowBounds().left + windowBounds().width - options.screenPadding; 228 | return {x: options.tooltipOffset.x + overflow, y: options.tooltipOffset.y}; 229 | } else { 230 | return options.tooltipOffset; 231 | } 232 | } else { 233 | if (doesExceedTopEdge(direction)) { 234 | overflow = options.screenPadding - tooltipBounds().top; 235 | return {x: options.tooltipOffset.x, y: options.tooltipOffset.y - overflow}; 236 | } else if (doesExceedBottomEdge(direction)) { 237 | overflow = tooltipBounds().top + tooltipBounds().height - 238 | windowBounds().top + windowBounds().height - options.screenPadding; 239 | return {x: options.tooltipOffset.x, y: options.tooltipOffset.y + overflow}; 240 | } else { 241 | return options.tooltipOffset; 242 | } 243 | } 244 | } 245 | 246 | } 247 | 248 | function TooltipDirection(value) { 249 | var self = this; 250 | self.value = value; 251 | 252 | self.opposite = function () { 253 | if (value === "up") { 254 | return TooltipDirection.DOWN; 255 | } else if (value === "down") { 256 | return TooltipDirection.UP; 257 | } else if (value === "left") { 258 | return TooltipDirection.RIGHT; 259 | } else if (value === "right") { 260 | return TooltipDirection.LEFT; 261 | } else { 262 | throw "No opposite tooltip direction for " + value; 263 | } 264 | }; 265 | 266 | self.cssClass = function () { 267 | return "is-" + value; 268 | }; 269 | 270 | self.isHorizontal = function () { 271 | return self.type() === "horizontal"; 272 | }; 273 | 274 | self.isVertical = function () { 275 | return self.type() === "vertical"; 276 | }; 277 | 278 | self.type = function () { 279 | return value === "left" || value === "right" ? "horizontal" : "vertical"; 280 | }; 281 | } 282 | 283 | TooltipDirection.LEFT = new TooltipDirection("left"); 284 | TooltipDirection.RIGHT = new TooltipDirection("right"); 285 | TooltipDirection.UP = new TooltipDirection("up"); 286 | TooltipDirection.DOWN = new TooltipDirection("down"); 287 | TooltipDirection.ALL = [ 288 | TooltipDirection.LEFT, 289 | TooltipDirection.RIGHT, 290 | TooltipDirection.UP, 291 | TooltipDirection.DOWN 292 | ]; 293 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function groupBy(coll, context) { 2 | return coll.reduce(function (memo, item) { 3 | var value = context(item); 4 | if (memo[value] === undefined) { 5 | memo[value] = []; 6 | } 7 | memo[value].push(item); 8 | return memo; 9 | }, {}); 10 | } 11 | 12 | function getRealOffset(content, offset) { 13 | var regexps = [[/(<[^>]*>)/g, 0], [/(&\w+;)/g, 1]]; 14 | var positions = regexps.reduce(function (memo, item) { 15 | var regexp = item[0]; 16 | var regexpLength = item[1]; 17 | var matches = content.match(regexp); 18 | var matchPos = 0; 19 | for (var matchIndex in (matches || [])) { 20 | if (matches.hasOwnProperty(matchIndex)) { 21 | var match = matches[matchIndex]; 22 | var matchOffset = content.substr(matchPos).search(regexp); 23 | memo.push([matchPos + matchOffset, matchPos + matchOffset + match.length, regexpLength]); 24 | matchPos += matchOffset + match.length; 25 | } 26 | } 27 | return memo; 28 | }, []).sort(function (a, b) { return a[0] - b[0]; }); 29 | var realOffset = offset; 30 | while (positions.length > 0 && realOffset > positions[0][0]) { 31 | var position = positions.shift(); 32 | var length = position[1] - position[0] - position[2]; 33 | realOffset += length; 34 | } 35 | return realOffset; 36 | } 37 | 38 | function groupEntitiesByLinesAndTypes(allEntities) { 39 | var result = {}; 40 | for (var type in allEntities) { 41 | var entities = allEntities[type]; 42 | for (var i in entities) { 43 | var entity = JSON.parse(JSON.stringify(entities[i])); 44 | entity.type = type; 45 | var line = parseInt(entity.line, 10); 46 | result[line] = result[line] || []; 47 | result[line].push(entity); 48 | } 49 | } 50 | return result; 51 | } 52 | 53 | function applyEntities(github, ref, content, entities, hrefCallback) { 54 | if (content.indexOf("crossdart-link") === -1) { 55 | var newLineContent = ""; 56 | var lastStop = 0; 57 | for (var index in entities) { 58 | var entity = entities[index]; 59 | var realOffset = getRealOffset(content, entity.offset); 60 | newLineContent += content.substr(lastStop, realOffset - lastStop); 61 | if (entity.type == "references") { 62 | var href = hrefCallback(entity); 63 | var isInternal = href.match(/^#/) || href.match(new RegExp(location.pathname)); 64 | var cssClass = "crossdart-link" + (!isInternal ? ' crossdart-link__external' : ''); 65 | newLineContent += ""; 66 | } else if (entity.type == "declarations") { 67 | var references = JSON.stringify(entity.references); 68 | newLineContent += ""; 69 | } 70 | var end = entity.offset + entity.length; 71 | var realEnd = getRealOffset(content, end); 72 | newLineContent += content.substr(realOffset, realEnd - realOffset); 73 | if (entity.type == "references") { 74 | newLineContent += ""; 75 | } else if (entity.type == "declarations") { 76 | newLineContent += ""; 77 | } 78 | lastStop = realEnd; 79 | } 80 | var lastEntity = entities[entities.length - 1]; 81 | var lastEnd = lastEntity.offset + lastEntity.length; 82 | var lastRealEnd = getRealOffset(content, lastEnd); 83 | newLineContent += content.substr(lastRealEnd); 84 | return newLineContent; 85 | } else { 86 | return content; 87 | } 88 | } 89 | --------------------------------------------------------------------------------