├── .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 |
--------------------------------------------------------------------------------