├── .editorconfig ├── .gitignore ├── .npmrc ├── .readme ├── markbot-logo.png ├── screenshot.png ├── split-view.png └── visual-diff.png ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── check-manager.js ├── checks │ ├── all-files │ │ ├── html-unique.js │ │ ├── task-generator.js │ │ └── task.js │ ├── content.js │ ├── css │ │ ├── best-practices.js │ │ ├── best-practices │ │ │ ├── stylelint.json │ │ │ └── viewport.js │ │ ├── properties.js │ │ ├── task-generator.js │ │ ├── task.js │ │ └── validation.js │ ├── files │ │ ├── task-generator.js │ │ └── task.js │ ├── functionality │ │ ├── defaults-service.js │ │ ├── task-generator.js │ │ └── task.js │ ├── git │ │ ├── best-practices.js │ │ ├── best-practices │ │ │ └── verb-whitelist.json │ │ ├── commits.js │ │ ├── status.js │ │ ├── task-generator.js │ │ └── task.js │ ├── html │ │ ├── accessibility.js │ │ ├── best-practices.js │ │ ├── best-practices │ │ │ ├── beautifier.json │ │ │ ├── close-p-on-same-line.js │ │ │ ├── code-style.js │ │ │ ├── document-tags.js │ │ │ ├── double-space.js │ │ │ ├── force-line-break-tags.json │ │ │ ├── force-line-breaks.js │ │ │ ├── htmlcs.json │ │ │ ├── htmlparser │ │ │ │ ├── LICENSE │ │ │ │ ├── Parser.js │ │ │ │ └── Tokenizer.js │ │ │ ├── indentation.js │ │ │ ├── max-empty-lines.js │ │ │ ├── missing-optional-closing-tags.js │ │ │ ├── multi-case-attrs.json │ │ │ ├── rule-adv-attr-lowercase.js │ │ │ ├── rule-adv-tag-pair.js │ │ │ ├── space-before-close-greater-than.js │ │ │ ├── specific-line-break-checks.json │ │ │ ├── svg-uppercase-tag-names.json │ │ │ ├── viewport.js │ │ │ └── void-elements.json │ │ ├── elements.js │ │ ├── outline.js │ │ ├── task-generator.js │ │ ├── task.js │ │ └── validation.js │ ├── javascript │ │ ├── best-practices.js │ │ ├── best-practices │ │ │ └── eslint.json │ │ ├── task-generator.js │ │ ├── task.js │ │ ├── validation.js │ │ └── validation │ │ │ └── eslint.json │ ├── live-website │ │ ├── task-generator.js │ │ └── task.js │ ├── markdown │ │ ├── task-generator.js │ │ ├── task.js │ │ ├── validation.js │ │ └── validation │ │ │ └── markdownlint.json │ ├── message-group.js │ ├── naming-conventions │ │ ├── extension-blacklist.json │ │ ├── file-blacklist.json │ │ ├── path-whitelist.json │ │ ├── task-generator.js │ │ └── task.js │ ├── performance │ │ ├── ignore-advice-ids.json │ │ ├── task-generator.js │ │ └── task.js │ ├── screenshots │ │ ├── default.css │ │ ├── default.js │ │ ├── defaults-service.js │ │ ├── differ.js │ │ ├── naming-service.js │ │ ├── task-generator.js │ │ └── task.js │ └── yaml │ │ ├── task-generator.js │ │ ├── task.js │ │ └── validation.js ├── classify.js ├── convert-path-to-url.js ├── dependency-checker.js ├── error-message-status.js ├── escape-shell.js ├── file-exists.js ├── files-to-ignore.json ├── functionality-injector.js ├── functionality-methods.js ├── hidden-browser-window-preload.js ├── list-dir.js ├── lock-matcher.js ├── locker.js ├── markbot-file-generator.js ├── markbot-ignore-parser.js ├── markbot-main.js ├── menu.js ├── networks.js ├── passcode.js ├── requirements-finder.js ├── server-html.js ├── server-language.js ├── server-manager.js ├── server-web-error.html ├── server-web-process.js ├── server-web.js ├── strip-path.js ├── task-pool-queue.js ├── task-pool.html ├── task-pool.js ├── user-agent-service.js ├── web-loader-queue.js └── web-loader.js ├── build ├── background.png ├── background@2x.png ├── icon.icns └── icon.ico ├── config.example.json ├── devtools-har-extension ├── index.html ├── index.js └── manifest.json ├── docs ├── images │ ├── git-win.jpg │ ├── jdk-mac.jpg │ ├── jdk-win.jpg │ └── xcode-select-install.jpg ├── install-git-command-line-tools-mac.md ├── install-git-command-line-tools-win.md ├── install-java-developer-kit-mac.md └── install-java-developer-kit-win.md ├── frontend ├── common.css ├── debug │ ├── debug.css │ ├── debug.html │ └── debug.js ├── differ │ ├── differ.css │ ├── differ.html │ └── differ.js ├── images │ ├── arrow.svg │ ├── bypassed.svg │ ├── check.svg │ ├── computing.svg │ ├── differ-bottom.svg │ ├── differ-line.svg │ ├── differ-top.svg │ ├── error.svg │ ├── focus-triangle.svg │ ├── folder-hover.svg │ ├── folder.svg │ ├── help-green.svg │ ├── help-red.svg │ ├── help-yellow.svg │ ├── pending.svg │ ├── spinner.gif │ ├── transparency-grid.svg │ ├── two-dots.svg │ └── warning.svg └── main │ ├── alert.css │ ├── dancing-robot.css │ ├── deps.css │ ├── main.css │ ├── main.html │ ├── main.js │ ├── robot-beeps.json │ ├── success-messages.json │ ├── time-estimator.js │ └── toolbar.css ├── markbot.js ├── package.json ├── scripts ├── gen-https-cert.sh ├── hash-passcode.js └── localhost.conf ├── templates ├── accessibility.yml ├── aria-landmarks.yml ├── basic-dropped-folder.yml ├── body-margin-0.yml ├── border-box.yml ├── box-sizing.yml ├── buttons.yml ├── css-order-grid-main.yml ├── css-order-grid-type-main.yml ├── css-order-modules-grid-type-main.yml ├── css-order-modules-grid-type-theme.yml ├── css-order-modules-main.yml ├── css-order-modules-type-main.yml ├── css-order-type-main.yml ├── css.yml ├── favicons.yml ├── forms.yml ├── git-1.yml ├── git-10.yml ├── git-2.yml ├── git-4.yml ├── google-fonts.yml ├── gridifier-unrestricted.yml ├── gridifier.yml ├── html-good-semantics.yml ├── html.yml ├── img-block-100.yml ├── img-flex.yml ├── js.yml ├── main-link-focus.yml ├── modulifier-buttons.yml ├── modulifier-embed.yml ├── modulifier-icons.yml ├── modulifier-list-group.yml ├── modulifier-unrestricted.yml ├── modulifier.yml ├── naming-restrict-live.yml ├── nav-focus.yml ├── nav-hover.yml ├── responsive-css.yml ├── responsive-font-sizes.yml ├── screenshots-320.yml ├── screenshots-all.yml ├── seo.yml ├── typografier-unrestricted.yml └── typografier.yml ├── vendor ├── css-validator │ └── .gitignore ├── html-validator │ └── .gitignore ├── languagetool │ ├── languagetool.properties │ └── words-to-add.txt └── pdfbox │ └── .gitignore └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | 7 | indent_style = space 8 | indent_size = 2 9 | 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | config.json 3 | 4 | npm-debug.log 5 | node_modules 6 | dist 7 | vendor/html-validator/* 8 | !vendor/html-validator/.gitignore 9 | vendor/css-validator/* 10 | !vendor/css-validator/.gitignore 11 | vendor/languagetool/* 12 | !vendor/languagetool/words-to-add.txt 13 | !vendor/languagetool/languagetool.properties 14 | build/background.tiff 15 | vendor/pdfbox/* 16 | 17 | app/*.cert 18 | app/*.pem 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Necessary, for pointing to Python/2.7 2 | # Node-Gyp will fail to run with newer versions of Python 3 | # https://github.com/nodejs/node-gyp#option-2 4 | python = "/usr/bin/python" 5 | -------------------------------------------------------------------------------- /.readme/markbot-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/.readme/markbot-logo.png -------------------------------------------------------------------------------- /.readme/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/.readme/screenshot.png -------------------------------------------------------------------------------- /.readme/split-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/.readme/split-view.png -------------------------------------------------------------------------------- /.readme/visual-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/.readme/visual-diff.png -------------------------------------------------------------------------------- /app/check-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const merge = require('merge-objects'); 4 | const markbotMain = require('./markbot-main'); 5 | const taskPool = require('./task-pool'); 6 | 7 | const availableChecks = { 8 | naming: { 9 | module: 'naming-conventions', 10 | }, 11 | liveWebsite: { 12 | module: 'live-website', 13 | }, 14 | git: { 15 | module: 'git', 16 | }, 17 | performance: { 18 | module: 'performance', 19 | type: taskPool.TYPE_SINGLE, 20 | priority: taskPool.PRIORITY_HIGH, 21 | }, 22 | allFiles: { 23 | module: 'all-files', 24 | }, 25 | html: { 26 | module: 'html', 27 | }, 28 | css: { 29 | module: 'css', 30 | }, 31 | js: { 32 | module: 'javascript', 33 | }, 34 | files: { 35 | module: 'files', 36 | }, 37 | md: { 38 | module: 'markdown', 39 | }, 40 | yml: { 41 | module: 'yaml', 42 | }, 43 | functionality: { 44 | module: 'functionality', 45 | type: taskPool.TYPE_LIVE, 46 | }, 47 | screenshots: { 48 | module: 'screenshots', 49 | type: taskPool.TYPE_LIVE, 50 | priority: taskPool.PRIORITY_LOW, 51 | }, 52 | }; 53 | 54 | const generateTasks = function (check, markbotFile, isCheater) { 55 | const module = require(`./checks/${check.module}/task-generator`); 56 | const tasks = module.generateTaskList(markbotFile, isCheater); 57 | 58 | tasks.forEach(function (task, i) { 59 | markbotMain.send('check-group:new', task.group, task.groupLabel); 60 | 61 | tasks[i] = merge(Object.assign({}, check), tasks[i]); 62 | tasks[i].cwd = markbotFile.cwd; 63 | 64 | if (!tasks[i].priority) tasks[i].priority = taskPool.PRIORITY_NORMAL; 65 | if (!tasks[i].type) tasks[i].type = taskPool.TYPE_STATIC; 66 | }); 67 | 68 | return tasks; 69 | }; 70 | 71 | const run = function (markbotFile, isCheater = null, next) { 72 | let allTasks = []; 73 | 74 | Object.keys(availableChecks).forEach(function (check) { 75 | allTasks = allTasks.concat(generateTasks(availableChecks[check], markbotFile, isCheater)); 76 | }); 77 | 78 | allTasks.forEach(function (task) { 79 | taskPool.add(task, task.type, task.priority); 80 | }); 81 | 82 | taskPool.start(next); 83 | }; 84 | 85 | const stop = function () { 86 | taskPool.stop(); 87 | }; 88 | 89 | module.exports = { 90 | run: run, 91 | stop: stop, 92 | }; 93 | -------------------------------------------------------------------------------- /app/checks/all-files/html-unique.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const cheerio = require('cheerio'); 6 | const exists = require(__dirname + '/../../file-exists'); 7 | 8 | module.exports.find = function (folderPath, file, uniqueElems) { 9 | const fullPath = path.resolve(folderPath + '/' + file.path); 10 | let fileContents; 11 | let code; 12 | let uniqueFinds = {}; 13 | 14 | if (!exists.check(fullPath)) return false; 15 | 16 | fileContents = fs.readFileSync(fullPath, 'utf8'); 17 | code = cheerio.load(fileContents); 18 | 19 | uniqueElems.forEach(function (elem) { 20 | let result; 21 | let sel = (typeof elem === 'string') ? elem : elem[0]; 22 | let key = (typeof elem === 'string') ? elem : elem[1]; 23 | 24 | try { 25 | result = code(sel); 26 | } catch (e) { 27 | return uniqueFinds; 28 | } 29 | 30 | if (result.length <= 0) return uniqueFinds; 31 | 32 | if (sel.match(/\]$/)) { 33 | uniqueFinds[key] = result.attr(sel.match(/\[([^\]]+)\]$/)[1]).trim(); 34 | } else { 35 | uniqueFinds[key] = result.html().trim(); 36 | } 37 | }); 38 | 39 | return uniqueFinds; 40 | }; 41 | -------------------------------------------------------------------------------- /app/checks/all-files/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile, isCheater) { 4 | var tasks = []; 5 | 6 | if (markbotFile.html && markbotFile.allFiles && markbotFile.allFiles.html && markbotFile.allFiles.html.unique) { 7 | tasks.push({ 8 | group: `html-unique-${Date.now()}`, 9 | groupLabel: 'All files', 10 | options: { 11 | files: markbotFile.html, 12 | unique: markbotFile.allFiles.html.unique, 13 | }, 14 | }); 15 | } 16 | 17 | return tasks; 18 | }; 19 | -------------------------------------------------------------------------------- /app/checks/all-files/task.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 6 | const htmlUnique = require(__dirname + '/checks/all-files/html-unique'); 7 | 8 | const group = taskDetails.group; 9 | const id = 'html-unique'; 10 | const label = 'HTML unique content'; 11 | 12 | let uniqueCapture = {}; 13 | let uniqueErrors = []; 14 | 15 | markbotMain.send('check-group:item-new', group, id, label); 16 | markbotMain.send('check-group:item-computing', group, id, label); 17 | 18 | taskDetails.options.files.forEach(function (file) { 19 | let uniqueFinds = htmlUnique.find(taskDetails.cwd, file, taskDetails.options.unique); 20 | 21 | if (uniqueFinds === false) uniqueErrors.push(`The \`${file.path}\` file cannot be found`); 22 | 23 | for (let uniq in uniqueFinds) { 24 | if (!uniqueCapture[uniq]) uniqueCapture[uniq] = {}; 25 | if (!uniqueCapture[uniq][uniqueFinds[uniq]]) uniqueCapture[uniq][uniqueFinds[uniq]] = []; 26 | uniqueCapture[uniq][uniqueFinds[uniq]].push(file.path); 27 | } 28 | }); 29 | 30 | for (let uniq in uniqueCapture) { 31 | for (let content in uniqueCapture[uniq]) { 32 | if (uniqueCapture[uniq][content].length > 1) { 33 | uniqueErrors.push(`These files share the same \`${uniq}\` but they all should be unique: \`${uniqueCapture[uniq][content].join('`, `')}\``); 34 | } 35 | } 36 | } 37 | 38 | markbotMain.send('check-group:item-complete', group, id, label, uniqueErrors); 39 | done(); 40 | }()); 41 | -------------------------------------------------------------------------------- /app/checks/content.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const merge = require('merge-objects'); 5 | const markbotMain = require('../markbot-main'); 6 | const messageGroup = require(`${__dirname}/message-group`); 7 | 8 | const cleanRegex = function (regex) { 9 | return regex.replace(/\\(?!\\)/g, ''); 10 | }; 11 | 12 | const convertToCheckObject = function (search, defaultMessage) { 13 | let obj = { 14 | check: false, 15 | regex: false, 16 | limit: false, 17 | message: '', 18 | customMessage: '', 19 | type: 'error', 20 | }; 21 | 22 | if (typeof search === 'string') { 23 | obj.regex = search; 24 | } else { 25 | if (Array.isArray(search)) { 26 | if (search.length > 1) obj.message = search[1]; 27 | obj.regex = search[0]; 28 | } else { 29 | obj = Object.assign(obj, search); 30 | 31 | if (obj.check) obj.regex = obj.check; 32 | 33 | if (Array.isArray(obj.regex)) { 34 | if (obj.regex.length > 1) obj.message = obj.regex[1]; 35 | obj.regex = obj.regex[0]; 36 | } 37 | } 38 | } 39 | 40 | if (!obj.message) obj.message = defaultMessage.replace(/\{\{regex\}\}/g, cleanRegex(obj.regex)); 41 | 42 | return obj; 43 | }; 44 | 45 | const convertToHasObject = function (search) { 46 | return convertToCheckObject(search, 'Expected to see this content: `{{regex}}`'); 47 | }; 48 | 49 | const convertToHasNotObject = function (search) { 50 | return convertToCheckObject(search, 'Unexpected `{{regex}}` — `{{regex}}` should not be used'); 51 | }; 52 | 53 | const bypass = function (checkGroup, checkId, checkLabel) { 54 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 55 | }; 56 | 57 | const findSearchErrors = function (fileContents, searches) { 58 | let allMessages = messageGroup.new(); 59 | 60 | searches.forEach(function (searchItem) { 61 | let search = convertToHasObject(searchItem); 62 | let matches = fileContents.match(new RegExp(search.regex, 'gm')); 63 | 64 | if (!matches) { 65 | allMessages = messageGroup.bind(search, allMessages); 66 | } 67 | 68 | if (search.limit && matches.length > search.limit) { 69 | let plural = (search.limit === 1) ? '' : 's'; 70 | search.message = `Expected to see the \`${search.regex}\` content at most ${search.limit} time${plural}`; 71 | allMessages = messageGroup.bind(search, allMessages); 72 | } 73 | }); 74 | 75 | return allMessages; 76 | }; 77 | 78 | const findSearchNotErrors = function (fileContents, searchNot) { 79 | let allMessages = messageGroup.new(); 80 | 81 | searchNot.forEach(function (searchItem) { 82 | let search = convertToHasNotObject(searchItem); 83 | 84 | if (fileContents.match(new RegExp(search.regex, 'gm'))) { 85 | allMessages = messageGroup.bind(search, allMessages); 86 | } 87 | }); 88 | 89 | return allMessages; 90 | }; 91 | 92 | const check = function (checkGroup, checkId, checkLabel, fileContents, search, searchNot, next) { 93 | let allMessages = {}; 94 | 95 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 96 | 97 | if (search) allMessages = merge(allMessages, findSearchErrors(fileContents, search)); 98 | if (searchNot) allMessages = merge(allMessages, findSearchNotErrors(fileContents, searchNot)); 99 | 100 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, allMessages.errors, allMessages.messages, allMessages.warnings); 101 | next(); 102 | }; 103 | 104 | module.exports.init = function (group) { 105 | return (function (g) { 106 | let checkGroup = g; 107 | let checkLabel = 'Expected content'; 108 | let checkId = 'content'; 109 | 110 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 111 | 112 | return { 113 | check: function (fileContents, search, searchNot, next) { 114 | check(checkGroup, checkId, checkLabel, fileContents, search, searchNot, next); 115 | }, 116 | bypass: function () { 117 | bypass(checkGroup, checkId, checkLabel); 118 | } 119 | }; 120 | }(group)); 121 | }; 122 | -------------------------------------------------------------------------------- /app/checks/css/best-practices.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const linter = require('stylelint'); 5 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 6 | const viewportChecker = require(__dirname + '/best-practices/viewport'); 7 | 8 | const ERROR_MESSAGE_STATUS = require(`${__dirname}/../../error-message-status`); 9 | 10 | const getStyleLintConfig = function () { 11 | const stylelintConfig = require(`${__dirname}/best-practices/stylelint.json`); 12 | 13 | stylelintConfig.plugins.push(`${__dirname}/../../../node_modules/stylelint-declaration-block-no-ignored-properties`); 14 | 15 | return stylelintConfig; 16 | }; 17 | 18 | const shouldIncludeError = function (message, line, lines, fileContents) { 19 | /* SVG overflow: hidden CSS */ 20 | if (message.match(/selector-root-no-composition/) && lines[line - 1] && lines[line - 1].match(/svg/)) return false; 21 | if (message.match(/root-no-standard-properties/) && lines[line - 2] && lines[line - 2].match(/svg/)) return false; 22 | 23 | if (message.match(/at-rule-empty-line-before/) && lines[line - 1] && lines[line - 1].match(/@[-\w]*viewport/)) return false; 24 | 25 | return true; 26 | }; 27 | 28 | const bypass = function (checkGroup, checkId, checkLabel) { 29 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 30 | }; 31 | 32 | const check = function (checkGroup, checkId, checkLabel, fileContents, lines, next) { 33 | let errors = []; 34 | let checkViewport; 35 | const stylelintConfig = getStyleLintConfig(); 36 | 37 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 38 | 39 | checkViewport = viewportChecker.check(fileContents, lines); 40 | 41 | if (checkViewport && checkViewport.length > 0) { 42 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, checkViewport, false, false, ERROR_MESSAGE_STATUS.SKIP); 43 | return next(); 44 | } 45 | 46 | linter.lint({code: fileContents, config: stylelintConfig}) 47 | .then(function (data) { 48 | if (data.results && data.results[0]) { 49 | data.results[0].warnings.forEach(function (item) { 50 | if (shouldIncludeError(item.text, item.line, lines, fileContents)) { 51 | errors.push(util.format('Line %d: %s', item.line, item.text.replace(/\(.+?\)$/, '').replace(/"/g, '`'))); 52 | } 53 | }); 54 | } 55 | 56 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 57 | next(); 58 | }) 59 | .catch(function (err) { 60 | if (err.reason && err.line) { 61 | errors.push(`Line ${err.line}: ${err.reason}`); 62 | } else { 63 | errors.push('There was an error running the test—please refresh and try again'); 64 | } 65 | 66 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 67 | next(); 68 | }) 69 | ; 70 | }; 71 | 72 | module.exports.init = function (group) { 73 | return (function (g) { 74 | const checkGroup = g; 75 | const checkLabel = 'Best practices & indentation'; 76 | const checkId = 'best-practices'; 77 | 78 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 79 | 80 | return { 81 | check: function (fileContents, lines, next) { 82 | check(checkGroup, checkId, checkLabel, fileContents, lines, next); 83 | }, 84 | bypass: function () { 85 | bypass(checkGroup, checkId, checkLabel); 86 | } 87 | }; 88 | }(group)); 89 | }; 90 | -------------------------------------------------------------------------------- /app/checks/css/best-practices/viewport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.check = function (fileContents, lines) { 4 | let maxScale = /@viewport\s*\{[^}]*?max-zoom/im; 5 | let noUserScale = /@viewport\s*\{[^}]*?user-zoom\s*:\s*fixed/im; 6 | let errors = []; 7 | 8 | if (fileContents.match(maxScale)) errors.push(`The \`@viewport\` tag should never specify \`max-zoom\``); 9 | if (fileContents.match(noUserScale)) errors.push(`The \`@viewport\` tag should never specify \`user-zoom: fixed\``); 10 | 11 | return errors; 12 | }; 13 | -------------------------------------------------------------------------------- /app/checks/css/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile, isCheater) { 4 | var tasks = []; 5 | 6 | if (markbotFile.css) { 7 | markbotFile.css.forEach(function (file) { 8 | let task = { 9 | group: `css-${file.path}-${Date.now()}`, 10 | groupLabel: file.path, 11 | options: { 12 | file: file, 13 | cheater: (isCheater.matches[file.path]) ? !isCheater.matches[file.path].equal : (isCheater.cheated) ? true : false, 14 | }, 15 | }; 16 | 17 | tasks.push(task); 18 | }); 19 | } 20 | 21 | return tasks; 22 | }; 23 | -------------------------------------------------------------------------------- /app/checks/files/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile) { 4 | var tasks = []; 5 | 6 | if (markbotFile.files) { 7 | let task = { 8 | group: `files-${Date.now()}`, 9 | groupLabel: 'Files & images', 10 | options: { 11 | files: markbotFile.files, 12 | }, 13 | }; 14 | 15 | tasks.push(task); 16 | } 17 | 18 | return tasks; 19 | }; 20 | -------------------------------------------------------------------------------- /app/checks/functionality/defaults-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const get = function (file) { 7 | return fs.readFileSync(path.resolve(`${__dirname}/${file}`), 'utf8'); 8 | }; 9 | 10 | module.exports = { 11 | get: get, 12 | }; 13 | -------------------------------------------------------------------------------- /app/checks/functionality/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile) { 4 | var tasks = []; 5 | 6 | if (markbotFile.functionality) { 7 | let task = { 8 | group: `functionality-${Date.now()}`, 9 | groupLabel: 'Functionality', 10 | options: { 11 | files: markbotFile.functionality, 12 | }, 13 | }; 14 | 15 | tasks.push(task); 16 | } 17 | 18 | return tasks; 19 | }; 20 | -------------------------------------------------------------------------------- /app/checks/git/commits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const util = require('util'); 6 | const gitCommits = require('git-commits'); 7 | const exists = require(__dirname + '/../../file-exists'); 8 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 9 | 10 | const matchesProfEmail = function (email, profEmails) { 11 | return (profEmails.indexOf(email) > -1); 12 | }; 13 | 14 | module.exports.check = function (fullPath, commitNum, ignoreCommitEmails, group, next) { 15 | const repoPath = path.resolve(fullPath + '/.git'); 16 | const id = 'commits'; 17 | const label = 'Number of commits'; 18 | let studentCommits = 0; 19 | let errors = []; 20 | let exists = false; 21 | 22 | markbotMain.send('check-group:item-new', group, id, label); 23 | 24 | try { 25 | exists = fs.statSync(repoPath).isDirectory(); 26 | } catch (e) { 27 | exists = false; 28 | } 29 | 30 | if (!exists) { 31 | markbotMain.send('check-group:item-complete', group, id, label, ['Not a Git repository']); 32 | return next(); 33 | } 34 | 35 | markbotMain.send('check-group:item-computing', group, id, label); 36 | 37 | gitCommits(repoPath) 38 | .on('data', function (commit) { 39 | if (!matchesProfEmail(commit.author.email, ignoreCommitEmails)) studentCommits++; 40 | }) 41 | .on('end', function () { 42 | if (studentCommits < commitNum) { 43 | errors.push(util.format('Not enough commits to the repository (has %d, expecting %d)', studentCommits, commitNum)); 44 | } 45 | 46 | markbotMain.send('check-group:item-complete', group, id, label, errors); 47 | next(); 48 | }) 49 | .on('error', function (err) { 50 | markbotMain.send('check-group:item-complete', group, id, label, [`Not a Git repository or no commits`]); 51 | next(); 52 | }) 53 | ; 54 | }; 55 | -------------------------------------------------------------------------------- /app/checks/git/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const exec = require('child_process').exec; 6 | const gitState = require('git-state'); 7 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 8 | 9 | module.exports.check = function (fullPath, gitOpts, group, next) { 10 | const allSynced = 'all-synced'; 11 | const allSyncedLabel = 'Everything synced'; 12 | const allCommitted = 'all-committed'; 13 | const allCommittedLabel = 'Everything committed'; 14 | const errors = ['There was an error getting the status of your Git repository']; 15 | // const opts = { cwd: fullPath }; 16 | // const cmd = 'git status --porcelain -b'; 17 | 18 | let status; 19 | 20 | if (gitOpts.allSynced) { 21 | markbotMain.send('check-group:item-new', group, allSynced, allSyncedLabel); 22 | markbotMain.send('check-group:item-computing', group, allSynced, allSyncedLabel); 23 | } 24 | 25 | if (gitOpts.allCommitted) { 26 | markbotMain.send('check-group:item-new', group, allCommitted, allCommittedLabel); 27 | markbotMain.send('check-group:item-computing', group, allCommitted, allCommittedLabel); 28 | } 29 | 30 | gitState.check(fullPath, function (err, status) { 31 | if (err) { 32 | markbotMain.send('check-group:item-complete', group, allSynced, allSyncedLabel, errors); 33 | markbotMain.send('check-group:item-complete', group, allCommitted, allCommittedLabel, errors); 34 | return next(); 35 | } 36 | 37 | if (gitOpts.allSynced) { 38 | if (status.ahead > 0) { 39 | let plural = (status.ahead === 1) ? '' : 's'; 40 | let isOrAre = (status.ahead === 1) ? 'is' : 'are'; 41 | 42 | markbotMain.send('check-group:item-complete', group, allSynced, allSyncedLabel, [`There ${isOrAre} ${status.ahead} commit${plural} waiting to be pushed`]); 43 | } else { 44 | markbotMain.send('check-group:item-complete', group, allSynced, allSyncedLabel); 45 | } 46 | } 47 | 48 | if (gitOpts.allCommitted) { 49 | if (status.dirty > 0 || status.untracked > 0) { 50 | let totalFiles = status.dirty + status.untracked; 51 | let plural = (totalFiles === 1) ? '' : 's'; 52 | let isOrAre = (totalFiles === 1) ? 'is' : 'are'; 53 | 54 | markbotMain.send('check-group:item-complete', group, allCommitted, allCommittedLabel, [`There ${isOrAre} ${totalFiles} file${plural} waiting to be committed`]); 55 | } else { 56 | markbotMain.send('check-group:item-complete', group, allCommitted, allCommittedLabel); 57 | } 58 | } 59 | 60 | next(); 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /app/checks/git/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let config = require(__dirname + '/../../../config.json'); 4 | 5 | module.exports.generateTaskList = function (markbotFile) { 6 | var tasks = []; 7 | 8 | if (markbotFile.commits || markbotFile.git) { 9 | let task = { 10 | group: `git-${Date.now()}`, 11 | groupLabel: 'Git & GitHub', 12 | options: {}, 13 | }; 14 | 15 | if (!markbotFile.git && markbotFile.commits) { 16 | task.options = { 17 | numCommits: markbotFile.commits, 18 | }; 19 | } else { 20 | task.options = markbotFile.git; 21 | } 22 | 23 | task.options.ignoreCommitEmails = config.ignoreCommitEmails; 24 | 25 | tasks.push(task); 26 | } 27 | 28 | return tasks; 29 | }; 30 | -------------------------------------------------------------------------------- /app/checks/git/task.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 6 | const commits = require(__dirname + '/checks/git/commits'); 7 | const status = require(__dirname + '/checks/git/status'); 8 | const bestPractices = require(__dirname + '/checks/git/best-practices'); 9 | 10 | const fullPath = path.resolve(taskDetails.cwd); 11 | 12 | let checksToComplete = 0; 13 | 14 | const checkIfDone = function () { 15 | checksToComplete--; 16 | 17 | if (checksToComplete <= 0) done(); 18 | }; 19 | 20 | if (taskDetails.options.numCommits) { 21 | checksToComplete++; 22 | commits.check(fullPath, taskDetails.options.numCommits, taskDetails.options.ignoreCommitEmails, taskDetails.group, checkIfDone); 23 | } 24 | 25 | if (taskDetails.options.allCommitted || taskDetails.options.allSynced) { 26 | checksToComplete++; 27 | status.check(fullPath, taskDetails.options, taskDetails.group, checkIfDone); 28 | } 29 | 30 | if (taskDetails.options.bestPractices) { 31 | checksToComplete++; 32 | bestPractices.check(fullPath, taskDetails.options.ignoreCommitEmails, taskDetails.group, checkIfDone); 33 | } 34 | }()); 35 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/beautifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 2, 3 | "preserve_newlines": true, 4 | "max_preserve_newlines": 10, 5 | "wrap_line_length": 0, 6 | "end_with_newline": true, 7 | "extra_liners": [], 8 | "indent_scripts": "normal", 9 | "space_in_paren": false, 10 | "space_in_empty_paren": false, 11 | "space_after_anon_function": true, 12 | "unformatted": [ 13 | "a", "span", "img", "bdo", "em", "strong", "dfn", "code", "samp", "kbd", "data", "time", 14 | "cite", "abbr", "acronym", "q", "sub", "sup", "tt", "i", "b", "small", "u", "s", "strike", 15 | "var", "ins", "del", "pre", "address", "dt", "h1", "h2", "h3", "h4", "h5", "h6", "textarea", 16 | "path", "use", "ruby", "rt", "rp", "mark", "bdi", "br", "wbr" 17 | ], 18 | "js": { 19 | "end_with_newline": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/close-p-on-same-line.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const findClosingPTagLine = function (line, lines) { 4 | let newLine = line + 1, totalLines = lines.length; 5 | 6 | if (/<\/p>/i.test(lines[line])) return line; 7 | 8 | while (newLine < totalLines) { 9 | if (/

/i.test(lines[newLine])) return newLine; 11 | 12 | newLine++; 13 | } 14 | 15 | return false; 16 | }; 17 | 18 | const nextLineStartsWithTag = function (line, lines) { 19 | const startsWithTag = /^<\/?[a-z]+/i; 20 | const next = line + 1; 21 | 22 | if (next < lines.length - 1) { 23 | if (startsWithTag.test(lines[next].trim())) return true; 24 | } 25 | 26 | return false; 27 | }; 28 | 29 | const closingPrevLineStartsWithTag = function (line, lines) { 30 | const startsWithTag = /^<\/?[a-z]+/i; 31 | const closingLine = findClosingPTagLine(line, lines); 32 | const prev = closingLine - 1; 33 | 34 | if (prev > 0) { 35 | if (startsWithTag.test(lines[prev].trim())) return true; 36 | } 37 | 38 | return false; 39 | }; 40 | 41 | const isImmediatelyFollowedByTag = function (line, lines) { 42 | const immediatelyFollowedByTag = /^]*>\s*<[a-z]/i; 43 | 44 | return immediatelyFollowedByTag.test(lines[line].trim()); 45 | }; 46 | 47 | const isImmediatelyPrecededByTag = function (line, lines) { 48 | const immediatelyPrecededByTag = /<[^>]+>\s*<\/p/; 49 | 50 | return immediatelyPrecededByTag.test(lines[line].trim()); 51 | }; 52 | 53 | const isImmediatelyPrecededByText = function (line, lines) { 54 | const immediatelyPrecededByText = /(.+)<\/p/; 55 | const matches = lines[line].trim().match(immediatelyPrecededByText); 56 | 57 | if (!matches || !matches[1]) return false; 58 | if (matches[1].trim() === '') return false; 59 | 60 | return true; 61 | }; 62 | 63 | const hasUnwrappedText = function (line, lines, closingLine) { 64 | const thisLine = lines[line].trim(); 65 | const hasStuffAfterTag = /^]*>.+/i; 66 | const lineStartsWithTag = /^<[a-z0-9]+/i; 67 | const lineEndsWithTag = /<\/[a-z0-9]+>$/i; 68 | 69 | if ( 70 | !isImmediatelyFollowedByTag(line, lines) 71 | && hasStuffAfterTag.test(thisLine) 72 | ) return true; 73 | 74 | for (let i = line + 1; i < closingLine; i++) { 75 | if (!lineStartsWithTag.test(lines[i].trim())) return true; 76 | if (!lineEndsWithTag.test(lines[i].trim())) return true; 77 | } 78 | 79 | if (isImmediatelyPrecededByText(closingLine, lines) && !isImmediatelyPrecededByTag(closingLine, lines)) return true; 80 | 81 | return false; 82 | }; 83 | 84 | const hasBrTags = function (line, lines, closingLine) { 85 | for (let i = line; i <= closingLine; i++) { 86 | if (/
]/i; 96 | let endsWithP = /<\/p>$/i; 97 | 98 | for (i; i < total; i++) { 99 | let line = lines[i].trim(); 100 | 101 | if (line.match(startsWithP) && !line.match(endsWithP)) { 102 | let closingLine = findClosingPTagLine(i, lines); 103 | 104 | if (!closingLine) { 105 | errors.push(`Line ${i + 1}: The \`

\` tag is not closed, closing \`

\` tag is missing`); 106 | break; 107 | } 108 | 109 | if (hasBrTags(i, lines, closingLine)) continue; 110 | 111 | if (hasUnwrappedText(i, lines, closingLine)) { 112 | errors.push(`Line ${i + 1}: The \`

\` tag has text outside of elements, so the closing \`

\` tag should on the same line as the opening tag`); 113 | break; 114 | } 115 | 116 | if (isImmediatelyFollowedByTag(i, lines)) { 117 | errors.push(`Line ${i + 1}: The opening \`

\` tag should be on its own line`); 118 | break; 119 | } 120 | 121 | if (isImmediatelyPrecededByTag(closingLine, lines)) { 122 | errors.push(`Line ${i + 1}: The closing \`

\` tag should be on its own line`); 123 | break; 124 | } 125 | } 126 | } 127 | 128 | return errors; 129 | }; 130 | 131 | module.exports = { 132 | check: check, 133 | }; 134 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/code-style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const linter = require('htmlcs'); 4 | const htmlcsOptions = require('./htmlcs.json'); 5 | const voidElements = require('./void-elements.json'); 6 | const voidElementsSearch = `(${voidElements.join('|')})`; 7 | const svgTags = require('./svg-uppercase-tag-names.json'); 8 | const svgTagsSearch = `(${svgTags.join('|')})`; 9 | 10 | // Replace the lowercase attr & tag pairing rules to support embedded SVG 11 | linter.addRule(require('./rule-adv-attr-lowercase')); 12 | linter.addRule(require('./rule-adv-tag-pair')); 13 | 14 | const shouldIncludeError = function (line, message, lines) { 15 | // Don’t want indent checking from this library, use Beautify instead 16 | if (message.match(/indent/ig)) return false; 17 | 18 | // Ignore SVG multi-case tags 19 | if (message.match(/tagname.*lowercase/ig)) { 20 | // Not quite sure why it needs to be the previous line 21 | if (lines[line - 1].match(new RegExp(svgTagsSearch))) return false; 22 | } 23 | 24 | // Ignore SVG void elements 25 | if (message.match(/tag.*is.*not.*paired/ig)) { 26 | // Not quite sure why it needs to be the previous line 27 | if (lines[line - 1].match(new RegExp(voidElementsSearch))) return false; 28 | } 29 | 30 | return true; 31 | }; 32 | 33 | const escapeTags = function (message) { 34 | return message.replace(//g, '>`'); 35 | }; 36 | 37 | const deConfusifyError = function (line, message, lines) { 38 | let finalMessage = escapeTags(message); 39 | 40 | if (message.match(/Attribute value must be closed by double quotes/i)) { 41 | let matches = lines[line - 1].match(/\<[^>]+?([\w-]+?)\s*=\s+/); 42 | 43 | if (matches) finalMessage = `There’s a space before or after the equals sign of the \`${matches[1]}\` attribute`; 44 | } 45 | 46 | return `Line ${line}: ${finalMessage}`; 47 | }; 48 | 49 | module.exports.check = function (fileContents, lines) { 50 | let lintResults; 51 | let errors = []; 52 | 53 | lintResults = linter.hint(fileContents, htmlcsOptions) 54 | 55 | if (lintResults.length > 0) { 56 | lintResults.forEach((item) => { 57 | if (shouldIncludeError(item.line, item.message, lines)) { 58 | errors.push(deConfusifyError(item.line, item.message, lines)); 59 | } 60 | }); 61 | } 62 | 63 | return errors; 64 | }; 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/document-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.check = function (fileContents, lines) { 4 | let errors = []; 5 | let check; 6 | let checks = { 7 | '` tag', 8 | '` tag', 9 | '': 'Closing `` tag', 10 | '` tag', 11 | '': 'Closing `` tag', 12 | '': 'Closing `` tag', 13 | }; 14 | 15 | for (check in checks) { 16 | if (!fileContents.match(new RegExp(check, 'gim'))) { 17 | errors.push(`Missing tag: ${checks[check]}`); 18 | } 19 | } 20 | 21 | return errors; 22 | }; 23 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/double-space.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.check = function (fileContents, lines) { 4 | let errors = []; 5 | 6 | lines.forEach(function (line, i) { 7 | let lineTrim = line.trim(); 8 | 9 | if (lineTrim.match(/ {2,}/) && !lineTrim.match(/^\<(?:pre|textarea|code)/)) { 10 | let codeHunk = lineTrim.match(/.{0,15} {2,}.{0,15}/)[0]; 11 | codeHunk = codeHunk.replace(/( {2,})/, '~~$1~~'); 12 | 13 | errors.push(`Line ${i + 1}: There are extra spaces in the code around \`…${codeHunk}…\``); 14 | } 15 | }); 16 | 17 | return errors; 18 | }; 19 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/force-line-break-tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | "header", 3 | "nav", 4 | "footer", 5 | "main", 6 | "section", 7 | "article", 8 | "aside", 9 | 10 | "ul", 11 | "ol", 12 | "li", 13 | "dl", 14 | "dt", 15 | "dd", 16 | 17 | "h1", 18 | "h2", 19 | "h3", 20 | "h4", 21 | "h5", 22 | "h6", 23 | 24 | "figure", 25 | "figcaption", 26 | "picture", 27 | "video", 28 | "audio", 29 | "source", 30 | "track", 31 | 32 | "div", 33 | "blockquote", 34 | "hr", 35 | "canvas", 36 | "details", 37 | "summary", 38 | "menu", 39 | "menuitem", 40 | "dialog", 41 | 42 | "html", 43 | "head", 44 | "body", 45 | "title", 46 | "link", 47 | "meta", 48 | "noscript", 49 | "template", 50 | 51 | "embed", 52 | "object", 53 | "param", 54 | "map", 55 | "area", 56 | 57 | "table", 58 | "thead", 59 | "tbody", 60 | "tfoot", 61 | "tr", 62 | "td", 63 | 64 | "form", 65 | "fieldset", 66 | "label", 67 | "input", 68 | "button", 69 | "select", 70 | "datalist", 71 | "option", 72 | "optgroup", 73 | "keygen", 74 | "output", 75 | "progress", 76 | "meter", 77 | "legend", 78 | 79 | "svg", 80 | "circle", 81 | "rect", 82 | "path", 83 | "line", 84 | "image", 85 | "ellipse", 86 | "polyline", 87 | "polygon", 88 | "tref", 89 | "fePointLight", 90 | "feColorMatrix", 91 | "feGaussianBlur", 92 | "animate", 93 | "animateTransform", 94 | "mpath", 95 | "clipPath", 96 | "defs", 97 | "linearGradient", 98 | "desc", 99 | "feBlend", 100 | "feComponentTransfer", 101 | "feComposite", 102 | "feConvolveMatrix", 103 | "feDiffuseLighting", 104 | "feDisplacementMap", 105 | "feDistantLight", 106 | "feFlood", 107 | "feFuncA", 108 | "feFuncB", 109 | "feFuncG", 110 | "feFuncR", 111 | "feImage", 112 | "feMergeNode", 113 | "feMerge", 114 | "feMorphology", 115 | "feOffset", 116 | "feSpecularLighting", 117 | "feSpotlight", 118 | "feTile", 119 | "feTurbulence", 120 | "filter", 121 | "marker", 122 | "mask", 123 | "pattern", 124 | "radialGradient", 125 | "stop", 126 | "symbol", 127 | "text", 128 | "textPath", 129 | "g", 130 | 131 | "p", 132 | "th" 133 | ] 134 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/force-line-breaks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const forceLineBreak = require('./force-line-break-tags.json'); 4 | const specificLineBreakChecks = require('./specific-line-break-checks.json'); 5 | 6 | const notProperLineBreaks = function (line) { 7 | const lineRegEx = '\<\/?(' + forceLineBreak.join('|') + ')(?:(?: [^>]*\>)|(?:\>))\\s*\<\/?(' + forceLineBreak.join('|') + ')'; 8 | let generalChecks = line.match(new RegExp(lineRegEx, 'i')); 9 | let isEmptyTag = (generalChecks && generalChecks[1] && generalChecks[2]) ? new RegExp(`^<${generalChecks[1]}[^>]*>$`, '') : false; 10 | let i = 0, total = specificLineBreakChecks.length; 11 | let specificCheck; 12 | 13 | if (generalChecks && (isEmptyTag && !isEmptyTag.test(line.trim()))) return generalChecks; 14 | 15 | for (i ; i < total; i++) { 16 | specificCheck = line.match(new RegExp(specificLineBreakChecks[i])); 17 | 18 | if (specificCheck) return specificCheck; 19 | }; 20 | 21 | return false; 22 | } 23 | 24 | module.exports.check = function (fileContents, lines) { 25 | let errors = []; 26 | let i = 0; 27 | let total = lines.length; 28 | 29 | for (i; i < total; i++) { 30 | let lineBreakIssues = notProperLineBreaks(lines[i]); 31 | 32 | if (lineBreakIssues) { 33 | errors.push(`Line ${i + 1}: The \`<${lineBreakIssues[1]}>\` and \`<${lineBreakIssues[2]}>\` elements should be on different lines`); 34 | break; 35 | } 36 | } 37 | 38 | return errors; 39 | }; 40 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/htmlcs.json: -------------------------------------------------------------------------------- 1 | { 2 | "asset-type": true, 3 | "attr-no-duplication": true, 4 | "attr-value-double-quotes": true, 5 | "bool-attribute-value": true, 6 | "button-name": true, 7 | "button-type": false, 8 | "charset": true, 9 | "css-in-head": true, 10 | "doctype": true, 11 | "html-lang": true, 12 | "id-class-ad-disabled": true, 13 | "ie-edge": false, 14 | "img-alt": false, 15 | "img-src": true, 16 | "img-title": true, 17 | "img-width-height": false, 18 | "indent-char": "space-2", 19 | "lowercase-class-with-hyphen": true, 20 | "lowercase-id-with-hyphen": true, 21 | "rel-stylesheet": true, 22 | "script-content": true, 23 | "self-close": "no-close", 24 | "spec-char-escape": true, 25 | "style-content": true, 26 | "style-disabled": true, 27 | "tagname-lowercase": true, 28 | "title-required": true, 29 | "unique-id": true, 30 | "viewport": true, 31 | "max-error": 0, 32 | "format": {}, 33 | "linters": {}, 34 | "default": true, 35 | 36 | "attr-lowercase": false, 37 | "adv-attr-lowercase": true, 38 | "tag-pair": false, 39 | "adv-tag-pair": true 40 | } 41 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/htmlparser/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010, 2011, Chris Winberry . All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. -------------------------------------------------------------------------------- /app/checks/html/best-practices/indentation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const beautifier = require('js-beautify').html; 5 | const beautifierOptions = require('./beautifier.json'); 6 | 7 | // Work around for Beautifier’s wrap max limit of 32786 8 | // https://github.com/beautify-web/js-beautify/blob/master/js/lib/beautify-html.js#L118 9 | if (beautifierOptions.wrap_line_length == 0) { 10 | beautifierOptions.wrap_line_length = Number.MAX_SAFE_INTEGER; 11 | } 12 | 13 | const grabChunk = function (line, lines, beautifiedLines) { 14 | var hunk = { saw: [], expected: [], line: 0 }; 15 | 16 | if (line > 2) { 17 | hunk.saw.push(lines[line - 2]); 18 | hunk.expected.push(beautifiedLines[line - 2]); 19 | hunk.line++; 20 | } 21 | 22 | if (line > 1) { 23 | hunk.saw.push(lines[line - 1]); 24 | hunk.expected.push(beautifiedLines[line - 1]); 25 | hunk.line++; 26 | } 27 | 28 | hunk.saw.push(lines[line]); 29 | hunk.expected.push(beautifiedLines[line]); 30 | 31 | if (lines.length > line + 1 && beautifiedLines.length > line + 1) { 32 | hunk.saw.push(lines[line + 1]); 33 | hunk.expected.push(beautifiedLines[line + 1]); 34 | } 35 | 36 | if (lines.length > line + 2 && beautifiedLines.length > line + 2) { 37 | hunk.saw.push(lines[line + 2]); 38 | hunk.expected.push(beautifiedLines[line + 2]); 39 | } 40 | 41 | return hunk; 42 | }; 43 | 44 | const shouldThrowBreakingError = function (line1, line2) { 45 | // Ignore addition of space before self-closing slash, like `/>` 46 | if (line1.trim().replace(/\/>$/, '').trim() == line2.trim().replace(/\/>$/, '').trim()) return false; 47 | 48 | return true; 49 | }; 50 | 51 | const isCatchableSpacingError = function (line) { 52 | let attrSpaceMatch = line.match(/\<[^>]+?([\w-]+?)\s+=/); 53 | 54 | if (attrSpaceMatch) return `There’s a space before or after the equals sign of the \`${attrSpaceMatch[1]}\` attribute`; 55 | 56 | return false; 57 | }; 58 | 59 | module.exports.check = function (fileContents, lines) { 60 | var 61 | errors = [], 62 | beautified = '', 63 | i = 0, 64 | total = lines.length, 65 | orgFrontSpace = false, 66 | goodFrontSpace = false, 67 | beautifiedLines 68 | ; 69 | 70 | beautified = beautifier(fileContents, beautifierOptions); 71 | beautifiedLines = beautified.toString().split(/[\n\u0085\u2028\u2029]|\r\n?/g); 72 | 73 | for (i; i < total; i++) { 74 | orgFrontSpace = lines[i].match(/^(\s*)/); 75 | 76 | if (!beautifiedLines[i]) continue; 77 | 78 | if (lines[i].trim() != beautifiedLines[i].trim()) { 79 | if (shouldThrowBreakingError(lines[i], beautifiedLines[i])) { 80 | let catchableError = isCatchableSpacingError(lines[i]); 81 | 82 | if (catchableError) { 83 | errors.push(`Line: ${i + 1}: ${catchableError}`); 84 | } else { 85 | errors.push({ 86 | type: 'code-diff', 87 | message: util.format('Around line %d: Unexpected spacing or indentation', i + 1), 88 | code: grabChunk(i, lines, beautifiedLines), 89 | status: true 90 | }); 91 | } 92 | 93 | break; 94 | } 95 | } 96 | 97 | goodFrontSpace = beautifiedLines[i].match(/^(\s*)/); 98 | 99 | if (orgFrontSpace[1].length != goodFrontSpace[1].length) { 100 | errors.push(util.format('Line %d: Expected indentation depth of %d spaces', i + 1, goodFrontSpace[1].length)); 101 | } 102 | } 103 | 104 | return errors; 105 | }; 106 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/max-empty-lines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | 5 | module.exports.check = function (fileContents, lines) { 6 | let errors = []; 7 | let i = 0 8 | let total = lines.length; 9 | let emptyMax = 1; 10 | let emptyCount = 0; 11 | 12 | for (i; i < total; i++) { 13 | let line = lines[i].trim(); 14 | 15 | if (line != '' && emptyCount > emptyMax) break; 16 | 17 | if (line == '') { 18 | emptyCount++ 19 | 20 | if (emptyCount > emptyMax) { 21 | errors = [util.format('Line %d: Exceeded recommended number of empty lines (has %d, expected %d)', i, emptyCount, emptyMax)]; 22 | } 23 | } else { 24 | emptyCount = 0; 25 | } 26 | } 27 | 28 | return errors; 29 | }; 30 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/missing-optional-closing-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const htmlparser = require('./htmlparser/Parser'); 4 | const voidElements = require('./void-elements.json'); 5 | 6 | const shouldIncludeError = function (tag) { 7 | if (voidElements.indexOf(tag) > -1) return false; 8 | 9 | return true; 10 | }; 11 | 12 | module.exports.check = function (fileContents, lines) { 13 | let errors = []; 14 | let parser; 15 | let stack = fileContents.match(/^((?:.|([\n\u0085\u2028\u2029]|\r\n))*?)]*>/)[0]; 16 | 17 | parser = new htmlparser({ 18 | onopentagname: function (name, attr) { 19 | stack += '<' + name + '[^>]*>'; 20 | }, 21 | ontext: function (text) { 22 | stack += text; 23 | }, 24 | onimpliedclosetag: function (name) { 25 | if (shouldIncludeError(name)) { 26 | let errorLines = stack.split(/[\n\u0085\u2028\u2029]|\r\n?/g); 27 | errors.push(`Line ${errorLines.length - 1}: Missing closing \`\` tag`); 28 | } 29 | }, 30 | onclosetag: function (name) { 31 | stack += ']*>'; 32 | } 33 | }, { 34 | decodeEntities: true, 35 | lowerCaseTags: true 36 | }); 37 | 38 | parser.write(fileContents.match(/]*>((?:.|([\n\u0085\u2028\u2029]|\r\n))*?)<\/body>/im)[1]); 39 | parser.end(); 40 | 41 | return errors; 42 | }; 43 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/multi-case-attrs.json: -------------------------------------------------------------------------------- 1 | [ 2 | "attributename", 3 | "attributetype", 4 | "basefrequencey", 5 | "calcmode", 6 | "clippathunits", 7 | "contentscripttype", 8 | "contentstyletype", 9 | "diffuseconstant", 10 | "edgemode", 11 | "externalresourcesrequired", 12 | "filterres", 13 | "filterunits", 14 | "gradienttransform", 15 | "gradientunits", 16 | "kernelmatrix", 17 | "kernalunitlength", 18 | "keysplines", 19 | "keytimes", 20 | "limitingconeangle", 21 | "markerheight", 22 | "markerunits", 23 | "markerwidth", 24 | "maskcontentunits", 25 | "maskunits", 26 | "numoctaves", 27 | "pathlength", 28 | "patterncontentunits", 29 | "patterntransform", 30 | "patternunits", 31 | "pointsatx", 32 | "pointsaty", 33 | "pointsatz", 34 | "preservealpha", 35 | "preserveaspectratio", 36 | "primitiveunits", 37 | "repeatcount", 38 | "repeatdur", 39 | "requiredfeatures", 40 | "specularconstant", 41 | "specularexponent", 42 | "stddeviation", 43 | "stitchtiles", 44 | "surfacescale", 45 | "targetx", 46 | "targety", 47 | "textlength", 48 | "viewbox", 49 | "xchannelselector", 50 | "ychannelselector", 51 | "allowreorder", 52 | "autoreverse", 53 | "baseprofile", 54 | "lengthadjust", 55 | "keypoints", 56 | "requiredextensions", 57 | "spreadmethod", 58 | "startoffset", 59 | "zoomandpan", 60 | "viewtarget", 61 | "tablevalues", 62 | "systemlanguage" 63 | ] 64 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/rule-adv-attr-lowercase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const attrs = require('./multi-case-attrs.json'); 4 | 5 | module.exports = { 6 | name: 'adv-attr-lowercase', 7 | desc: 'Attribute name must be lowercase.', 8 | target: 'parser', 9 | 10 | lint: function (getCfg, parser, reporter) { 11 | parser.tokenizer.on('attribname', function (name) { 12 | if (!getCfg()) return; 13 | 14 | if (attrs.indexOf(name.toLowerCase()) > -1) return; 15 | 16 | if (name !== name.toLowerCase()) { 17 | reporter.warn( 18 | this._sectionStart, 19 | '029', 20 | 'Attribute name must be lowercase.' 21 | ); 22 | } 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/rule-adv-tag-pair.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const voidElements = require('./void-elements.json'); 4 | 5 | module.exports = { 6 | name: 'adv-tag-pair', 7 | desc: 'Tag must be paired.', 8 | target: 'parser', 9 | 10 | lint: function (getCfg, parser, reporter) { 11 | let stack = []; 12 | 13 | const check = function (tag) { 14 | if (voidElements.indexOf(tag.name) < 0) { 15 | reporter.warn( 16 | tag.pos, 17 | '035', 18 | 'Tag ' + tag.name + ' is not paired.' 19 | ); 20 | } 21 | }; 22 | 23 | parser.on('opentag', function (name) { 24 | stack.push({ 25 | name: name.toLowerCase(), 26 | pos: this.startIndex 27 | }); 28 | }); 29 | 30 | // do close & check unclosed tags 31 | parser.tokenizer.on('closetag', function (name) { 32 | if (!getCfg()) { 33 | return; 34 | } 35 | 36 | name = name.toLowerCase(); 37 | 38 | // find the matching tag 39 | var l = stack.length; 40 | var i = l - 1; 41 | for (; i >= 0; i--) { 42 | if (stack[i].name === name) { 43 | break; 44 | } 45 | } 46 | 47 | // if the matching tag found, 48 | // all tags after the macthing tag are unpaired 49 | if (i >= 0) { 50 | for (var j = i + 1; j < l; j++) { 51 | check(stack[j]); 52 | } 53 | stack = stack.slice(0, i); 54 | } 55 | }); 56 | 57 | // check left tags 58 | parser.on('end', function () { 59 | if (!getCfg()) { 60 | return; 61 | } 62 | 63 | // all unclosed tags in the end are unpaired 64 | stack.forEach(check); 65 | }); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/space-before-close-greater-than.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.check = function (fileContents, lines) { 4 | let errors = []; 5 | 6 | lines.forEach(function (line, i) { 7 | let lineTrim = line.trim(); 8 | 9 | if (lineTrim.match(/ \>/)) { 10 | let codeHunk = lineTrim.match(/.{0,15} \>.{0,15}/)[0]; 11 | codeHunk = codeHunk.replace(/( \>)/, '~~$1~~'); 12 | 13 | errors.push(`Line ${i + 1}: There are extra spaces before the closing \`>\` in this tag: \`…${codeHunk}…\``); 14 | } 15 | }); 16 | 17 | return errors; 18 | }; 19 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/specific-line-break-checks.json: -------------------------------------------------------------------------------- 1 | [ 2 | "\\<\/?(figure)[^>]*\\>\\s*\\<\/?(img)", 3 | "\\<\/?(img)[^>]*\\>\\s*\\<\/?(figcaption)", 4 | "\\<\/?(div)[^>]*\\>\\s*\\<\/?(img)" 5 | ] 6 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/svg-uppercase-tag-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | "fePointLight", 3 | "feColorMatrix", 4 | "feGaussianBlur", 5 | "animateTransform", 6 | "clipPath", 7 | "linearGradient", 8 | "feBlend", 9 | "feComponentTransfer", 10 | "feComposite", 11 | "feConvolveMatrix", 12 | "feDiffuseLighting", 13 | "feDisplacementMap", 14 | "feFlood", 15 | "feFuncA", 16 | "feFuncB", 17 | "feFuncG", 18 | "feFuncR", 19 | "feImage", 20 | "feMerge", 21 | "feMergeNode", 22 | "feMorphology", 23 | "feOffset", 24 | "feSpecularLighting", 25 | "feSpotlight", 26 | "feTile", 27 | "feTurbulence", 28 | "radialGradient", 29 | "textPath" 30 | ] 31 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/viewport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.check = function (fileContents, lines) { 4 | let maxScale = /]*?viewport[^>]*?maximum-scale/im; 5 | let noUserScale = /]*?viewport[^>]*?user-scalable\s*?=\s*no/im; 6 | let errors = []; 7 | 8 | if (fileContents.match(maxScale)) errors.push(`The \`viewport\` tag should never specify \`maximum-scale\``); 9 | if (fileContents.match(noUserScale)) errors.push(`The \`viewport\` tag should never specify \`user-scalable=no\``); 10 | 11 | return errors; 12 | }; 13 | -------------------------------------------------------------------------------- /app/checks/html/best-practices/void-elements.json: -------------------------------------------------------------------------------- 1 | [ 2 | "area", 3 | "base", 4 | "br", 5 | "col", 6 | "embed", 7 | "hr", 8 | "img", 9 | "input", 10 | "keygen", 11 | "link", 12 | "meta", 13 | "param", 14 | "source", 15 | "track", 16 | "wbr", 17 | 18 | "use", 19 | "circle", 20 | "rect", 21 | "path", 22 | "line", 23 | "image", 24 | "ellipse", 25 | "polyline", 26 | "polygon", 27 | "fePointLight", 28 | "feColorMatrix", 29 | "feMergeNode", 30 | "feOffset", 31 | "feTile", 32 | "feGaussianBlur", 33 | "feBlend", 34 | "feFlood", 35 | "feFuncA", 36 | "feFuncB", 37 | "feFuncG", 38 | "feFuncR", 39 | "feImage", 40 | "feComposite", 41 | "feSpotlight", 42 | "feMorphology", 43 | "fePointLight", 44 | "feTurbulence", 45 | "feDistantLight", 46 | "feDisplacementMap", 47 | "feConvolveMatrix", 48 | "stop", 49 | "animate", 50 | "animateTransform", 51 | "mpath", 52 | "tref" 53 | ] 54 | -------------------------------------------------------------------------------- /app/checks/html/outline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const parse5 = require('parse5'); 5 | const htmlparser2Adapter = require('parse5-htmlparser2-tree-adapter'); 6 | const cheerio = require('cheerio'); 7 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 8 | 9 | const getLevel = function (elem) { 10 | return parseInt(elem.name.slice(1), 10); 11 | } 12 | 13 | const bypass = function (checkGroup, checkId, checkLabel) { 14 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 15 | }; 16 | 17 | const check = function (checkGroup, checkId, checkLabel, fileContents, next) { 18 | let code = {}; 19 | let outline = []; 20 | let headings = []; 21 | let errors = []; 22 | let messages = []; 23 | let lastLevel = 1; 24 | let lastLevelText = ''; 25 | const parsedHtml = parse5.parse(fileContents, { 26 | treeAdapter: htmlparser2Adapter, 27 | sourceCodeLocationInfo: true, 28 | }); 29 | 30 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 31 | 32 | code = cheerio.load(parsedHtml.children); 33 | headings = code('h1, h2, h3, h4, h5, h6'); 34 | 35 | if (headings.length <= 0) { 36 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, ['There are no headings in the document — there should be at least an `

`']); 37 | return next(); 38 | } 39 | 40 | if (getLevel(headings[0]) != 1) { 41 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, [`Line ${headings[0].sourceCodeLocation.startLine}: The first heading in the document is an \`\` — but documents must start with an \`

\``]); 42 | return next(); 43 | } 44 | 45 | lastLevelText = headings.eq(0).html(); 46 | 47 | headings.each(function (i, elem) { 48 | let level = getLevel(elem); 49 | let text = cheerio(elem).text(); 50 | let outlineItem = { 51 | text: `\`\` ${text}`, 52 | depth: level, 53 | hasError: false, 54 | }; 55 | 56 | if (i === 0) { 57 | outline.push(outlineItem); 58 | return; 59 | } 60 | 61 | if (level == 1) { 62 | errors.push(`Line ${elem.sourceCodeLocation.startLine}: Another \`

\` was found, \`

${text}

\` — there can only be one \`

\` per page`); 63 | outlineItem.text = `\`\` ***${text}***`; 64 | outlineItem.hasError = true; 65 | } 66 | 67 | if (level > lastLevel + 1) { 68 | errors.push(`Line ${elem.sourceCodeLocation.startLine}: Heading, \`${text}\`, is level ${level} but the previous heading was level ${lastLevel} \`${lastLevelText}\``); 69 | outlineItem.text = `\`\` ***${text}***`; 70 | outlineItem.hasError = true; 71 | } 72 | 73 | outline.push(outlineItem); 74 | lastLevel = level; 75 | lastLevelText = cheerio(elem).html(); 76 | }); 77 | 78 | if (errors.length <= 0) { 79 | messages.push({ 80 | type: 'outline', 81 | message: 'The heading outline generated from the HTML is acceptable:', 82 | items: outline, 83 | }); 84 | } else { 85 | errors.push({ 86 | type: 'outline', 87 | message: 'The heading outline generated from the HTML is out of order:', 88 | items: outline, 89 | }); 90 | } 91 | 92 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors, messages); 93 | next(); 94 | }; 95 | 96 | module.exports.init = function (group) { 97 | return (function (g) { 98 | const checkGroup = g; 99 | const checkLabel = 'Heading structure'; 100 | const checkId = 'outline'; 101 | 102 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 103 | 104 | return { 105 | check: function (fileContents, next) { 106 | check(checkGroup, checkId, checkLabel, fileContents, next); 107 | }, 108 | bypass: function () { 109 | bypass(checkGroup, checkId, checkLabel); 110 | } 111 | }; 112 | }(group)); 113 | }; 114 | -------------------------------------------------------------------------------- /app/checks/html/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const taskPool = require(`${__dirname}/../../task-pool`); 4 | 5 | module.exports.generateTaskList = function (markbotFile, isCheater) { 6 | var tasks = []; 7 | 8 | if (markbotFile.html) { 9 | markbotFile.html.forEach(function (file) { 10 | let task = { 11 | group: `html-${file.path}-${Date.now()}`, 12 | groupLabel: file.path, 13 | options: { 14 | file: file, 15 | cheater: (isCheater.matches[file.path]) ? !isCheater.matches[file.path].equal : (isCheater.cheated) ? true : false, 16 | }, 17 | }; 18 | 19 | if (file.accessibility) { 20 | task.type = taskPool.TYPE_LIVE; 21 | } else { 22 | task.type = taskPool.TYPE_STATIC; 23 | } 24 | 25 | tasks.push(task); 26 | }); 27 | } 28 | 29 | return tasks; 30 | }; 31 | -------------------------------------------------------------------------------- /app/checks/html/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const path = require('path'); 5 | const http = require('http'); 6 | const exec = require('child_process').exec; 7 | const escapeShell = require(`${__dirname}/../../escape-shell`); 8 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 9 | const serverManager = require('electron').remote.require('./app/server-manager'); 10 | const userAgentService = require(`${__dirname}/../../user-agent-service`); 11 | 12 | const shouldIncludeError = function (message, line) { 13 | // The standard info: using HTML parser 14 | if (!line && message.match(/content-type.*text\/html/i)) return false; 15 | 16 | // The schema message 17 | if (!line && message.match(/schema.*html/i)) return false; 18 | 19 | // Google fonts validation error with vertical pipes 20 | if (message.match(/bad value.*fonts.*google.*\|/i)) return false; 21 | 22 | // Elements that "don't need" specific roles 23 | if (message.match(/element.*does not need.*role/i)) return false; 24 | 25 | return true; 26 | }; 27 | 28 | const bypass = function (checkGroup, checkId, checkLabel) { 29 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 30 | }; 31 | 32 | const check = function (checkGroup, checkId, checkLabel, fullPath, fileContents, lines, next) { 33 | const validatorPath = path.resolve(__dirname.replace(/app.asar[\/\\]/, 'app.asar.unpacked/') + '/../../../vendor/html-validator'); 34 | const hostInfo = serverManager.getHostInfo('html'); 35 | const crashMessage = 'Unable to connect to the HTML validator; the background process may have crashed. Please quit & restart Markbot.'; 36 | let messages = {}; 37 | let errors = []; 38 | 39 | const requestOpts = { 40 | hostname: hostInfo.hostname, 41 | port: hostInfo.port, 42 | path: '/?out=json&level=error&parser=html5', 43 | method: 'POST', 44 | protocol: `${hostInfo.protocol}:`, 45 | headers: { 46 | 'Content-Type': 'text/html; charset=utf-8', 47 | 'Content-Length': Buffer.byteLength(fileContents), 48 | 'User-Agent': userAgentService.get(), 49 | } 50 | }; 51 | 52 | const req = http.request(requestOpts, (res) => { 53 | let data = []; 54 | 55 | res.setEncoding('utf8'); 56 | 57 | res.on('data', (chunk) => { 58 | data.push(chunk); 59 | }); 60 | 61 | res.on('end', (chunk) => { 62 | if (data.length > 0) { 63 | try { 64 | messages = JSON.parse(data.join('')); 65 | } catch (e) { 66 | errors.push(crashMessage); 67 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 68 | markbotMain.send('restart', crashMessage); 69 | return next(errors); 70 | } 71 | 72 | if (messages.messages) { 73 | messages.messages.forEach(function (item) { 74 | if (shouldIncludeError(item.message, item.line)) { 75 | errors.push(util.format('Line %d: %s', item.lastLine, item.message.replace(/“/g, '`').replace(/”/g, '`'))); 76 | } 77 | }); 78 | } 79 | 80 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 81 | return next(errors); 82 | } else { 83 | errors.push(crashMessage); 84 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 85 | markbotMain.send('restart', crashMessage); 86 | return next(errors); 87 | } 88 | }); 89 | }); 90 | 91 | req.on('error', () => { 92 | errors.push(crashMessage); 93 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 94 | markbotMain.send('restart', crashMessage); 95 | return next(errors); 96 | }); 97 | 98 | markbotMain.debug(`@@${validatorPath}@@`); 99 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 100 | 101 | req.end(fileContents, 'utf8'); 102 | }; 103 | 104 | module.exports.init = function (group) { 105 | return (function (g) { 106 | const checkGroup = g; 107 | const checkId = 'validation'; 108 | const checkLabel = 'Validation'; 109 | 110 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 111 | 112 | return { 113 | check: function (fullPath, fileContents, lines, next) { 114 | check(checkGroup, checkId, checkLabel, fullPath, fileContents, lines, next); 115 | }, 116 | bypass: function () { 117 | bypass(checkGroup, checkId, checkLabel); 118 | } 119 | }; 120 | }(group)); 121 | }; 122 | -------------------------------------------------------------------------------- /app/checks/javascript/best-practices.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const linter = require('eslint').linter; 5 | const linterConfig = require(__dirname + '/best-practices/eslint.json'); 6 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 7 | 8 | const bypass = function (checkGroup, checkId, checkLabel) { 9 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 10 | }; 11 | 12 | const check = function (checkGroup, checkId, checkLabel, fileContents, lines, next) { 13 | let messages = {}; 14 | let errors = []; 15 | 16 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 17 | messages = linter.verify(fileContents, linterConfig); 18 | 19 | if (messages) { 20 | messages.forEach(function (item) { 21 | errors.push(util.format('Line %d: %s', item.line, item.message.replace(/\.$/, ''))); 22 | }); 23 | } 24 | 25 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 26 | next(); 27 | }; 28 | 29 | module.exports.init = function (group) { 30 | return (function (g) { 31 | const checkGroup = g; 32 | const checkLabel = 'Best practices & indentation'; 33 | const checkId = 'best-practices'; 34 | 35 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 36 | 37 | return { 38 | check: function (fileContents, lines, next) { 39 | check(checkGroup, checkId, checkLabel, fileContents, lines, next); 40 | }, 41 | bypass: function () { 42 | bypass(checkGroup, checkId, checkLabel); 43 | } 44 | }; 45 | }(group)); 46 | }; 47 | -------------------------------------------------------------------------------- /app/checks/javascript/best-practices/eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6 4 | }, 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "es6": true, 9 | "jquery": true, 10 | "worker": true, 11 | "serviceworker": true 12 | }, 13 | "rules": { 14 | "array-bracket-spacing": 2, 15 | "block-spacing": 2, 16 | "brace-style": 2, 17 | "camelcase": 2, 18 | "comma-spacing": 2, 19 | "comma-style": 2, 20 | "computed-property-spacing": 2, 21 | "eol-last": 2, 22 | "indent": [2, 2, {"SwitchCase": 1}], 23 | "jsx-quotes": [2, "prefer-double"], 24 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 25 | "keyword-spacing": 2, 26 | "linebreak-style": [2, "unix"], 27 | "lines-around-comment": 2, 28 | "max-depth": 2, 29 | "max-nested-callbacks": 2, 30 | "new-cap": 2, 31 | "new-parens": 2, 32 | "newline-after-var": 2, 33 | "newline-per-chained-call": [2, {"ignoreChainWithDepth": 3}], 34 | "no-array-constructor": 2, 35 | "no-bitwise": 2, 36 | "no-lonely-if": 2, 37 | "no-mixed-spaces-and-tabs": 2, 38 | "no-multiple-empty-lines": [2, {"max": 1, "maxEOF": 1, "maxBOF": 0}], 39 | "no-negated-condition": 2, 40 | "no-nested-ternary": 2, 41 | "no-new-object": 2, 42 | "no-spaced-func": 2, 43 | "no-trailing-spaces": 2, 44 | "no-underscore-dangle": 2, 45 | "no-unneeded-ternary": 2, 46 | "no-whitespace-before-property": 2, 47 | "object-curly-spacing": 2, 48 | "operator-assignment": 2, 49 | "quotes": [2, "single", "avoid-escape"], 50 | "semi-spacing": [2, {"before": false, "after": true}], 51 | "space-before-blocks": 2, 52 | "space-before-function-paren": 2, 53 | "space-in-parens": 2, 54 | "space-infix-ops": 2, 55 | "space-unary-ops": 2, 56 | "spaced-comment": 2, 57 | "wrap-regex": 2 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/checks/javascript/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile, isCheater) { 4 | var tasks = []; 5 | 6 | if (markbotFile.js) { 7 | markbotFile.js.forEach(function (file) { 8 | let task = { 9 | group: `js-${file.path}-${Date.now()}`, 10 | groupLabel: file.path, 11 | options: { 12 | file: file, 13 | cheater: (isCheater.matches[file.path]) ? !isCheater.matches[file.path].equal : (isCheater.cheated) ? true : false, 14 | }, 15 | }; 16 | 17 | tasks.push(task); 18 | }); 19 | } 20 | 21 | return tasks; 22 | }; 23 | -------------------------------------------------------------------------------- /app/checks/javascript/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const path = require('path'); 5 | const linter = require('eslint').linter; 6 | const linterConfig = require(__dirname + '/validation/eslint.json'); 7 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 8 | 9 | const bypass = function (checkGroup, checkId, checkLabel) { 10 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 11 | }; 12 | 13 | const check = function (checkGroup, checkId, checkLabel, fileContents, lines, next) { 14 | let messages = {}; 15 | let errors = []; 16 | 17 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 18 | messages = linter.verify(fileContents, linterConfig); 19 | 20 | if (messages) { 21 | messages.forEach(function (item) { 22 | errors.push(util.format('Line %d: %s', item.line, item.message.replace(/\.$/, ''))); 23 | }); 24 | } 25 | 26 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 27 | next(errors); 28 | }; 29 | 30 | module.exports.init = function (group) { 31 | return (function (g) { 32 | const checkGroup = g; 33 | const checkId = 'validation'; 34 | const checkLabel = 'Validation'; 35 | 36 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 37 | 38 | return { 39 | check: function (fileContents, lines, next) { 40 | check(checkGroup, checkId, checkLabel, fileContents, lines, next); 41 | }, 42 | bypass: function () { 43 | bypass(checkGroup, checkId, checkLabel); 44 | } 45 | }; 46 | }(group)); 47 | }; 48 | -------------------------------------------------------------------------------- /app/checks/javascript/validation/eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 6 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true, 10 | "jquery": true, 11 | "worker": true, 12 | "serviceworker": true 13 | }, 14 | "rules": { 15 | "valid-jsdoc": 2, 16 | "array-callback-return": 2, 17 | "complexity": 2, 18 | "consistent-return": 2, 19 | "curly": 2, 20 | "dot-location": [2, "property"], 21 | "dot-notation": 2, 22 | "guard-for-in": 2, 23 | "no-caller": 2, 24 | "no-div-regex": 2, 25 | "no-else-return": 2, 26 | "no-empty-function": 2, 27 | "no-eq-null": 2, 28 | "no-eval": 2, 29 | "no-extend-native": 2, 30 | "no-extra-bind": 2, 31 | "no-extra-label": 2, 32 | "no-floating-decimal": 2, 33 | "no-implicit-coercion": [2, { "boolean": false }], 34 | "no-implied-eval": 2, 35 | "no-invalid-this": 2, 36 | "no-iterator": 2, 37 | "no-lone-blocks": 2, 38 | "no-multi-str": 2, 39 | "no-native-reassign": 2, 40 | "no-new": 2, 41 | "no-new-func": 2, 42 | "no-new-wrappers": 2, 43 | "no-octal-escape": 2, 44 | "no-param-reassign": 2, 45 | "no-process-env": 2, 46 | "no-proto": 2, 47 | "no-return-assign": 2, 48 | "no-script-url": 2, 49 | "no-self-compare": 2, 50 | "no-sequences": 2, 51 | "no-throw-literal": 2, 52 | "no-unmodified-loop-condition": 2, 53 | "no-unused-expressions": 2, 54 | "no-useless-call": 2, 55 | "no-useless-concat": 2, 56 | "no-void": 2, 57 | "no-with": 2, 58 | "radix": 2, 59 | "vars-on-top": 2, 60 | "wrap-iife": 2, 61 | "yoda": 2, 62 | "no-catch-shadow": 2, 63 | "no-label-var": 2, 64 | "no-shadow": 2, 65 | "no-shadow-restricted-names": 2, 66 | "no-undef-init": 2, 67 | "no-use-before-define": 2, 68 | "arrow-parens": 2, 69 | "arrow-spacing": 2, 70 | "generator-star-spacing": [2, { "before": true, "after": true }], 71 | "no-confusing-arrow": 2, 72 | "no-const-assign": 2, 73 | "no-useless-constructor": 2, 74 | "semi": 2 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/checks/live-website/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile) { 4 | var tasks = []; 5 | 6 | if (markbotFile.liveWebsite && markbotFile.repo) { 7 | let task = { 8 | group: `live-website-${Date.now()}`, 9 | groupLabel: 'Live website', 10 | options: { 11 | repo: markbotFile.repo, 12 | username: markbotFile.username, 13 | }, 14 | }; 15 | 16 | tasks.push(task); 17 | } 18 | 19 | return tasks; 20 | }; 21 | -------------------------------------------------------------------------------- /app/checks/live-website/task.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const https = require('https'); 5 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 6 | const userAgentService = require(`${__dirname}/user-agent-service`); 7 | 8 | const group = taskDetails.group; 9 | const repo = taskDetails.options.repo; 10 | const username = taskDetails.options.username; 11 | const id = 'live-website'; 12 | const label = 'Online'; 13 | const errors = [`**Your website is not online.** Double check that all the commits have been pushed & that the \`index.html\` file, on GitHub’s website, follows the naming conventions. @@https://${username.toLowerCase()}.github.io/${repo}/@@`]; 14 | const dtNow = Date.now(); 15 | const opts = { 16 | method: 'HEAD', 17 | host: `${username.toLowerCase()}.github.io`, 18 | path: `/${repo}/?dt=${dtNow}`, 19 | headers: { 20 | 'User-Agent': userAgentService.get(), 21 | } 22 | }; 23 | 24 | markbotMain.send('check-group:item-new', group, id, label); 25 | markbotMain.send('check-group:item-computing', group, id, label); 26 | 27 | https.get(opts, function (res) { 28 | if(res.statusCode >= 200 && res.statusCode <= 299) { 29 | markbotMain.send('check-group:item-complete', group, id, label, false, [`**Your website is online!** Check it out in your browser or on your mobile device: @@https://${username.toLowerCase()}.github.io/${repo}/@@`]); 30 | } else { 31 | markbotMain.send('check-group:item-complete', group, id, label, errors); 32 | } 33 | 34 | done(); 35 | }).on('error', function (e) { 36 | markbotMain.send('check-group:item-complete', group, id, label, errors); 37 | done(); 38 | }); 39 | }()); 40 | -------------------------------------------------------------------------------- /app/checks/markdown/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile, isCheater) { 4 | var tasks = []; 5 | 6 | if (markbotFile.md) { 7 | markbotFile.md.forEach(function (file) { 8 | let task = { 9 | group: `md-${file.path}-${Date.now()}`, 10 | groupLabel: file.path, 11 | options: { 12 | file: file, 13 | cheater: (isCheater.matches[file.path]) ? !isCheater.matches[file.path].equal : (isCheater.cheated) ? true : false, 14 | }, 15 | }; 16 | 17 | tasks.push(task); 18 | }); 19 | } 20 | 21 | return tasks; 22 | }; 23 | -------------------------------------------------------------------------------- /app/checks/markdown/task.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 7 | const exists = require(__dirname + '/file-exists'); 8 | const validation = require(__dirname + '/checks/markdown/validation'); 9 | const content = require(__dirname + '/checks/content'); 10 | 11 | const group = taskDetails.group; 12 | const file = taskDetails.options.file; 13 | const isCheater = taskDetails.options.cheater; 14 | 15 | let checksToComplete = 0; 16 | 17 | const checkIfDone = function () { 18 | checksToComplete--; 19 | 20 | if (checksToComplete <= 0) done(); 21 | }; 22 | 23 | const check = function () { 24 | const fullPath = path.resolve(taskDetails.cwd + '/' + file.path); 25 | let errors = []; 26 | let fileContents = ''; 27 | let validationChecker; 28 | let contentChecker; 29 | 30 | const bypassAllChecks = function (f) { 31 | checksToComplete = 0; 32 | 33 | if (f.valid) validationChecker.bypass(); 34 | if (f.search || f.searchNot) contentChecker.bypass(); 35 | }; 36 | 37 | checksToComplete++; 38 | markbotMain.send('check-group:item-new', group, 'exists', 'Exists'); 39 | 40 | if (file.locked) { 41 | checksToComplete++; 42 | markbotMain.send('check-group:item-new', group, 'unchanged', 'Unchanged'); 43 | 44 | if (isCheater) { 45 | markbotMain.send('check-group:item-complete', group, 'unchanged', 'Unchanged', [`The \`${file.path}\` should not be changed`]); 46 | } else { 47 | markbotMain.send('check-group:item-complete', group, 'unchanged', 'Unchanged'); 48 | } 49 | 50 | checkIfDone(); 51 | } else { 52 | if (file.valid) { 53 | checksToComplete++; 54 | validationChecker = validation.init(group); 55 | } 56 | 57 | if (file.search || file.searchNot) { 58 | checksToComplete++; 59 | contentChecker = content.init(group); 60 | } 61 | } 62 | 63 | if (!exists.check(fullPath)) { 64 | markbotMain.send('check-group:item-complete', group, 'exists', 'Exists', [`The file \`${file.path}\` is missing or misspelled`]); 65 | bypassAllChecks(file); 66 | checkIfDone(); 67 | return; 68 | } 69 | 70 | fs.readFile(fullPath, 'utf8', function (err, fileContents) { 71 | if (fileContents.trim() == '') { 72 | markbotMain.send('check-group:item-complete', group, 'exists', 'Exists', [`The file \`${file.path}\` is empty`]); 73 | bypassAllChecks(file); 74 | checkIfDone(); 75 | return; 76 | } 77 | 78 | markbotMain.send('check-group:item-complete', group, 'exists', 'Exists'); 79 | checkIfDone(); 80 | 81 | if (file.locked) return; 82 | 83 | if (file.valid) validationChecker.check(fullPath, fileContents, checkIfDone); 84 | 85 | if (file.search || file.searchNot) { 86 | if (file.search && !file.searchNot) contentChecker.check(fileContents, file.search, [], checkIfDone); 87 | if (!file.search && file.searchNot) contentChecker.check(fileContents, [], file.searchNot, checkIfDone); 88 | if (file.search && file.searchNot) contentChecker.check(fileContents, file.search, file.searchNot, checkIfDone); 89 | } 90 | }); 91 | }; 92 | 93 | check(); 94 | }()); 95 | -------------------------------------------------------------------------------- /app/checks/markdown/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const util = require('util'); 5 | const markdownlint = require('markdownlint'); 6 | const frontMatter = require('front-matter'); 7 | const S = require('string'); 8 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 9 | const markdownLintConfig = require(`${__dirname}/validation/markdownlint.json`); 10 | 11 | const bypass = function (checkGroup, checkId, checkLabel) { 12 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 13 | }; 14 | 15 | const check = function (checkGroup, checkId, checkLabel, fullPath, fileContents, next) { 16 | let filename = path.parse(fullPath).base; 17 | let fm; 18 | let errors = []; 19 | let markdownlintOpts = { 20 | config: markdownLintConfig, 21 | strings: {}, 22 | }; 23 | 24 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 25 | markdownlintOpts.strings[filename] = fileContents; 26 | 27 | try { 28 | fm = frontMatter(fileContents); 29 | } catch (e) { 30 | if (e.reason && e.mark) { 31 | let line = e.mark.line + 1; 32 | let reason = S(e.reason).humanize(); 33 | 34 | errors.push(`Line ${line}: ${reason}`); 35 | } else { 36 | let reason = (e.reason) ? ' — ' + S(e.reason).humanize() : ''; 37 | 38 | errors.push(`There was a validation error in the YAML front matter${reason}`); 39 | } 40 | } 41 | 42 | markdownlint(markdownlintOpts, (err, results) => { 43 | if (err) errors.push('There was an error validating the Markdown file'); 44 | 45 | if (results && results[filename]) { 46 | results[filename].forEach((item) => { 47 | let details = (item.errorDetail) ? ` — ${item.errorDetail}` : ''; 48 | let context = (item.errorContext) ? `, near \`${item.errorContext}\`` : ''; 49 | 50 | errors.push(`Line ${item.lineNumber}: ${item.ruleDescription}${details}${context}`); 51 | }); 52 | } 53 | 54 | if (errors.length > 0) { 55 | errors.unshift({ 56 | type: 'intro', 57 | message: 'Refer to the Markdown & YAML cheat sheet to help understand these errors:', 58 | link: 'https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/', 59 | linkText: 'https://mkbt.io/md-yml-cheat-sheet/', 60 | }); 61 | } 62 | 63 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 64 | next(errors); 65 | }); 66 | }; 67 | 68 | module.exports.init = function (group) { 69 | return (function (g) { 70 | const checkGroup = g; 71 | const checkId = 'validation'; 72 | const checkLabel = 'Validation & best practices'; 73 | 74 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 75 | 76 | return { 77 | check: function (fullPath, fileContents, next) { 78 | check(checkGroup, checkId, checkLabel, fullPath, fileContents, next); 79 | }, 80 | bypass: function () { 81 | bypass(checkGroup, checkId, checkLabel); 82 | } 83 | }; 84 | }(group)); 85 | }; 86 | -------------------------------------------------------------------------------- /app/checks/markdown/validation/markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "header-increment": false, 3 | "first-header-h1": false, 4 | "header-style": { 5 | "style": "atx" 6 | }, 7 | "no-missing-space-atx": true, 8 | "no-multiple-space-atx": true, 9 | "blanks-around-headers": true, 10 | "header-start-left": true, 11 | "no-duplicate-header": false, 12 | "single-h1": true, 13 | "no-trailing-punctuation": { 14 | "punctuation": ",;:" 15 | }, 16 | "ul-style": { 17 | "style": "dash" 18 | }, 19 | "list-indent": true, 20 | "ul-start-left": true, 21 | "ul-indent": { 22 | "indent": 2 23 | }, 24 | "ol-prefix": { 25 | "style": "ordered" 26 | }, 27 | "list-marker-space": { 28 | "ul_single": 1, 29 | "ol_single": 1, 30 | "ul_multi": 1, 31 | "ol_multi": 1 32 | }, 33 | "blanks-around-lists": true, 34 | "no-trailing-spaces": { 35 | "br_spaces": 0 36 | }, 37 | "no-hard-tabs": true, 38 | "no-reversed-links": true, 39 | "no-multiple-blanks": true, 40 | "line-length": false, 41 | "commands-show-output": true, 42 | "no-multiple-space-blockquote": true, 43 | "no-blanks-blockquote": true, 44 | "blanks-around-fences": true, 45 | "no-inline-html": false, 46 | "no-bare-urls": true, 47 | "hr-style": { 48 | "style": "---" 49 | }, 50 | "no-emphasis-as-header": true, 51 | "no-space-in-emphasis": true, 52 | "no-space-in-code": true, 53 | "no-space-in-links": true, 54 | "fenced-code-language": true, 55 | "first-line-h1": false, 56 | "no-empty-links": true 57 | } 58 | -------------------------------------------------------------------------------- /app/checks/message-group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createMessageGroup = function () { 4 | return { 5 | errors: [], 6 | warnings: [], 7 | messages: [], 8 | }; 9 | }; 10 | 11 | const bindMessageGroup = function (check, allMessages) { 12 | let messageBefore = ''; 13 | let theMessage = (check.customMessage) ? check.customMessage : check.message; 14 | 15 | if (check.lines) { 16 | let plural = (check.lines.length > 1) ? 's' : ''; 17 | 18 | messageBefore = `Line${plural} ${check.lines.join(', ')}: `; 19 | } 20 | 21 | switch (check.type) { 22 | case 'warning': 23 | allMessages.warnings.push(`${messageBefore}${theMessage}`); 24 | break; 25 | case 'message': 26 | allMessages.messages.push(`${messageBefore}${theMessage}`); 27 | break; 28 | default: 29 | allMessages.errors.push(`${messageBefore}${theMessage}`); 30 | } 31 | 32 | return allMessages; 33 | }; 34 | 35 | module.exports = { 36 | new: createMessageGroup, 37 | bind: bindMessageGroup, 38 | }; 39 | -------------------------------------------------------------------------------- /app/checks/naming-conventions/extension-blacklist.json: -------------------------------------------------------------------------------- 1 | [ 2 | "pdf", 3 | "ai", 4 | "psd", 5 | "indd", 6 | "indb", 7 | 8 | "zip", 9 | "gz", 10 | "tar", 11 | "7z", 12 | 13 | "otf", 14 | "ttf", 15 | "woff", 16 | "eot", 17 | "ttc", 18 | 19 | "mov", 20 | "mp4", 21 | "m4v", 22 | "f4v", 23 | "f4p", 24 | "ogv", 25 | "webm", 26 | "flv", 27 | "mp3", 28 | "m4a", 29 | "f4a", 30 | "f4b", 31 | "oga", 32 | "ogg", 33 | "opus" 34 | ] 35 | -------------------------------------------------------------------------------- /app/checks/naming-conventions/file-blacklist.json: -------------------------------------------------------------------------------- 1 | [ 2 | "npm-debug.log" 3 | ] 4 | -------------------------------------------------------------------------------- /app/checks/naming-conventions/path-whitelist.json: -------------------------------------------------------------------------------- 1 | [ 2 | ".git" 3 | ] 4 | -------------------------------------------------------------------------------- /app/checks/naming-conventions/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile) { 4 | var tasks = []; 5 | 6 | if (markbotFile.naming || markbotFile.restrictFileTypes) { 7 | let task = { 8 | group: `naming-${Date.now()}`, 9 | groupLabel: 'Naming & file restrictions', 10 | options: {}, 11 | }; 12 | 13 | if (markbotFile.naming) task.options.naming = true; 14 | if (markbotFile.restrictFileTypes) task.options.restrictFileTypes = true; 15 | if (markbotFile.namingIgnore) task.options.namingIgnore = markbotFile.namingIgnore; 16 | 17 | tasks.push(task); 18 | } 19 | 20 | return tasks; 21 | }; 22 | -------------------------------------------------------------------------------- /app/checks/naming-conventions/task.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 6 | const listDir = require(__dirname + '/list-dir'); 7 | const stripPath = require(__dirname + '/strip-path'); 8 | 9 | const extsBlackList = require(__dirname + '/checks/naming-conventions/extension-blacklist.json'); 10 | const extsBlackListSearch = `(${extsBlackList.join('|')})$`; 11 | const fileBlackList = require(__dirname + '/checks/naming-conventions/file-blacklist.json'); 12 | const fileBlackListSearch = `(${fileBlackList.join('|')})`; 13 | const pathsWhiteList = require(__dirname + '/checks/naming-conventions/path-whitelist.json'); 14 | const pathsWhiteListSearch = `^(${pathsWhiteList.join('|').replace(/\./ig, '\.')})`; 15 | 16 | const fullPath = path.resolve(taskDetails.cwd); 17 | 18 | const checkNaming = function (file) { 19 | const retinaGraphics = /\@2x\.(jpg|png)$/g; 20 | let cleanFile = stripPath(file, fullPath); 21 | let errors = []; 22 | 23 | if (retinaGraphics.test(cleanFile)) { 24 | cleanFile = cleanFile.replace(retinaGraphics, ''); 25 | } 26 | 27 | if (cleanFile !== cleanFile.replace(/[^a-z0-9\-\.\/\\]/g, '')) { 28 | errors.push(`\`${cleanFile.replace(/\\/, '/')}\`: Doesn’t follow naming conventions`); 29 | } 30 | 31 | return errors; 32 | }; 33 | 34 | const checkRestrictedFiles = function (file) { 35 | const cleanFile = stripPath(file, fullPath); 36 | let errors = []; 37 | 38 | if (cleanFile.match(pathsWhiteListSearch)) return; 39 | 40 | if (cleanFile.match(extsBlackListSearch) || cleanFile.match(fileBlackListSearch)) { 41 | errors.push(`\`${cleanFile}\`: Shouldn’t be inside the repository`); 42 | } 43 | 44 | return errors; 45 | }; 46 | 47 | const namingLabel = 'Consistent naming'; 48 | const restrictedLabel = 'Restricted files'; 49 | 50 | if (taskDetails.options.naming) { 51 | markbotMain.send('check-group:item-new', taskDetails.group, 'naming', namingLabel); 52 | markbotMain.send('check-group:item-computing', taskDetails.group, 'naming'); 53 | } 54 | 55 | if (taskDetails.options.restrictFileTypes) { 56 | markbotMain.send('check-group:item-new', taskDetails.group, 'file-types', restrictedLabel); 57 | markbotMain.send('check-group:item-computing', taskDetails.group, 'file-types'); 58 | } 59 | 60 | listDir(fullPath, function(files) { 61 | let namingErrors = []; 62 | let restrictedErrors = []; 63 | 64 | const introError = { 65 | type: 'intro', 66 | message: 'Refer to the naming conventions cheat sheet to help understand these errors:', 67 | link: 'https://learn-the-web.algonquindesign.ca/topics/naming-paths-cheat-sheet/', 68 | linkText: 'https://mkbt.io/name-cheat-sheet/', 69 | }; 70 | 71 | files.forEach(function (file) { 72 | let ignore = false; 73 | 74 | if (taskDetails.options.namingIgnore) { 75 | taskDetails.options.namingIgnore.forEach(function (ignoreFile) { 76 | if (file.endsWith(ignoreFile)) ignore = true; 77 | }) 78 | } 79 | 80 | if (ignore) return; 81 | 82 | if (taskDetails.options.naming) namingErrors = namingErrors.concat(checkNaming(file)); 83 | if (taskDetails.options.restrictFileTypes) restrictedErrors = restrictedErrors.concat(checkRestrictedFiles(file)); 84 | }); 85 | 86 | if (taskDetails.options.naming) { 87 | if (namingErrors.length > 0) namingErrors.unshift(introError); 88 | 89 | markbotMain.send('check-group:item-complete', taskDetails.group, 'naming', namingLabel, namingErrors); 90 | } 91 | 92 | if (taskDetails.options.restrictFileTypes) { 93 | if (restrictedErrors.length > 0) restrictedErrors.unshift(introError); 94 | 95 | markbotMain.send('check-group:item-complete', taskDetails.group, 'file-types', restrictedLabel, restrictedErrors); 96 | } 97 | 98 | done(); 99 | }); 100 | }()); 101 | -------------------------------------------------------------------------------- /app/checks/performance/ignore-advice-ids.json: -------------------------------------------------------------------------------- 1 | [ 2 | "assetsRedirects", 3 | "cacheHeaders", 4 | "cacheHeadersLong", 5 | "compressAssets", 6 | "connectionKeepAlive", 7 | "fewRequestsPerDomain", 8 | "headerSize", 9 | "optimalCssSize", 10 | "privateAssets", 11 | "avoidScalingImages", 12 | "fastRender", 13 | "inlineCss", 14 | "spof", 15 | "thirdPartyAsyncJs", 16 | "userTiming" 17 | ] 18 | -------------------------------------------------------------------------------- /app/checks/performance/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile) { 4 | var tasks = []; 5 | 6 | if (markbotFile.performance) { 7 | let task = { 8 | group: `performance-${Date.now()}`, 9 | groupLabel: 'Performance', 10 | options: { 11 | files: markbotFile.performance, 12 | }, 13 | }; 14 | 15 | tasks.push(task); 16 | } 17 | 18 | return tasks; 19 | }; 20 | -------------------------------------------------------------------------------- /app/checks/screenshots/default.css: -------------------------------------------------------------------------------- 1 | *, *::before, *::after { 2 | -webkit-animation: none !important; 3 | animation: none !important; 4 | -webkit-transition: none !important; 5 | transition: none !important; 6 | } 7 | -------------------------------------------------------------------------------- /app/checks/screenshots/default.js: -------------------------------------------------------------------------------- 1 | let windowResizeEventThrottle; 2 | 3 | window.addEventListener('resize', function () { 4 | clearTimeout(windowResizeEventThrottle); 5 | 6 | windowResizeEventThrottle = setTimeout(function () { 7 | clearTimeout(windowResizeEventThrottle); 8 | 9 | window.requestAnimationFrame(function () { 10 | window.requestAnimationFrame(function () { 11 | window.requestAnimationFrame(function () { 12 | window.requestAnimationFrame(function () { 13 | window.__markbot.sendMessageToWindow(taskRunnerId, listenerLabel, windowId, document.documentElement.clientWidth, document.documentElement.offsetHeight); 14 | }); 15 | }); 16 | }); 17 | }); 18 | }, 200); 19 | }); 20 | 21 | return windowId; 22 | -------------------------------------------------------------------------------- /app/checks/screenshots/defaults-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const get = function (file) { 7 | return fs.readFileSync(path.resolve(`${__dirname}/${file}`), 'utf8'); 8 | }; 9 | 10 | module.exports = { 11 | get: get, 12 | }; 13 | -------------------------------------------------------------------------------- /app/checks/screenshots/naming-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const is = require('electron-is'); 5 | const classify = require(__dirname + '/../../classify'); 6 | 7 | const SCREENSHOT_PREFIX = 'markbot'; 8 | const REFERENCE_SCREENSHOT_FOLDER = 'screenshots'; 9 | 10 | let app; 11 | 12 | if (is.renderer()) { 13 | app = require('electron').remote.app; 14 | } else { 15 | app = require('electron').app; 16 | } 17 | 18 | const removeExtensionFromFile = function (filename) { 19 | return filename.replace(/\.html$/, ''); 20 | }; 21 | 22 | const makeScreenshotBasename = function (file) { 23 | const labelExtra = (file.label) ? `-${file.label}` : ''; 24 | 25 | return classify(removeExtensionFromFile(file.path) + labelExtra); 26 | }; 27 | 28 | const getScreenshotFilename = function (filename, width, prefix) { 29 | let pre = (prefix) ? `${prefix}-` : ''; 30 | 31 | return `${pre}${classify(filename.replace(/\.html$/, ''))}-${width}.png`; 32 | }; 33 | 34 | const getScreenshotPath = function (projectPath, filename, width, genRefScreens) { 35 | let formattedFilename, imgPath, fullPath; 36 | 37 | if (genRefScreens) { 38 | formattedFilename = getScreenshotFilename(filename, width); 39 | imgPath = path.resolve(path.resolve(projectPath) + '/' + REFERENCE_SCREENSHOT_FOLDER); 40 | fullPath = path.resolve(imgPath + '/' + formattedFilename); 41 | } else { 42 | formattedFilename = getScreenshotFilename(filename, width, SCREENSHOT_PREFIX); 43 | fullPath = path.resolve(app.getPath('temp') + '/' + formattedFilename); 44 | } 45 | 46 | return fullPath; 47 | }; 48 | 49 | module.exports = { 50 | REFERENCE_SCREENSHOT_FOLDER: REFERENCE_SCREENSHOT_FOLDER, 51 | getScreenshotFilename: getScreenshotFilename, 52 | getScreenshotPath: getScreenshotPath, 53 | makeScreenshotBasename: makeScreenshotBasename, 54 | }; 55 | -------------------------------------------------------------------------------- /app/checks/screenshots/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile) { 4 | var tasks = []; 5 | 6 | if (markbotFile.screenshots) { 7 | let task = { 8 | group: `screenshots-${Date.now()}`, 9 | groupLabel: 'Screenshots', 10 | options: { 11 | files: markbotFile.screenshots, 12 | }, 13 | }; 14 | 15 | tasks.push(task); 16 | } 17 | 18 | return tasks; 19 | }; 20 | -------------------------------------------------------------------------------- /app/checks/yaml/task-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generateTaskList = function (markbotFile, isCheater) { 4 | var tasks = []; 5 | 6 | if (markbotFile.yml) { 7 | markbotFile.yml.forEach(function (file) { 8 | let task = { 9 | group: `yml-${file.path}-${Date.now()}`, 10 | groupLabel: file.path, 11 | options: { 12 | file: file, 13 | cheater: (isCheater.matches[file.path]) ? !isCheater.matches[file.path].equal : (isCheater.cheated) ? true : false, 14 | }, 15 | }; 16 | 17 | tasks.push(task); 18 | }); 19 | } 20 | 21 | return tasks; 22 | }; 23 | -------------------------------------------------------------------------------- /app/checks/yaml/task.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 7 | const exists = require(__dirname + '/file-exists'); 8 | const validation = require(__dirname + '/checks/yaml/validation'); 9 | const content = require(__dirname + '/checks/content'); 10 | 11 | const group = taskDetails.group; 12 | const file = taskDetails.options.file; 13 | const isCheater = taskDetails.options.cheater; 14 | 15 | let checksToComplete = 0; 16 | 17 | const checkIfDone = function () { 18 | checksToComplete--; 19 | 20 | if (checksToComplete <= 0) done(); 21 | }; 22 | 23 | const check = function () { 24 | const fullPath = path.resolve(taskDetails.cwd + '/' + file.path); 25 | let errors = []; 26 | let fileContents = ''; 27 | let validationChecker; 28 | let contentChecker; 29 | 30 | const bypassAllChecks = function (f) { 31 | checksToComplete = 0; 32 | 33 | if (f.valid) validationChecker.bypass(); 34 | if (f.search || f.searchNot) contentChecker.bypass(); 35 | }; 36 | 37 | checksToComplete++; 38 | markbotMain.send('check-group:item-new', group, 'exists', 'Exists'); 39 | 40 | if (file.locked) { 41 | checksToComplete++; 42 | markbotMain.send('check-group:item-new', group, 'unchanged', 'Unchanged'); 43 | 44 | if (isCheater) { 45 | markbotMain.send('check-group:item-complete', group, 'unchanged', 'Unchanged', [`The \`${file.path}\` should not be changed`]); 46 | } else { 47 | markbotMain.send('check-group:item-complete', group, 'unchanged', 'Unchanged'); 48 | } 49 | 50 | checkIfDone(); 51 | } else { 52 | if (file.valid) { 53 | checksToComplete++; 54 | validationChecker = validation.init(group); 55 | } 56 | 57 | if (file.search || file.searchNot) { 58 | checksToComplete++; 59 | contentChecker = content.init(group); 60 | } 61 | } 62 | 63 | if (!exists.check(fullPath)) { 64 | markbotMain.send('check-group:item-complete', group, 'exists', 'Exists', [`The file \`${file.path}\` is missing or misspelled`]); 65 | bypassAllChecks(file); 66 | checkIfDone(); 67 | return; 68 | } 69 | 70 | fs.readFile(fullPath, 'utf8', function (err, fileContents) { 71 | if (fileContents.trim() == '') { 72 | markbotMain.send('check-group:item-complete', group, 'exists', 'Exists', [`The file \`${file.path}\` is empty`]); 73 | bypassAllChecks(file); 74 | checkIfDone(); 75 | return; 76 | } 77 | 78 | markbotMain.send('check-group:item-complete', group, 'exists', 'Exists'); 79 | checkIfDone(); 80 | 81 | if (file.locked) return; 82 | 83 | if (file.valid) validationChecker.check(fullPath, fileContents, checkIfDone); 84 | 85 | if (file.search || file.searchNot) { 86 | if (file.search && !file.searchNot) contentChecker.check(fileContents, file.search, [], checkIfDone); 87 | if (!file.search && file.searchNot) contentChecker.check(fileContents, [], file.searchNot, checkIfDone); 88 | if (file.search && file.searchNot) contentChecker.check(fileContents, file.search, file.searchNot, checkIfDone); 89 | } 90 | }); 91 | }; 92 | 93 | check(); 94 | }()); 95 | -------------------------------------------------------------------------------- /app/checks/yaml/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const util = require('util'); 5 | const yaml = require('js-yaml'); 6 | const S = require('string'); 7 | const markbotMain = require('electron').remote.require('./app/markbot-main'); 8 | 9 | const bypass = function (checkGroup, checkId, checkLabel) { 10 | markbotMain.send('check-group:item-bypass', checkGroup, checkId, checkLabel, ['Skipped because of previous errors']); 11 | }; 12 | 13 | const check = function (checkGroup, checkId, checkLabel, fullPath, fileContents, next) { 14 | let filename = path.parse(fullPath).base; 15 | let yamlData; 16 | let errors = []; 17 | 18 | markbotMain.send('check-group:item-computing', checkGroup, checkId); 19 | 20 | try { 21 | yamlData = yaml.safeLoad(fileContents); 22 | } catch (e) { 23 | if (e.reason && e.mark) { 24 | let line = e.mark.line + 1; 25 | let reason = S(e.reason).humanize(); 26 | 27 | errors.push(`Line ${line}: ${reason}`); 28 | } else { 29 | let reason = (e.reason) ? ' — ' + S(e.reason).humanize() : ''; 30 | 31 | errors.push(`There was a validation error in the YAML data${reason}`); 32 | } 33 | } 34 | 35 | if (errors.length > 0) { 36 | errors.unshift({ 37 | type: 'intro', 38 | message: 'Refer to the Markdown & YAML cheat sheet to help understand these errors:', 39 | link: 'https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/', 40 | linkText: 'https://mkbt.io/md-yml-cheat-sheet/', 41 | }); 42 | } 43 | 44 | markbotMain.send('check-group:item-complete', checkGroup, checkId, checkLabel, errors); 45 | next(errors); 46 | }; 47 | 48 | module.exports.init = function (group) { 49 | return (function (g) { 50 | const checkGroup = g; 51 | const checkId = 'validation'; 52 | const checkLabel = 'Validation & best practices'; 53 | 54 | markbotMain.send('check-group:item-new', checkGroup, checkId, checkLabel); 55 | 56 | return { 57 | check: function (fullPath, fileContents, next) { 58 | check(checkGroup, checkId, checkLabel, fullPath, fileContents, next); 59 | }, 60 | bypass: function () { 61 | bypass(checkGroup, checkId, checkLabel); 62 | } 63 | }; 64 | }(group)); 65 | }; 66 | -------------------------------------------------------------------------------- /app/classify.js: -------------------------------------------------------------------------------- 1 | module.exports = function (str) { 2 | return str.trim().toLowerCase().replace(/[^a-z0-9\-]/ig, '-').replace(/\-+/g, '-'); 3 | }; 4 | -------------------------------------------------------------------------------- /app/convert-path-to-url.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This function is mainly to work around Windows issues 5 | * The CSS validator doesn’t accept Windows paths because of back slashes 6 | * the path needs to be a valid URL 7 | */ 8 | module.exports = function (path) { 9 | let urlPath = path.replace(/\\/g, '/'); 10 | 11 | if (urlPath[0] !== '/') urlPath = '/' + urlPath; 12 | 13 | return urlPath; 14 | }; 15 | -------------------------------------------------------------------------------- /app/dependency-checker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exec = require('child_process').exec; 4 | 5 | let previousCheck = false; 6 | 7 | const hasGit = function () { 8 | return new Promise((resolve, reject) => { 9 | exec('git --version', (err, data, stderr) => { 10 | const commandFailed = /command failed/i; 11 | const licenseRequired = /agree.*xcode.*license/i; 12 | const unableToFind = /unable.*find.*git.*path/i; 13 | 14 | if (err && commandFailed.test(err)) { 15 | if (data && licenseRequired.test(data)) { 16 | // log.error('### Dependency: Git; must agree to license'); 17 | // log.error(data); 18 | return resolve(false); 19 | } 20 | 21 | if (data && unableToFind.test(data)) { 22 | // log.error('### Dependency: Git; not in PATH'); 23 | // log.error(data); 24 | return resolve(false); 25 | } 26 | } 27 | 28 | if (data && data.match(/git version/i)) { 29 | // log.info('### Dependency: Git; found'); 30 | return resolve(true); 31 | } 32 | 33 | // log.error('### Dependency: Git; not found'); 34 | return resolve(false); 35 | }); 36 | }); 37 | }; 38 | 39 | const hasJava = function () { 40 | return new Promise((resolve, reject) => { 41 | exec('java -version', (err, data, stderr) => { 42 | if ((data && data.match(/(java|openjdk) version/i)) || (stderr && stderr.match(/(java|openjdk) version/i))) { 43 | // log.info('### Dependency: Java; found'); 44 | return resolve(true); 45 | } 46 | 47 | // log.error('### Dependency: Java; not found'); 48 | return resolve(false); 49 | }); 50 | }); 51 | }; 52 | 53 | const check = function (next) { 54 | if (previousCheck) return next(previousCheck); 55 | 56 | // log.info('## Dependencies'); 57 | 58 | Promise.all([ 59 | hasGit(), 60 | hasJava(), 61 | ]).then((results) => { 62 | previousCheck = { 63 | hasMissingDependencies: (results.includes(false)) ? true : false, 64 | hasGit: results[0], 65 | hasJava: results[1], 66 | }; 67 | next(previousCheck); 68 | }).catch((e) => { 69 | next({ 70 | hasMissingDependencies: true, 71 | hasGit: false, 72 | hasJava: false, 73 | }); 74 | }); 75 | }; 76 | 77 | module.exports = { 78 | check: check, 79 | }; 80 | -------------------------------------------------------------------------------- /app/error-message-status.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DEFAULT: false, 3 | BYPASS: 'BYPASS', 4 | SKIP: 'SKIP', 5 | }; 6 | -------------------------------------------------------------------------------- /app/escape-shell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (cmd) { 4 | return '"' + cmd.replace(/(["'$`\\])/g, '\\$1') + '"'; 5 | }; 6 | -------------------------------------------------------------------------------- /app/file-exists.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | module.exports.check = function (path) { 6 | var exists = false; 7 | 8 | try { 9 | let stats = fs.statSync(path); 10 | exists = (stats.isFile() || stats.isDirectory()); 11 | } catch (e) { 12 | exists = false; 13 | } 14 | 15 | return exists; 16 | }; 17 | -------------------------------------------------------------------------------- /app/files-to-ignore.json: -------------------------------------------------------------------------------- 1 | [ 2 | "README\\.md", 3 | "node_modules", 4 | "\\.", 5 | "LICENSE", 6 | "_site", 7 | "tmp", 8 | "Desktop\\.ini", 9 | "Thumbs\\.db" 10 | ] 11 | -------------------------------------------------------------------------------- /app/functionality-injector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | let injectionJs = false; 7 | 8 | const makeExecTestJs = function (js, testIndex, label, testWinId) { 9 | if (!injectionJs) injectionJs = fs.readFileSync(path.resolve(`${__dirname}/functionality-methods.js`), 'utf8'); 10 | 11 | return ` 12 | (function () { 13 | ${injectionJs} 14 | 15 | __MarkbotInjectedFunctions.testIndex = ${testIndex}; 16 | __MarkbotInjectedFunctions.browserWindowId = ${testWinId}; 17 | __MarkbotInjectedFunctions.taskRunnerId = ${taskRunnerId}; 18 | __MarkbotInjectedFunctions.doneLabel = '__markbot-functionality-test-done-${label}'; 19 | __MarkbotInjectedFunctions.passLabel = '__markbot-functionality-test-pass-${label}'; 20 | __MarkbotInjectedFunctions.failLabel = '__markbot-functionality-test-fail-${label}'; 21 | __MarkbotInjectedFunctions.debugLabel = '__markbot-functionality-test-debug-${label}'; 22 | 23 | __MarkbotInjectedFunctions.send('mouseMove', { x: -10, y: -10 }, () => { 24 | (function ($, $$, css, bounds, offset, on, ev, send, hover, activate, done, pass, fail, debug) { 25 | 'use strict'; 26 | 27 | try { 28 | eval(${js}); 29 | } catch (e) { 30 | __MarkbotInjectedFunctions.debugFail(e); 31 | } 32 | }( 33 | __MarkbotInjectedFunctions.$, 34 | __MarkbotInjectedFunctions.$$, 35 | __MarkbotInjectedFunctions.css, 36 | __MarkbotInjectedFunctions.bounds, 37 | __MarkbotInjectedFunctions.offset, 38 | __MarkbotInjectedFunctions.on, 39 | __MarkbotInjectedFunctions.ev, 40 | __MarkbotInjectedFunctions.send, 41 | __MarkbotInjectedFunctions.hover, 42 | __MarkbotInjectedFunctions.activate, 43 | __MarkbotInjectedFunctions.done, 44 | __MarkbotInjectedFunctions.pass, 45 | __MarkbotInjectedFunctions.fail, 46 | __MarkbotInjectedFunctions.debug 47 | )); 48 | }); 49 | }()); 50 | `; 51 | }; 52 | 53 | const runCode = function (win, testJs, testIndex, listenerLabel) { 54 | let bindFunction = ` 55 | (function () { 56 | 'use strict'; 57 | 58 | window.__markbot.playAnimations(); 59 | setTimeout(() => { 60 | __MarkbotInjectedFunctions.fail('The Markbot requirements test code took too long to run or didn’t execute the required \`done()\` or \`pass()\` functions'); 61 | }, 7000); 62 | 63 | ${testJs.trim()} 64 | }()); 65 | `; 66 | 67 | let js = makeExecTestJs(JSON.stringify(bindFunction), testIndex, listenerLabel, win.id); 68 | 69 | win.webContents.executeJavaScript(js); 70 | }; 71 | 72 | module.exports = { 73 | runCode: runCode, 74 | }; 75 | -------------------------------------------------------------------------------- /app/list-dir.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const dir = require('node-dir'); 5 | const stripPath = require('./strip-path'); 6 | const markbotIgnoreParser = require('./markbot-ignore-parser'); 7 | const filesToIgnore = require('./files-to-ignore.json'); 8 | 9 | module.exports = function (dirPath, next) { 10 | const fullPath = path.resolve(dirPath); 11 | const matcher = new RegExp(`^(?:${filesToIgnore.join('|')})`); 12 | 13 | markbotIgnoreParser.parse(fullPath, (ignoreFiles) => { 14 | const ignoreMatcher = (ignoreFiles.length > 0) ? new RegExp(`^(?:${ignoreFiles.join('|')})`) : false; 15 | 16 | dir.files(fullPath, (err, files) =>{ 17 | let errors = []; 18 | 19 | files = files.filter( (file) => { 20 | const strippedPath = stripPath(file, fullPath); 21 | const cleanFileName = path.parse(file).base; 22 | 23 | if (matcher.test(strippedPath)) return false; 24 | if (matcher.test(cleanFileName)) return false; 25 | 26 | if (ignoreMatcher) { 27 | if (ignoreMatcher.test(strippedPath)) return false; 28 | if (ignoreMatcher.test(cleanFileName)) return false; 29 | } 30 | 31 | return true; 32 | }); 33 | 34 | next(files); 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /app/lock-matcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.match = function (primary, secondary, markbotIgnoreFile) { 4 | let isCheater = false; 5 | let matches = {}; 6 | 7 | if (primary) { 8 | for (let key in primary) { 9 | if (secondary[key] && primary[key] == secondary[key]) { 10 | matches[key] = { 11 | equal: true, 12 | expectedHash: primary[key], 13 | actualHash: secondary[key], 14 | }; 15 | } else { 16 | matches[key] = { 17 | equal: false, 18 | expectedHash: primary[key], 19 | actualHash: secondary[key], 20 | }; 21 | isCheater = true; 22 | } 23 | } 24 | } else { 25 | isCheater = true; 26 | } 27 | 28 | if (!matches.markbot) { 29 | isCheater = true; 30 | matches.markbot = { 31 | equal: false, 32 | expectedHash: false, 33 | actualHash: false, 34 | }; 35 | } 36 | 37 | if (!matches.markbotignore && markbotIgnoreFile.length > 0) { 38 | isCheater = true; 39 | matches.markbotignore = { 40 | equal: false, 41 | expectedHash: false, 42 | actualHash: false, 43 | }; 44 | } 45 | 46 | return { 47 | cheated: isCheater, 48 | matches: matches 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /app/locker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const yaml = require('js-yaml'); 7 | const exists = require('./file-exists'); 8 | const markbotMain = require('./markbot-main'); 9 | 10 | const getHash = function () { 11 | return crypto.createHash('sha256'); 12 | }; 13 | 14 | const readLockFile = function (filePath) { 15 | let tmpLocks; 16 | 17 | if (exists.check(filePath)) { 18 | try { 19 | tmpLocks = yaml.safeLoad(fs.readFileSync(filePath, 'utf8')); 20 | } catch (e) { 21 | let ln = (e.mark && e.mark.line) ? e.mark.line + 1 : '?'; 22 | markbotMain.debug(`Error in the MarkbotLockFile, line ${ln}: ${e.message}`); 23 | } 24 | 25 | if (!tmpLocks) tmpLocks = {}; 26 | } 27 | 28 | return tmpLocks; 29 | }; 30 | 31 | const saveLockFile = function (filePath, locks) { 32 | fs.writeFileSync(filePath, JSON.stringify(locks, null, 2), 'utf8'); 33 | }; 34 | 35 | const getLockForString = function (str, passcodeHash) { 36 | return getHash().update(new Buffer(str + passcodeHash, 'utf-8')).digest('hex'); 37 | }; 38 | 39 | const getLockForFile = function (filePath, passcodeHash) { 40 | let hasher = getHash(); 41 | 42 | hasher.update(fs.readFileSync(filePath)); 43 | hasher.update(new Buffer(passcodeHash, 'utf-8')); 44 | 45 | return hasher.digest('hex'); 46 | }; 47 | 48 | module.exports.new = function (passcodeHash) { 49 | return (function (hash) { 50 | let locks = {}; 51 | 52 | return { 53 | getLocks: function () { 54 | return locks; 55 | }, 56 | reset: function () { 57 | locks = {}; 58 | }, 59 | read: function (filePath) { 60 | locks = readLockFile(filePath); 61 | return locks; 62 | }, 63 | save: function (filePath) { 64 | saveLockFile(filePath, locks); 65 | }, 66 | lockString: function (key, str) { 67 | locks[key] = getLockForString(str, hash) 68 | }, 69 | lockFile: function (key, filePath) { 70 | locks[key] = getLockForFile(filePath, hash); 71 | } 72 | }; 73 | }(passcodeHash)); 74 | }; 75 | -------------------------------------------------------------------------------- /app/markbot-ignore-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const exists = require('./file-exists'); 6 | 7 | const MARKBOT_IGNORE_FILE = '.markbotignore'; 8 | 9 | const parse = function (fullPath, next) { 10 | const ignoreFilePath = path.resolve(`${fullPath}/${MARKBOT_IGNORE_FILE}`); 11 | 12 | if (!exists.check(ignoreFilePath)) return next([]); 13 | 14 | fs.readFile(ignoreFilePath, 'utf8', (err, data) => { 15 | let lines; 16 | 17 | if (data.trim() === '') return next([]); 18 | 19 | lines = data.split(/[\n\u0085\u2028\u2029]|\r\n?/g) 20 | .map((line) => line.trim()) 21 | .filter((line) => (line)) 22 | ; 23 | 24 | next(lines); 25 | }); 26 | }; 27 | 28 | module.exports = { 29 | parse: parse, 30 | }; 31 | -------------------------------------------------------------------------------- /app/markbot-main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const is = require('electron-is'); 4 | 5 | let markbotMain; 6 | 7 | const init = function (main) { 8 | let electron; 9 | 10 | if (is.renderer()) { 11 | electron = require('electron').remote; 12 | markbotMain = electron.BrowserWindow.fromId(electron.getGlobal('markbotMainWindow')).webContents; 13 | } else { 14 | electron = require('electron'); 15 | markbotMain = electron.BrowserWindow.fromId(global.markbotMainWindow).webContents; 16 | } 17 | }; 18 | 19 | const destroy = function () { 20 | markbotMain = null; 21 | } 22 | 23 | const send = function (label, ...messages) { 24 | init(); 25 | markbotMain.send(label, ...messages); 26 | destroy(); 27 | }; 28 | 29 | const debug = function (...messages) { 30 | send('debug', ...messages); 31 | }; 32 | 33 | const isDebug = function () { 34 | if (is.renderer()) { 35 | return require('electron').remote.getGlobal('DEBUG'); 36 | } else { 37 | return global.DEBUG; 38 | } 39 | }; 40 | 41 | module.exports = { 42 | send: send, 43 | debug: debug, 44 | isDebug: isDebug, 45 | }; 46 | -------------------------------------------------------------------------------- /app/networks.js: -------------------------------------------------------------------------------- 1 | const kilobitsToKiloBytes = function (num) { 2 | return (num / 8) * 1024; 3 | }; 4 | 5 | // These networks match Google Chrome's default network throttle settings 6 | let networks = { 7 | 'WIFI-FAST': { 8 | latency: 2, 9 | downloadThroughput: kilobitsToKiloBytes(30000), 10 | uploadThroughput: kilobitsToKiloBytes(15000), 11 | }, 12 | 'WIFI-REGULAR': { 13 | latency: 15, 14 | downloadThroughput: kilobitsToKiloBytes(10000), 15 | uploadThroughput: kilobitsToKiloBytes(4000), 16 | }, 17 | DSL: { 18 | latency: 5, 19 | downloadThroughput: kilobitsToKiloBytes(2000), 20 | uploadThroughput: kilobitsToKiloBytes(1000), 21 | }, 22 | '4G-REGULAR': { 23 | latency: 20, 24 | downloadThroughput: kilobitsToKiloBytes(4000), 25 | uploadThroughput: kilobitsToKiloBytes(3000), 26 | }, 27 | '3G-GOOD': { 28 | latency: 40, 29 | downloadThroughput: kilobitsToKiloBytes(1500), 30 | uploadThroughput: kilobitsToKiloBytes(750), 31 | }, 32 | '3G-REGULAR': { 33 | latency: 100, 34 | downloadThroughput: kilobitsToKiloBytes(750), 35 | uploadThroughput: kilobitsToKiloBytes(250), 36 | }, 37 | '2G-GOOD': { 38 | latency: 150, 39 | downloadThroughput: kilobitsToKiloBytes(450), 40 | uploadThroughput: kilobitsToKiloBytes(150), 41 | }, 42 | '2G-REGULAR': { 43 | latency: 300, 44 | downloadThroughput: kilobitsToKiloBytes(250), 45 | uploadThroughput: kilobitsToKiloBytes(50), 46 | }, 47 | GPRS: { 48 | latency: 500, 49 | downloadThroughput: kilobitsToKiloBytes(50), 50 | uploadThroughput: kilobitsToKiloBytes(20), 51 | }, 52 | }; 53 | 54 | networks['WIFI'] = networks['WIFI-REGULAR']; 55 | networks['4G'] = networks['4G-REGULAR']; 56 | networks['3G'] = networks['3G-GOOD']; 57 | networks['2G'] = networks['2G-GOOD']; 58 | 59 | module.exports = networks; 60 | -------------------------------------------------------------------------------- /app/passcode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | module.exports = (function () { 6 | 7 | const getHasher = function (secret) { 8 | return crypto.createHmac('sha512', secret); 9 | }; 10 | 11 | const hash = function (passcode, secret) { 12 | return getHasher(secret).update(new Buffer(passcode, 'utf-8')).digest('hex'); 13 | }; 14 | 15 | const matches = function (passcode, secret, passcodeHash) { 16 | return (hash(passcode, secret) == passcodeHash); 17 | }; 18 | 19 | return { 20 | hash: hash, 21 | matches: matches 22 | }; 23 | 24 | }()); 25 | -------------------------------------------------------------------------------- /app/requirements-finder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const exists = require('./file-exists'); 5 | const markbotMain = require('./markbot-main'); 6 | const screenshotNamingService = require('./checks/screenshots/naming-service'); 7 | 8 | let missingFiles = []; 9 | 10 | const lockMarkbotFile = function (locker, markbotFile) { 11 | locker.lockString('markbot', JSON.stringify(markbotFile)); 12 | }; 13 | 14 | const lockMarkbotIgnoreFile = function (locker, markbotIgnoreFile) { 15 | locker.lockString('markbotignore', JSON.stringify(markbotIgnoreFile)); 16 | }; 17 | 18 | const lockFiles = function (locker, currentFolderPath, files) { 19 | files.forEach(function (file) { 20 | let filePath = path.resolve(currentFolderPath + '/' + file.path); 21 | 22 | if (!file.locked) return; 23 | 24 | if (!exists.check(filePath)) { 25 | missingFiles.push(file.path); 26 | return; 27 | } 28 | 29 | if (file.locked) locker.lockFile(file.path, filePath); 30 | }); 31 | }; 32 | 33 | const lockScreenshots = function (locker, currentFolderPath, files) { 34 | files.forEach(function (file) { 35 | let screenshotSizes = (Array.isArray(file.sizes)) ? file.sizes.slice(0) : Object.keys(file.sizes); 36 | 37 | screenshotSizes.forEach(function (size) { 38 | let screenshotFileName = screenshotNamingService.getScreenshotFilename(screenshotNamingService.makeScreenshotBasename(file), size); 39 | let screenshotPath = path.resolve(currentFolderPath + '/' + screenshotNamingService.REFERENCE_SCREENSHOT_FOLDER + '/' + screenshotFileName); 40 | 41 | if (!exists.check(screenshotPath)) { 42 | missingFiles.push(screenshotFileName); 43 | return; 44 | } 45 | 46 | locker.lockFile(screenshotFileName, screenshotPath); 47 | }); 48 | }); 49 | }; 50 | 51 | const lock = function (locker, currentFolderPath, markbotFileParsed, markbotFile, markbotIgnoreFile) { 52 | missingFiles = []; 53 | locker.reset(); 54 | 55 | lockMarkbotFile(locker, markbotFile); 56 | lockMarkbotIgnoreFile(locker, markbotIgnoreFile); 57 | 58 | if (markbotFile.html) lockFiles(locker, currentFolderPath, markbotFile.html); 59 | if (markbotFile.css) lockFiles(locker, currentFolderPath, markbotFile.css); 60 | if (markbotFile.js) lockFiles(locker, currentFolderPath, markbotFile.js); 61 | if (markbotFile.screenshots) lockScreenshots(locker, currentFolderPath, markbotFile.screenshots); 62 | if (markbotFileParsed.screenshots) lockScreenshots(locker, currentFolderPath, markbotFileParsed.screenshots); 63 | 64 | missingFiles = [...new Set(missingFiles)]; 65 | 66 | if (missingFiles.length > 0) { 67 | markbotMain.send('alert', `The following files could not be locked because they’re missing:\n• ${missingFiles.join('\n• ')}`); 68 | } 69 | }; 70 | 71 | module.exports = { 72 | lock: lock 73 | }; 74 | -------------------------------------------------------------------------------- /app/server-html.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const is = require('electron-is'); 5 | const {spawn} = require('child_process'); 6 | 7 | let opts = { 8 | detached: true, 9 | }; 10 | 11 | let args = [ 12 | '-cp', 13 | 'vnu.jar', 14 | 'nu.validator.servlet.Main', 15 | ]; 16 | 17 | let server; 18 | let app; 19 | 20 | if (is.renderer()) { 21 | app = require('electron').remote.app; 22 | } else { 23 | app = require('electron').app; 24 | } 25 | 26 | const start = function (port) { 27 | const validatorPath = path.resolve(__dirname.replace(/app.asar[\/\\]/, 'app.asar.unpacked/') + '/../vendor/html-validator'); 28 | 29 | args.push(port); 30 | opts.cwd = validatorPath; 31 | 32 | return new Promise((resolve, reject) => { 33 | server = spawn('java', args, opts); 34 | server.unref(); 35 | 36 | server.stderr.on('data', (data) => { 37 | // if (process.platform === 'darwin') { 38 | let message = data.toString('utf8'); 39 | // let info = /INFO/; 40 | let started = /Started @\d+m?s/i; 41 | 42 | // if (!info.test(message)) reject('There was an error starting the HTML validator'); 43 | if (started.test(message)) resolve(); 44 | // } 45 | }); 46 | 47 | server.stdout.on('data', (data) => { 48 | let message = data.toString('utf8'); 49 | let started = /Started @\d+m?s/i; 50 | 51 | if (started.test(message)) resolve(); 52 | }); 53 | }); 54 | }; 55 | 56 | const stop = function () { 57 | try { 58 | server.kill(); 59 | } catch (e) { 60 | console.log('HTML server already stopped.'); 61 | } 62 | }; 63 | 64 | module.exports = { 65 | start: start, 66 | stop: stop, 67 | }; 68 | -------------------------------------------------------------------------------- /app/server-language.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const is = require('electron-is'); 5 | const {spawn} = require('child_process'); 6 | 7 | let opts = { 8 | detached: true, 9 | }; 10 | 11 | let args = [ 12 | '-cp', 13 | 'languagetool-server.jar', 14 | 'org.languagetool.server.HTTPServer', 15 | // This makes start-up much, much longer, but the first commit test much shorter 16 | // It’s currently disabled because I prefer the user experience of faster start-up with slower first commit 17 | // '--config', 18 | // 'languagetool.properties', 19 | '--port', 20 | ]; 21 | 22 | let server; 23 | let app; 24 | 25 | if (is.renderer()) { 26 | app = require('electron').remote.app; 27 | } else { 28 | app = require('electron').app; 29 | } 30 | 31 | const start = function (port) { 32 | const validatorPath = path.resolve(__dirname.replace(/app.asar[\/\\]/, 'app.asar.unpacked/') + '/../vendor/languagetool'); 33 | 34 | args.push(port); 35 | opts.cwd = validatorPath; 36 | 37 | return new Promise((resolve, reject) => { 38 | server = spawn('java', args, opts); 39 | server.unref(); 40 | 41 | server.stderr.on('data', (data) => { 42 | reject(data.toString('utf8')); 43 | }); 44 | 45 | server.stdout.on('data', (data) => { 46 | let message = data.toString('utf8'); 47 | let started = /server started/i; 48 | 49 | if (started.test(message)) resolve(); 50 | }); 51 | }); 52 | }; 53 | 54 | const stop = function () { 55 | try { 56 | server.kill(); 57 | } catch (e) { 58 | console.log('LanguageTool server already stopped.'); 59 | } 60 | }; 61 | 62 | module.exports = { 63 | start: start, 64 | stop: stop, 65 | }; 66 | -------------------------------------------------------------------------------- /app/server-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getPort = require('get-port'); 4 | const markbotMain = require(`${__dirname}/markbot-main`); 5 | const serverWeb = require(`${__dirname}/server-web`); 6 | const serverHtml = require(`${__dirname}/server-html`); 7 | const serverLanguage = require(`${__dirname}/server-language`); 8 | 9 | let serversStarting = false; 10 | let serversRunning = false; 11 | 12 | const servers = { 13 | web: { 14 | port: 8000, 15 | protocol: 'https', 16 | }, 17 | html: { 18 | port: 8001, 19 | protocol: 'http', 20 | }, 21 | language: { 22 | port: 8002, 23 | protocol: 'http', 24 | }, 25 | }; 26 | 27 | const setPorts = function (ports) { 28 | servers.web.port = ports[0]; 29 | servers.html.port = ports[1]; 30 | servers.language.port = ports[2]; 31 | }; 32 | 33 | const start = function (next) { 34 | if (serversRunning) return next(); 35 | if (serversStarting) return; 36 | 37 | serversStarting = true; 38 | 39 | Promise.all([ 40 | getPort(), 41 | getPort(), 42 | getPort(), 43 | ]).then((ports) => { 44 | setPorts(ports); 45 | 46 | Promise.all([ 47 | serverWeb.start(servers.web.port), 48 | serverHtml.start(servers.html.port), 49 | serverLanguage.start(servers.language.port), 50 | ]).then(() => { 51 | serversRunning = true; 52 | next(); 53 | }).catch((reason) => { 54 | markbotMain.send('restart', `Internal servers won’t start: “${reason}”. Please quit & restart Markbot.`); 55 | }); 56 | }); 57 | }; 58 | 59 | const stop = function () { 60 | serversStarting = false; 61 | serversRunning = false; 62 | serverWeb.stop(); 63 | serverHtml.stop(); 64 | serverLanguage.stop(); 65 | }; 66 | 67 | const getHost = function (server) { 68 | if (!servers[server]) return false; 69 | 70 | return `${servers[server].protocol}://127.0.0.1:${servers[server].port}`; 71 | }; 72 | 73 | const getHostInfo = function (server) { 74 | if (!servers[server]) return false; 75 | 76 | return { 77 | hostname: '127.0.0.1', 78 | port: servers[server].port, 79 | protocol: servers[server].protocol, 80 | }; 81 | }; 82 | 83 | const getServer = function (server) { 84 | switch (server) { 85 | case 'web': 86 | return serverWeb; 87 | break; 88 | case 'html': 89 | return serverHtml; 90 | break; 91 | case 'language': 92 | return serverLanguage; 93 | break; 94 | default: 95 | return false; 96 | } 97 | }; 98 | 99 | module.exports = { 100 | start: start, 101 | stop: stop, 102 | getHost: getHost, 103 | getHostInfo: getHostInfo, 104 | getServer: getServer, 105 | }; 106 | -------------------------------------------------------------------------------- /app/server-web-process.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DIRECTORY_INDEX = '/index.html'; 4 | const HTTPS_KEY = `${__dirname}/https-key.pem`; 5 | const HTTPS_CERT = `${__dirname}/https-cert.pem`; 6 | 7 | const zlib = require('zlib'); 8 | const path = require('path'); 9 | const fs = require('fs'); 10 | const https = require('https'); 11 | const mimeTypes = require('mime-types'); 12 | const finalhandler = require('finalhandler'); 13 | const exists = require('./file-exists'); 14 | const pkg = require('../package'); 15 | 16 | const errorView = path.resolve(`${__dirname}/server-web-error.html`); 17 | 18 | let webServer; 19 | let staticDir = path.resolve(__dirname.replace(/app.asar[\/\\]/, 'app.asar.unpacked/') + '/../http-public'); 20 | 21 | const getErrorPage = function (errcode) { 22 | return fs 23 | .readFileSync(errorView, 'utf-8') 24 | .replace(/{{errno}}/g, errcode) 25 | .replace(/{{markbotversion}}/g, pkg.version) 26 | ; 27 | }; 28 | 29 | const isRunning = function () { 30 | return !!(webServer && webServer.listening); 31 | }; 32 | 33 | const start = function (port) { 34 | fs.readFile(HTTPS_KEY, 'utf8', (err, httpsKey) => { 35 | fs.readFile(HTTPS_CERT, 'utf8', (err, httpsCert) => { 36 | webServer = https.createServer({key: httpsKey, cert: httpsCert}, (request, response) => { 37 | if (request.url.indexOf('?') > -1) request.url = request.url.substr(0, request.url.indexOf('?')); 38 | if (request.url == '/') request.url = DIRECTORY_INDEX; 39 | 40 | let filePath = path.resolve(staticDir + request.url); 41 | let extname = path.extname(filePath); 42 | let acceptEncoding = request.headers['accept-encoding']; 43 | 44 | if (!acceptEncoding) acceptEncoding = ''; 45 | 46 | if (exists.check(filePath)) { 47 | let mime = mimeTypes.contentType(path.extname(filePath)); 48 | let headers = { 49 | // 'Last-Modified': fs.statSync(filePath).mtime, 50 | // 'Cache-Control': 'max-age=2592000', // 30 days 51 | }; 52 | 53 | if (mime) headers['Content-Type'] = mime; 54 | 55 | fs.readFile(filePath, (error, content) => { 56 | if (error) { 57 | response.writeHead(404); 58 | response.end(getErrorPage(404), 'utf-8'); 59 | } else { 60 | let raw = fs.createReadStream(filePath); 61 | 62 | if (acceptEncoding.match(/\bdeflate\b/)) { 63 | headers['Content-Encoding'] = 'deflate'; 64 | response.writeHead(200, headers); 65 | raw.pipe(zlib.createDeflate()).pipe(response); 66 | } else if (acceptEncoding.match(/\bgzip\b/)) { 67 | headers['Content-Encoding'] = 'gzip'; 68 | response.writeHead(200, headers); 69 | raw.pipe(zlib.createGzip()).pipe(response); 70 | } else { 71 | response.writeHead(200, headers); 72 | raw.pipe(response); 73 | } 74 | } 75 | }); 76 | } else { 77 | response.writeHead(404); 78 | response.end(getErrorPage(404), 'utf-8'); 79 | } 80 | }); 81 | 82 | webServer.listen(port, () => { 83 | process.send({ running: true }); 84 | }); 85 | }); 86 | }); 87 | }; 88 | 89 | const stop = function () { 90 | try { 91 | webServer.close(); 92 | webServer = null; 93 | staticDir = null; 94 | process.kill(); 95 | } catch (e) { 96 | console.log('Web server is already stopped.'); 97 | } 98 | }; 99 | 100 | const setRoot = function (dir) { 101 | staticDir = dir; 102 | }; 103 | 104 | process.on('message', (msg) => { 105 | if (msg.start) start(msg.start); 106 | if (msg.stop) stop(); 107 | if (msg.root) setRoot(msg.root); 108 | }); 109 | -------------------------------------------------------------------------------- /app/server-web.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const is = require('electron-is'); 5 | const {fork} = require('child_process'); 6 | 7 | let opts = { 8 | silent: true, 9 | }; 10 | 11 | let server; 12 | let app; 13 | 14 | if (is.renderer()) { 15 | app = require('electron').remote.app; 16 | } else { 17 | app = require('electron').app; 18 | } 19 | 20 | const start = function (port) { 21 | return new Promise((resolve, reject) => { 22 | server = fork(`${__dirname}/server-web-process.js`, opts); 23 | 24 | server.stderr.on('data', (data) => { 25 | reject(data.toString('utf8')); 26 | }); 27 | 28 | server.send({ start: port }, (err) => { 29 | if (err) return reject(err.message); 30 | }); 31 | 32 | server.on('message', (msg) => { 33 | if (msg.running) resolve(); 34 | }); 35 | }); 36 | }; 37 | 38 | const stop = function () { 39 | try { 40 | server.send({ stop: true }); 41 | server.kill(); 42 | } catch (e) { 43 | console.log('Web server already stopped.'); 44 | } 45 | }; 46 | 47 | const setRoot = function (root) { 48 | server.send({ root: root }); 49 | }; 50 | 51 | module.exports = { 52 | start: start, 53 | stop: stop, 54 | setRoot: setRoot, 55 | }; 56 | -------------------------------------------------------------------------------- /app/strip-path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = function (file, fullPath) { 6 | const normalizedFile = path.normalize(file); 7 | const normalizedFullPath = path.normalize(fullPath); 8 | 9 | return normalizedFile.replace(normalizedFullPath, '').replace(/^[\/\\]/, ''); 10 | }; 11 | -------------------------------------------------------------------------------- /app/task-pool-queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const create = function () { 4 | let queueStart = []; 5 | let queue = []; 6 | let queueEnd = []; 7 | 8 | const addStart = function (task) { 9 | queueStart.push(task); 10 | }; 11 | 12 | const addEnd = function (task) { 13 | queueEnd.push(task); 14 | }; 15 | 16 | const add = function (task) { 17 | queue.push(task); 18 | }; 19 | 20 | const next = function () { 21 | if (queueStart.length > 0) return queueStart.shift(); 22 | if (queue.length > 0) return queue.shift(); 23 | if (queueEnd.length > 0) return queueEnd.shift(); 24 | 25 | return false; 26 | }; 27 | 28 | const has = function () { 29 | if (queueStart.length > 0) return true; 30 | if (queue.length > 0) return true; 31 | if (queueEnd.length > 0) return true; 32 | 33 | return false; 34 | }; 35 | 36 | const length = function () { 37 | return queueStart.length + queue.length + queueEnd.length; 38 | }; 39 | 40 | return { 41 | add: add, 42 | addStart: addStart, 43 | addEnd: addEnd, 44 | next: next, 45 | has: has, 46 | length: length, 47 | }; 48 | }; 49 | 50 | module.exports = { 51 | create: create, 52 | }; 53 | -------------------------------------------------------------------------------- /app/task-pool.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Task 6 | 7 | 8 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/user-agent-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pkg = require('../package'); 4 | 5 | module.exports.get = function () { 6 | return `Markbot/${pkg.version} (+https://github.com/thomasjbradley/markbot)`; 7 | }; 8 | -------------------------------------------------------------------------------- /app/web-loader-queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webLoader = require('./web-loader'); 4 | 5 | let loadQueue = []; 6 | let currentWindow; 7 | 8 | const add = function (queueUrl, queueOpts, queueNext) { 9 | loadQueue.push({ 10 | url: queueUrl, 11 | opts: queueOpts, 12 | next: queueNext, 13 | }); 14 | 15 | if (loadQueue.length === 1 && !currentWindow) next(); 16 | }; 17 | 18 | const next = function () { 19 | let current; 20 | 21 | if (currentWindow) { 22 | webLoader.destroy(currentWindow); 23 | currentWindow = null; 24 | } 25 | 26 | if (loadQueue.length <= 0) return; 27 | 28 | current = loadQueue.shift(); 29 | 30 | webLoader.load(current.url, current.opts, function (win, har) { 31 | currentWindow = win; 32 | current.next(win, har); 33 | }); 34 | }; 35 | 36 | module.exports = { 37 | add: add, 38 | next: next, 39 | }; 40 | -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/build/background.png -------------------------------------------------------------------------------- /build/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/build/background@2x.png -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/build/icon.ico -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "progressinatorApi": "https://progress.learn-the-web.algonquindesign.ca/api/v1", 3 | "ignoreCommitEmails": [ 4 | "your-git@email-address.ca" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /devtools-har-extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DevTools HAR Extension 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /devtools-har-extension/index.js: -------------------------------------------------------------------------------- 1 | var LOAD_INDICATOR = 'https://did-finish-load/'; 2 | 3 | chrome.devtools.network.onRequestFinished.addListener(function (request) { 4 | if (request.request.url === LOAD_INDICATOR) { 5 | chrome.devtools.network.getHAR(function (har) { 6 | har.entries = har.entries.filter(function (e) { 7 | return e.request.url !== LOAD_INDICATOR; 8 | }); 9 | chrome.devtools.inspectedWindow.eval(`window.__markbot.sendMessageToWindow(window.__markbot.getCurrentTaskWindowId(), "__markbot-hidden-browser-har-generation-succeeded", ${JSON.stringify({log:har})});`); 10 | }); 11 | } 12 | }); 13 | 14 | chrome.devtools.inspectedWindow.eval('window.__markbot.sendMessageToWindow(window.__markbot.getCurrentTaskWindowId(), "__markbot-hidden-browser-devtools-loaded");'); 15 | -------------------------------------------------------------------------------- /devtools-har-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtools-har-extension", 3 | "description": "DevTools HAR Extension", 4 | "version": "1.0.0", 5 | "manifest_version": 2, 6 | "devtools_page": "index.html" 7 | } 8 | -------------------------------------------------------------------------------- /docs/images/git-win.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/docs/images/git-win.jpg -------------------------------------------------------------------------------- /docs/images/jdk-mac.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/docs/images/jdk-mac.jpg -------------------------------------------------------------------------------- /docs/images/jdk-win.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/docs/images/jdk-win.jpg -------------------------------------------------------------------------------- /docs/images/xcode-select-install.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/docs/images/xcode-select-install.jpg -------------------------------------------------------------------------------- /docs/install-git-command-line-tools-mac.md: -------------------------------------------------------------------------------- 1 | # Install the Git command-line tools on MacOS 2 | 3 | *Markbot requires a few extra tools installed on your computer to function properly.* 4 | 5 | **The Git command-line tools allow Markbot to check your commits & push status, etc.** 6 | 7 | Open the application on your computer called “Terminal”, it’s inside `Applications > Utilities`. 8 | 9 | Type exactly this: 10 | 11 | ``` 12 | xcode-select --install 13 | ``` 14 | 15 | Hit `Return` 16 | 17 | It will guide you through the download and installation process. 18 | 19 | ![](images/xcode-select-install.jpg) 20 | 21 | *That’s all!* 22 | -------------------------------------------------------------------------------- /docs/install-git-command-line-tools-win.md: -------------------------------------------------------------------------------- 1 | # Install the Git command-line tools on Windows 2 | 3 | *Markbot requires a few extra tools installed on your computer to function properly.* 4 | 5 | **The Git command-line tools allow Markbot to check your commits & push status, etc.** 6 | 7 | **Go to [Git’s download page](https://git-scm.com/download/win) and download the Windows version.** 8 | 9 | Install Git onto your computer. One of the setup screens has options we have to change. 10 | 11 | *On the “Adjusting your PATH environment” screen, switch to “Use Git from the Windows Command Prompt”.* 12 | 13 | ![](images/git-win.jpg) 14 | 15 | *That’s all!* 16 | -------------------------------------------------------------------------------- /docs/install-java-developer-kit-mac.md: -------------------------------------------------------------------------------- 1 | # Install the Java Development Kit on MacOS 2 | 3 | *Markbot requires a few extra tools installed on your computer to function properly.* 4 | 5 | **The Java Development Kit allows Markbot to run the HTML validator, CSS validator & and the language checker.** 6 | 7 | ### [Go to the OpenJDK download page ➔](https://adoptopenjdk.net/) 8 | 9 | ![](images/jdk-mac.jpg) 10 | 11 | **Choose the “latest” version of OpenJDK at the bottom of the list.** 12 | 13 | 1. Download the JDK for your computer. 14 | 3. Double click the installer and let it do its thing. 15 | 5. Delete the downloaded file. 16 | 17 | *That’s all!* 18 | -------------------------------------------------------------------------------- /docs/install-java-developer-kit-win.md: -------------------------------------------------------------------------------- 1 | # Install the Java Development Kit on Windows 2 | 3 | *Markbot requires a few extra tools installed on your computer to function properly.* 4 | 5 | **The Java Development Kit allows Markbot to run the HTML validator, CSS validator & and the language checker.** 6 | 7 | ### [Go to the OpenJDK download page ➔](https://adoptopenjdk.net/) 8 | 9 | ![](images/jdk-win.jpg) 10 | 11 | **Choose the “latest” version of OpenJDK at the bottom of the list.** 12 | 13 | 1. Download the JDK for your computer. 14 | 3. Double click the installer and let it do its thing. 15 | 5. Delete the downloaded file. 16 | 17 | *That’s all!* 18 | -------------------------------------------------------------------------------- /frontend/common.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-purple-light: #d9d8e3; 3 | --color-purple-medium: #a8a6af; 4 | 5 | --color-beige-light: #f5f2f0; 6 | --color-beige-medium: #dfdcda; 7 | 8 | --color-grey-light: #ccc; 9 | --color-grey: #a8a6af; 10 | --color-grey-medium: #63666a; 11 | --color-grey-dark: #333; 12 | 13 | --control-font-size: .875rem; 14 | --control-font-size-larger: 1.1rem; 15 | --control-font-size-huge: 1.4rem; 16 | --control-border-radius: .4rem; 17 | --control-padding: 0 .875rem; 18 | --control-padding-huge: .2em .875rem; 19 | --control-border-color: rgba(0, 0, 0, .2); 20 | --control-border-color-darker: rgba(0, 0, 0, .3); 21 | --control-border-color-lighter: rgba(0, 0, 0, .1); 22 | --control-border-color-connection: rgba(0, 0, 0, .1); 23 | } 24 | 25 | @font-face { font-family: "SF Mono"; font-weight: 200; font-style: normal; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-Light.otf"); } 26 | @font-face { font-family: "SF Mono"; font-weight: 200; font-style: italic; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-LightItalic.otf"); } 27 | @font-face { font-family: "SF Mono"; font-weight: 400; font-style: normal; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-Regular.otf"); } 28 | @font-face { font-family: "SF Mono"; font-weight: 400; font-style: italic; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-RegularItalic.otf"); } 29 | @font-face { font-family: "SF Mono"; font-weight: 500; font-style: normal; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-Medium.otf"); } 30 | @font-face { font-family: "SF Mono"; font-weight: 500; font-style: italic; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-MediumItalic.otf"); } 31 | @font-face { font-family: "SF Mono"; font-weight: 600; font-style: normal; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-Semibold.otf"); } 32 | @font-face { font-family: "SF Mono"; font-weight: 600; font-style: italic; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-SemiboldItalic.otf"); } 33 | @font-face { font-family: "SF Mono"; font-weight: 700; font-style: normal; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-Bold.otf"); } 34 | @font-face { font-family: "SF Mono"; font-weight: 700; font-style: italic; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-BoldItalic.otf"); } 35 | @font-face { font-family: "SF Mono"; font-weight: 800; font-style: normal; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-Heavy.otf"); } 36 | @font-face { font-family: "SF Mono"; font-weight: 800; font-style: italic; src: url("file:///Applications/Utilities/Terminal.app/Contents/Resources/Fonts/SFMono-HeavyItalic.otf"); } 37 | 38 | html { 39 | box-sizing: border-box; 40 | overflow: hidden; 41 | text-rendering: optimizeLegibility; 42 | border-radius: 6px; 43 | 44 | font-size: 12px; 45 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 46 | line-height: 1.5; 47 | } 48 | 49 | *, *::before, *::after { 50 | box-sizing: inherit; 51 | } 52 | 53 | body { 54 | height: 100vh; 55 | margin: 0; 56 | overflow: hidden; 57 | padding: 0; 58 | width: 100vw; 59 | 60 | border-radius: 6px; 61 | -webkit-backface-visibility: hidden; 62 | backface-visibility: hidden; 63 | -webkit-perspective: 1000; 64 | perspective: 1000; 65 | -webkit-transform: translateZ(0); 66 | transform: translateZ(0); 67 | 68 | cursor: default; 69 | -webkit-user-select: none; 70 | -moz-user-select: none; 71 | -ms-user-select: none; 72 | user-select: none; 73 | } 74 | 75 | a { 76 | color: #4484c2; 77 | } 78 | 79 | a:hover { 80 | color: #2262a0; 81 | } 82 | -------------------------------------------------------------------------------- /frontend/debug/debug.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | -webkit-user-select: auto; 4 | -moz-user-select: auto; 5 | -ms-user-select: auto; 6 | user-select: auto; 7 | overflow-y: auto; 8 | } 9 | 10 | main { 11 | padding: 12px; 12 | } 13 | 14 | details { 15 | margin-bottom: .75rem; 16 | padding-bottom: .75rem; 17 | 18 | border-bottom: 3px solid #f5f2f0; 19 | } 20 | 21 | summary { 22 | display: block; 23 | 24 | cursor: pointer; 25 | 26 | color: #a8a6af; 27 | font-weight: bold; 28 | } 29 | 30 | summary span { 31 | color: #000; 32 | } 33 | 34 | details[open] summary { 35 | margin-bottom: .3em; 36 | } 37 | 38 | .cheater { 39 | color: #f33; 40 | } 41 | 42 | ul { 43 | margin: 0; 44 | padding-left: 1.2em; 45 | 46 | list-style-type: none; 47 | } 48 | 49 | li { 50 | padding: .3em 0; 51 | position: relative; 52 | 53 | border-top: 1px solid #dfdcda; 54 | } 55 | 56 | li::before { 57 | content: "•"; 58 | left: -1.7em; 59 | position: absolute; 60 | top: 0.4em; 61 | width: 1.7em; 62 | 63 | color: #a8a6af; 64 | text-align: center; 65 | } 66 | 67 | code { 68 | padding: .25em .45em; 69 | 70 | background-color: #f5f2f0; 71 | 72 | color: #444; 73 | font-size: 11px; 74 | font-family: Menlo, monospace; 75 | line-height: 1.5; 76 | } 77 | -------------------------------------------------------------------------------- /frontend/debug/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Activity — Markbot 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/debug/debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const electron = require('electron'); 4 | const ipcRenderer = electron.ipcRenderer; 5 | const shell = electron.shell; 6 | 7 | let messages = document.querySelector('#main'); 8 | let group; 9 | 10 | const closeOldGroups = function () { 11 | let details = document.querySelectorAll('details'); 12 | 13 | if (!details) return; 14 | 15 | [].forEach.call(details, function (elem) { 16 | elem.removeAttribute('open'); 17 | elem.open = false; 18 | }); 19 | }; 20 | 21 | ipcRenderer.on('__markbot-debug', function (e, ...args) { 22 | let li = document.createElement('li'); 23 | 24 | li.innerHTML = args.join(' '); 25 | 26 | if (typeof args[0] === 'string' && args[0].match(/^cheater/i)) { 27 | li.innerHTML = `${li.textContent}`; 28 | } 29 | 30 | if (li.innerHTML.match(/@@/)) { 31 | li.innerHTML = li.innerHTML.replace(/@@(.+?)@@/g, '$1'); 32 | } 33 | 34 | if (li.innerHTML.match(/`/)) { 35 | li.innerHTML = li.innerHTML.replace(/`(.+?)`/g, '$1'); 36 | } 37 | 38 | group.appendChild(li); 39 | }); 40 | 41 | ipcRenderer.on('__markbot-debug-group', function (e, label) { 42 | let details = document.createElement('details'); 43 | let summary = document.createElement('summary'); 44 | let li = document.createElement('li'); 45 | 46 | closeOldGroups(); 47 | 48 | li.innerHTML = `${label}`; 49 | group = document.createElement('ul'); 50 | summary.innerHTML = `${label.match(/\/([^/]+)$/)[1]}`; 51 | details.setAttribute('open', true); 52 | 53 | group.appendChild(li); 54 | details.appendChild(summary); 55 | details.appendChild(group); 56 | messages.appendChild(details); 57 | }); 58 | 59 | document.addEventListener('click', function (e) { 60 | if (e.target.matches('a')) { 61 | e.preventDefault(); 62 | 63 | if (e.target.getAttribute('href').slice(0, 4) === 'http') { 64 | shell.openExternal(e.target.getAttribute('href')); 65 | } else { 66 | shell.showItemInFolder(e.target.getAttribute('href')); 67 | } 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /frontend/differ/differ.css: -------------------------------------------------------------------------------- 1 | html { 2 | border-radius: 0 0 6px 6px; 3 | } 4 | 5 | body { 6 | overflow: hidden; 7 | background: url("../images/transparency-grid.svg") repeat 0 0 / 24px 24px #ccc; 8 | border-radius: 0 0 6px 6px; 9 | } 10 | 11 | img, 12 | svg { 13 | pointer-events: none; 14 | } 15 | 16 | .invisible { 17 | position: absolute; 18 | left: -30px; 19 | } 20 | 21 | .invisible-text { 22 | position: absolute; 23 | right: -100px; 24 | } 25 | 26 | main { 27 | height: calc(100vh - 29px); 28 | } 29 | 30 | footer { 31 | bottom: 0; 32 | left: 0; 33 | padding: 4px 6px; 34 | position: fixed; 35 | width: 100%; 36 | 37 | background-color: #63666a; 38 | border-top: 1px solid #333; 39 | } 40 | 41 | label { 42 | cursor: pointer; 43 | } 44 | 45 | .tabs { 46 | margin: 0; 47 | padding: 0; 48 | list-style-type: none; 49 | text-align: left; 50 | } 51 | 52 | .tabs > li { 53 | display: inline-block; 54 | } 55 | 56 | .tabs label { 57 | display: inline-block; 58 | padding: 4px 12px; 59 | 60 | background-color: #a8a6af; 61 | border-radius: 12px; 62 | 63 | color: #fff; 64 | font-size: 12px; 65 | line-height: 12px; 66 | text-transform: uppercase; 67 | } 68 | 69 | #radio-split:checked ~ footer [for="radio-split"], 70 | #radio-difference:checked ~ footer [for="radio-difference"] { 71 | background-color: #333; 72 | } 73 | 74 | .tab { 75 | display: none; 76 | height: 100%; 77 | overflow: hidden; 78 | position: relative; 79 | } 80 | 81 | .scrollable { 82 | height: 100%; 83 | position: relative; 84 | overflow-x: hidden; 85 | overflow-y: auto; 86 | } 87 | 88 | .scrollable-wrap { 89 | position: relative; 90 | overflow: hidden; 91 | } 92 | 93 | #radio-split:checked ~ main #split, 94 | #radio-difference:checked ~ main #difference { 95 | display: block; 96 | } 97 | 98 | #radio-diff-ref:checked ~ main [for="radio-diff-ref"], 99 | #radio-diff-new:checked ~ main [for="radio-diff-new"] { 100 | background-color: rgba(99, 102, 106, .9); 101 | border-color: #333; 102 | color: #fff; 103 | } 104 | 105 | .img-ref, 106 | .img-new, 107 | .img-diff { 108 | overflow: hidden; 109 | position: absolute; 110 | left: 0; 111 | top: 0; 112 | width: 100%; 113 | } 114 | 115 | #diff-img-ref-cover, 116 | #diff-img-new { 117 | display: none; 118 | } 119 | 120 | .img-diff { 121 | opacity: .5; 122 | } 123 | 124 | #radio-diff-new:checked ~ main #diff-img-ref-cover, 125 | #radio-diff-new:checked ~ main #diff-img-new { 126 | display: block; 127 | } 128 | 129 | .tab-split { 130 | cursor: col-resize; 131 | } 132 | 133 | #split-img-new { 134 | -webkit-clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%); 135 | } 136 | 137 | #diff-img-ref-cover, 138 | #split-img-ref-cover { 139 | bottom: 0; 140 | left: 0; 141 | overflow: hidden; 142 | position: absolute; 143 | right: 0; 144 | top: 0; 145 | width: 100%; 146 | 147 | background: url("../images/transparency-grid.svg") repeat 0 0 / 24px 24px fixed #ccc; 148 | } 149 | 150 | #split-img-ref-cover { 151 | -webkit-clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%); 152 | } 153 | 154 | .splitter { 155 | bottom: 0; 156 | left: calc(50% - 12px); 157 | overflow: hidden; 158 | position: absolute; 159 | top: 0; 160 | width: 24px; 161 | 162 | background: 163 | url("../images/differ-top.svg") no-repeat center top / contain, 164 | url("../images/differ-bottom.svg") no-repeat center bottom / contain, 165 | url("../images/differ-line.svg") repeat-y center bottom / contain 166 | ; 167 | cursor: col-resize; 168 | 169 | text-indent: 100%; 170 | white-space: nowrap; 171 | } 172 | 173 | .split-label { 174 | bottom: 4px; 175 | padding: 5px 9px 5px; 176 | position: absolute; 177 | 178 | background-color: rgba(255, 255, 255, .9); 179 | border: 2px solid #63666a; 180 | 181 | color: #333; 182 | font-size: 12px; 183 | font-weight: bold; 184 | line-height: 12px; 185 | text-transform: uppercase; 186 | } 187 | 188 | .split-label-ref { 189 | left: 0; 190 | border-left: 0; 191 | border-radius: 0 12px 12px 0; 192 | } 193 | 194 | .split-label-new { 195 | right: 0; 196 | border-right: 0; 197 | border-radius: 12px 0 0 12px; 198 | } 199 | 200 | #diff-range { 201 | bottom: 6px; 202 | left: 50%; 203 | position: absolute; 204 | height: 14px; 205 | width: 150px; 206 | 207 | -webkit-appearance: none; 208 | background-color: transparent; 209 | border: 2px solid #000; 210 | border-radius: 14px; 211 | transform: translateX(-50%); 212 | } 213 | 214 | #diff-range:focus { 215 | outline: none; 216 | } 217 | 218 | #diff-range::-webkit-slider-thumb { 219 | width: 16px; 220 | height: 16px; 221 | 222 | -webkit-appearance: none; 223 | background-color: #000; 224 | border: 2px solid #999; 225 | box-shadow: 0 0 0 4px #000; 226 | border-radius: 50%; 227 | } 228 | -------------------------------------------------------------------------------- /frontend/differ/differ.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Screenshot Differences — Markbot 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
Drag to see the differences
39 | Reference 40 | Yours 41 |
42 |
43 | 44 |
45 | 51 |
52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /frontend/differ/differ.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const diffWrap = document.querySelector('.tab-split'); 4 | const splitter = document.querySelector('.splitter'); 5 | const splitImgNew = document.getElementById('split-img-new'); 6 | const splitImgRefCover = document.getElementById('split-img-ref-cover'); 7 | const diffRange = document.getElementById('diff-range'); 8 | const diff = document.getElementById('diff-img-diff'); 9 | 10 | const moveSplitter = function (x) { 11 | let newX = x - splitter.offsetWidth / 2; 12 | 13 | splitter.style.left = `${newX}px`; 14 | }; 15 | 16 | const cropImage = function (x) { 17 | splitImgNew.style.webkitClipPath = `polygon(${x}px 0%, 100% 0%, 100% 100%, ${x}px 100%)`; 18 | splitImgRefCover.style.webkitClipPath = `polygon(${x}px 0%, 100% 0%, 100% 100%, ${x}px 100%)`; 19 | }; 20 | 21 | const moveDiffer = function (x) { 22 | if (x >= 0 && x <= document.documentElement.clientWidth) { 23 | moveSplitter(x); 24 | cropImage(x); 25 | } 26 | }; 27 | 28 | const handleMouseMove = function (e) { 29 | moveDiffer(e.pageX) 30 | }; 31 | 32 | const adjustDiff = function (opacity) { 33 | diff.style.opacity = opacity; 34 | }; 35 | 36 | const calcTallestImage = function (imgRef, imgNew) { 37 | let scrollableWraps = document.querySelectorAll('.scrollable-wrap'); 38 | let refAR = imgRef.offsetHeight / imgRef.offsetWidth; 39 | let newAR = imgNew.offsetHeight / imgNew.offsetWidth; 40 | 41 | if (imgRef.offsetHeight > imgNew.offsetHeight) { 42 | [].forEach.call(scrollableWraps, function (elem) { 43 | elem.style.paddingTop = (refAR * 100) + '%'; 44 | }); 45 | } else { 46 | [].forEach.call(scrollableWraps, function (elem) { 47 | elem.style.paddingTop = (newAR * 100) + '%'; 48 | }); 49 | } 50 | }; 51 | 52 | const calcHeightOnLoad = function () { 53 | let imgRef = document.getElementById('split-img-ref'); 54 | let imgNew = document.getElementById('split-img-new'); 55 | let imgRefLoaded = false; 56 | let imgNewLoaded = false; 57 | let allLoaded = false; 58 | 59 | imgRef.addEventListener('load', function () { 60 | imgRefLoaded = true; 61 | 62 | if (imgRefLoaded & imgNewLoaded & !allLoaded) { 63 | imgNewLoaded = true; 64 | calcTallestImage(imgRef, imgNew); 65 | } 66 | }); 67 | 68 | imgNew.addEventListener('load', function () { 69 | imgNewLoaded = true; 70 | 71 | if (imgRefLoaded & imgNewLoaded & !allLoaded) { 72 | allLoaded = true; 73 | calcTallestImage(imgRef, imgNew); 74 | } 75 | }); 76 | }; 77 | 78 | const setWindowTitle = function (imgPath) { 79 | let pathMatches = imgPath.match(/\-(\d+)\.png$/); 80 | 81 | document.title = pathMatches[1] + 'px'; 82 | }; 83 | 84 | const setImages = function (imgsJson) { 85 | let imgs = JSON.parse(imgsJson.replace(/\\/g, '/')); 86 | 87 | calcHeightOnLoad(); 88 | 89 | document.getElementById('split-img-ref').src = `${imgs.ref}?${Date.now()}`; 90 | document.getElementById('split-img-new').src = `${imgs.new}?${Date.now()}`; 91 | document.getElementById('diff-img-ref').src = `${imgs.ref}?${Date.now()}`; 92 | document.getElementById('diff-img-new').src = `${imgs.new}?${Date.now()}`; 93 | document.getElementById('diff-img-diff').src = `${imgs.diff}?${Date.now()}`; 94 | 95 | diffRange.value = 0.5; 96 | moveDiffer(document.documentElement.clientWidth / 2); 97 | setWindowTitle(imgs.ref); 98 | }; 99 | 100 | diffWrap.addEventListener('mousedown', function (e) { 101 | moveDiffer(e.pageX); 102 | document.addEventListener('mousemove', handleMouseMove); 103 | }); 104 | 105 | document.addEventListener('mouseup', function (e) { 106 | document.removeEventListener('mousemove', handleMouseMove); 107 | }); 108 | 109 | diffRange.addEventListener('change', function (e) { 110 | adjustDiff(diffRange.value); 111 | }); 112 | 113 | diffRange.addEventListener('input', function (e) { 114 | adjustDiff(diffRange.value); 115 | }); 116 | 117 | adjustDiff(diffRange.value); 118 | -------------------------------------------------------------------------------- /frontend/images/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/images/bypassed.svg: -------------------------------------------------------------------------------- 1 | error 2 | -------------------------------------------------------------------------------- /frontend/images/check.svg: -------------------------------------------------------------------------------- 1 | check -------------------------------------------------------------------------------- /frontend/images/computing.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/images/differ-bottom.svg: -------------------------------------------------------------------------------- 1 | differ -------------------------------------------------------------------------------- /frontend/images/differ-line.svg: -------------------------------------------------------------------------------- 1 | differ -------------------------------------------------------------------------------- /frontend/images/differ-top.svg: -------------------------------------------------------------------------------- 1 | differ -------------------------------------------------------------------------------- /frontend/images/error.svg: -------------------------------------------------------------------------------- 1 | error -------------------------------------------------------------------------------- /frontend/images/focus-triangle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/images/folder-hover.svg: -------------------------------------------------------------------------------- 1 | folder 2 | -------------------------------------------------------------------------------- /frontend/images/folder.svg: -------------------------------------------------------------------------------- 1 | folder -------------------------------------------------------------------------------- /frontend/images/help-green.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/images/help-red.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/images/help-yellow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/images/pending.svg: -------------------------------------------------------------------------------- 1 | start 2 | -------------------------------------------------------------------------------- /frontend/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasjbradley/markbot/7ef522b1f4c21bcb660780eaa19706533c303617/frontend/images/spinner.gif -------------------------------------------------------------------------------- /frontend/images/transparency-grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/images/two-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /frontend/images/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/main/alert.css: -------------------------------------------------------------------------------- 1 | .alert-wrap { 2 | display: flex; 3 | flex-flow: column wrap; 4 | align-items: center; 5 | justify-content: center; 6 | left: 0; 7 | bottom: 0; 8 | right: 0; 9 | position: fixed; 10 | top: 38px; 11 | z-index: 3000; 12 | 13 | background-color: rgba(0, 0, 0, .5); 14 | } 15 | 16 | .alert { 17 | padding: 2em; 18 | width: 34em; 19 | 20 | background-color: #fff; 21 | border-radius: 4px; 22 | 23 | font-size: 1.125rem; 24 | } 25 | 26 | .alert:focus { 27 | outline: none; 28 | box-shadow: 0 0 0 2px rgba(0, 0, 0, .5); 29 | } 30 | 31 | .alert p { 32 | margin: 0 0 1.5em; 33 | } 34 | 35 | .btn-wrap { 36 | text-align: center; 37 | } 38 | 39 | .alert-btn { 40 | font-size: 1.125rem; 41 | } 42 | -------------------------------------------------------------------------------- /frontend/main/dancing-robot.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --song-length: 7s; 3 | } 4 | 5 | #dancing-robot { 6 | height: 180px; 7 | width: 180px; 8 | 9 | animation: bounce .5s infinite alternate; 10 | overflow: visible; 11 | opacity: .2; 12 | } 13 | 14 | #arm-left { 15 | transform-origin: 57px 358px; 16 | animation: wave 1s ease-in-out infinite alternate; 17 | } 18 | 19 | #arm-right { 20 | transform-origin: 584px 357px; 21 | animation: wave 1s ease-in-out infinite alternate; 22 | } 23 | 24 | #antenna-right { 25 | animation: bounce .5s infinite alternate steps(2); 26 | } 27 | 28 | #antenna-left { 29 | animation: bounce .5s infinite alternate steps(2); 30 | } 31 | 32 | @keyframes bounce { 33 | 34 | 0% { 35 | transform: translateY(0); 36 | } 37 | 38 | 100% { 39 | transform: translateY(4px); 40 | } 41 | 42 | } 43 | 44 | @keyframes wave { 45 | 46 | 0% { 47 | transform: rotate(0); 48 | } 49 | 50 | 100% { 51 | transform: rotate(-180deg); 52 | } 53 | 54 | } 55 | 56 | .sign-frame { 57 | margin: -23px auto 0; 58 | } 59 | 60 | .robot-song { 61 | font-style: italic; 62 | font-size: 1.25rem; 63 | opacity: .4; 64 | } 65 | 66 | .robot-song span { 67 | display: block; 68 | } 69 | 70 | .line-1 { 71 | animation: line-highlight-1 var(--song-length) linear infinite; 72 | } 73 | 74 | .line-2 { 75 | animation: line-highlight-2 var(--song-length) linear infinite; 76 | } 77 | 78 | .line-3 { 79 | animation: line-highlight-3 var(--song-length) linear infinite; 80 | } 81 | 82 | .line-4 { 83 | animation: line-highlight-4 var(--song-length) linear infinite; 84 | } 85 | 86 | @keyframes line-highlight-1 { 87 | 88 | 0% { 89 | font-weight: bold; 90 | } 91 | 92 | 24% { 93 | font-weight: bold; 94 | } 95 | 96 | 25% { 97 | font-weight: normal; 98 | } 99 | 100 | } 101 | 102 | @keyframes line-highlight-2 { 103 | 104 | 0% { 105 | font-weight: normal; 106 | } 107 | 108 | 24% { 109 | font-weight: normal; 110 | } 111 | 112 | 25% { 113 | font-weight: bold; 114 | } 115 | 116 | 49% { 117 | font-weight: bold; 118 | } 119 | 120 | 50% { 121 | font-weight: normal; 122 | } 123 | 124 | } 125 | 126 | @keyframes line-highlight-3 { 127 | 128 | 0% { 129 | font-weight: normal; 130 | } 131 | 132 | 49% { 133 | font-weight: normal; 134 | } 135 | 136 | 50% { 137 | font-weight: bold; 138 | } 139 | 140 | 74% { 141 | font-weight: bold; 142 | } 143 | 144 | 75% { 145 | font-weight: normal; 146 | } 147 | 148 | } 149 | 150 | @keyframes line-highlight-4 { 151 | 152 | 0% { 153 | font-weight: normal; 154 | } 155 | 156 | 74% { 157 | font-weight: normal; 158 | } 159 | 160 | 75% { 161 | font-weight: bold; 162 | } 163 | 164 | 99% { 165 | font-weight: bold; 166 | } 167 | 168 | 100% { 169 | font-weight: normal; 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /frontend/main/deps.css: -------------------------------------------------------------------------------- 1 | .deps { 2 | margin: 0 auto; 3 | max-width: 36em; 4 | 5 | text-align: left; 6 | } 7 | 8 | .deps h1 { 9 | text-align: left; 10 | } 11 | 12 | .deps-list { 13 | margin: 0; 14 | padding: 0; 15 | 16 | border-top: 1px solid rgba(0, 0, 0, .3); 17 | 18 | list-style-type: none; 19 | } 20 | 21 | .deps-list > li { 22 | padding: 1.5rem 0; 23 | display: flex; 24 | flex-direction: row; 25 | align-items: center; 26 | 27 | border-bottom: 1px solid rgba(0, 0, 0, .3); 28 | } 29 | 30 | .deps-list > li[data-state="hidden"] { 31 | display: none; 32 | } 33 | 34 | .deps-list h1 { 35 | margin: 0; 36 | } 37 | 38 | .deps-list h2 { 39 | margin: 0; 40 | } 41 | 42 | .deps p { 43 | margin-bottom: 1.5rem; 44 | margin-top: .6rem; 45 | 46 | font-size: 1.25rem; 47 | opacity: .8; 48 | } 49 | 50 | .deps a { 51 | font-size: 1.25rem; 52 | } 53 | 54 | .deps small { 55 | display: block; 56 | padding-top: .6rem; 57 | } 58 | 59 | .deps .logo { 60 | display: block; 61 | margin: 0 auto; 62 | width: 80px; 63 | } 64 | 65 | .deps .icon { 66 | margin-right: 24px; 67 | 68 | opacity: .6; 69 | } 70 | 71 | .os-darwin .dep-win { 72 | display: none; 73 | } 74 | 75 | .os-win .dep-mac, 76 | .os-win32 .dep-mac { 77 | display: none; 78 | } 79 | -------------------------------------------------------------------------------- /frontend/main/robot-beeps.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Beep beep boop boop", 3 | "Beep bup beep bup", 4 | "Bep bop Bep bop", 5 | "Bing bing bong bong", 6 | "Bleep bleep bloop bloop", 7 | "Bop bup bop bup", 8 | "Click clack click clack", 9 | "Tsche chu chu chu tsche", 10 | "Zing zing zang zang", 11 | "Zoot zot zoot zot" 12 | ] 13 | -------------------------------------------------------------------------------- /frontend/main/success-messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Booyakasha", 3 | "Way to go", 4 | "Super-duper", 5 | "Awesome", 6 | "Cowabunga", 7 | "Rad", 8 | "Amazeballs", 9 | "Sweet", 10 | "Cool", 11 | "Nice", 12 | "Fantastic", 13 | "Geronimo", 14 | "Whamo", 15 | "Superb", 16 | "Stupendous", 17 | "Mathmatical", 18 | "All clear" 19 | ] 20 | -------------------------------------------------------------------------------- /frontend/main/time-estimator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const gitCommits = require('git-commits'); 6 | 7 | const matchesProfEmail = function (email, profEmails) { 8 | return profEmails.includes(email); 9 | }; 10 | 11 | const getFolderStartTime = function (stats) { 12 | return Math.round(stats.birthtimeMs / 1000); 13 | }; 14 | 15 | const getFolderEndTime = function (stats) { 16 | return Math.round(stats.mtimeMs / 1000); 17 | } 18 | 19 | const getStartTime = function (stats, commits = []) { 20 | const folderStart = getFolderStartTime(stats); 21 | const firstCommit = (commits.length > 0) ? commits[0].author.timestamp : Math.round(Date.now() / 1000); 22 | 23 | return new Date(((folderStart < firstCommit) ? folderStart : firstCommit) * 1000); 24 | }; 25 | 26 | const getEndTime = function (stats, commits = []) { 27 | const folderEnd = getFolderEndTime(stats); 28 | const lastCommit = (commits.length > 0) ? commits[commits.length - 1].author.timestamp : false; 29 | 30 | return new Date(((lastCommit !== false) ? lastCommit : folderEnd) * 1000); 31 | }; 32 | 33 | const getCommitTimeIntervals = function (commits, hoursThreshold) { 34 | let commitDates = commits.map((commit) => commit.author.timestamp).sort((a, b) => a - b); 35 | let totalDates = commitDates.length; 36 | let intervals = []; 37 | 38 | hoursThreshold = hoursThreshold * 60 * 60; 39 | 40 | for (let i = 1; i < totalDates; i++) { 41 | let timeDiff = commitDates[i] - commitDates[i - 1]; 42 | if (timeDiff <= hoursThreshold) { 43 | intervals.push(timeDiff); 44 | } else { 45 | // Add .5 hour extra for large gaps in time 46 | intervals.push(.5 * 60 * 60); 47 | } 48 | } 49 | 50 | return intervals; 51 | }; 52 | 53 | const calculateCommitTimes = function (commits, hoursThreshold = 2) { 54 | return getCommitTimeIntervals(commits, hoursThreshold).reduce((sum, val) => sum + Math.abs(val), 0); 55 | }; 56 | 57 | const getTimeEstimate = function (repo, ignoreCommitEmails, next) { 58 | let allCommits = []; 59 | let errorResults = { 60 | start: new Date(), 61 | end: new Date(), 62 | estimatedTime: 1, 63 | numCommits: false, 64 | }; 65 | 66 | const throwErrorResults = function () { 67 | next(errorResults); 68 | }; 69 | 70 | fs.stat(repo, (err, stats) => { 71 | if (err) return throwErrorResults(); 72 | 73 | const repoBirthtime = getFolderStartTime(stats); 74 | const repoMtime = getFolderEndTime(stats); 75 | 76 | errorResults.start = new Date(repoBirthtime * 1000); 77 | errorResults.end = new Date(repoMtime * 1000); 78 | errorResults.estimatedTime = parseFloat(Math.round((repoMtime - repoBirthtime) / 60 / 60 * 100) / 100).toFixed(2); 79 | 80 | gitCommits(path.resolve(`${repo}/.git`)) 81 | .on('data', (commit) => { 82 | if (!matchesProfEmail(commit.author.email, ignoreCommitEmails)) allCommits.push(commit); 83 | }) 84 | .on('end', () => { 85 | allCommits.sort((a, b) => a.author.timestamp - b.author.timestamp); 86 | 87 | next({ 88 | start: getStartTime(stats, allCommits), 89 | end: getEndTime(stats, allCommits), 90 | estimatedTime: parseFloat(Math.round(calculateCommitTimes(allCommits) / 60 / 60 * 100) / 100).toFixed(2), 91 | numCommits: allCommits.length, 92 | }); 93 | }) 94 | .on('error', throwErrorResults) 95 | ; 96 | }); 97 | }; 98 | 99 | module.exports = { 100 | getTimeEstimate: getTimeEstimate, 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Markbot", 3 | "version": "8.2.3", 4 | "license": "GPL", 5 | "description": "An automated marking application for code projects in the Algonquin College Graphic Design program.", 6 | "author": "Thomas J Bradley ", 7 | "homepage": "https://github.com/thomasjbradley/markbot", 8 | "bugs": "https://github.com/thomasjbradley/markbot/issues", 9 | "private": true, 10 | "main": "markbot.js", 11 | "dependencies": { 12 | "axe-core": "^3.1.2", 13 | "calipers": "^2.0.1", 14 | "calipers-jpeg": "^2.0.0", 15 | "calipers-png": "^2.0.0", 16 | "calipers-svg": "^2.0.0", 17 | "cheerio": "^1.0.0-rc.2", 18 | "css": "^2.2.4", 19 | "css-select": "^2.0.0", 20 | "electron-is": "^3.0.0", 21 | "entities": "^1.1.1", 22 | "eslint": "^5.6.1", 23 | "exif": "^0.6.0", 24 | "finalhandler": "^1.1.1", 25 | "fix-path": "^2.1.0", 26 | "front-matter": "^2.3.0", 27 | "get-port": "^4.0.0", 28 | "git-commits": "^1.2.0", 29 | "git-state": "^4.1.0", 30 | "glob": "^7.1.3", 31 | "htmlcs": "^0.4.1", 32 | "image-size": "^0.6.3", 33 | "jimp": "^0.5.3", 34 | "js-beautify": "1.7.5", 35 | "js-yaml": "^3.12.0", 36 | "markdownlint": "^0.11.0", 37 | "merge-objects": "^1.0.5", 38 | "mime-types": "^2.1.20", 39 | "mkdirp": "^0.5.1", 40 | "node-dir": "^0.1.16", 41 | "parse5": "^5.1.0", 42 | "parse5-htmlparser2-tree-adapter": "^5.1.0", 43 | "png-itxt": "^2.0.0", 44 | "portfinder": "^1.0.17", 45 | "request": "^2.88.0", 46 | "string": "^3.3.3", 47 | "stylelint": "^9.6.0", 48 | "stylelint-declaration-block-no-ignored-properties": "^1.1.0", 49 | "webcoach": "^2.2.0", 50 | "xml2js": "^0.4.19" 51 | }, 52 | "devDependencies": { 53 | "devtron": "^1.4.0", 54 | "electron": "^3.0.2", 55 | "electron-builder": "^20.28.4" 56 | }, 57 | "scripts": { 58 | "postinstall": "electron-builder install-app-deps", 59 | "debug": "NODE_ENV=development electron markbot.js", 60 | "start": "electron markbot.js", 61 | "pack-files": "xattr -cr .", 62 | "pack-bg": "tiffutil -cathidpicheck ./build/background.png ./build/background@2x.png -out ./build/background.tiff", 63 | "pack-mac": "npm run pack-files && build -m --dir", 64 | "pack-win": "npm run pack-files && build -w --dir", 65 | "pack": "npm run pack-files && build -mw --dir", 66 | "build-mac": "npm run pack-bg && npm run pack-files && build -m", 67 | "build-win": "npm run pack-files && build -w", 68 | "build": "npm run pack-bg && npm run pack-files && build -mw", 69 | "hash-passcode": "node ./scripts/hash-passcode.js", 70 | "gen-https-cert": "./scripts/gen-https-cert.sh" 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "https://github.com/thomasjbradley/markbot.git" 75 | }, 76 | "build": { 77 | "appId": "ca.thomasjbradley.markbot", 78 | "copyright": "© Thomas J Bradley", 79 | "productName": "Markbot", 80 | "compression": "maximum", 81 | "asarUnpack": [ 82 | "vendor" 83 | ], 84 | "mac": { 85 | "target": "dmg", 86 | "category": "public.app-category.developer-tools", 87 | "extendInfo": { 88 | "CFBundleDocumentTypes": [ 89 | { 90 | "CFBundleTypeRole": "Editor", 91 | "LSHandlerRank": "Alternate", 92 | "LSItemContentTypes": [ 93 | "public.directory", 94 | "com.apple.bundle", 95 | "com.apple.resolvable" 96 | ] 97 | } 98 | ] 99 | } 100 | }, 101 | "win": { 102 | "target": "nsis" 103 | }, 104 | "nsis": { 105 | "perMachine": true 106 | }, 107 | "dmg": { 108 | "title": "Install Markbot", 109 | "iconSize": 100, 110 | "window": { 111 | "x": 200, 112 | "y": 200 113 | }, 114 | "contents": [ 115 | { 116 | "x": 494, 117 | "y": 270, 118 | "type": "link", 119 | "path": "/Applications" 120 | }, 121 | { 122 | "x": 210, 123 | "y": 270, 124 | "type": "file" 125 | } 126 | ] 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /scripts/gen-https-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | openssl req -config ./scripts/localhost.conf -new -x509 -sha256 -newkey rsa:2048 -nodes -keyout ./app/https-key.pem -days 1460 -subj "/C=CA/ST=Ontario/L=Ottawa/O=Markbot/OU=Markbot/CN=Markbot" -out ./app/https-cert.pem 4 | -------------------------------------------------------------------------------- /scripts/hash-passcode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const passcode = require('../app/passcode'); 7 | 8 | let config = require('../config.json'); 9 | 10 | config.secret = crypto.randomBytes(32).toString('hex'); 11 | config.passcodeHash = passcode.hash(process.env.MARKBOT_LOCK_PASSCODE, config.secret); 12 | fs.writeFileSync(path.resolve(__dirname, '../config.json'), JSON.stringify(config, null, 2), 'utf8'); 13 | -------------------------------------------------------------------------------- /scripts/localhost.conf: -------------------------------------------------------------------------------- 1 | # From: https://stackoverflow.com/a/27931596/133776 2 | 3 | [ req ] 4 | default_bits = 2048 5 | default_keyfile = server-key.pem 6 | distinguished_name = subject 7 | req_extensions = req_ext 8 | x509_extensions = x509_ext 9 | string_mask = utf8only 10 | 11 | # The Subject DN can be formed using X501 or RFC 4514 (see RFC 4519 for a description). 12 | # Its sort of a mashup. For example, RFC 4514 does not provide emailAddress. 13 | [ subject ] 14 | countryName = CA 15 | countryName_default = CA 16 | 17 | stateOrProvinceName = Ontario 18 | stateOrProvinceName_default = Ontario 19 | 20 | localityName = Ottawa 21 | localityName_default = Ottawa 22 | 23 | organizationName = Markbot 24 | organizationName_default = Markbot 25 | 26 | # Use a friendly name here because its presented to the user. The server's DNS 27 | # names are placed in Subject Alternate Names. Plus, DNS names here is deprecated 28 | # by both IETF and CA/Browser Forums. If you place a DNS name here, then you 29 | # must include the DNS name in the SAN too (otherwise, Chrome and others that 30 | # strictly follow the CA/Browser Baseline Requirements will fail). 31 | commonName = Markbot 32 | commonName_default = Markbot 33 | 34 | emailAddress = hey@thomasjbradley.ca 35 | emailAddress_default = hey@thomasjbradley.ca 36 | 37 | # Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ... 38 | [ x509_ext ] 39 | 40 | subjectKeyIdentifier = hash 41 | authorityKeyIdentifier = keyid, issuer 42 | 43 | # You only need digitalSignature below. *If* you don't allow 44 | # RSA Key transport (i.e., you use ephemeral cipher suites), then 45 | # omit keyEncipherment because that's key transport. 46 | basicConstraints = CA:FALSE 47 | keyUsage = digitalSignature, keyEncipherment 48 | subjectAltName = @alternate_names 49 | nsComment = "OpenSSL Generated Certificate" 50 | 51 | # RFC 5280, Section 4.2.1.12 makes EKU optional 52 | # CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused 53 | # In either case, you probably only need serverAuth. 54 | # extendedKeyUsage = serverAuth, clientAuth 55 | 56 | # Section req_ext is used when generating a certificate signing request. I.e., openssl req ... 57 | [ req_ext ] 58 | 59 | subjectKeyIdentifier = hash 60 | 61 | basicConstraints = CA:FALSE 62 | keyUsage = digitalSignature, keyEncipherment 63 | subjectAltName = @alternate_names 64 | nsComment = "OpenSSL Generated Certificate" 65 | 66 | # RFC 5280, Section 4.2.1.12 makes EKU optional 67 | # CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused 68 | # In either case, you probably only need serverAuth. 69 | # extendedKeyUsage = serverAuth, clientAuth 70 | 71 | [ alternate_names ] 72 | 73 | DNS.1 = localhost 74 | DNS.2 = 127.0.0.1 75 | 76 | # IPv6 localhost 77 | # DNS.8 = ::1 78 | -------------------------------------------------------------------------------- /templates/aria-landmarks.yml: -------------------------------------------------------------------------------- 1 | allFiles: 2 | html: 3 | has: 4 | - check: 'header[role="banner"]' 5 | message: 'The role of “banner” should be assigned to the `
` tag' 6 | - check: '[role="banner"]' 7 | message: 'The “banner” role is used to define the header of the whole website so there should only be one per page' 8 | limit: 1 9 | 10 | - check: 'main[role="main"], div[role="main"], article[role="main"], section[role="main"]' 11 | message: 'The role of “main” should be assigned to the `
` tag—or at least `
`, `
` or `
` if `
` doesn’t make sense' 12 | - check: '[role="main"]' 13 | message: 'The “main” role is used to define the main content of the whole page so there should only be one per page' 14 | limit: 1 15 | 16 | - check: 'nav[role="navigation"]' 17 | message: 'The role of “navigation” should be assigned to the `