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