├── .babelrc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test ├── .htmlhintrc ├── .htmlhintrc-broken ├── fixtures ├── error │ ├── error-two.js │ ├── error.js │ ├── template-two.html │ └── template.html └── rules │ └── myNewRule.js ├── htmlhint.json └── test.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "@babel/preset-env" ] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Fixes**: # 2 | 3 | 🚨 Please review the [guidelines for contributing](./CONTRIBUTING.md) and our [code of conduct](./CODE_OF_CONDUCT.md) to this repository. 🚨 4 | **Please complete these steps and check these boxes (by putting an x inside the brackets) before filing your PR:** 5 | 6 | - [ ] Check the commit's or even all commits' message styles matches our requested structure. 7 | - [ ] Check your code additions will fail neither code linting checks nor unit test. 8 | 9 | #### Short description of what this resolves: 10 | 11 | #### Proposed changes: 12 | 13 | ## - 14 | 15 | - 16 | 17 | 👍 Thank you! 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | test/unit/coverage 7 | test/e2e/reports 8 | selenium-debug.log 9 | dist/ 10 | .tmp/ 11 | coverage 12 | .nyc_output 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.11.3 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | node_js: 6 | - '10' 7 | - '9' 8 | - '8' 9 | - '6' 10 | notifications: 11 | email: false 12 | install: 13 | - npm install -g codecov 14 | - npm install 15 | after_success: 16 | - npm run codecov 17 | branches: 18 | only: 19 | - master 20 | - /^greenkeeper/.*$/ 21 | except: 22 | - /^v\d+\.\d+\.\d+$/ 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.1 (2018-03-06) 2 | * Fix compatibility with webpack 4 3 | 4 | # 1.3.0 (2017-06-02) 5 | * Allow absolute paths to the config file 6 | * Warn if the config file was not found 7 | 8 | # 1.2.0 (2017-04-25) 9 | * Add the `rulesDir` option - thanks to @Benoit-Vasseur 10 | * Fix webpack 2 loader syntax in the readme - thanks to @jaythomas 11 | 12 | # 1.1.2 (2017-02-22) 13 | * Upgrade loader-utils to remove deprecation warning 14 | * Update readme to reflect webpack 2 config style 15 | 16 | # 1.1.1 (2017-01-11) 17 | * Handle BOM encoding in config files 18 | 19 | # 1.1.0 (2016-12-16) 20 | * Add the `outputReport` report option - thanks to @colinwkirk 21 | 22 | # 1.0.0 (2016-09-05) 23 | * Breaking - dropped support for node < 4.0.0 24 | * Tests rewritten as integration rather than unit tests for better reliability 25 | 26 | # 0.1.0 (2015-07-14) 27 | _Very first, initial release_. 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at thedaviddias@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## Introduction 4 | 5 | First, thank you for considering contributing to HTMLHint! It's people like you that make the open source community such a great community! 😊 6 | 7 | We welcome any type of contribution, not only code. You can help with 8 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) 9 | - **Marketing**: writing blog posts, howto's, printing stickers, ... 10 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... 11 | - **Code**: take a look at the [open issues](https://github.com/htmlhint/htmlhint-loader/issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. 12 | 13 | ## Your First Contribution 14 | 15 | Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 16 | 17 | ## Submitting code 18 | 19 | Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. 20 | 21 | ## Code review process 22 | 23 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. 24 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? 25 | 26 | ## Questions 27 | 28 | If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). 29 | You can also reach us at htmlhint@gmail.com. 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2018 / Matt Lewis @mattlewis (hello@mattlewis.me) 4 | 5 | Copyright (c) 2018 / HTMLHint (htmlhint@gmail.com) / David Dias @thedaviddias (thedaviddias@gmail.com) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | HTMLHint Loader 4 |
5 |

6 | 7 |

A Webpack loader for HTMLHint

8 | 9 |

10 | 11 | Travis Build Status 12 | 13 | 14 | Codecov 15 | 16 | 17 | NPM count 18 | 19 | MIT Licence 20 | 21 | Chat 22 | 23 | 24 | Chat 25 | 26 |

27 | 28 |

