From 25992661642824200fb672da8c6fe39b20bdd441 Mon Sep 17 00:00:00 2001 From: Artem Solovev Date: Sat, 30 Mar 2019 20:51:58 +0300 Subject: [PATCH] #4 #26 #28 #31 #44 #48 Major update --- README.md | 2 +- manifest.json | 2 +- package-lock.json | 22 ++- package.json | 6 +- src/inject.ts | 467 ++++++++++++---------------------------------- src/types.ts | 25 +++ src/utils.ts | 11 +- tsconfig.json | 2 +- tslint.json | 12 +- 9 files changed, 194 insertions(+), 355 deletions(-) create mode 100644 src/types.ts diff --git a/README.md b/README.md index 8a0f097..62c6083 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ___ # Browser extension GLOC ( [Chrome](https://chrome.google.com/webstore/detail/gloc-github-counter-lines/kaodcnpebhdbpaeeemkiobcokcnegdki?utm_source=chrome-ntp-icon), [Opera]() ) ![Chrome](screens/browsers/chrome.png) ![Opera](screens/browsers/opera.png) -## **Current version: 7.7.1** ( 2019, 24 march ) +## **Current version: 8.0.0** ( 2019, 30 march ) ## **Initial release 2.0.1** ( 2017, 12 february ) diff --git a/manifest.json b/manifest.json index 89ef8d6..25257a3 100644 --- a/manifest.json +++ b/manifest.json @@ -6,7 +6,7 @@ "short_name": "__MSG_shortName__", "author": "__MSG_author__", "description": "__MSG_description__", - "version": "7.7.1", + "version": "8.0.0", "browser_action": { "default_icon": "img/icon128.png", diff --git a/package-lock.json b/package-lock.json index ef30120..d9c0259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gloc", - "version": "7.6.17", + "version": "7.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -131,6 +131,21 @@ "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=", "dev": true }, + "@types/jquery": { + "version": "3.3.29", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.29.tgz", + "integrity": "sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -5237,6 +5252,11 @@ "dev": true, "optional": true }, + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", diff --git a/package.json b/package.json index e9a4535..56cdef7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gloc", - "version": "7.7.1", + "version": "8.0.0", "description": "Extension counts the number of lines of code in GitHub pages.", "main": "index.js", "scripts": { @@ -24,6 +24,7 @@ "homepage": "https://github.com/artem-solovev/gloc#readme", "devDependencies": { "@types/chrome": "0.0.81", + "@types/jquery": "^3.3.29", "babel-core": "^6.26.3", "babel-eslint": "^8.2.6", "babel-loader": "^7.1.5", @@ -45,5 +46,8 @@ "url-loader": "^1.1.2", "webpack": "^4.29.0", "webpack-cli": "^3.3.0" + }, + "dependencies": { + "jquery": "^3.3.1" } } diff --git a/src/inject.ts b/src/inject.ts index 2178c54..dd6c0b4 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -3,8 +3,11 @@ * * Licensed GPL-2.0 © Artem Solovev */ -import { log } from './utils'; -import { APP_CLASSNAME, TRIES_DEFAULT, REPO_CLASS } from './constants'; +import * as $ from 'jquery'; + +import { log, isEmpty } from './utils'; +import { APP_CLASSNAME, TRIES_DEFAULT } from './constants'; +import { LOCATION, InitialData, GithubError, CodeFrequency, WeeklyAggregate } from './types'; /** @@ -23,135 +26,127 @@ let githubToken: string = null; githubToken = result['x-github-token']; } - insertLocForRepo(); + gloc(); - insertLocForDir(); + $(document).on('pjax:complete', () => { + gloc(); + }); }); })(); -/** - * PART 1. - * Renders in DOM in front of the each of the acceptable file LOC - */ +const gloc = (): void => { + init() + .then(res => { + appendLoc(res); + log('info', res); + }) + .catch(err => log('err', err)); +}; /** - * Renders total LOC into DOM + * REFACTORED */ -const insertLocForRepo = () => { - const nodeToMount = document.getElementsByClassName(REPO_CLASS)[0]; - const userRepos = document.querySelectorAll('#user-repositories-list h3 a'); - const organisationRepos = document.querySelectorAll('.repo-list h3 a'); - const recommendedRepos = document.querySelectorAll( - '#recommended-repositories-container h3 a' - ); - - // Add LOC to single repo - if (nodeToMount) { - appendLoc(getRepoName(), nodeToMount); - } - - let repos: NodeListOf = null; - - if (userRepos.length) { - repos = userRepos; - } else if (organisationRepos.length) { - repos = organisationRepos; - } else if (recommendedRepos.length) { - repos = recommendedRepos; - } - - if (repos) { - const links: Element[] = Array.prototype.slice.call(repos); +const init = (): Promise => { + /** + * Current user's location + */ + const current: InitialData = { + location: null, + link: null, + }; - links.map((elem) => { - const link = elem.getAttribute('href'); - appendLoc(link, elem); - }); + // User's repos + const user = document.querySelectorAll('#user-repositories-list h3 a'); + const isUser = user.length > 0 ? LOCATION.USER : false; + + // Organisation's repos + const organisation = document.querySelectorAll('.repo-list h3 a'); + const isOrganisation = organisation.length > 0 ? LOCATION.ORGANIZATION : false; + + // Recommended repos + const recommended = document.querySelectorAll('#recommended-repositories-container h3 a'); + const isRecommended = recommended.length > 0 ? LOCATION.RECOMMENDED : false; + + // Single repo + const single: HTMLAnchorElement = document.querySelector('.repohead-details-container h1 strong a'); + const isSingle = single ? LOCATION.SINGLE : false; + + if (isUser) { + current.location = LOCATION.USER; + current.link = Array.prototype.slice.call(user); + } else if (isOrganisation) { + current.location = LOCATION.ORGANIZATION; + current.link = Array.prototype.slice.call(organisation); + } else if (isRecommended) { + current.location = LOCATION.RECOMMENDED; + current.link = Array.prototype.slice.call(recommended); + } else if (isSingle) { + current.location = LOCATION.SINGLE; + current.link = [single]; + } else { + current.location = LOCATION.UNKNOWN; } -}; -/** - * Gets repo name from current location - * @return {string} - */ -const getRepoName = () => { - const repo = location.pathname; - if (repo && typeof repo === 'string') { - return repo.endsWith('/') - ? repo.slice(0, -1) - : repo; + if (current.location !== LOCATION.UNKNOWN && current.link.length > 0) { + return Promise.resolve(current); } else { - return ''; + return Promise.reject('Error: unknown location for counting LOC (lines of code)'); } }; -/** - * Appends LOC to ELEMENT - * @param {string} repoName - * @param {Element} element - */ -const appendLoc = (repoName: string, element: Element) => { - getGloc(repoName, TRIES_DEFAULT) - .then((lines: number) => element.innerHTML += getBadgeWithLines(lines)) - .catch((e: any) => log('e', e)); -}; +const appendLoc = (config: InitialData) => { + config.link.map((anchor) => { + const reponame = anchor.getAttribute('href'); -/** - * Returns badge container for LOC with LOC - * @param {number} lines - LOC - * @return {html} - */ -const getBadgeWithLines = (lines: number) => { - return ( - `
- - ${chrome.i18n.getMessage('lines')} - - - ${lines} - -
` - ); + if (reponame) { + getLoc(reponame, TRIES_DEFAULT) + .then(loc => setLoc(anchor, loc || 'Stat is unavailable')) + .catch(err => console.error(`Error by setting LOC for ${reponame}`, err)); + } + }); }; /** - * Counts LOC - * @param {string} repo - /user/repo + * @param {string} reponame /user/repo * @param {number} tries - * @return {promise} + * @returns {Promise} */ -const getGloc = (repoName: string, tries: number): Promise => { - if (!repoName) { - return Promise.reject(new Error('No repositories !')); - } - +const getLoc = (reponame: string, tries: number): Promise => { if (tries === 0) { - return Promise.reject( - new Error('Repo: ' + repoName + '; Too many requests to API !') - ); + return Promise.reject('Repo: ' + reponame + '; Too many requests to API !'); } - const url = tokenizeUrl(setApiUrl(repoName)); + const url = tokenizeUrl(getApiUrl(reponame)); return fetch(url) - .then(x => x.json()) - .then(x => - x.reduce((total: number, changes: number[]) => total + changes[1] + changes[2], 0) - ) - .catch(err => getGloc(repoName, tries - 1)); + .then(response => response.json()) + .then(stat => { + const isEmptyResponse = isEmpty(stat); + if (stat && !isEmptyResponse) { + return calculate(stat); + } + console.error(`Error by getting stat for ${reponame}. Response -->`, stat); + return null; + }) + .catch((err: GithubError) => { + if (err.message) { + console.error('\t err', err); + } else { + getLoc(reponame, tries - 1); + } + }); +}; + +const setLoc = (anchor: HTMLAnchorElement, loc: number | string) => { + anchor.innerHTML += getBadgeWithLines(loc); }; /** - * Setter for url * @param {string} repo - /user/repo * @return {string} */ -const setApiUrl = (repoName: string) => `https://api.github.com/repos${repoName}/stats/code_frequency`; +const getApiUrl = (repoName: string) => + `https://api.github.com/repos${repoName}/stats/code_frequency`; /** * Adds token to URL @@ -167,254 +162,30 @@ const tokenizeUrl = (url: string) => { return ''; }; +const calculate = (stat: CodeFrequency): number => { + return stat.reduce((total: number, changes: WeeklyAggregate) => + total + changes[1] + changes[2], 0); +}; + /** - * PART 2. - * Renders in DOM in front of the each of the acceptable file LOC + * Returns badge container for LOC with LOC + * @param {number | string} lines - LOC | Error + * @return {html} */ -const insertLocForDir = () => { - /** - * File extensions which plugin counts - * - * https://www.file-extensions.org/filetype/extension/name/source-code-and-script-files - */ - const acceptableExtensions = [ - 'as', - 'asm', - 'asp', - 'aspx', - 'bash', - 'bat', - 'c', - 'cbl', - 'cc', - 'cfc', - 'clj', - 'cs', - 'css', - 'cpp', - 'comp', - 'cso', - 'dart', - 'd', - 'do', - 'dpr', - 'el', - 'ejs', - 'f90', - 'frag', - 'gitignore', - 'geom', - 'glsl', - 'h', - 'hs', - 'hpp', - 'html', - 'haml', - 'hlsl', - 'java', - 'js', - 'json', - 'jsp', - 'jade', - 'jsx', - 'kt', - 'kts', - 'lisp', - 'lua', - 'less', - 'm', - 'md', - 'mk', - 'mm', - 'pas', - 'php', - 'pl', - 'prl', - 'pxd', - 'py', - 'pyx', - 'pyw', - 'r', - 'rb', - 's', - 'ss', - 'scala', - 'ser', - 'sh', - 'sql', - 'swift', - 'svg', - 'sass', - 'scss', - 'ts', - 'tmpl', - 'tpl', - 'tsx', - 'tese', - 'tesc', - 'vb', - 'vert', - 'ui', - 'win', - 'xml', - 'yaml', - 'yml' - ]; - - // Get links for files in current directory (swith them into array) - const nodeList = document.querySelectorAll('tbody .js-navigation-open'); - const fileLinks = Array.prototype.slice.call(nodeList); - - // object with LOCs for each file's extension in current dir { 'md': 000, 'txt': 001, ... } - const locCollection: Record = {}; - - const DOM_APP_ID = 'Gloc-counter'; - - /** - * Checks link's object - * @param {HTMLAnchorElement} link - tag - * @return {boolean} - */ - const isAcceptableFile = (link: HTMLAnchorElement) => { - const fileExt = getExtension(link); - const hasTitle = link.title !== ''; - const hasProperType = typeof link.title === typeof 'str'; - const isAcceptableFile = acceptableExtensions.indexOf(fileExt) !== -1; - - return (hasTitle || hasProperType) && isAcceptableFile; - }; - - /** - * Retrieves file's extension - * @param {HTMLAnchorElement} link - tag - * @return {string} - */ - const getExtension = (link: HTMLAnchorElement) => { - const title = link.title; - const fileExt = title.split('.'); - - return fileExt[fileExt.length - 1]; - }; - - /** - * Gets plain html file from the link - * @param {HTMLAnchorElement} link - * @param {function} parsePlainHTML - */ - const getHtmlFile = ( - link: HTMLAnchorElement, - parsePlainHTML: (plainHTML: string, link: HTMLAnchorElement) => void - ) => { - const xmlHttp = new XMLHttpRequest(); - - xmlHttp.onreadystatechange = () => { - if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { - parsePlainHTML(xmlHttp.responseText, link); - } - }; - - xmlHttp.open('GET', link.href, true); - xmlHttp.send(null); - }; - - /** - * Parses plain html file ( extracts LOC ) - * @param {string} plainHTML - * @param {HTMLAnchorElement} link - tag - */ - const parsePlainHTML = (plainHTML: string, link: HTMLAnchorElement) => { - const rowLoc = plainHTML.match(/\d+ lines/g); // console.log( rowLoc ) --> 00 lines - - if (!rowLoc || rowLoc.length === 0) { - log('w', 'Cannot parse file from ' + link); - return; - } - - const loc = Number(rowLoc[0].replace('lines', '')); // console.log( loc ) ==> 00 - - addCurrentLoc(locCollection, getExtension(link), loc); - - renderLocByExtensions(); - - renderLocForFile(link, loc); - }; - - /** - * Adds LOC value to collection of LOC by extensions - * @param {object} collection - * @param {string} fileExt - * @param {number} loc - */ - const addCurrentLoc = ( - collection: Record, - fileExt: string, - loc: number - ) => { - if (fileExt in collection) { - collection[fileExt] += loc; - } else { - collection[fileExt] = loc; - } - }; - - /** - * Renders LOC in DOM by file extensions - */ - const renderLocByExtensions = () => { - const commitTease = document.getElementsByClassName('commit-tease')[0]; - let locDisplay = document.getElementById(DOM_APP_ID); - - if (!locDisplay) { - locDisplay = document.createElement('div'); - locDisplay.id = DOM_APP_ID; - commitTease.appendChild(locDisplay); - } - - const locTitle = `
${chrome.i18n.getMessage('totalDirLoc')} `; - - locDisplay.innerHTML = locTitle + stringifyLocCollection(locCollection); - }; - - /** - * Converts object to string - * @param {object} collection - * @return {string} - */ - const stringifyLocCollection = (collection: Record) => { - const arr = []; - - let totalLoc = 0; - - for (const key in collection) { - arr.push(key + ' - ' + String(collection[key])); - totalLoc += collection[key]; - arr.sort(); - } - - return ` - ${totalLoc} -
- ${chrome.i18n.getMessage('totalExtLoc')} -
 ${arr.join(',
 ')}`; - - }; - - /** - * Renders in DOM LOC for current link - * @param {HTMLAnchorElement} link - * @param {number} loc - */ - const renderLocForFile = (link: HTMLAnchorElement, loc: number) => { - const str = `${link.title} ${loc} ${chrome.i18n.getMessage('lines')}`; - - document.getElementById(link.id).innerHTML = str; - }; - - fileLinks - .filter((link: HTMLAnchorElement) => { - return isAcceptableFile(link); - }) - .map((link: HTMLAnchorElement) => { - getHtmlFile(link, parsePlainHTML); - }); +const getBadgeWithLines = (lines: number | string) => { + return ( + `
+ + ${chrome.i18n.getMessage('lines')} + + + ${lines} + +
` + ); }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..8339811 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,25 @@ +export interface InitialData { + location: LOCATION; + link: HTMLAnchorElement[]; +} + +export enum LOCATION { + USER = 'USER', + ORGANIZATION = 'ORGANIZATION', + RECOMMENDED = 'RECOMMENDED', + SINGLE = 'SINGLE', + UNKNOWN = 'UNKNOWN', +} + +export interface GithubError { + message: string; + documentation_url: string; +} + +export type CodeFrequency = WeeklyAggregate[]; + +export type WeeklyAggregate = [ + number, // total + number, // additions + number, // deletions +]; diff --git a/src/utils.ts b/src/utils.ts index 750d72f..1afaa90 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,7 @@ import { APP_NAME } from './constants'; * @param {string} type - info, warn, err * @param {string} str - info for output */ -export const log = (type: string, str: string) => { +export const log = (type: string, str: any) => { switch (type) { case 'i': console.info(APP_NAME + ': ' + str); @@ -27,3 +27,12 @@ export const translateElements = (ids: string[]) => { element.innerHTML = chrome.i18n.getMessage(id); }); }; + +export const isEmpty = (obj: object) => { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + return false; + } + } + return true; +}; diff --git a/tsconfig.json b/tsconfig.json index 66add18..46e4cb8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "baseUrl": "types", "typeRoots": ["types"], "allowJs": true, - "types": ["chrome"], + "types": ["chrome", "jquery"], "lib": ["es2015.core", "dom", "es6"], "sourceMap": true, "plugins": [ diff --git a/tslint.json b/tslint.json index 0dcb13d..bd6fa1d 100644 --- a/tslint.json +++ b/tslint.json @@ -113,7 +113,17 @@ "ordered-imports": false, "prefer-conditional-expression": false, "radix": false, - "trailing-comma": false, + "trailing-comma": [ + true, + { + "multiline": { + "objects": "always", + "arrays": "always", + "functions": "never", + "typeLiterals": "ignore" + }, + "esSpecCompliant": true + }], "align": false, "eofline": false,