29 |   How To UseContributingRoadmapWebsite 30 |

31 | 32 | ## Table of Contents 33 | 34 | - **[Install](#install)** 35 | - **[Usage](#usage)** 36 | - **[Options](#options)** 37 | 38 | ## Install 39 | 40 | ``` 41 | npm install htmlhint-loader 42 | ``` 43 | 44 | ## Usage 45 | 46 | ```javascript 47 | module.exports = { 48 | module: { 49 | rules: [{ 50 | enforce: 'pre', 51 | test: /\.html/, 52 | loader: 'htmlhint-loader', 53 | exclude: /node_modules/ 54 | }] 55 | } 56 | } 57 | ``` 58 | 59 | ### Options 60 | 61 | You can directly pass some [htmlhint rules](https://github.com/thedaviddias/HTMLHint/wiki/Rules) by 62 | 63 | - Adding a query string to the loader for this loader usage only 64 | 65 | ```javascript 66 | module.exports = { 67 | module: { 68 | rules: [{ 69 | enforce: 'pre', 70 | test: /\.html/, 71 | loader: 'htmlhint-loader?{tagname-lowercase: true}', 72 | exclude: /node_modules/ 73 | }] 74 | } 75 | } 76 | ``` 77 | 78 | - Adding a `htmlhint` entry in your webpack loader options: 79 | 80 | ```javascript 81 | module.exports = { 82 | module: { 83 | rules: [{ 84 | enforce: 'pre', 85 | test: /\.html/, 86 | loader: 'htmlhint-loader', 87 | exclude: /node_modules/, 88 | options: { 89 | configFile: 'path/.htmlhintrc' 90 | } 91 | }] 92 | } 93 | } 94 | ``` 95 | 96 | #### `configFile` 97 | 98 | A path to a json file containing the set of htmlhint rules you would like applied to this project. *By default all rules are turned off and it is up to you to enable them.* 99 | 100 | Example file: 101 | ```javascript 102 | { 103 | "tagname-lowercase": true, 104 | "attr-lowercase": true, 105 | "attr-value-double-quotes": true 106 | } 107 | ``` 108 | 109 | #### `formatter` (default: a function that pretty prints any warnings and errors) 110 | 111 | The function is called with an array of messages direct for htmlhint and must return a string. 112 | 113 | #### `emitAs` (default: `null`) 114 | 115 | What to emit errors and warnings as. Set to `warning` to always emit errors as warnings and `error` to always emit warnings as errors. By default the plugin will auto detect whether to emit as a warning or an error. 116 | 117 | #### `failOnError` (default `false`) 118 | 119 | Whether to force webpack to fail the build on a htmlhint error 120 | 121 | #### `failOnWarning` (default `false`) 122 | 123 | Whether to force webpack to fail the build on a htmlhint warning 124 | 125 | #### `customRules` 126 | 127 | Any custom rules you would like added to htmlhint. Specify as an array like so: 128 | ```javascript 129 | module.exports = { 130 | module: { 131 | rules: [{ 132 | enforce: 'pre', 133 | test: /\.html/, 134 | loader: 'htmlhint-loader', 135 | exclude: /node_modules/, 136 | options: { 137 | customRules: [{ 138 | id: 'my-rule-name', 139 | description: 'Example description', 140 | init: function(parser, reporter) { 141 | //see htmlhint docs / source for what to put here 142 | } 143 | }] 144 | } 145 | }] 146 | } 147 | } 148 | ``` 149 | 150 | #### `rulesDir` 151 | 152 | You can add a path to a folder containing your custom rules. 153 | See below for the format of the rule, it is not the same as HTMLHINT - you can pass a value to a rule. 154 | ```javascript 155 | // webpack config 156 | module.exports = { 157 | module: { 158 | rules: [{ 159 | enforce: 'pre', 160 | test: /\.html/, 161 | loader: 'htmlhint-loader', 162 | exclude: /node_modules/, 163 | options: { 164 | rulesDir: path.join(__dirname, 'rules/'), 165 | 'my-new-rule': 'this is pass to the rule (option)' 166 | } 167 | }] 168 | } 169 | } 170 | ``` 171 | 172 | ```javascript 173 | // rules/myNewRule.js 174 | const id = 'my-new-rule'; 175 | 176 | module.exports = { 177 | id, 178 | rule: function(HTMLHint, option /* = 'this is pass to the rule (option)' */) { 179 | HTMLHint.addRule({ 180 | id, 181 | description: 'my-new-rule', 182 | init: () => { 183 | //see htmlhint docs / source for what to put here 184 | } 185 | }); 186 | } 187 | }; 188 | ``` 189 | 190 | ##### `outputReport` (default: `false`) 191 | Write the output of the errors to a file, for example a checkstyle xml file for use for reporting on Jenkins CI 192 | 193 | The `filePath` is relative to the webpack config: output.path 194 | The use of [name] is supported when linting multiple files. 195 | You can pass in a different formatter for the output file, if none is passed in the default/configured formatter will be used 196 | 197 | ```javascript 198 | module.exports = { 199 | module: { 200 | rules: [{ 201 | enforce: 'pre', 202 | test: /\.html/, 203 | loader: 'htmlhint-loader', 204 | exclude: /node_modules/, 205 | options: { 206 | outputReport: { 207 | filePath: 'checkstyle-[name].xml', 208 | formatter(messages) { 209 | // convert messages to a string that will be written to the file 210 | return messagesFormattedToString; 211 | } 212 | } 213 | } 214 | }] 215 | } 216 | } 217 | ``` 218 | 219 | ## Licence 220 | 221 | Project initially created by [@mattlewis](https://github.com/mattlewis92) and transferred to the [HTMLHint](https://github.com/htmlhint) organization. 222 | 223 | Logo HTMLHint 224 | 225 | [MIT License](./LICENSE) 226 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import {HTMLHint} from 'htmlhint'; 6 | import loaderUtils from 'loader-utils'; 7 | import chalk from 'chalk'; 8 | import stripBom from 'strip-bom'; 9 | import glob from 'glob'; 10 | 11 | function formatMessage(message) { 12 | let {evidence} = message; 13 | const {line} = message; 14 | const {col} = message; 15 | const detail = typeof message.line === 'undefined' ? 16 | chalk.yellow('GENERAL') : `${chalk.yellow('L' + line)}${chalk.red(':')}${chalk.yellow('C' + col)}`; 17 | 18 | if (col === 0) { 19 | evidence = chalk.red('?') + evidence; 20 | } else if (col > evidence.length) { 21 | evidence = chalk.red(evidence + ' '); 22 | } else { 23 | evidence = `${evidence.slice(0, col - 1)}${chalk.red(evidence[col - 1])}${evidence.slice(col)}`; 24 | } 25 | 26 | return { 27 | message: `${chalk.red('[')}${detail}${chalk.red(']')}${chalk.yellow(' ' + message.message)} (${message.rule.id})`, 28 | evidence: evidence // eslint-disable-line object-shorthand 29 | }; 30 | } 31 | 32 | function defaultFormatter(messages) { 33 | let output = ''; 34 | 35 | messages.forEach(message => { 36 | const formatted = formatMessage(message); 37 | output += formatted.message + '\n'; 38 | output += formatted.evidence + '\n'; 39 | }); 40 | 41 | return output.trim(); 42 | } 43 | 44 | function loadCustomRules(options) { 45 | let rulesDir = options.rulesDir.replace(/\\/g, '/'); 46 | if (fs.existsSync(rulesDir)) { 47 | if (fs.statSync(rulesDir).isDirectory()) { 48 | rulesDir += /\/$/.test(rulesDir) ? '' : '/'; 49 | rulesDir += '**/*.js'; 50 | glob.sync(rulesDir, { 51 | dot: false, 52 | nodir: true, 53 | strict: false, 54 | silent: true 55 | }).forEach(file => { 56 | loadRule(file, options); 57 | }); 58 | } else { 59 | loadRule(rulesDir, options); 60 | } 61 | } 62 | } 63 | 64 | function loadRule(filepath, options) { 65 | options = options || {}; 66 | filepath = path.resolve(filepath); 67 | const ruleObj = require(filepath); // eslint-disable-line import/no-dynamic-require 68 | const ruleOption = options[ruleObj.id]; // We can pass a value to the rule 69 | ruleObj.rule(HTMLHint, ruleOption); 70 | } 71 | 72 | function lint(source, options, webpack, done) { 73 | try { 74 | if (options.customRules) { 75 | options.customRules.forEach(rule => HTMLHint.addRule(rule)); 76 | } 77 | if (options.rulesDir) { 78 | loadCustomRules(options); 79 | } 80 | 81 | const report = HTMLHint.verify(source, options); 82 | if (report.length > 0) { 83 | const reportByType = { 84 | warning: report.filter(message => message.type === 'warning'), 85 | error: report.filter(message => message.type === 'error') 86 | }; 87 | 88 | // Add filename for each results so formatter can have relevant filename 89 | report.forEach(r => { 90 | r.filePath = webpack.resourcePath; 91 | }); 92 | 93 | const messages = options.formatter(report); 94 | if (options.outputReport && options.outputReport.filePath) { 95 | let reportOutput; 96 | // If a different formatter is passed in as an option use that 97 | if (options.outputReport.formatter) { 98 | reportOutput = options.outputReport.formatter(report); 99 | } else { 100 | reportOutput = messages; 101 | } 102 | const filePath = loaderUtils.interpolateName(webpack, options.outputReport.filePath, { 103 | content: report.map(r => r.filePath).join('\n') 104 | }); 105 | webpack.emitFile(filePath, reportOutput); 106 | } 107 | 108 | let emitter = reportByType.error.length > 0 ? webpack.emitError : webpack.emitWarning; 109 | if (options.emitAs === 'error') { 110 | emitter = webpack.emitError; 111 | } else if (options.emitAs === 'warning') { 112 | emitter = webpack.emitWarning; 113 | } 114 | 115 | emitter(new Error(options.formatter(report))); 116 | 117 | if (reportByType.error.length > 0 && options.failOnError) { 118 | throw new Error('Module failed because of a htmlhint error.'); 119 | } 120 | 121 | if (reportByType.warning.length > 0 && options.failOnWarning) { 122 | throw new Error('Module failed because of a htmlhint warning.'); 123 | } 124 | } 125 | 126 | done(null, source); 127 | } catch (error) { 128 | done(error); 129 | } 130 | } 131 | 132 | module.exports = function (source) { 133 | const DEFAULT_CONFIG_FILE = '.htmlhintrc'; 134 | const options = Object.assign( 135 | { // Loader defaults 136 | formatter: defaultFormatter, 137 | emitAs: null, // Can be either warning or error 138 | failOnError: false, 139 | failOnWarning: false, 140 | customRules: [], 141 | configFile: DEFAULT_CONFIG_FILE 142 | }, 143 | this.options && this.options.htmlhint ? this.options.htmlhint : {}, // User defaults 144 | loaderUtils.getOptions(this) // Loader query string 145 | ); 146 | 147 | this.cacheable(); 148 | 149 | const done = this.async(); 150 | 151 | let configFilePath = options.configFile; 152 | if (!path.isAbsolute(configFilePath)) { 153 | configFilePath = path.join(process.cwd(), configFilePath); 154 | } 155 | 156 | if (fs.existsSync(configFilePath)) { 157 | fs.readFile(configFilePath, 'utf8', (err, configString) => { 158 | if (err) { 159 | done(err); 160 | } else { 161 | try { 162 | const htmlHintConfig = JSON.parse(stripBom(configString)); 163 | lint(source, Object.assign(options, htmlHintConfig), this, done); 164 | } catch (error) { 165 | done(new Error('Could not parse the htmlhint config file')); 166 | } 167 | } 168 | }); 169 | } else { 170 | if (configFilePath !== path.join(process.cwd(), DEFAULT_CONFIG_FILE)) { 171 | console.warn(`Could not find htmlhint config file in ${configFilePath}`); 172 | } 173 | lint(source, options, this, done); 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmlhint-loader", 3 | "version": "1.3.1", 4 | "description": "A webpack loader for htmlhint", 5 | "main": "index.js", 6 | "scripts": { 7 | "pretest": "npm run lint", 8 | "test": "nyc mocha --require @babel/register", 9 | "test:debug": "mocha --inspect --debug-brk", 10 | "codecov": "cat ./coverage/lcov.info | codecov", 11 | "lint": "xo index.js test/test.spec.js", 12 | "start": "mocha --require @babel/register --watch", 13 | "preversion": "npm test", 14 | "postversion": "npm publish" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/htmlhint/htmlhint-loader.git" 19 | }, 20 | "keywords": [ 21 | "webpack", 22 | "loader", 23 | "htmlhint", 24 | "linter", 25 | "lint", 26 | "html" 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/htmlhint/htmlhint-loader/issues" 31 | }, 32 | "homepage": "https://github.com/htmlhint/htmlhint-loader#readme", 33 | "dependencies": { 34 | "chalk": "^2.4.1", 35 | "glob": "^7.1.3", 36 | "htmlhint": "^0.10.1", 37 | "loader-utils": "^1.0.2", 38 | "strip-bom": "^3.0.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.0.0-rc.3", 42 | "@babel/preset-env": "^7.0.0-rc.3", 43 | "@babel/register": "^7.0.0", 44 | "chai": "^4.0.1", 45 | "codecov-lite": "^0.1.3", 46 | "coffeescript": "^2.3.1", 47 | "mocha": "^5.2.0", 48 | "nyc": "^13.0.1", 49 | "raw-loader": "^0.5.1", 50 | "sinon": "^6.1.5", 51 | "sinon-chai": "^3.2.0", 52 | "strip-ansi": "^4.0.0", 53 | "webpack": "^4.17.2", 54 | "xo": "^0.23.0" 55 | }, 56 | "engines": { 57 | "node": ">=6.0.0" 58 | }, 59 | "files": [ 60 | "index.js" 61 | ], 62 | "xo": { 63 | "space": true, 64 | "envs": [ 65 | "node", 66 | "mocha" 67 | ] 68 | }, 69 | "nyc": { 70 | "reporter": [ 71 | "lcovonly", 72 | "text-summary", 73 | "html" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": true 3 | } 4 | -------------------------------------------------------------------------------- /test/.htmlhintrc-broken: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase: true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/error/error-two.js: -------------------------------------------------------------------------------- 1 | var templatetwo = require('./template-two.html') 2 | -------------------------------------------------------------------------------- /test/fixtures/error/error.js: -------------------------------------------------------------------------------- 1 | var template = require('./template.html'); -------------------------------------------------------------------------------- /test/fixtures/error/template-two.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/fixtures/error/template.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/fixtures/rules/myNewRule.js: -------------------------------------------------------------------------------- 1 | const id = 'my-new-rule'; 2 | 3 | module.exports = { 4 | id, 5 | rule: function(HTMLHint, option) { 6 | HTMLHint.addRule({ 7 | id, 8 | description: 'my-new-rule', 9 | init: option 10 | }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /test/htmlhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "attr-value-double-quotes": true, 3 | "attr-no-duplication": true, 4 | "tag-pair": true, 5 | "tag-self-close": true, 6 | "spec-char-escape": true, 7 | "id-unique": true, 8 | "src-not-empty": true, 9 | "title-require": true, 10 | "head-script-disabled": true, 11 | "alt-require": true, 12 | "doctype-html5": true, 13 | "style-disabled": true, 14 | "inline-style-disabled": true, 15 | "inline-script-disabled": true, 16 | "space-tab-mixed-disabled": true, 17 | "id-class-ad-disabled": true, 18 | "attr-unsafe-chars": true 19 | } -------------------------------------------------------------------------------- /test/test.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import chai from 'chai'; 6 | import sinon from 'sinon'; 7 | import sinonChai from 'sinon-chai'; 8 | import webpack from 'webpack'; 9 | import stripAnsi from 'strip-ansi'; 10 | 11 | chai.use(sinonChai); 12 | const {expect} = chai; 13 | 14 | const webpackBase = { 15 | mode: 'development', 16 | output: { 17 | path: path.join(__dirname, 'output') 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.html$/, 22 | loader: path.join(__dirname, '../index'), 23 | exclude: /node_modules/, 24 | enforce: 'pre' 25 | }, { 26 | test: /\.html$/, 27 | loader: 'raw-loader', 28 | exclude: /node_modules/ 29 | }] 30 | } 31 | }; 32 | 33 | const expectedErrorMessage = '[L1:C1] The html element name of [ BR ] must be in lowercase. (tagname-lowercase)\n
'; 34 | 35 | describe('htmlhint loader', () => { 36 | it('should not emit an error if there are no rules enabled', done => { 37 | webpack(Object.assign({}, webpackBase, { 38 | entry: path.join(__dirname, 'fixtures/error/error.js') 39 | }), (err, stats) => { 40 | if (err) { 41 | done(err); 42 | } else { 43 | expect(stats.hasErrors()).to.equal(false); 44 | done(); 45 | } 46 | }); 47 | }); 48 | 49 | it('should emit an error', done => { 50 | webpack(Object.assign({}, webpackBase, { 51 | 52 | entry: path.join(__dirname, 'fixtures/error/error.js'), 53 | plugins: [ 54 | new webpack.LoaderOptionsPlugin({ 55 | options: { 56 | htmlhint: { 57 | 'tagname-lowercase': true 58 | } 59 | } 60 | }) 61 | ] 62 | }), (err, stats) => { 63 | if (err) { 64 | done(err); 65 | } else { 66 | expect(stats.hasErrors()).to.equal(true); 67 | expect(stripAnsi(stats.compilation.errors[0].message)).to.have.string(expectedErrorMessage); 68 | done(); 69 | } 70 | }); 71 | }); 72 | 73 | it('should emit errors as warnings', done => { 74 | webpack(Object.assign({}, webpackBase, { 75 | entry: path.join(__dirname, 'fixtures/error/error.js'), 76 | plugins: [ 77 | new webpack.LoaderOptionsPlugin({ 78 | options: { 79 | htmlhint: { 80 | 'tagname-lowercase': true, 81 | emitAs: 'warning' 82 | } 83 | } 84 | }) 85 | ] 86 | }), (err, stats) => { 87 | if (err) { 88 | done(err); 89 | } else { 90 | expect(stats.hasErrors()).to.equal(false); 91 | expect(stats.hasWarnings()).to.equal(true); 92 | expect(stripAnsi(stats.compilation.warnings[0].message)).to.have.string(expectedErrorMessage); 93 | done(); 94 | } 95 | }); 96 | }); 97 | 98 | it('should produce results to two file', done => { 99 | const outputFilename = 'outputReport-[name].txt'; 100 | const expectedOutFilenames = ['outputReport-template.txt', 'outputReport-template-two.txt']; 101 | 102 | webpack(Object.assign({}, webpackBase, { 103 | entry: [ 104 | `${__dirname}/fixtures/error/error.js`, 105 | `${__dirname}/fixtures/error/error-two.js` 106 | ], 107 | plugins: [ 108 | new webpack.LoaderOptionsPlugin({ 109 | options: { 110 | htmlhint: { 111 | 'tagname-lowercase': true, 112 | outputReport: { 113 | filePath: outputFilename 114 | } 115 | } 116 | } 117 | }) 118 | ] 119 | }), err => { 120 | if (err) { 121 | done(err); 122 | } else { 123 | const file1Content = fs.readFileSync(`${__dirname}/output/${expectedOutFilenames[0]}`, 'utf8'); 124 | expect(stripAnsi(expectedErrorMessage)).to.equal(stripAnsi(file1Content)); 125 | const file2Content = fs.readFileSync(`${__dirname}/output/${expectedOutFilenames[1]}`, 'utf8'); 126 | expect(stripAnsi(expectedErrorMessage)).to.equal(stripAnsi(file2Content)); 127 | done(); 128 | } 129 | }); 130 | }); 131 | 132 | it('should use the htmlhintrc file', done => { 133 | webpack(Object.assign({}, webpackBase, { 134 | entry: path.join(__dirname, 'fixtures/error/error.js'), 135 | plugins: [ 136 | new webpack.LoaderOptionsPlugin({ 137 | options: { 138 | htmlhint: { 139 | configFile: 'test/.htmlhintrc' 140 | } 141 | } 142 | }) 143 | ] 144 | }), (err, stats) => { 145 | if (err) { 146 | done(err); 147 | } else { 148 | expect(stats.hasErrors()).to.equal(true); 149 | expect(stripAnsi(stats.compilation.errors[0].message)).to.have.string(expectedErrorMessage); 150 | done(); 151 | } 152 | }); 153 | }); 154 | 155 | it('should handle the htmlhintrc file being invalid json', done => { 156 | webpack(Object.assign({}, webpackBase, { 157 | entry: path.join(__dirname, 'fixtures/error/error.js'), 158 | plugins: [ 159 | new webpack.LoaderOptionsPlugin({ 160 | options: { 161 | htmlhint: { 162 | configFile: 'test/.htmlhintrc-broken' 163 | } 164 | } 165 | }) 166 | ] 167 | }), (err, stats) => { 168 | expect(err).to.equal(null); 169 | expect(stats.compilation.errors[0].message.indexOf('Could not parse the htmlhint config file') > -1).to.equal(true); 170 | done(); 171 | }); 172 | }); 173 | 174 | it('should allow custom rules', done => { 175 | const ruleCalled = sinon.spy(); 176 | 177 | webpack(Object.assign({}, webpackBase, { 178 | entry: path.join(__dirname, 'fixtures/error/error.js'), 179 | plugins: [ 180 | new webpack.LoaderOptionsPlugin({ 181 | options: { 182 | htmlhint: { 183 | customRules: [{ 184 | id: 'my-rule-name', 185 | description: 'Example description', 186 | init: ruleCalled 187 | }], 188 | 'my-rule-name': true 189 | } 190 | } 191 | }) 192 | ] 193 | }), () => { 194 | expect(ruleCalled).to.have.been.callCount(1); 195 | done(); 196 | }); 197 | }); 198 | 199 | it('should allow rulesDir', done => { 200 | const ruleCalled = sinon.spy(); 201 | 202 | webpack(Object.assign({}, webpackBase, { 203 | entry: path.join(__dirname, 'fixtures/error/error.js'), 204 | plugins: [ 205 | new webpack.LoaderOptionsPlugin({ 206 | options: { 207 | htmlhint: { 208 | rulesDir: path.join(__dirname, 'fixtures/rules'), 209 | 'my-new-rule': ruleCalled 210 | } 211 | } 212 | }) 213 | ] 214 | }), () => { 215 | expect(ruleCalled).to.have.been.callCount(1); 216 | done(); 217 | }); 218 | }); 219 | 220 | it('should handle utf-8 BOM encoded configs', done => { 221 | webpack(Object.assign({}, webpackBase, { 222 | entry: path.join(__dirname, 'fixtures/error/error.js'), 223 | plugins: [ 224 | new webpack.LoaderOptionsPlugin({ 225 | options: { 226 | htmlhint: { 227 | configFile: 'test/htmlhint.json' 228 | } 229 | } 230 | }) 231 | ] 232 | }), (err, stats) => { 233 | if (err) { 234 | done(err); 235 | } else { 236 | expect(stats.hasErrors()).to.equal(false); 237 | done(); 238 | } 239 | }); 240 | }); 241 | 242 | it('should allow absolute config file paths', done => { 243 | webpack(Object.assign({}, webpackBase, { 244 | entry: path.join(__dirname, 'fixtures/error/error.js'), 245 | plugins: [ 246 | new webpack.LoaderOptionsPlugin({ 247 | options: { 248 | htmlhint: { 249 | configFile: path.join(__dirname, '.htmlhintrc') 250 | } 251 | } 252 | }) 253 | ] 254 | }), (err, stats) => { 255 | if (err) { 256 | done(stats); 257 | } else { 258 | expect(stats.hasErrors()).to.equal(true); 259 | expect(stripAnsi(stats.compilation.errors[0].message)).to.have.string(expectedErrorMessage); 260 | done(); 261 | } 262 | }); 263 | }); 264 | }); 265 | --------------------------------------------------------------------------------