Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: htmlhint/HTMLHint
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.13.0
Choose a base ref
...
head repository: htmlhint/HTMLHint
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.13.1
Choose a head ref

Commits on May 18, 2020

  1. fix: github token

    thedaviddias committed May 18, 2020

    Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    18441f6 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d9ef347 View commit details
  3. chore(deps-dev): bump @semantic-release/github from 7.0.5 to 7.0.6 (#396

    )
    
    Bumps [@semantic-release/github](https://github.com/semantic-release/github) from 7.0.5 to 7.0.6.
    - [Release notes](https://github.com/semantic-release/github/releases)
    - [Commits](semantic-release/github@v7.0.5...v7.0.6)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    dependabot-preview[bot] authored May 18, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9a6592e View commit details
  4. chore(deps-dev): bump rollup from 2.10.0 to 2.10.3 (#403)

    Bumps [rollup](https://github.com/rollup/rollup) from 2.10.0 to 2.10.3.
    - [Release notes](https://github.com/rollup/rollup/releases)
    - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
    - [Commits](rollup/rollup@v2.10.0...v2.10.3)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    Co-authored-by: David Dias <thedaviddias@gmail.com>
    dependabot-preview[bot] and thedaviddias authored May 18, 2020

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    c48c3e2 View commit details

Commits on May 19, 2020

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    48ca676 View commit details
  2. fix(attr-no-unnecessary-whitespace): fix when equals symbol in value (#…

    …405)
    
    Co-authored-by: Shinigami <chrissi92@hotmail.de>
    nicolashenry and Shinigami authored May 19, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    fe4f3c6 View commit details
  3. chore: modernize code / prefer arrow functions (#393)

    * chore: add lint rule prefer-arrow-callback
    
    * chore: apply lint rule prefer-arrow-callback
    
    * chore: apply lint rule arrow-body-style
    
    * chore: dont assign this to self
    
    * chore: transform functions to be sure
    
    * chore: improve eslint-disable
    
    * chore: self is not defined
    Shinigami authored May 19, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4f3bd50 View commit details
  4. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    506f25a View commit details
  5. fix: eslint fix

    Christopher Quadflieg committed May 19, 2020
    Copy the full SHA
    820ff8f View commit details
  6. chore: run lint on bin/htmlhint

    Christopher Quadflieg committed May 19, 2020
    Copy the full SHA
    d912caf View commit details
  7. chore: modernize code / prefer-template (#398)

    * chore: add lint rules like prefer-template
    
    * chore: apply lint rules
    
    * chore: improve eslint-disable
    
    * chore: fix missing prefer-templates
    
    * chore: use addition assignment
    
    * chore: add lint rule quotes
    Shinigami authored May 19, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    3899188 View commit details
  8. fix: correctly call hint queue drain (#409)

    * fix: correctly call hint queue drain
    
    * chore: check stdout output
    Shinigami authored May 19, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7b63cb2 View commit details
  9. chore: modernize code / no-var (#394)

    * chore: add lint rule no-var
    
    * chore: apply lint rule no-var
    
    * chore: add lint rule prefer-const
    
    * chore: apply lint rule prefer-const
    
    * chore: improve eslint-disable
    
    * chore: use const
    
    * chore: lint bin/htmlhint
    Shinigami authored May 19, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    1ccca7e View commit details

Commits on May 22, 2020

  1. chore: update ci workflow (#412)

    Shinigami authored May 22, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c42c643 View commit details

Commits on May 25, 2020

  1. fix: fix url repo pkg (#413)

    * fix: fix url repo pkg
    
    * fix path
    thedaviddias authored May 25, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    b7f5786 View commit details
  2. chore: migrate to TypeScript (no runtime code changes) (#410)

    * chore: add typescript as devDependencies
    
    * chore: create basic tsconfig.json configs
    
    * chore: move files
    
    * chore: rename bin files to .ts
    
    * fix: switch tsconfig excludes
    
    * chore: rename core files to .ts
    
    * chore: use folder build as outDir
    
    * chore: create build-bin.sh
    
    * chore: use new rollup input
    
    * chore: use build script to build with build.sh
    
    * fix: add ./ prefix
    
    * chore: support build on windows
    
    * chore: use import syntax
    
    * chore: add @types/*
    
    * chore: add typedefs to formatter.ts
    
    * chore: add typedefs
    
    * chore: configure eslint for typescript
    
    * fix: add +x to build-* files
    
    * chore: target ES2018 for bin
    
    * chore: add typedefs
    
    * chore: add typedefs
    
    * chore: add typedefs
    
    * chore: disable some eslint rules for now
    
    * chore: revert max-warnings
    
    * chore: add TODOs, disable errors and warnings
    
    * chore: update devDependencies
    
    * chore: update eslint config
    
    * chore: use XmlObject
    
    * chore: also lint tests with typescript support
    
    * chore: disable unsupported typescript version
    
    * chore: lint with type-checking
    
    * chore: remove comment
    Shinigami authored May 25, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    bdd82b5 View commit details
  3. Merge branch 'develop' into beta

    Christopher Quadflieg committed May 25, 2020
    Copy the full SHA
    db69dd3 View commit details

Commits on May 27, 2020

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    df9daa0 View commit details

Commits on May 30, 2020

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2aec895 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    62c2f3c View commit details
  3. Merge branch 'develop' into beta

    Christopher Quadflieg committed May 30, 2020
    Copy the full SHA
    d08ec47 View commit details
  4. chore(release): 0.13.1-beta.1 [skip ci]

    ## [0.13.1-beta.1](v0.13.0...v0.13.1-beta.1) (2020-05-30)
    
    ### Bug Fixes
    
    * add missing branches to action release ([#425](#425)) ([62c2f3c](62c2f3c))
    * add missing plugins for docusaurus ([#402](#402)) ([d9ef347](d9ef347))
    * correctly call hint queue drain ([#409](#409)) ([7b63cb2](7b63cb2))
    * dupplicate ga ([#407](#407)) ([506f25a](506f25a))
    * eslint fix ([820ff8f](820ff8f))
    * fix url repo pkg ([#413](#413)) ([b7f5786](b7f5786))
    * **attr-no-unnecessary-whitespace:** fix when equals symbol in value ([#405](#405)) ([fe4f3c6](fe4f3c6))
    * github token ([18441f6](18441f6))
    semantic-release-bot committed May 30, 2020
    Copy the full SHA
    135c9ba View commit details
  5. refactor: migrate to TypeScript (runtime code changes) (#423)

    * refactor: use es6 spread-syntax
    
    * refactor: use named imports
    
    * refactor: use named imports
    
    * chore: improve windows build script
    
    * chore: improve exports and visibility
    
    * refactor: resolve bin ts-expect-errors
    
    * fix: iterate through all formats
    
    * chore: add test for checkstyle formatter
    
    * fix: ignore html and xml test files
    
    * chore: rename to checkstyle.xml
    
    * chore: add test for formatter compact
    
    * test: add test for formatter default
    
    * test: add test for formatter html
    
    * test: add test for formatter json
    
    * test: add test for formatter junit
    
    * test: add test for formatter markdown
    
    * test: add test for formatter unix
    
    * chore: add typedefs
    
    * fix: nocolor defaults to false
    
    * revert: revert nocolor default false
    
    * chore: test colors.enable
    
    * fix: force enable colors if not nocolor
    
    * fix: @ts-expect-error
    
    * fix: @ts-expect-error
    
    * fix: @ts-expect-error
    
    * fix: @ts-expect-error
    
    * fix: @ts-expect-error
    
    * fix: @ts-expect-error
    
    * refactor: tags-check and ruleset
    
    * chore: improve Ruleset typing
    
    * refactor: inline rule parsing
    
    * fix: some lint rules
    
    * chore: add cast
    Shinigami authored May 30, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0637fad View commit details
  6. chore: ignore formatting of CHANGELOG.md

    Christopher Quadflieg committed May 30, 2020
    Copy the full SHA
    cfa4918 View commit details
  7. chore(deps-dev): bump eslint from 7.0.0 to 7.1.0 (#422)

    Bumps [eslint](https://github.com/eslint/eslint) from 7.0.0 to 7.1.0.
    - [Release notes](https://github.com/eslint/eslint/releases)
    - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
    - [Commits](eslint/eslint@v7.0.0...v7.1.0)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    Co-authored-by: David Dias <thedaviddias@gmail.com>
    dependabot-preview[bot] and thedaviddias authored May 30, 2020

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    36ab078 View commit details
  8. chore(deps-dev): bump @semantic-release/github from 7.0.6 to 7.0.7 (#415

    )
    
    Bumps [@semantic-release/github](https://github.com/semantic-release/github) from 7.0.6 to 7.0.7.
    - [Release notes](https://github.com/semantic-release/github/releases)
    - [Commits](semantic-release/github@v7.0.6...v7.0.7)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    Co-authored-by: David Dias <thedaviddias@gmail.com>
    dependabot-preview[bot] and thedaviddias authored May 30, 2020

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    14c9e79 View commit details
  9. chore(deps-dev): bump semantic-release from 17.0.7 to 17.0.8 (#419)

    Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 17.0.7 to 17.0.8.
    - [Release notes](https://github.com/semantic-release/semantic-release/releases)
    - [Commits](semantic-release/semantic-release@v17.0.7...v17.0.8)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    Co-authored-by: David Dias <thedaviddias@gmail.com>
    dependabot-preview[bot] and thedaviddias authored May 30, 2020

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    1de6ff7 View commit details
  10. Merge branch 'develop' into beta

    Christopher Quadflieg committed May 30, 2020
    Copy the full SHA
    f92b167 View commit details

Commits on May 31, 2020

  1. refactor: simplify build and rename bin to cli (#428)

    * refactor: simplify build and rename bin to cli
    
    * chore: remove unnecessary build folder
    
    * chore: disable invalid lint rules
    Shinigami authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9af5937 View commit details
  2. chore(deps-dev): bump mocha from 7.1.2 to 7.2.0 (#416)

    Bumps [mocha](https://github.com/mochajs/mocha) from 7.1.2 to 7.2.0.
    - [Release notes](https://github.com/mochajs/mocha/releases)
    - [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
    - [Commits](mochajs/mocha@v7.1.2...v7.2.0)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    dependabot-preview[bot] authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    963b823 View commit details
  3. chore(deps-dev): bump lint-staged from 10.2.2 to 10.2.7 (#426)

    Bumps [lint-staged](https://github.com/okonet/lint-staged) from 10.2.2 to 10.2.7.
    - [Release notes](https://github.com/okonet/lint-staged/releases)
    - [Commits](lint-staged/lint-staged@v10.2.2...v10.2.7)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    Co-authored-by: David Dias <thedaviddias@gmail.com>
    Co-authored-by: Shinigami <chrissi92@hotmail.de>
    3 people authored May 31, 2020

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    000ef48 View commit details
  4. chore(deps-dev): bump rollup from 2.10.3 to 2.12.0 (#429)

    Bumps [rollup](https://github.com/rollup/rollup) from 2.10.3 to 2.12.0.
    - [Release notes](https://github.com/rollup/rollup/releases)
    - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
    - [Commits](rollup/rollup@v2.10.3...v2.12.0)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    dependabot-preview[bot] authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5096c1d View commit details
  5. chore(deps-dev): bump @rollup/plugin-node-resolve from 7.1.3 to 8.0.0 (

    …#414)
    
    Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins) from 7.1.3 to 8.0.0.
    - [Release notes](https://github.com/rollup/plugins/releases)
    - [Commits](rollup/plugins@node-resolve-v7.1.3...node-resolve-v8.0.0)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    dependabot-preview[bot] authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0485ac5 View commit details
  6. chore(deps-dev): bump @rollup/plugin-commonjs from 11.1.0 to 12.0.0 (#…

    …418)
    
    Bumps [@rollup/plugin-commonjs](https://github.com/rollup/plugins) from 11.1.0 to 12.0.0.
    - [Release notes](https://github.com/rollup/plugins/releases)
    - [Commits](rollup/plugins@commonjs-v11.1.0...commonjs-v12.0.0)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    dependabot-preview[bot] authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a752328 View commit details
  7. chore(deps-dev): bump rollup-plugin-terser from 5.3.0 to 6.1.0 (#417)

    Bumps [rollup-plugin-terser](https://github.com/TrySound/rollup-plugin-terser) from 5.3.0 to 6.1.0.
    - [Release notes](https://github.com/TrySound/rollup-plugin-terser/releases)
    - [Commits](TrySound/rollup-plugin-terser@v5.3.0...v6.1.0)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
    
    Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
    dependabot-preview[bot] authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    aa5d81e View commit details
  8. Merge branch 'develop' into beta

    Christopher Quadflieg committed May 31, 2020
    Copy the full SHA
    526c1e3 View commit details
  9. fix: remove unused dependency esm (#430)

    The esm dependency is no longer used anywhere
    Shinigami authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7e8429d View commit details
  10. chore(release): 0.13.1-beta.2 [skip ci]

    ## [0.13.1-beta.2](v0.13.1-beta.1...v0.13.1-beta.2) (2020-05-31)
    
    ### Bug Fixes
    
    * remove unused dependency esm ([#430](#430)) ([7e8429d](7e8429d))
    semantic-release-bot committed May 31, 2020
    Copy the full SHA
    e28e312 View commit details
  11. chore: add coverage codecov (#387)

    * chore: add coverage codecov
    
    * fix: remove success
    
    * fix: add missing script and deps
    
    * fix: add codecov
    thedaviddias authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    78fa94b View commit details
  12. Merge branch 'develop' of https://github.com/htmlhint/HTMLHint into beta

    # Conflicts:
    #	package-lock.json
    #	package.json
    thedaviddias committed May 31, 2020

    Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    562135c View commit details
  13. chore: update nyc ts (#434)

    * chore: only exclude bin/htmlhint
    
    * chore: exclude src/**/*.ts
    Shinigami authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    dcd0a48 View commit details
  14. test: check line by line (#435)

    * test: check checkstyle line by line
    
    * test: check compact, html and unix line by line
    
    * test: improve formatter json test
    Shinigami authored May 31, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4e4e07b View commit details
  15. ci: cleanup

    Christopher Quadflieg committed May 31, 2020
    Copy the full SHA
    c75cd80 View commit details
  16. chore(release): 0.13.1 [skip ci]

    ## [0.13.1](v0.13.0...v0.13.1) (2020-05-31)
    
    ### Bug Fixes
    
    * add missing branches to action release ([#425](#425)) ([62c2f3c](62c2f3c))
    * add missing plugins for docusaurus ([#402](#402)) ([d9ef347](d9ef347))
    * correctly call hint queue drain ([#409](#409)) ([7b63cb2](7b63cb2))
    * dupplicate ga ([#407](#407)) ([506f25a](506f25a))
    * eslint fix ([820ff8f](820ff8f))
    * fix url repo pkg ([#413](#413)) ([b7f5786](b7f5786))
    * github token ([18441f6](18441f6))
    * remove unused dependency esm ([#430](#430)) ([7e8429d](7e8429d))
    * **attr-no-unnecessary-whitespace:** fix when equals symbol in value ([#405](#405)) ([fe4f3c6](fe4f3c6))
    semantic-release-bot committed May 31, 2020
    Copy the full SHA
    c4a7de4 View commit details
Showing with 6,231 additions and 2,780 deletions.
  1. +11 −0 .eslintignore
  2. +31 −1 .eslintrc.js
  3. +14 −2 .github/workflows/ci.yml
  4. +6 −1 .github/workflows/release.yml
  5. +6 −1 .prettierignore
  6. +36 −0 CHANGELOG.md
  7. +4 −4 README.md
  8. +0 −66 bin/formatter.js
  9. +0 −28 bin/formatters/compact.js
  10. +0 −52 bin/formatters/default.js
  11. +0 −41 bin/formatters/html.js
  12. +0 −7 bin/formatters/json.js
  13. +0 −44 bin/formatters/markdown.js
  14. +0 −32 bin/formatters/unix.js
  15. +2 −433 bin/htmlhint
  16. +11 −0 build-linux.sh
  17. +10 −0 build-windows.bat
  18. +17 −0 build.js
  19. +6 −0 codecov.yml
  20. +4 −2 commitizen.config.js
  21. +1 −1 docs/user-guide/list-rules.md
  22. +1,616 −553 package-lock.json
  23. +43 −19 package.json
  24. +1 −1 rollup.config.js
  25. +112 −0 src/cli/formatter.ts
  26. +17 −13 bin/formatters/checkstyle.js → src/cli/formatters/checkstyle.ts
  27. +39 −0 src/cli/formatters/compact.ts
  28. +59 −0 src/cli/formatters/default.ts
  29. +32 −0 src/cli/formatters/html.ts
  30. +9 −0 src/cli/formatters/json.ts
  31. +14 −11 bin/formatters/junit.js → src/cli/formatters/junit.ts
  32. +44 −0 src/cli/formatters/markdown.ts
  33. +38 −0 src/cli/formatters/unix.ts
  34. +501 −0 src/cli/htmlhint.ts
  35. +0 −170 src/core.js
  36. +165 −0 src/core/core.ts
  37. +121 −74 src/{htmlparser.js → core/htmlparser.ts}
  38. BIN src/{ → core}/img/htmlhint.png
  39. +91 −0 src/core/reporter.ts
  40. +12 −11 src/{rules/alt-require.js → core/rules/alt-require.ts}
  41. +16 −16 src/{rules/attr-lowercase.js → core/rules/attr-lowercase.ts}
  42. +32 −0 src/core/rules/attr-no-duplication.ts
  43. +29 −0 src/core/rules/attr-no-unnecessary-whitespace.ts
  44. +18 −18 src/{rules/attr-sorted.js → core/rules/attr-sorted.ts}
  45. +35 −0 src/core/rules/attr-unsafe-chars.ts
  46. +11 −13 src/{rules/attr-value-double-quotes.js → core/rules/attr-value-double-quotes.ts}
  47. +27 −0 src/core/rules/attr-value-not-empty.ts
  48. +11 −13 src/{rules/attr-value-single-quotes.js → core/rules/attr-value-single-quotes.ts}
  49. +48 −0 src/core/rules/attr-whitespace.ts
  50. +7 −6 src/{rules/doctype-first.js → core/rules/doctype-first.ts}
  51. +8 −7 src/{rules/doctype-html5.js → core/rules/doctype-html5.ts}
  52. +13 −11 src/{rules/head-script-disabled.js → core/rules/head-script-disabled.ts}
  53. +12 −16 src/{rules/href-abs-or-rel.js → core/rules/href-abs-or-rel.ts}
  54. +12 −14 src/{rules/id-class-ad-disabled.js → core/rules/id-class-ad-disabled.ts}
  55. +20 −19 src/{rules/id-class-value.js → core/rules/id-class-value.ts}
  56. +13 −12 src/{rules/id-unique.js → core/rules/id-unique.ts}
  57. 0 src/{rules/index.js → core/rules/index.ts}
  58. +40 −0 src/core/rules/inline-script-disabled.ts
  59. +27 −0 src/core/rules/inline-style-disabled.ts
  60. +21 −15 src/{rules/input-requires-label.js → core/rules/input-requires-label.ts}
  61. +6 −8 src/{rules/script-disabled.js → core/rules/script-disabled.ts}
  62. +22 −21 src/{rules/space-tab-mixed-disabled.js → core/rules/space-tab-mixed-disabled.ts}
  63. +25 −0 src/core/rules/spec-char-escape.ts
  64. +12 −16 src/{rules/src-not-empty.js → core/rules/src-not-empty.ts}
  65. +6 −6 src/{rules/style-disabled.js → core/rules/style-disabled.ts}
  66. +90 −0 src/core/rules/tag-pair.ts
  67. +9 −8 src/{rules/tag-self-close.js → core/rules/tag-self-close.ts}
  68. +27 −0 src/core/rules/tagname-lowercase.ts
  69. +22 −0 src/core/rules/tagname-specialchars.ts
  70. +161 −0 src/core/rules/tags-check.ts
  71. +16 −12 src/{rules/title-require.js → core/rules/title-require.ts}
  72. +75 −0 src/core/types.ts
  73. +0 −53 src/reporter.js
  74. +0 −32 src/rules/attr-no-duplication.js
  75. +0 −30 src/rules/attr-no-unnecessary-whitespace.js
  76. +0 −39 src/rules/attr-unsafe-chars.js
  77. +0 −27 src/rules/attr-value-not-empty.js
  78. +0 −49 src/rules/attr-whitespace.js
  79. +0 −40 src/rules/inline-script-disabled.js
  80. +0 −27 src/rules/inline-style-disabled.js
  81. +0 −26 src/rules/spec-char-escape.js
  82. +0 −91 src/rules/tag-pair.js
  83. +0 −24 src/rules/tagname-lowercase.js
  84. +0 −23 src/rules/tagname-specialchars.js
  85. +0 −202 src/rules/tags-check.js
  86. +52 −0 test/cli/formatters/checkstyle.spec.js
  87. +97 −0 test/cli/formatters/checkstyle.xml
  88. +54 −0 test/cli/formatters/compact.spec.js
  89. +94 −0 test/cli/formatters/compact.txt
  90. +31 −0 test/cli/formatters/default.spec.js
  91. +1 −0 test/cli/formatters/html.html
  92. +53 −0 test/cli/formatters/html.spec.js
  93. +1,204 −0 test/cli/formatters/json.json
  94. +58 −0 test/cli/formatters/json.spec.js
  95. +34 −0 test/cli/formatters/junit.spec.js
  96. +34 −0 test/cli/formatters/markdown.spec.js
  97. +54 −0 test/cli/formatters/unix.spec.js
  98. +94 −0 test/cli/formatters/unix.txt
  99. +29 −8 test/core.spec.js
  100. +32 −2 test/executable.spec.js
  101. +31 −32 test/htmlparser.spec.js
  102. +14 −14 test/rules/alt-require.spec.js
  103. +7 −7 test/rules/attr-lowercase.spec.js
  104. +3 −3 test/rules/attr-no-duplication.spec.js
  105. +0 −31 test/rules/attr-no-unnecessary-whitespace.js
  106. +33 −0 test/rules/attr-no-unnecessary-whitespace.spec.js
  107. +4 −4 test/rules/attr-sort.spec.js
  108. +4 −4 test/rules/attr-unsafe-chars.spec.js
  109. +3 −3 test/rules/attr-value-double-quotes.spec.js
  110. +4 −4 test/rules/attr-value-not-empty.spec.js
  111. +7 −7 test/rules/attr-value-single-quotes.spec.js
  112. +10 −10 test/rules/attr-whitespace.spec.js
  113. +2 −2 test/rules/default.spec.js
  114. +3 −3 test/rules/doctype-first.spec.js
  115. +3 −3 test/rules/doctype-html5.spec.js
  116. +6 −6 test/rules/head-require.spec.js
  117. +6 −6 test/rules/head-script-disabled.spec.js
  118. +5 −5 test/rules/href-abs-or-rel.spec.js
  119. +4 −4 test/rules/id-class-ad-disabled.spec.js
  120. +32 −32 test/rules/id-class-value.spec.js
  121. +3 −3 test/rules/id-unique.spec.js
  122. +4 −4 test/rules/inline-script-disabled.spec.js
  123. +2 −2 test/rules/inline-style-disabled.spec.js
  124. +23 −21 test/rules/input-requires-label.spec.js
  125. +15 −11 test/rules/script-disabled.spec.js
  126. +12 −12 test/rules/space-tab-mixed-disabled.spec.js
  127. +17 −17 test/rules/spec-char-escape.spec.js
  128. +4 −4 test/rules/src-not-empty.spec.js
  129. +3 −3 test/rules/style-disabled.spec.js
  130. +4 −4 test/rules/tag-pair.spec.js
  131. +3 −3 test/rules/tag-self-close.spec.js
  132. +3 −3 test/rules/tagname-lowercase.spec.js
  133. +7 −7 test/rules/tagname-specialchars.spec.js
  134. +22 −22 test/rules/tags-check.spec.js
  135. +5 −5 test/rules/title-require.spec.js
  136. +7 −0 tsconfig.cli.json
  137. +4 −0 tsconfig.core.json
  138. +15 −0 tsconfig.json
  139. +14 −0 tsconfig.lint.json
  140. +16 −8 website/docusaurus.config.js
  141. +2 −0 website/package.json
11 changes: 11 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules

# Automatically generated directories
bin
dist

# Website directory
website

# eslint cannot lint itself
.eslintrc.js
32 changes: 31 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
module.exports = {
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
extends: [
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier/@typescript-eslint',
],
env: {
browser: true,
amd: true,
node: true,
mocha: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
project: './tsconfig.lint.json',
warnOnUnsupportedTypeScriptVersion: false,
},
plugins: ['@typescript-eslint'],
rules: {
'arrow-body-style': ['error'],
'no-template-curly-in-string': 'error',
'no-useless-concat': 'error',
'no-useless-escape': 'off',
'no-var': 'error',
'one-var': ['error', 'never'],
'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
'prefer-const': 'error',
'prefer-template': 'error',
quotes: ['error', 'single', { avoidEscape: true }],
'template-curly-spacing': 'error',
'template-tag-spacing': 'error',

'@typescript-eslint/adjacent-overload-signatures': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
},
}
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -4,15 +4,16 @@ on:
push:
branches:
- master
- develop
- beta
- 'feat/**'
- 'fix/**'
- 'perf/**'
- 'chore/**'
- 'refactor/**'
pull_request:
branches:
- master
- develop
- beta

jobs:
build:
@@ -43,3 +44,14 @@ jobs:

- name: Run tests
run: npm test

- name: Run coverage
if: matrix.os == 'ubuntu-latest' && matrix.node == '12.x'
run: npm run test:coverage

- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.node == '12.x'
uses: codecov/codecov-action@v1
with:
file: ./coverage/lcov.info
fail_ci_if_error: true
7 changes: 6 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -4,6 +4,11 @@ on:
push:
branches:
- master
- next
- next-major
- next-minor
- alpha
- beta

jobs:
release:
@@ -30,5 +35,5 @@ jobs:
- name: Semantic Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
7 changes: 6 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@
package-lock.json
npm-debug.log*
.nyc_output
test/html

# Ignore formatting of ***x*** to **_x_**
.github/PULL_REQUEST_TEMPLATE.md
@@ -22,3 +21,9 @@ test/html
/website/.docusaurus
/website/plugins
/website/static

/test/**/*.html
/test/**/*.xml

# CHANGELOG.md will be generated by semantic-release
CHANGELOG.md
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
## [0.13.1](https://github.com/htmlhint/HTMLHint/compare/v0.13.0...v0.13.1) (2020-05-31)


### Bug Fixes

* add missing branches to action release ([#425](https://github.com/htmlhint/HTMLHint/issues/425)) ([62c2f3c](https://github.com/htmlhint/HTMLHint/commit/62c2f3c4dbc31235f644da42b1eeccd8d73c83aa))
* add missing plugins for docusaurus ([#402](https://github.com/htmlhint/HTMLHint/issues/402)) ([d9ef347](https://github.com/htmlhint/HTMLHint/commit/d9ef3476bfb935314a38dabfbd5ad055deec7b6a))
* correctly call hint queue drain ([#409](https://github.com/htmlhint/HTMLHint/issues/409)) ([7b63cb2](https://github.com/htmlhint/HTMLHint/commit/7b63cb282dc41c8757913d92aefc11ddcaab6039))
* dupplicate ga ([#407](https://github.com/htmlhint/HTMLHint/issues/407)) ([506f25a](https://github.com/htmlhint/HTMLHint/commit/506f25a31d52b66ef58a12eb258bed3bf517146b))
* eslint fix ([820ff8f](https://github.com/htmlhint/HTMLHint/commit/820ff8fe4714bc3ba47565eb58d87910d60934e8))
* fix url repo pkg ([#413](https://github.com/htmlhint/HTMLHint/issues/413)) ([b7f5786](https://github.com/htmlhint/HTMLHint/commit/b7f5786a803fb2a31c5819b70b608b133f2e1e51))
* github token ([18441f6](https://github.com/htmlhint/HTMLHint/commit/18441f6c54fe540ac86cdedbc61846a2662d78c8))
* remove unused dependency esm ([#430](https://github.com/htmlhint/HTMLHint/issues/430)) ([7e8429d](https://github.com/htmlhint/HTMLHint/commit/7e8429dc8d57f6cbf891570d644f72729644826d))
* **attr-no-unnecessary-whitespace:** fix when equals symbol in value ([#405](https://github.com/htmlhint/HTMLHint/issues/405)) ([fe4f3c6](https://github.com/htmlhint/HTMLHint/commit/fe4f3c6732d76ec78a19e97d2640e63be3dd6735))

## [0.13.1-beta.2](https://github.com/htmlhint/HTMLHint/compare/v0.13.1-beta.1...v0.13.1-beta.2) (2020-05-31)


### Bug Fixes

* remove unused dependency esm ([#430](https://github.com/htmlhint/HTMLHint/issues/430)) ([7e8429d](https://github.com/htmlhint/HTMLHint/commit/7e8429dc8d57f6cbf891570d644f72729644826d))

## [0.13.1-beta.1](https://github.com/htmlhint/HTMLHint/compare/v0.13.0...v0.13.1-beta.1) (2020-05-30)


### Bug Fixes

* add missing branches to action release ([#425](https://github.com/htmlhint/HTMLHint/issues/425)) ([62c2f3c](https://github.com/htmlhint/HTMLHint/commit/62c2f3c4dbc31235f644da42b1eeccd8d73c83aa))
* add missing plugins for docusaurus ([#402](https://github.com/htmlhint/HTMLHint/issues/402)) ([d9ef347](https://github.com/htmlhint/HTMLHint/commit/d9ef3476bfb935314a38dabfbd5ad055deec7b6a))
* correctly call hint queue drain ([#409](https://github.com/htmlhint/HTMLHint/issues/409)) ([7b63cb2](https://github.com/htmlhint/HTMLHint/commit/7b63cb282dc41c8757913d92aefc11ddcaab6039))
* dupplicate ga ([#407](https://github.com/htmlhint/HTMLHint/issues/407)) ([506f25a](https://github.com/htmlhint/HTMLHint/commit/506f25a31d52b66ef58a12eb258bed3bf517146b))
* eslint fix ([820ff8f](https://github.com/htmlhint/HTMLHint/commit/820ff8fe4714bc3ba47565eb58d87910d60934e8))
* fix url repo pkg ([#413](https://github.com/htmlhint/HTMLHint/issues/413)) ([b7f5786](https://github.com/htmlhint/HTMLHint/commit/b7f5786a803fb2a31c5819b70b608b133f2e1e51))
* **attr-no-unnecessary-whitespace:** fix when equals symbol in value ([#405](https://github.com/htmlhint/HTMLHint/issues/405)) ([fe4f3c6](https://github.com/htmlhint/HTMLHint/commit/fe4f3c6732d76ec78a19e97d2640e63be3dd6735))
* github token ([18441f6](https://github.com/htmlhint/HTMLHint/commit/18441f6c54fe540ac86cdedbc61846a2662d78c8))

# [0.13.0](https://github.com/htmlhint/HTMLHint/compare/v0.12.2...v0.13.0) (2020-05-18)


8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@
</p>

<p align="center">
  <a href="#installation-and-usage">How To Use</a> • <a href="#contributing">Contributing</a> • <a href="http://roadmap.htmlhint.io/">Roadmap</a> • <a href="https://htmlhint.com">Website</a>
  <a href="#installation-and-usage">How To Use</a> • <a href="#contributing">Contributing</a> • <a href="https://htmlhint.com">Website</a>
</p>

## Table of Contents
@@ -116,9 +116,9 @@ Inline rules in `test.html`:

## 📙 Docs

1. [How to use](https://github.com/htmlhint/HTMLHint/wiki/Usage)
2. [All Rules](https://github.com/htmlhint/HTMLHint/wiki/Rules)
3. [How to Develop](https://github.com/htmlhint/HTMLHint/wiki/Developer-guide)
1. [How to use](https://htmlhint.com/docs/user-guide/usage/cli)
2. [All Rules](https://htmlhint.com/docs/user-guide/list-rules)
3. [How to Develop](CONTRIBUTING.md)

## © License

66 changes: 0 additions & 66 deletions bin/formatter.js

This file was deleted.

28 changes: 0 additions & 28 deletions bin/formatters/compact.js

This file was deleted.

52 changes: 0 additions & 52 deletions bin/formatters/default.js

This file was deleted.

41 changes: 0 additions & 41 deletions bin/formatters/html.js

This file was deleted.

7 changes: 0 additions & 7 deletions bin/formatters/json.js

This file was deleted.

44 changes: 0 additions & 44 deletions bin/formatters/markdown.js

This file was deleted.

32 changes: 0 additions & 32 deletions bin/formatters/unix.js

This file was deleted.

435 changes: 2 additions & 433 deletions bin/htmlhint
100755 → 100644

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions build-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -x

# Cleanup
rm -Rf ./dist

# Prepare
npx tsc

# Build core
npm run build:rollup
10 changes: 10 additions & 0 deletions build-windows.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@echo off

:: Cleanup
IF EXIST dist ( rmdir dist /q /s ) ELSE ( echo dist folder not present )

:: Prepare
call npx tsc

:: Build core
npm run build:rollup
17 changes: 17 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { execSync } = require('child_process')

const os = require('os')

const type = os.type()

switch (type) {
case 'Linux':
case 'Darwin':
execSync('./build-linux.sh', { stdio: 'inherit' })
break
case 'Windows_NT':
execSync('build-windows.bat', { stdio: 'inherit' })
break
default:
throw new Error(`Unsupported OS found: ${type}`)
}
6 changes: 6 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
coverage:
status:
project:
default:
target: auto # this is default
if_not_found: success # no commit found? still set a success
6 changes: 4 additions & 2 deletions commitizen.config.js
Original file line number Diff line number Diff line change
@@ -14,7 +14,8 @@ const types = [
},
{
value: 'style',
name: `style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)`,
name:
'style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
},
{
value: 'refactor',
@@ -30,7 +31,8 @@ const types = [
},
{
value: 'chore',
name: `chore: Changes to the build process or auxiliary tools and libraries such as documentation generation`,
name:
'chore: Changes to the build process or auxiliary tools and libraries such as documentation generation',
},
{
value: 'revert',
2 changes: 1 addition & 1 deletion docs/user-guide/list-rules.md
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ title: List of rules
- [`doctype-html5`](/docs/user-guide/rules/doctype-html5): Invalid doctype.
- [`head-script-disabled`](/docs/user-guide/rules/head-script-disabled): The `<script>` tag cannot be used in a tag.
- [`style-disabled`](/docs/user-guide/rules/style-disabled): `<style>` tags cannot be used.
- [title-require](/docs/user-guide/rules/title-require): `<title>` must be present in tag.
- [`title-require`](/docs/user-guide/rules/title-require): `<title>` must be present in tag.

### Attributes

2,169 changes: 1,616 additions & 553 deletions package-lock.json

Large diffs are not rendered by default.

62 changes: 43 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "htmlhint",
"version": "0.13.0",
"version": "0.13.1",
"description": "The Static Code Analysis Tool for your HTML",
"repository": {
"type": "git",
"url": "https://github.com/htmlhint/HTMLHint"
"url": "git+https://github.com/htmlhint/HTMLHint.git"
},
"bugs": {
"url": "https://github.com/htmlhint/HTMLHint/issues"
@@ -21,16 +21,18 @@
"htmlhint": "./bin/htmlhint"
},
"main": "dist/htmlhint.js",
"module": "src/core.js",
"module": "dist/core/core.js",
"scripts": {
"commit": "npx git-cz",
"prettier": "prettier --write .",
"test": "mocha --recursive \"./test/**/*.spec.js\"",
"build": "npm run build:min && npm run build:unmin",
"build": "node build.js",
"build:rollup": "npm run build:min && npm run build:unmin",
"build:min": "rollup -c --environment NODE_ENV:production --file dist/htmlhint.min.js",
"build:unmin": "rollup -c",
"lint:fix": "eslint src/ test/ bin/ --ext .js --max-warnings 0",
"lint": "eslint --fix src/ test/ bin/ --ext .js --max-warnings 0"
"lint": "eslint . --ext .js,.ts --max-warnings 0",
"lint:fix": "eslint --fix . --ext .js,.ts --max-warnings 0",
"test:coverage": "nyc npm run test"
},
"husky": {
"hooks": {
@@ -59,41 +61,47 @@
"async": "3.2.0",
"colors": "1.4.0",
"commander": "5.1.0",
"esm": "3.2.25",
"glob": "7.1.6",
"parse-glob": "3.0.4",
"path-parse": "1.0.6",
"request": "2.88.2",
"strip-json-comments": "3.1.0",
"xml": "1.0.1"
},
"devDependencies": {
"@commitlint/cli": "8.3.5",
"@commitlint/config-conventional": "8.3.4",
"@rollup/plugin-commonjs": "11.1.0",
"@rollup/plugin-node-resolve": "7.1.3",
"@rollup/plugin-commonjs": "12.0.0",
"@rollup/plugin-node-resolve": "8.0.0",
"@semantic-release/changelog": "5.0.1",
"@semantic-release/commit-analyzer": "8.0.1",
"@semantic-release/git": "9.0.0",
"@semantic-release/github": "7.0.5",
"@semantic-release/github": "7.0.7",
"@semantic-release/npm": "7.0.5",
"@semantic-release/release-notes-generator": "9.0.1",
"@types/async": "3.2.3",
"@types/glob": "7.1.1",
"@types/parse-glob": "3.0.29",
"@types/request": "2.48.5",
"@types/xml": "1.0.5",
"@typescript-eslint/eslint-plugin": "3.0.2",
"@typescript-eslint/parser": "3.0.2",
"commitizen": "4.1.2",
"commitlint": "8.3.5",
"eslint": "7.0.0",
"eslint": "7.1.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-prettier": "3.1.3",
"expect.js": "0.3.1",
"husky": "4.2.5",
"istanbul": "0.4.5",
"lint-staged": "10.2.2",
"mocha": "7.1.2",
"lint-staged": "10.2.7",
"mocha": "7.2.0",
"nyc": "15.0.1",
"prettier": "2.0.5",
"pretty-quick": "2.0.1",
"rollup": "2.10.0",
"rollup": "2.12.0",
"rollup-plugin-babel": "4.4.0",
"rollup-plugin-terser": "5.3.0",
"semantic-release": "17.0.7"
"rollup-plugin-terser": "6.1.0",
"semantic-release": "17.0.8",
"typescript": "3.9.3"
},
"release": {
"plugins": [
@@ -108,5 +116,21 @@
"files": [
"bin",
"dist"
]
],
"nyc": {
"all": true,
"include": [
"dist/htmlhint.js"
],
"exclude": [
"**/*.spec.js",
"src/**/*.ts",
"bin/htmlhint"
],
"reporter": [
"text",
"lcov"
],
"sourceMap": false
}
}
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'

const config = {
input: './src/core.js',
input: './dist/core/core.js',
output: {
file: 'dist/htmlhint.js',
format: 'umd',
112 changes: 112 additions & 0 deletions src/cli/formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { EventEmitter } from 'events'
import { sync as globSync } from 'glob'
import { parse, resolve } from 'path'
import type { HTMLHint as IHTMLHint } from '../core/core'
import type { Hint, Ruleset } from '../core/types'

let HTMLHint: typeof IHTMLHint
let options: { nocolor?: boolean }

// load formatters
const mapFormatters = loadFormatters()
const arrSupportedFormatters: string[] = []

for (const formatterName in mapFormatters) {
if (formatterName !== 'default') {
arrSupportedFormatters.push(formatterName)
}
}

// load all formatters
function loadFormatters() {
const arrFiles = globSync('./formatters/*.js', {
cwd: __dirname,
dot: false,
nodir: true,
strict: false,
silent: true,
})

const mapFormatters: { [name: string]: FormatterCallback } = {}
arrFiles.forEach((file) => {
const fileInfo = parse(file)
const formatterPath = resolve(__dirname, file)
mapFormatters[fileInfo.name] = require(formatterPath)
})

return mapFormatters
}

export interface FormatterFileEvent {
file: string
messages: Hint[]
time: number
}

export interface FormatterConfigEvent {
ruleset: Ruleset
configPath?: string
}

export interface FormatterEndEvent {
arrAllMessages: {
file: string
messages: Hint[]
time: number
}[]
allFileCount: number
allHintFileCount: number
allHintCount: number
time: number
}

export interface Formatter extends EventEmitter {
getSupported(): typeof arrSupportedFormatters
init(tmpHTMLHint: typeof IHTMLHint, tmpOptions: { nocolor?: boolean }): void
setFormat(format: string): void

emit(event: 'start'): boolean
on(event: 'start', listener: () => void): this

emit(event: 'file', arg: FormatterFileEvent): boolean
on(event: 'file', listener: (event: FormatterFileEvent) => void): this

emit(event: 'config', arg: FormatterConfigEvent): boolean
on(event: 'config', listener: (event: FormatterConfigEvent) => void): this

emit(event: 'end', arg: FormatterEndEvent): boolean
on(event: 'end', listener: (event: FormatterEndEvent) => void): this
}

const formatter: Formatter = new EventEmitter() as Formatter

formatter.getSupported = function () {
return arrSupportedFormatters
}

formatter.init = function (tmpHTMLHint, tmpOptions) {
HTMLHint = tmpHTMLHint
options = tmpOptions
}

formatter.setFormat = function (format) {
const formatHandel = mapFormatters[format]

if (formatHandel === undefined) {
console.log(
'No supported formatter, supported formatters: %s'.red,
arrSupportedFormatters.join(', ')
)
process.exit(1)
} else {
formatHandel(formatter, HTMLHint, options)
}
}

export type FormatterCallback = (
formatter: Formatter,
HTMLHint: typeof IHTMLHint,
options: { nocolor?: boolean }
) => void

module.exports = formatter
30 changes: 17 additions & 13 deletions bin/formatters/checkstyle.js → src/cli/formatters/checkstyle.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
var xml = require('xml')
import * as xml from 'xml'
import type { XmlObject } from 'xml'
import { FormatterCallback } from '../formatter'

var checkstyleFormatter = function (formatter) {
formatter.on('end', function (event) {
var arrFiles = []
var arrAllMessages = event.arrAllMessages
const checkstyleFormatter: FormatterCallback = function (formatter) {
formatter.on('end', (event) => {
const arrFiles: XmlObject[] = []
const arrAllMessages = event.arrAllMessages

arrAllMessages.forEach(function (fileInfo) {
var arrMessages = fileInfo.messages
var arrErrors = []
arrAllMessages.forEach((fileInfo) => {
const arrMessages = fileInfo.messages
const arrErrors: XmlObject[] = []

arrMessages.forEach(function (message) {
arrMessages.forEach((message) => {
arrErrors.push({
error: {
_attr: {
line: message.line,
column: message.col,
severity: message.type,
message: message.message,
source: 'htmlhint.' + message.rule.id,
source: `htmlhint.${message.rule.id}`,
},
},
})
@@ -30,18 +32,20 @@ var checkstyleFormatter = function (formatter) {
name: fileInfo.file,
},
},
].concat(arrErrors),
...arrErrors,
],
})
})

var objXml = {
const objXml: XmlObject = {
checkstyle: [
{
_attr: {
version: '4.3',
},
},
].concat(arrFiles),
...arrFiles,
],
}

console.log(
39 changes: 39 additions & 0 deletions src/cli/formatters/compact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as colors from 'colors'
import { FormatterCallback } from '../formatter'

const compactFormatter: FormatterCallback = function (
formatter,
HTMLHint,
options
) {
const nocolor = options.nocolor

if (nocolor !== false) {
colors.enable()
}

formatter.on('file', (event) => {
event.messages.forEach((message) => {
console.log(
'%s: line %d, col %d, %s - %s (%s)',
event.file,
message.line,
message.col,
message.type,
message.message,
message.rule.id
)
})
})

formatter.on('end', (event) => {
const allHintCount = event.allHintCount
if (allHintCount > 0) {
console.log('')
const message = '%d problems'
console.log(nocolor ? message : message.red, event.allHintCount)
}
})
}

module.exports = compactFormatter
59 changes: 59 additions & 0 deletions src/cli/formatters/default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { FormatterCallback } from '../formatter'

const defaultFormatter: FormatterCallback = function (
formatter,
HTMLHint,
options
) {
const nocolor = !!options.nocolor

formatter.on('start', () => {
console.log('')
})

formatter.on('config', (event) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const configPath = event.configPath!
console.log(' Config loaded: %s', nocolor ? configPath : configPath.cyan)
console.log('')
})

formatter.on('file', (event) => {
console.log(` ${event.file.white}`)

const arrLogs = HTMLHint.format(event.messages, {
colors: !nocolor,
indent: 6,
})

arrLogs.forEach((str) => {
console.log(str)
})

console.log('')
})

formatter.on('end', (event) => {
const allFileCount = event.allFileCount
const allHintCount = event.allHintCount
const allHintFileCount = event.allHintFileCount
const time = event.time
let message

if (allHintCount > 0) {
message = 'Scanned %d files, found %d errors in %d files (%d ms)'
console.log(
nocolor ? message : message.red,
allFileCount,
allHintCount,
allHintFileCount,
time
)
} else {
message = 'Scanned %d files, no errors found (%d ms).'
console.log(nocolor ? message : message.green, allFileCount, time)
}
})
}

module.exports = defaultFormatter
32 changes: 32 additions & 0 deletions src/cli/formatters/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { writeFileSync } from 'fs'
import { FormatterCallback } from '../formatter'

const htmlFormatter: FormatterCallback = function (formatter) {
formatter.on('end', (event) => {
let fileContent = '<html>'
fileContent += '<head><title>HTML Hint Violation Report</title></head>'
fileContent += '<body>'
fileContent += '<center><h2>Violation Report</h2></center>'

fileContent += '<table border="1">'
fileContent +=
'<tr><th>Number#</th><th>File Name</th><th>Line Number</th><th>Message</th></tr>'

for (const { file, messages } of event.arrAllMessages) {
fileContent += messages
.map(
({ line, message }, i) =>
`<tr><td>${
i + 1
}</td><td>${file}</td><td>${line}</td><td>${message}</td></tr>`
)
.join('')
}

fileContent += '</table></body></html>'
console.log(fileContent)
writeFileSync('report.html', fileContent)
})
}

module.exports = htmlFormatter
9 changes: 9 additions & 0 deletions src/cli/formatters/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FormatterCallback } from '../formatter'

const jsonFormatter: FormatterCallback = function (formatter) {
formatter.on('end', (event) => {
console.log(JSON.stringify(event.arrAllMessages))
})
}

module.exports = jsonFormatter
25 changes: 14 additions & 11 deletions bin/formatters/junit.js → src/cli/formatters/junit.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
var xml = require('xml')
import * as xml from 'xml'
import type { XmlObject } from 'xml'
import { FormatterCallback } from '../formatter'

var junitFormatter = function (formatter, HTMLHint) {
formatter.on('end', function (event) {
var arrTestcase = []
var arrAllMessages = event.arrAllMessages
const junitFormatter: FormatterCallback = function (formatter, HTMLHint) {
formatter.on('end', (event) => {
const arrTestcase: XmlObject[] = []
const arrAllMessages = event.arrAllMessages

arrAllMessages.forEach(function (fileInfo) {
var arrMessages = fileInfo.messages
var arrLogs = HTMLHint.format(arrMessages)
arrAllMessages.forEach((fileInfo) => {
const arrMessages = fileInfo.messages
const arrLogs = HTMLHint.format(arrMessages)

arrTestcase.push({
testcase: [
@@ -20,7 +22,7 @@ var junitFormatter = function (formatter, HTMLHint) {
{
failure: {
_attr: {
message: 'Found ' + arrMessages.length + ' errors',
message: `Found ${arrMessages.length} errors`,
},
_cdata: arrLogs.join('\r\n'),
},
@@ -29,7 +31,7 @@ var junitFormatter = function (formatter, HTMLHint) {
})
})

var objXml = {
const objXml: XmlObject = {
testsuites: [
{
testsuite: [
@@ -41,7 +43,8 @@ var junitFormatter = function (formatter, HTMLHint) {
failures: arrAllMessages.length,
},
},
].concat(arrTestcase),
...arrTestcase,
],
},
],
}
44 changes: 44 additions & 0 deletions src/cli/formatters/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FormatterCallback } from '../formatter'

const markdownFormatter: FormatterCallback = function (formatter, HTMLHint) {
formatter.on('end', (event) => {
console.log('# TOC')

const arrToc: string[] = []
const arrContents: string[] = []
const arrAllMessages = event.arrAllMessages

arrAllMessages.forEach((fileInfo) => {
const filePath = fileInfo.file
const arrMessages = fileInfo.messages
let errorCount = 0
let warningCount = 0

arrMessages.forEach((message) => {
if (message.type === 'error') {
errorCount++
} else {
warningCount++
}
})

arrToc.push(` - [${filePath}](#${filePath})`)
arrContents.push(`<a name="${filePath}" />`)
arrContents.push(`# ${filePath}`)
arrContents.push('')
arrContents.push(`Found ${errorCount} errors, ${warningCount} warnings`)

const arrLogs = HTMLHint.format(arrMessages)
arrContents.push('')
arrLogs.forEach((log) => {
arrContents.push(` ${log}`)
})
arrContents.push('')
})

console.log(`${arrToc.join('\r\n')}\r\n`)
console.log(arrContents.join('\r\n'))
})
}

module.exports = markdownFormatter
38 changes: 38 additions & 0 deletions src/cli/formatters/unix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as colors from 'colors'
import { FormatterCallback } from '../formatter'

const unixFormatter: FormatterCallback = function (
formatter,
HTMLHint,
options
) {
const nocolor = options.nocolor

if (nocolor !== false) {
colors.enable()
}

formatter.on('file', (event) => {
event.messages.forEach((message) => {
console.log(
[
event.file,
message.line,
message.col,
` ${message.message} [${message.type}/${message.rule.id}]`,
].join(':')
)
})
})

formatter.on('end', (event) => {
const allHintCount = event.allHintCount
if (allHintCount > 0) {
console.log('')
const message = '%d problems'
console.log(nocolor ? message : message.red, event.allHintCount)
}
})
}

module.exports = unixFormatter
501 changes: 501 additions & 0 deletions src/cli/htmlhint.ts

Large diffs are not rendered by default.

170 changes: 0 additions & 170 deletions src/core.js

This file was deleted.

165 changes: 165 additions & 0 deletions src/core/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import HTMLParser from './htmlparser'
import Reporter from './reporter'
import * as HTMLRules from './rules'
import { Hint, Rule, Ruleset } from './types'

export interface FormatOptions {
colors?: boolean
indent?: number
}

class HTMLHintCore {
public rules: { [id: string]: Rule } = {}
public readonly defaultRuleset: Ruleset = {
'tagname-lowercase': true,
'attr-lowercase': true,
'attr-value-double-quotes': true,
'doctype-first': true,
'tag-pair': true,
'spec-char-escape': true,
'id-unique': true,
'src-not-empty': true,
'attr-no-duplication': true,
'title-require': true,
}

public addRule(rule: Rule) {
this.rules[rule.id] = rule
}

public verify(html: string, ruleset: Ruleset = this.defaultRuleset) {
if (Object.keys(ruleset).length === 0) {
ruleset = this.defaultRuleset
}

// parse inline ruleset
html = html.replace(
/^\s*<!--\s*htmlhint\s+([^\r\n]+?)\s*-->/i,
(all, strRuleset: string) => {
// For example:
// all is '<!-- htmlhint alt-require:true-->'
// strRuleset is 'alt-require:true'
strRuleset.replace(
/(?:^|,)\s*([^:,]+)\s*(?:\:\s*([^,\s]+))?/g,
(all, ruleId: string, value: string | undefined) => {
// For example:
// all is 'alt-require:true'
// ruleId is 'alt-require'
// value is 'true'

ruleset[ruleId] =
value !== undefined && value.length > 0 ? JSON.parse(value) : true

return ''
}
)

return ''
}
)

const parser = new HTMLParser()
const reporter = new Reporter(html, ruleset)

const rules = this.rules
let rule: Rule

for (const id in ruleset) {
rule = rules[id]
if (rule !== undefined && ruleset[id] !== false) {
rule.init(parser, reporter, ruleset[id])
}
}

parser.parse(html)

return reporter.messages
}

public format(arrMessages: Hint[], options: FormatOptions = {}) {
const arrLogs: string[] = []
const colors = {
white: '',
grey: '',
red: '',
reset: '',
}

if (options.colors) {
colors.white = '\x1b[37m'
colors.grey = '\x1b[90m'
colors.red = '\x1b[31m'
colors.reset = '\x1b[39m'
}

const indent = options.indent || 0

arrMessages.forEach((hint) => {
const leftWindow = 40
const rightWindow = leftWindow + 20
let evidence = hint.evidence
const line = hint.line
const col = hint.col
const evidenceCount = evidence.length
let leftCol = col > leftWindow + 1 ? col - leftWindow : 1
let rightCol =
evidence.length > col + rightWindow ? col + rightWindow : evidenceCount

if (col < leftWindow + 1) {
rightCol += leftWindow - col + 1
}

evidence = evidence.replace(/\t/g, ' ').substring(leftCol - 1, rightCol)

// add ...
if (leftCol > 1) {
evidence = `...${evidence}`
leftCol -= 3
}
if (rightCol < evidenceCount) {
evidence += '...'
}

// show evidence
arrLogs.push(
`${colors.white + repeatStr(indent)}L${line} |${
colors.grey
}${evidence}${colors.reset}`
)

// show pointer & message
let pointCol = col - leftCol
// add double byte character
// eslint-disable-next-line no-control-regex
const match = evidence.substring(0, pointCol).match(/[^\u0000-\u00ff]/g)
if (match !== null) {
pointCol += match.length
}

arrLogs.push(
`${
colors.white +
repeatStr(indent) +
repeatStr(String(line).length + 3 + pointCol)
}^ ${colors.red}${hint.message} (${hint.rule.id})${colors.reset}`
)
})

return arrLogs
}
}

// repeat string
function repeatStr(n: number, str?: string) {
return new Array(n + 1).join(str || ' ')
}

export const HTMLHint = new HTMLHintCore()

Object.keys(HTMLRules).forEach((key) => {
// TODO: need a fix
// @ts-expect-error
HTMLHint.addRule(HTMLRules[key])
})

export { HTMLRules, Reporter, HTMLParser }
195 changes: 121 additions & 74 deletions src/htmlparser.js → src/core/htmlparser.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,93 @@
class HTMLParser {
constructor() {
export interface Attr {
name: string
value: string
quote: string
index: number
raw: string
}

export interface Block {
tagName: string
attrs: Attr[]
type: string
raw: string
pos: number
line: number
col: number
content: string
long: boolean
close: string
lastEvent?: Partial<Block>
}

export type Listener = (event: Block) => void

export default class HTMLParser {
public lastEvent: Partial<Block> | null

private _listeners: { [type: string]: Listener[] }
private _mapCdataTags: { [tagName: string]: boolean }
private _arrBlocks: Array<Partial<Block>>

public constructor() {
this._listeners = {}
this._mapCdataTags = this.makeMap('script,style')
this._arrBlocks = []
this.lastEvent = null
}

makeMap(str) {
var obj = {}
var items = str.split(',')
public makeMap(
str: string
): {
[key: string]: boolean
} {
const obj: { [key: string]: boolean } = {}
const items = str.split(',')

for (var i = 0; i < items.length; i++) {
for (let i = 0; i < items.length; i++) {
obj[items[i]] = true
}

return obj
}

parse(html) {
var self = this
var mapCdataTags = self._mapCdataTags

// eslint-disable-next-line
var regTag = /<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[^\s"'>\/=\x00-\x0F\x7F\x80-\x9F]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]*))?)*?)\s*(\/?))>/g,
// eslint-disable-next-line
regAttr = /\s*([^\s"'>\/=\x00-\x0F\x7F\x80-\x9F]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"'>]*)))?/g,
regLine = /\r?\n/g

var match
var matchIndex
var lastIndex = 0
var tagName
var arrAttrs
var tagCDATA
var attrsCDATA
var arrCDATA
var lastCDATAIndex = 0
var text
var lastLineIndex = 0
var line = 1
var arrBlocks = self._arrBlocks

self.fire('start', {
public parse(html: string): void {
const mapCdataTags = this._mapCdataTags

// eslint-disable-next-line no-control-regex
const regTag = /<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[^\s"'>\/=\x00-\x0F\x7F\x80-\x9F]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]*))?)*?)\s*(\/?))>/g
// eslint-disable-next-line no-control-regex
const regAttr = /\s*([^\s"'>\/=\x00-\x0F\x7F\x80-\x9F]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"'>]*)))?/g
const regLine = /\r?\n/g

let match: RegExpExecArray | null
let matchIndex: number
let lastIndex = 0
let tagName: string
let arrAttrs: Attr[]
let tagCDATA: string | null = null
let attrsCDATA: Attr[] | undefined
let arrCDATA: string[] = []
let lastCDATAIndex = 0
let text: string
let lastLineIndex = 0
let line = 1
const arrBlocks = this._arrBlocks

this.fire('start', {
pos: 0,
line: 1,
col: 1,
})

// Memory block
function saveBlock(type, raw, pos, data) {
var col = pos - lastLineIndex + 1
const saveBlock = (
type: string,
raw: string,
pos: number,
data?: Partial<Block>
) => {
const col = pos - lastLineIndex + 1
if (data === undefined) {
data = {}
}
@@ -58,10 +96,9 @@ class HTMLParser {
data.line = line
data.col = col
arrBlocks.push(data)
self.fire(type, data)
this.fire(type, data)

// eslint-disable-next-line
var lineMatch
let lineMatch: RegExpExecArray | null
while ((lineMatch = regLine.exec(raw))) {
line++
lastLineIndex = pos + regLine.lastIndex
@@ -91,8 +128,8 @@ class HTMLParser {
attrs: attrsCDATA,
})
tagCDATA = null
attrsCDATA = null
arrCDATA = null
attrsCDATA = undefined
arrCDATA = []
}

if (!tagCDATA) {
@@ -110,18 +147,18 @@ class HTMLParser {
if ((tagName = match[4])) {
// Label start
arrAttrs = []
var attrs = match[5]
var attrMatch
var attrMatchCount = 0
const attrs = match[5]
let attrMatch
let attrMatchCount = 0

while ((attrMatch = regAttr.exec(attrs))) {
var name = attrMatch[1]
var quote = attrMatch[2]
const name = attrMatch[1]
const quote = attrMatch[2]
? attrMatch[2]
: attrMatch[4]
? attrMatch[4]
: ''
var value = attrMatch[3]
const value = attrMatch[3]
? attrMatch[3]
: attrMatch[5]
? attrMatch[5]
@@ -172,19 +209,19 @@ class HTMLParser {
saveBlock('text', text, lastIndex)
}

self.fire('end', {
this.fire('end', {
pos: lastIndex,
line: line,
col: html.length - lastLineIndex + 1,
})
}

addListener(types, listener) {
var _listeners = this._listeners
var arrTypes = types.split(/[,\s]/)
var type
public addListener(types: string, listener: Listener): void {
const _listeners = this._listeners
const arrTypes = types.split(/[,\s]/)
let type

for (var i = 0, l = arrTypes.length; i < l; i++) {
for (let i = 0, l = arrTypes.length; i < l; i++) {
type = arrTypes[i]
if (_listeners[type] === undefined) {
_listeners[type] = []
@@ -193,15 +230,15 @@ class HTMLParser {
}
}

fire(type, data) {
public fire(type: string, data?: Partial<Block>): void {
if (data === undefined) {
data = {}
}
data.type = type
var self = this
var listeners = []
var listenersType = self._listeners[type]
var listenersAll = self._listeners['all']

let listeners: Listener[] = []
const listenersType = this._listeners[type]
const listenersAll = this._listeners['all']

if (listenersType !== undefined) {
listeners = listeners.concat(listenersType)
@@ -210,23 +247,25 @@ class HTMLParser {
listeners = listeners.concat(listenersAll)
}

var lastEvent = self.lastEvent
const lastEvent = this.lastEvent
if (lastEvent !== null) {
delete lastEvent['lastEvent']
data.lastEvent = lastEvent
}

self.lastEvent = data
this.lastEvent = data

for (var i = 0, l = listeners.length; i < l; i++) {
listeners[i].call(self, data)
for (let i = 0, l = listeners.length; i < l; i++) {
// TODO: we may improve where data is actually a Block or a Partial<Block>
// @ts-expect-error
listeners[i].call(this, data)
}
}

removeListener(type, listener) {
var listenersType = this._listeners[type]
public removeListener(type: string, listener: Listener): void {
const listenersType: Listener[] | undefined = this._listeners[type]
if (listenersType !== undefined) {
for (var i = 0, l = listenersType.length; i < l; i++) {
for (let i = 0, l = listenersType.length; i < l; i++) {
if (listenersType[i] === listener) {
listenersType.splice(i, 1)
break
@@ -235,12 +274,18 @@ class HTMLParser {
}
}

fixPos(event, index) {
var text = event.raw.substr(0, index)
var arrLines = text.split(/\r?\n/)
var lineCount = arrLines.length - 1
var line = event.line
var col
public fixPos(
event: Block,
index: number
): {
line: number
col: number
} {
const text = event.raw.substr(0, index)
const arrLines = text.split(/\r?\n/)
const lineCount = arrLines.length - 1
let line = event.line
let col: number

if (lineCount > 0) {
line += lineCount
@@ -255,17 +300,19 @@ class HTMLParser {
}
}

getMapAttrs(arrAttrs) {
var mapAttrs = {}
var attr
public getMapAttrs(
arrAttrs: Attr[]
): {
[name: string]: string
} {
const mapAttrs: { [name: string]: string } = {}
let attr: Attr

for (var i = 0, l = arrAttrs.length; i < l; i++) {
for (let i = 0, l = arrAttrs.length; i < l; i++) {
attr = arrAttrs[i]
mapAttrs[attr.name] = attr.value
}

return mapAttrs
}
}

export default HTMLParser
File renamed without changes
91 changes: 91 additions & 0 deletions src/core/reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Hint, ReportType, Rule, Ruleset } from './types'

export default class Reporter {
public html: string
public lines: string[]
public brLen: number
public ruleset: Ruleset
public messages: Hint[]

public constructor(html: string, ruleset: Ruleset) {
this.html = html
this.lines = html.split(/\r?\n/)
const match = /\r?\n/.exec(html)

this.brLen = match !== null ? match[0].length : 0
this.ruleset = ruleset
this.messages = []
}

public info(
message: string,
line: number,
col: number,
rule: Rule,
raw: string
): void {
this.report(ReportType.info, message, line, col, rule, raw)
}

public warn(
message: string,
line: number,
col: number,
rule: Rule,
raw: string
): void {
this.report(ReportType.warning, message, line, col, rule, raw)
}

public error(
message: string,
line: number,
col: number,
rule: Rule,
raw: string
): void {
this.report(ReportType.error, message, line, col, rule, raw)
}

private report(
type: ReportType,
message: string,
line: number,
col: number,
rule: Rule,
raw: string
) {
const lines = this.lines
const brLen = this.brLen
let evidence = ''
let evidenceLen = 0

for (let i = line - 1, lineCount = lines.length; i < lineCount; i++) {
evidence = lines[i]
evidenceLen = evidence.length
if (col > evidenceLen && line < lineCount) {
line++
col -= evidenceLen
if (col !== 1) {
col -= brLen
}
} else {
break
}
}

this.messages.push({
type: type,
message: message,
raw: raw,
evidence: evidence,
line: line,
col: col,
rule: {
id: rule.id,
description: rule.description,
link: `https://github.com/thedaviddias/HTMLHint/wiki/${rule.id}`,
} as Rule,
})
}
}
23 changes: 12 additions & 11 deletions src/rules/alt-require.js → src/core/rules/alt-require.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { Rule } from '../types'

export default {
id: 'alt-require',
description:
'The alt attribute of an <img> element must be present and alt attribute of area[href] and input[type=image] must have a value.',
init: function (parser, reporter) {
var self = this
parser.addListener('tagstart', function (event) {
var tagName = event.tagName.toLowerCase()
var mapAttrs = parser.getMapAttrs(event.attrs)
var col = event.col + tagName.length + 1
var selector
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase()
const mapAttrs = parser.getMapAttrs(event.attrs)
const col = event.col + tagName.length + 1
let selector

if (tagName === 'img' && !('alt' in mapAttrs)) {
reporter.warn(
'An alt attribute must be present on <img> elements.',
event.line,
col,
self,
this,
event.raw
)
} else if (
@@ -25,14 +26,14 @@ export default {
if (!('alt' in mapAttrs) || mapAttrs['alt'] === '') {
selector = tagName === 'area' ? 'area[href]' : 'input[type=image]'
reporter.warn(
'The alt attribute of ' + selector + ' must have a value.',
`The alt attribute of ${selector} must have a value.`,
event.line,
col,
self,
this,
event.raw
)
}
}
})
},
}
} as Rule
32 changes: 16 additions & 16 deletions src/rules/attr-lowercase.js → src/core/rules/attr-lowercase.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Rule } from '../types'

/**
* testAgainstStringOrRegExp
*
* @param {string} value string to test
* @param {string|RegExp} comparison raw string or regex string
* @returns {boolean}
* @param value string to test
* @param comparison raw string or regex string
*/
function testAgainstStringOrRegExp(value, comparison) {
function testAgainstStringOrRegExp(value: string, comparison: string | RegExp) {
// If it's a RegExp, test directly
if (comparison instanceof RegExp) {
return comparison.test(value)
@@ -41,32 +42,31 @@ function testAgainstStringOrRegExp(value, comparison) {
export default {
id: 'attr-lowercase',
description: 'All attribute names must be in lowercase.',
init: function (parser, reporter, options) {
var self = this
var exceptions = Array.isArray(options) ? options : []
init(parser, reporter, options) {
const exceptions = Array.isArray(options) ? options : []

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var attr
var col = event.col + event.tagName.length + 1
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (var i = 0, l = attrs.length; i < l; i++) {
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]
var attrName = attr.name
const attrName = attr.name

if (
!exceptions.find((exp) => testAgainstStringOrRegExp(attrName, exp)) &&
attrName !== attrName.toLowerCase()
) {
reporter.error(
'The attribute name of [ ' + attrName + ' ] must be in lowercase.',
`The attribute name of [ ${attrName} ] must be in lowercase.`,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
}
})
},
}
} as Rule
32 changes: 32 additions & 0 deletions src/core/rules/attr-no-duplication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Rule } from '../types'

export default {
id: 'attr-no-duplication',
description: 'Elements cannot have duplicate attributes.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
let attrName: string
const col = event.col + event.tagName.length + 1

const mapAttrName: { [name: string]: boolean } = {}

for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]
attrName = attr.name

if (mapAttrName[attrName] === true) {
reporter.error(
`Duplicate of attribute name [ ${attr.name} ] was found.`,
event.line,
col + attr.index,
this,
attr.raw
)
}
mapAttrName[attrName] = true
}
})
},
} as Rule
29 changes: 29 additions & 0 deletions src/core/rules/attr-no-unnecessary-whitespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Rule } from '../types'

export default {
id: 'attr-no-unnecessary-whitespace',
description: 'No spaces between attribute names and values.',
init(parser, reporter, options) {
const exceptions: string[] = Array.isArray(options) ? options : []

parser.addListener('tagstart', (event) => {
const attrs = event.attrs
const col = event.col + event.tagName.length + 1

for (let i = 0; i < attrs.length; i++) {
if (exceptions.indexOf(attrs[i].name) === -1) {
const match = /(\s*)=(\s*)/.exec(attrs[i].raw.trim())
if (match && (match[1].length !== 0 || match[2].length !== 0)) {
reporter.error(
`The attribute '${attrs[i].name}' must not have spaces between the name and value.`,
event.line,
col + attrs[i].index,
this,
attrs[i].raw
)
}
}
}
})
},
} as Rule
36 changes: 18 additions & 18 deletions src/rules/attr-sorted.js → src/core/rules/attr-sorted.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Rule } from '../types'

export default {
id: 'attr-sorted',
description: 'Attribute tags must be in proper order.',
init: function (parser, reporter) {
var self = this
var orderMap = {}
var sortOrder = [
init(parser, reporter) {
const orderMap: { [key: string]: number } = {}
const sortOrder = [
'class',
'id',
'name',
@@ -18,20 +19,20 @@ export default {
'role',
]

for (var i = 0; i < sortOrder.length; i++) {
for (let i = 0; i < sortOrder.length; i++) {
orderMap[sortOrder[i]] = i
}

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var listOfAttributes = []
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
const listOfAttributes = []

for (var i = 0; i < attrs.length; i++) {
for (let i = 0; i < attrs.length; i++) {
listOfAttributes.push(attrs[i].name)
}

var originalAttrs = JSON.stringify(listOfAttributes)
listOfAttributes.sort(function (a, b) {
const originalAttrs = JSON.stringify(listOfAttributes)
listOfAttributes.sort((a, b) => {
if (orderMap[a] == undefined && orderMap[b] == undefined) {
return 0
}
@@ -45,16 +46,15 @@ export default {

if (originalAttrs !== JSON.stringify(listOfAttributes)) {
reporter.error(
'Inaccurate order ' +
originalAttrs +
' should be in hierarchy ' +
JSON.stringify(listOfAttributes) +
' ',
`Inaccurate order ${originalAttrs} should be in hierarchy ${JSON.stringify(
listOfAttributes
)} `,
event.line,
event.col,
self
this,
event.raw
)
}
})
},
}
} as Rule
35 changes: 35 additions & 0 deletions src/core/rules/attr-unsafe-chars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Rule } from '../types'

export default {
id: 'attr-unsafe-chars',
description: 'Attribute values cannot contain unsafe chars.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1
// exclude \x09(\t), \x0a(\r), \x0d(\n)
// eslint-disable-next-line no-misleading-character-class, no-control-regex
const regUnsafe = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/
let match

for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]
match = regUnsafe.exec(attr.value)

if (match !== null) {
const unsafeCode = escape(match[0])
.replace(/%u/, '\\u')
.replace(/%/, '\\x')
reporter.warn(
`The value of attribute [ ${attr.name} ] cannot contain an unsafe char [ ${unsafeCode} ].`,
event.line,
col + attr.index,
this,
attr.raw
)
}
}
})
},
} as Rule
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import { Rule } from '../types'

export default {
id: 'attr-value-double-quotes',
description: 'Attribute values must be in double quotes.',
init: function (parser, reporter) {
var self = this

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var attr
var col = event.col + event.tagName.length + 1
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (var i = 0, l = attrs.length; i < l; i++) {
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (
(attr.value !== '' && attr.quote !== '"') ||
(attr.value === '' && attr.quote === "'")
) {
reporter.error(
'The value of attribute [ ' +
attr.name +
' ] must be in double quotes.',
`The value of attribute [ ${attr.name} ] must be in double quotes.`,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
}
})
},
}
} as Rule
27 changes: 27 additions & 0 deletions src/core/rules/attr-value-not-empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Rule } from '../types'

export default {
id: 'attr-value-not-empty',
description: 'All attributes must have values.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (attr.quote === '' && attr.value === '') {
reporter.warn(
`The attribute [ ${attr.name} ] must have a value.`,
event.line,
col + attr.index,
this,
attr.raw
)
}
}
})
},
} as Rule
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import { Rule } from '../types'

export default {
id: 'attr-value-single-quotes',
description: 'Attribute values must be in single quotes.',
init: function (parser, reporter) {
var self = this

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var attr
var col = event.col + event.tagName.length + 1
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (var i = 0, l = attrs.length; i < l; i++) {
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (
(attr.value !== '' && attr.quote !== "'") ||
(attr.value === '' && attr.quote === '"')
) {
reporter.error(
'The value of attribute [ ' +
attr.name +
' ] must be in single quotes.',
`The value of attribute [ ${attr.name} ] must be in single quotes.`,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
}
})
},
}
} as Rule
48 changes: 48 additions & 0 deletions src/core/rules/attr-whitespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Rule } from '../types'

export default {
id: 'attr-whitespace',
description:
'All attributes should be separated by only one space and not have leading/trailing whitespace.',
init(parser, reporter, options) {
const exceptions: Array<string | boolean> = Array.isArray(options)
? options
: []

parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

attrs.forEach((elem) => {
attr = elem
const attrName = elem.name

if (exceptions.indexOf(attrName) !== -1) {
return
}

// Check first and last characters for spaces
if (elem.value.trim() !== elem.value) {
reporter.error(
`The attributes of [ ${attrName} ] must not have trailing whitespace.`,
event.line,
col + attr.index,
this,
attr.raw
)
}

if (elem.value.replace(/ +(?= )/g, '') !== elem.value) {
reporter.error(
`The attributes of [ ${attrName} ] must be separated by only one space.`,
event.line,
col + attr.index,
this,
attr.raw
)
}
})
})
},
} as Rule
13 changes: 7 additions & 6 deletions src/rules/doctype-first.js → src/core/rules/doctype-first.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Listener } from '../htmlparser'
import { Rule } from '../types'

export default {
id: 'doctype-first',
description: 'Doctype must be declared first.',
init: function (parser, reporter) {
var self = this

var allEvent = function (event) {
init(parser, reporter) {
const allEvent: Listener = (event) => {
if (
event.type === 'start' ||
(event.type === 'text' && /^\s*$/.test(event.raw))
@@ -20,7 +21,7 @@ export default {
'Doctype must be declared first.',
event.line,
event.col,
self,
this,
event.raw
)
}
@@ -30,4 +31,4 @@ export default {

parser.addListener('all', allEvent)
},
}
} as Rule
15 changes: 8 additions & 7 deletions src/rules/doctype-html5.js → src/core/rules/doctype-html5.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Listener } from '../htmlparser'
import { Rule } from '../types'

export default {
id: 'doctype-html5',
description: 'Invalid doctype. Use: "<!DOCTYPE html>"',
init: function (parser, reporter) {
var self = this

function onComment(event) {
init(parser, reporter) {
const onComment: Listener = (event) => {
if (
event.long === false &&
event.content.toLowerCase() !== 'doctype html'
@@ -13,18 +14,18 @@ export default {
'Invalid doctype. Use: "<!DOCTYPE html>"',
event.line,
event.col,
self,
this,
event.raw
)
}
}

function onTagStart() {
const onTagStart: Listener = () => {
parser.removeListener('comment', onComment)
parser.removeListener('tagstart', onTagStart)
}

parser.addListener('all', onComment)
parser.addListener('tagstart', onTagStart)
},
}
} as Rule
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Listener } from '../htmlparser'
import { Rule } from '../types'

export default {
id: 'head-script-disabled',
description: 'The <script> tag cannot be used in a <head> tag.',
init: function (parser, reporter) {
var self = this
var reScript = /^(text\/javascript|application\/javascript)$/i
var isInHead = false
init(parser, reporter) {
const reScript = /^(text\/javascript|application\/javascript)$/i
let isInHead = false

function onTagStart(event) {
var mapAttrs = parser.getMapAttrs(event.attrs)
var type = mapAttrs.type
var tagName = event.tagName.toLowerCase()
const onTagStart: Listener = (event) => {
const mapAttrs = parser.getMapAttrs(event.attrs)
const type = mapAttrs.type
const tagName = event.tagName.toLowerCase()

if (tagName === 'head') {
isInHead = true
@@ -24,13 +26,13 @@ export default {
'The <script> tag cannot be used in a <head> tag.',
event.line,
event.col,
self,
this,
event.raw
)
}
}

function onTagEnd(event) {
const onTagEnd: Listener = (event) => {
if (event.tagName.toLowerCase() === 'head') {
parser.removeListener('tagstart', onTagStart)
parser.removeListener('tagend', onTagEnd)
@@ -40,4 +42,4 @@ export default {
parser.addListener('tagstart', onTagStart)
parser.addListener('tagend', onTagEnd)
},
}
} as Rule
28 changes: 12 additions & 16 deletions src/rules/href-abs-or-rel.js → src/core/rules/href-abs-or-rel.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Rule } from '../types'

export default {
id: 'href-abs-or-rel',
description: 'An href attribute must be either absolute or relative.',
init: function (parser, reporter, options) {
var self = this

var hrefMode = options === 'abs' ? 'absolute' : 'relative'
init(parser, reporter, options) {
const hrefMode = options === 'abs' ? 'absolute' : 'relative'

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var attr
var col = event.col + event.tagName.length + 1
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (var i = 0, l = attrs.length; i < l; i++) {
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (attr.name === 'href') {
@@ -21,14 +21,10 @@ export default {
/^https?:\/\//.test(attr.value) === true)
) {
reporter.warn(
'The value of the href attribute [ ' +
attr.value +
' ] must be ' +
hrefMode +
'.',
`The value of the href attribute [ ${attr.value} ] must be ${hrefMode}.`,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
@@ -37,4 +33,4 @@ export default {
}
})
},
}
} as Rule
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
import { Rule } from '../types'

export default {
id: 'id-class-ad-disabled',
description:
'The id and class attributes cannot use the ad keyword, it will be blocked by adblock software.',
init: function (parser, reporter) {
var self = this

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var attr
var attrName
var col = event.col + event.tagName.length + 1
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
let attrName
const col = event.col + event.tagName.length + 1

for (var i = 0, l = attrs.length; i < l; i++) {
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]
attrName = attr.name

if (/^(id|class)$/i.test(attrName)) {
if (/(^|[-_])ad([-_]|$)/i.test(attr.value)) {
reporter.warn(
'The value of attribute ' +
attrName +
' cannot use the ad keyword.',
`The value of attribute ${attrName} cannot use the ad keyword.`,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
}
}
})
},
}
} as Rule
39 changes: 20 additions & 19 deletions src/rules/id-class-value.js → src/core/rules/id-class-value.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Rule } from '../types'

export default {
id: 'id-class-value',
description:
'The id and class attribute values must meet the specified rules.',
init: function (parser, reporter, options) {
var self = this
var arrRules = {
init(parser, reporter, options) {
const arrRules: { [option: string]: { regId: RegExp; message: string } } = {
underline: {
regId: /^[a-z\d]+(_[a-z\d]+)*$/,
message:
@@ -21,28 +22,28 @@ export default {
'The id and class attribute values must meet the camelCase style.',
},
}
var rule
let rule: { regId: RegExp; message: string } | boolean

if (typeof options === 'string') {
rule = arrRules[options]
} else {
rule = options
rule = options as { regId: RegExp; message: string }
}

if (rule && rule.regId) {
var regId = rule.regId
var message = rule.message
if (typeof rule === 'object' && rule.regId) {
let regId = rule.regId
const message = rule.message

if (!(regId instanceof RegExp)) {
regId = new RegExp(regId)
}

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var attr
var col = event.col + event.tagName.length + 1
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (var i = 0, l1 = attrs.length; i < l1; i++) {
for (let i = 0, l1 = attrs.length; i < l1; i++) {
attr = attrs[i]

if (attr.name.toLowerCase() === 'id') {
@@ -51,24 +52,24 @@ export default {
message,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
}

if (attr.name.toLowerCase() === 'class') {
var arrClass = attr.value.split(/\s+/g)
var classValue
const arrClass = attr.value.split(/\s+/g)
let classValue

for (var j = 0, l2 = arrClass.length; j < l2; j++) {
for (let j = 0, l2 = arrClass.length; j < l2; j++) {
classValue = arrClass[j]
if (classValue && regId.test(classValue) === false) {
reporter.warn(
message,
event.line,
col + attr.index,
self,
this,
classValue
)
}
@@ -78,4 +79,4 @@ export default {
})
}
},
}
} as Rule
25 changes: 13 additions & 12 deletions src/rules/id-unique.js → src/core/rules/id-unique.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Rule } from '../types'

export default {
id: 'id-unique',
description: 'The value of id attributes must be unique.',
init: function (parser, reporter) {
var self = this
var mapIdCount = {}
init(parser, reporter) {
const mapIdCount: { [id: string]: number } = {}

parser.addListener('tagstart', function (event) {
var attrs = event.attrs
var attr
var id
var col = event.col + event.tagName.length + 1
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
let id
const col = event.col + event.tagName.length + 1

for (var i = 0, l = attrs.length; i < l; i++) {
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (attr.name.toLowerCase() === 'id') {
@@ -26,10 +27,10 @@ export default {

if (mapIdCount[id] > 1) {
reporter.error(
'The id value [ ' + id + ' ] must be unique.',
`The id value [ ${id} ] must be unique.`,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
@@ -39,4 +40,4 @@ export default {
}
})
},
}
} as Rule
File renamed without changes.
40 changes: 40 additions & 0 deletions src/core/rules/inline-script-disabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Rule } from '../types'

export default {
id: 'inline-script-disabled',
description: 'Inline script cannot be used.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1
let attrName
const reEvent = /^on(unload|message|submit|select|scroll|resize|mouseover|mouseout|mousemove|mouseleave|mouseenter|mousedown|load|keyup|keypress|keydown|focus|dblclick|click|change|blur|error)$/i

for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]
attrName = attr.name.toLowerCase()

if (reEvent.test(attrName) === true) {
reporter.warn(
`Inline script [ ${attr.raw} ] cannot be used.`,
event.line,
col + attr.index,
this,
attr.raw
)
} else if (attrName === 'src' || attrName === 'href') {
if (/^\s*javascript:/i.test(attr.value)) {
reporter.warn(
`Inline script [ ${attr.raw} ] cannot be used.`,
event.line,
col + attr.index,
this,
attr.raw
)
}
}
}
})
},
} as Rule
27 changes: 27 additions & 0 deletions src/core/rules/inline-style-disabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Rule } from '../types'

export default {
id: 'inline-style-disabled',
description: 'Inline style cannot be used.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (attr.name.toLowerCase() === 'style') {
reporter.warn(
`Inline style [ ${attr.raw} ] cannot be used.`,
event.line,
col + attr.index,
this,
attr.raw
)
}
}
})
},
} as Rule
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Block } from '../htmlparser'
import { Rule } from '../types'

export default {
id: 'input-requires-label',
description: 'All [ input ] tags must have a corresponding [ label ] tag. ',
init: function (parser, reporter) {
var self = this
var labelTags = []
var inputTags = []
init(parser, reporter) {
const labelTags: Array<{
event: Block
col: number
forValue?: string
}> = []
const inputTags: Array<{ event: Block; col: number; id?: string }> = []

parser.addListener('tagstart', function (event) {
var tagName = event.tagName.toLowerCase()
var mapAttrs = parser.getMapAttrs(event.attrs)
var col = event.col + tagName.length + 1
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase()
const mapAttrs = parser.getMapAttrs(event.attrs)
const col = event.col + tagName.length + 1

if (tagName === 'input') {
inputTags.push({ event: event, col: col, id: mapAttrs['id'] })
@@ -22,28 +28,28 @@ export default {
}
})

parser.addListener('end', function () {
inputTags.forEach(function (inputTag) {
parser.addListener('end', () => {
inputTags.forEach((inputTag) => {
if (!hasMatchingLabelTag(inputTag)) {
reporter.warn(
'No matching [ label ] tag found.',
inputTag.event.line,
inputTag.col,
self,
this,
inputTag.event.raw
)
}
})
})

function hasMatchingLabelTag(inputTag) {
var found = false
labelTags.forEach(function (labelTag) {
function hasMatchingLabelTag(inputTag: { id?: string }) {
let found = false
labelTags.forEach((labelTag) => {
if (inputTag.id && inputTag.id === labelTag.forValue) {
found = true
}
})
return found
}
},
}
} as Rule
14 changes: 6 additions & 8 deletions src/rules/script-disabled.js → src/core/rules/script-disabled.ts
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { Rule } from '../types'

export default {
id: 'script-disabled',
description: 'The <script> tag cannot be used.',
init: function (parser, reporter) {
'use strict'

var self = this

parser.addListener('tagstart', function (event) {
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
if (event.tagName.toLowerCase() === 'script') {
reporter.error(
'The <script> tag cannot be used.',
event.line,
event.col,
self,
this,
event.raw
)
}
})
},
}
} as Rule
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
import { Rule } from '../types'

export default {
id: 'space-tab-mixed-disabled',
description: 'Do not mix tabs and spaces for indentation.',
init: function (parser, reporter, options) {
var self = this
var indentMode = 'nomix'
var spaceLengthRequire = null
init(parser, reporter, options) {
let indentMode = 'nomix'
let spaceLengthRequire: number | '' | null = null

if (typeof options === 'string') {
var match = options.match(/^([a-z]+)(\d+)?/)
indentMode = match[1]
spaceLengthRequire = match[2] && parseInt(match[2], 10)
const match = /^([a-z]+)(\d+)?/.exec(options)
if (match) {
indentMode = match[1]
spaceLengthRequire = match[2] && parseInt(match[2], 10)
}
}

parser.addListener('text', function (event) {
var raw = event.raw
var reMixed = /(^|\r?\n)([ \t]+)/g
var match
parser.addListener('text', (event) => {
const raw = event.raw
const reMixed = /(^|\r?\n)([ \t]+)/g
let match

while ((match = reMixed.exec(raw))) {
var fixedPos = parser.fixPos(event, match.index + match[1].length)
const fixedPos = parser.fixPos(event, match.index + match[1].length)
if (fixedPos.col !== 1) {
continue
}

var whiteSpace = match[2]
const whiteSpace = match[2]
if (indentMode === 'space') {
if (spaceLengthRequire) {
if (
/^ +$/.test(whiteSpace) === false ||
whiteSpace.length % spaceLengthRequire !== 0
) {
reporter.warn(
'Please use space for indentation and keep ' +
spaceLengthRequire +
' length.',
`Please use space for indentation and keep ${spaceLengthRequire} length.`,
fixedPos.line,
1,
self,
this,
event.raw
)
}
@@ -46,7 +47,7 @@ export default {
'Please use space for indentation.',
fixedPos.line,
1,
self,
this,
event.raw
)
}
@@ -56,19 +57,19 @@ export default {
'Please use tab for indentation.',
fixedPos.line,
1,
self,
this,
event.raw
)
} else if (/ +\t|\t+ /.test(whiteSpace) === true) {
reporter.warn(
'Do not mix tabs and spaces for indentation.',
fixedPos.line,
1,
self,
this,
event.raw
)
}
}
})
},
}
} as Rule
25 changes: 25 additions & 0 deletions src/core/rules/spec-char-escape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Rule } from '../types'

export default {
id: 'spec-char-escape',
description: 'Special characters must be escaped.',
init(parser, reporter) {
parser.addListener('text', (event) => {
const raw = event.raw
// TODO: improve use-cases for &
const reSpecChar = /([<>])|( \& )/g
let match

while ((match = reSpecChar.exec(raw))) {
const fixedPos = parser.fixPos(event, match.index)
reporter.error(
`Special characters must be escaped : [ ${match[0]} ].`,
fixedPos.line,
fixedPos.col,
this,
event.raw
)
}
})
},
} as Rule
28 changes: 12 additions & 16 deletions src/rules/src-not-empty.js → src/core/rules/src-not-empty.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Rule } from '../types'

export default {
id: 'src-not-empty',
description: 'The src attribute of an img(script,link) must have a value.',
init: function (parser, reporter) {
var self = this

parser.addListener('tagstart', function (event) {
var tagName = event.tagName
var attrs = event.attrs
var attr
var col = event.col + tagName.length + 1
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName
const attrs = event.attrs
let attr
const col = event.col + tagName.length + 1

for (var i = 0, l = attrs.length; i < l; i++) {
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (
@@ -21,18 +21,14 @@ export default {
attr.value === ''
) {
reporter.error(
'The attribute [ ' +
attr.name +
' ] of the tag [ ' +
tagName +
' ] must have a value.',
`The attribute [ ${attr.name} ] of the tag [ ${tagName} ] must have a value.`,
event.line,
col + attr.index,
self,
this,
attr.raw
)
}
}
})
},
}
} as Rule
12 changes: 6 additions & 6 deletions src/rules/style-disabled.js → src/core/rules/style-disabled.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { Rule } from '../types'

export default {
id: 'style-disabled',
description: '<style> tags cannot be used.',
init: function (parser, reporter) {
var self = this

parser.addListener('tagstart', function (event) {
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
if (event.tagName.toLowerCase() === 'style') {
reporter.warn(
'The <style> tag cannot be used.',
event.line,
event.col,
self,
this,
event.raw
)
}
})
},
}
} as Rule
90 changes: 90 additions & 0 deletions src/core/rules/tag-pair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Block } from '../htmlparser'
import { Rule } from '../types'

export default {
id: 'tag-pair',
description: 'Tag must be paired.',
init(parser, reporter) {
const stack: Array<Partial<Block>> = []
const mapEmptyTags = parser.makeMap(
'area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,track,command,source,keygen,wbr'
) //HTML 4.01 + HTML 5

parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase()
if (mapEmptyTags[tagName] === undefined && !event.close) {
stack.push({
tagName: tagName,
line: event.line,
raw: event.raw,
})
}
})

parser.addListener('tagend', (event) => {
const tagName = event.tagName.toLowerCase()

// Look up the matching start tag
let pos
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].tagName === tagName) {
break
}
}

if (pos >= 0) {
const arrTags = []
for (let i = stack.length - 1; i > pos; i--) {
arrTags.push(`</${stack[i].tagName}>`)
}

if (arrTags.length > 0) {
const lastEvent = stack[stack.length - 1]
reporter.error(
`Tag must be paired, missing: [ ${arrTags.join(
''
)} ], start tag match failed [ ${lastEvent.raw} ] on line ${
lastEvent.line
}.`,
event.line,
event.col,
this,
event.raw
)
}
stack.length = pos
} else {
reporter.error(
`Tag must be paired, no start tag: [ ${event.raw} ]`,
event.line,
event.col,
this,
event.raw
)
}
})

parser.addListener('end', (event) => {
const arrTags = []

for (let i = stack.length - 1; i >= 0; i--) {
arrTags.push(`</${stack[i].tagName}>`)
}

if (arrTags.length > 0) {
const lastEvent = stack[stack.length - 1]
reporter.error(
`Tag must be paired, missing: [ ${arrTags.join(
''
)} ], open tag match failed [ ${lastEvent.raw} ] on line ${
lastEvent.line
}.`,
event.line,
event.col,
this,
''
)
}
})
},
} as Rule
17 changes: 9 additions & 8 deletions src/rules/tag-self-close.js → src/core/rules/tag-self-close.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { Rule } from '../types'

export default {
id: 'tag-self-close',
description: 'Empty tags must be self closed.',
init: function (parser, reporter) {
var self = this
var mapEmptyTags = parser.makeMap(
init(parser, reporter) {
const mapEmptyTags = parser.makeMap(
'area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,track,command,source,keygen,wbr'
) //HTML 4.01 + HTML 5

parser.addListener('tagstart', function (event) {
var tagName = event.tagName.toLowerCase()
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase()
if (mapEmptyTags[tagName] !== undefined) {
if (!event.close) {
reporter.warn(
'The empty tag : [ ' + tagName + ' ] must be self closed.',
`The empty tag : [ ${tagName} ] must be self closed.`,
event.line,
event.col,
self,
this,
event.raw
)
}
}
})
},
}
} as Rule
27 changes: 27 additions & 0 deletions src/core/rules/tagname-lowercase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Rule } from '../types'

export default {
id: 'tagname-lowercase',
description: 'All html element names must be in lowercase.',
init(parser, reporter, options) {
const exceptions: Array<string | boolean> = Array.isArray(options)
? options
: []

parser.addListener('tagstart,tagend', (event) => {
const tagName = event.tagName
if (
exceptions.indexOf(tagName) === -1 &&
tagName !== tagName.toLowerCase()
) {
reporter.error(
`The html element name of [ ${tagName} ] must be in lowercase.`,
event.line,
event.col,
this,
event.raw
)
}
})
},
} as Rule
22 changes: 22 additions & 0 deletions src/core/rules/tagname-specialchars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Rule } from '../types'

export default {
id: 'tagname-specialchars',
description: 'All html element names must be in lowercase.',
init(parser, reporter) {
const specialchars = /[^a-zA-Z0-9\-:_]/

parser.addListener('tagstart,tagend', (event) => {
const tagName = event.tagName
if (specialchars.test(tagName)) {
reporter.error(
`The html element name of [ ${tagName} ] contains special character.`,
event.line,
event.col,
this,
event.raw
)
}
})
},
} as Rule
161 changes: 161 additions & 0 deletions src/core/rules/tags-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Rule } from '../types'

let tagsTypings: Record<string, Record<string, unknown>> = {
a: {
selfclosing: false,
attrsRequired: ['href', 'title'],
redundantAttrs: ['alt'],
},
div: {
selfclosing: false,
},
main: {
selfclosing: false,
redundantAttrs: ['role'],
},
nav: {
selfclosing: false,
redundantAttrs: ['role'],
},
script: {
attrsOptional: [
['async', 'async'],
['defer', 'defer'],
],
},
img: {
selfclosing: true,
attrsRequired: ['src', 'alt', 'title'],
},
}

export default {
id: 'tags-check',
description: 'Checks html tags.',
init(parser, reporter, options: Record<string, Record<string, unknown>>) {
tagsTypings = { ...tagsTypings, ...options }

parser.addListener('tagstart', (event) => {
const attrs = event.attrs
const col = event.col + event.tagName.length + 1

const tagName = event.tagName.toLowerCase()

if (tagsTypings[tagName]) {
const currentTagType = tagsTypings[tagName]

if (currentTagType.selfclosing === true && !event.close) {
reporter.warn(
`The <${tagName}> tag must be selfclosing.`,
event.line,
event.col,
this,
event.raw
)
} else if (currentTagType.selfclosing === false && event.close) {
reporter.warn(
`The <${tagName}> tag must not be selfclosing.`,
event.line,
event.col,
this,
event.raw
)
}

if (Array.isArray(currentTagType.attrsRequired)) {
const attrsRequired: Array<string | string[]> =
currentTagType.attrsRequired
attrsRequired.forEach((id) => {
if (Array.isArray(id)) {
const copyOfId = id.map((a) => a)
const realID = copyOfId.shift()
const values = copyOfId

if (attrs.some((attr) => attr.name === realID)) {
attrs.forEach((attr) => {
if (
attr.name === realID &&
values.indexOf(attr.value) === -1
) {
reporter.error(
`The <${tagName}> tag must have attr '${realID}' with one value of '${values.join(
"' or '"
)}'.`,
event.line,
col,
this,
event.raw
)
}
})
} else {
reporter.error(
`The <${tagName}> tag must have attr '${realID}'.`,
event.line,
col,
this,
event.raw
)
}
} else if (
!attrs.some((attr) => id.split('|').indexOf(attr.name) !== -1)
) {
reporter.error(
`The <${tagName}> tag must have attr '${id}'.`,
event.line,
col,
this,
event.raw
)
}
})
}

if (Array.isArray(currentTagType.attrsOptional)) {
const attrsOptional: string[][] = currentTagType.attrsOptional
attrsOptional.forEach((id) => {
if (Array.isArray(id)) {
const copyOfId = id.map((a) => a)
const realID = copyOfId.shift()
const values = copyOfId

if (attrs.some((attr) => attr.name === realID)) {
attrs.forEach((attr) => {
if (
attr.name === realID &&
values.indexOf(attr.value) === -1
) {
reporter.error(
`The <${tagName}> tag must have optional attr '${realID}' with one value of '${values.join(
"' or '"
)}'.`,
event.line,
col,
this,
event.raw
)
}
})
}
}
})
}

if (Array.isArray(currentTagType.redundantAttrs)) {
const redundantAttrs: string[] = currentTagType.redundantAttrs
redundantAttrs.forEach((attrName) => {
if (attrs.some((attr) => attr.name === attrName)) {
reporter.error(
`The attr '${attrName}' is redundant for <${tagName}> and should be ommited.`,
event.line,
col,
this,
event.raw
)
}
})
}
}
})
},
} as Rule
28 changes: 16 additions & 12 deletions src/rules/title-require.js → src/core/rules/title-require.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { Block, Listener } from '../htmlparser'
import { Rule } from '../types'

export default {
id: 'title-require',
description: '<title> must be present in <head> tag.',
init: function (parser, reporter) {
var self = this
var headBegin = false
var hasTitle = false
init(parser, reporter) {
let headBegin = false
let hasTitle = false

function onTagStart(event) {
var tagName = event.tagName.toLowerCase()
const onTagStart: Listener = (event) => {
const tagName = event.tagName.toLowerCase()
if (tagName === 'head') {
headBegin = true
} else if (tagName === 'title' && headBegin) {
hasTitle = true
}
}

function onTagEnd(event) {
var tagName = event.tagName.toLowerCase()
const onTagEnd: Listener = (event) => {
const tagName = event.tagName.toLowerCase()
if (hasTitle && tagName === 'title') {
var lastEvent = event.lastEvent
// TODO: fix this error
// @ts-expect-error
const lastEvent: Block = event.lastEvent
if (
lastEvent.type !== 'text' ||
(lastEvent.type === 'text' && /^\s*$/.test(lastEvent.raw) === true)
@@ -27,7 +31,7 @@ export default {
'<title></title> must not be empty.',
event.line,
event.col,
self,
this,
event.raw
)
}
@@ -37,7 +41,7 @@ export default {
'<title> must be present in <head> tag.',
event.line,
event.col,
self,
this,
event.raw
)
}
@@ -50,4 +54,4 @@ export default {
parser.addListener('tagstart', onTagStart)
parser.addListener('tagend', onTagEnd)
},
}
} as Rule
75 changes: 75 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { HTMLParser, Reporter } from './core'

export interface Rule {
id: string
description: string
link?: string
init(parser: HTMLParser, reporter: Reporter, options: unknown): void
}

export interface Ruleset {
'alt-require'?: boolean
'attr-lowercase'?: boolean | Array<string | RegExp>
'attr-no-duplication'?: boolean
'attr-no-unnecessary-whitespace'?: boolean
'attr-sorted'?: boolean
'attr-unsafe-chars'?: boolean
'attr-value-double-quotes'?: boolean
'attr-value-not-empty'?: boolean
'attr-value-single-quotes'?: boolean
'attr-whitespace'?: boolean
'doctype-first'?: boolean
'doctype-html5'?: boolean
'head-script-disabled'?: boolean
'href-abs-or-rel'?: 'abs' | 'rel'
'id-class-ad-disabled'?: boolean
'id-class-value'?:
| 'underline'
| 'dash'
| 'hump'
| { regId: RegExp; message: string }
'id-unique'?: boolean
'inline-script-disabled'?: boolean
'inline-style-disabled'?: boolean
'input-requires-label'?: boolean
'script-disabled'?: boolean
'space-tab-mixed-disabled'?:
| boolean
| 'space'
| 'space1'
| 'space2'
| 'space3'
| 'space4'
| 'space5'
| 'space6'
| 'space7'
| 'space8'
| 'tab'
'spec-char-escape'?: boolean
'src-not-empty'?: boolean
'style-disabled'?: boolean
'tag-pair'?: boolean
'tag-self-close'?: boolean
'tagname-lowercase'?: boolean
'tagname-specialchars'?: boolean
'tags-check'?: { [tagName: string]: Record<string, unknown> }
'title-require'?: boolean
// There may be other unknown rules
[ruleId: string]: unknown
}

export const enum ReportType {
error = 'error',
warning = 'warning',
info = 'info',
}

export interface Hint {
type: ReportType
message: string
raw: string
evidence: string
line: number
col: number
rule: Rule
}
53 changes: 0 additions & 53 deletions src/reporter.js

This file was deleted.

32 changes: 0 additions & 32 deletions src/rules/attr-no-duplication.js

This file was deleted.

30 changes: 0 additions & 30 deletions src/rules/attr-no-unnecessary-whitespace.js

This file was deleted.

39 changes: 0 additions & 39 deletions src/rules/attr-unsafe-chars.js

This file was deleted.

27 changes: 0 additions & 27 deletions src/rules/attr-value-not-empty.js

This file was deleted.

49 changes: 0 additions & 49 deletions src/rules/attr-whitespace.js

This file was deleted.

40 changes: 0 additions & 40 deletions src/rules/inline-script-disabled.js

This file was deleted.

27 changes: 0 additions & 27 deletions src/rules/inline-style-disabled.js

This file was deleted.

26 changes: 0 additions & 26 deletions src/rules/spec-char-escape.js

This file was deleted.

91 changes: 0 additions & 91 deletions src/rules/tag-pair.js

This file was deleted.

24 changes: 0 additions & 24 deletions src/rules/tagname-lowercase.js

This file was deleted.

23 changes: 0 additions & 23 deletions src/rules/tagname-specialchars.js

This file was deleted.

202 changes: 0 additions & 202 deletions src/rules/tags-check.js

This file was deleted.

52 changes: 52 additions & 0 deletions test/cli/formatters/checkstyle.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const fs = require('fs')
const path = require('path')

describe('CLI', () => {
describe('Formatter: checkstyle', () => {
it('should have stdout output with formatter checkstyle', (done) => {
const expected = fs
.readFileSync(path.resolve(__dirname, 'checkstyle.xml'), 'utf8')
.replace(
'{{path}}',
path.resolve(__dirname, '../../html/executable.html')
)
// TODO: we need to fix windows backslash
.replace('html\\executable.html', 'html/executable.html')

const expectedParts = expected.split('\n')

ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
'--format',
'checkstyle',
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).not.to.equal('')

const stdoutParts = stdout.split('\n')

expect(stdoutParts.length).to.be.equal(expectedParts.length)

for (let i = 0; i < stdoutParts.length; i++) {
const lineIndicator = `[L${i + 1}]: `
expect(`${lineIndicator}${stdoutParts[i]}`).to.be.equal(
`${lineIndicator}${expectedParts[i]}`
)
}

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
97 changes: 97 additions & 0 deletions test/cli/formatters/checkstyle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="{{path}}">
<error line="8" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="8" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="8" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="9" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="9" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="9" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="10" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="10" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="10" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="11" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="11" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="11" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="12" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="12" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="12" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="13" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="13" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="13" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="14" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="14" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="14" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="15" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="15" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="15" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="16" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="16" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="16" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="17" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="17" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="17" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="18" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="18" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="18" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="19" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="19" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="19" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="20" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="20" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="20" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="21" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="21" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="21" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="22" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="22" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="22" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="23" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="23" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="23" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="24" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="24" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="24" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="25" column="22" severity="error" message="Tag must be paired, no start tag: [ &lt;/input&gt; ]" source="htmlhint.tag-pair"/>
<error line="26" column="3" severity="error" message="Special characters must be escaped : [ &lt; ]." source="htmlhint.spec-char-escape"/>
<error line="26" column="18" severity="error" message="Special characters must be escaped : [ &gt; ]." source="htmlhint.spec-char-escape"/>
<error line="28" column="11" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="29" column="9" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="30" column="7" severity="error" message="Tag must be paired, no start tag: [ &lt;/hello&gt; ]" source="htmlhint.tag-pair"/>
<error line="31" column="5" severity="error" message="Tag must be paired, no start tag: [ &lt;/test&gt; ]" source="htmlhint.tag-pair"/>
<error line="32" column="3" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="38" column="19" severity="error" message="The value of attribute [ class ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="38" column="28" severity="error" message="The value of attribute [ what ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="38" column="36" severity="error" message="The value of attribute [ something ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="44" column="3" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="50" column="19" severity="error" message="The value of attribute [ class ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="50" column="28" severity="error" message="The value of attribute [ what ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="50" column="36" severity="error" message="The value of attribute [ something ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="56" column="3" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="62" column="19" severity="error" message="The value of attribute [ class ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="62" column="28" severity="error" message="The value of attribute [ what ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="62" column="36" severity="error" message="The value of attribute [ something ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="68" column="3" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="74" column="19" severity="error" message="The value of attribute [ class ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="74" column="28" severity="error" message="The value of attribute [ what ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="74" column="36" severity="error" message="The value of attribute [ something ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="80" column="3" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="81" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="82" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="83" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="84" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="85" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="86" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="87" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="88" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="89" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="90" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="91" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="92" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="93" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="94" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="95" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="96" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="97" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
</file>
</checkstyle>
54 changes: 54 additions & 0 deletions test/cli/formatters/compact.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const fs = require('fs')
const path = require('path')

describe('CLI', () => {
describe('Formatter: compact', () => {
it('should have stdout output with formatter compact', (done) => {
const expected = fs
.readFileSync(path.resolve(__dirname, 'compact.txt'), 'utf8')
.replace(
/\{\{path\}\}/g,
path
.resolve(__dirname, '../../html/executable.html')
// TODO: we need to fix windows backslash
.replace('html\\executable.html', 'html/executable.html')
)
.replace(/\\u001b/g, '\u001b')

const expectedParts = expected.split('\n')

ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
'--format',
'compact',
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).not.to.equal('')

const stdoutParts = stdout.split('\n')

expect(stdoutParts.length).to.be.equal(expectedParts.length)

for (let i = 0; i < stdoutParts.length; i++) {
const lineIndicator = `[L${i + 1}]: `
expect(`${lineIndicator}${stdoutParts[i]}`).to.be.equal(
`${lineIndicator}${expectedParts[i]}`
)
}

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
94 changes: 94 additions & 0 deletions test/cli/formatters/compact.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{{path}}: line 8, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 8, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 8, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 9, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 9, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 9, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 10, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 10, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 10, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 11, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 11, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 11, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 12, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 12, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 12, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 13, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 13, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 13, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 14, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 14, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 14, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 15, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 15, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 15, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 16, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 16, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 16, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 17, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 17, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 17, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 18, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 18, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 18, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 19, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 19, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 19, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 20, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 20, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 20, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 21, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 21, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 21, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 22, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 22, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 22, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 23, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 23, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 23, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 24, col 7, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 24, col 14, error - The value of attribute [ bad ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 24, col 14, error - Duplicate of attribute name [ bad ] was found. (attr-no-duplication)
{{path}}: line 25, col 22, error - Tag must be paired, no start tag: [ </input> ] (tag-pair)
{{path}}: line 26, col 3, error - Special characters must be escaped : [ < ]. (spec-char-escape)
{{path}}: line 26, col 18, error - Special characters must be escaped : [ > ]. (spec-char-escape)
{{path}}: line 28, col 11, error - Tag must be paired, no start tag: [ </div> ] (tag-pair)
{{path}}: line 29, col 9, error - Tag must be paired, no start tag: [ </div> ] (tag-pair)
{{path}}: line 30, col 7, error - Tag must be paired, no start tag: [ </hello> ] (tag-pair)
{{path}}: line 31, col 5, error - Tag must be paired, no start tag: [ </test> ] (tag-pair)
{{path}}: line 32, col 3, error - Tag must be paired, no start tag: [ </div> ] (tag-pair)
{{path}}: line 38, col 19, error - The value of attribute [ class ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 38, col 28, error - The value of attribute [ what ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 38, col 36, error - The value of attribute [ something ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 44, col 3, error - Tag must be paired, no start tag: [ </div> ] (tag-pair)
{{path}}: line 50, col 19, error - The value of attribute [ class ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 50, col 28, error - The value of attribute [ what ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 50, col 36, error - The value of attribute [ something ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 56, col 3, error - Tag must be paired, no start tag: [ </div> ] (tag-pair)
{{path}}: line 62, col 19, error - The value of attribute [ class ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 62, col 28, error - The value of attribute [ what ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 62, col 36, error - The value of attribute [ something ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 68, col 3, error - Tag must be paired, no start tag: [ </div> ] (tag-pair)
{{path}}: line 74, col 19, error - The value of attribute [ class ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 74, col 28, error - The value of attribute [ what ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 74, col 36, error - The value of attribute [ something ] must be in double quotes. (attr-value-double-quotes)
{{path}}: line 80, col 3, error - Tag must be paired, no start tag: [ </div> ] (tag-pair)
{{path}}: line 81, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 82, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 83, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 84, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 85, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 86, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 87, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 88, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 89, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 90, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 91, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 92, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 93, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 94, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 95, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 96, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)
{{path}}: line 97, col 1, error - Tag must be paired, no start tag: [ </bad> ] (tag-pair)

\u001b[31m92 problems\u001b[39m
31 changes: 31 additions & 0 deletions test/cli/formatters/default.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const path = require('path')

describe('CLI', () => {
describe('Formatter: default', () => {
it('should have stdout output with formatter default', (done) => {
ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).to.contain(
'Tag must be paired, no start tag: [ </bad> ] (tag-pair)'
)
expect(stdout).to.contain('\u001b[31m')
expect(stdout).to.contain('1 files, found 92 errors in 1 files (')

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
1 change: 1 addition & 0 deletions test/cli/formatters/html.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><title>HTML Hint Violation Report</title></head><body><center><h2>Violation Report</h2></center><table border="1"><tr><th>Number#</th><th>File Name</th><th>Line Number</th><th>Message</th></tr><tr><td>1</td><td>{{path}}</td><td>8</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>2</td><td>{{path}}</td><td>8</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>3</td><td>{{path}}</td><td>8</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>4</td><td>{{path}}</td><td>9</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>5</td><td>{{path}}</td><td>9</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>6</td><td>{{path}}</td><td>9</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>7</td><td>{{path}}</td><td>10</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>8</td><td>{{path}}</td><td>10</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>9</td><td>{{path}}</td><td>10</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>10</td><td>{{path}}</td><td>11</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>11</td><td>{{path}}</td><td>11</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>12</td><td>{{path}}</td><td>11</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>13</td><td>{{path}}</td><td>12</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>14</td><td>{{path}}</td><td>12</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>15</td><td>{{path}}</td><td>12</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>16</td><td>{{path}}</td><td>13</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>17</td><td>{{path}}</td><td>13</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>18</td><td>{{path}}</td><td>13</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>19</td><td>{{path}}</td><td>14</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>20</td><td>{{path}}</td><td>14</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>21</td><td>{{path}}</td><td>14</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>22</td><td>{{path}}</td><td>15</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>23</td><td>{{path}}</td><td>15</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>24</td><td>{{path}}</td><td>15</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>25</td><td>{{path}}</td><td>16</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>26</td><td>{{path}}</td><td>16</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>27</td><td>{{path}}</td><td>16</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>28</td><td>{{path}}</td><td>17</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>29</td><td>{{path}}</td><td>17</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>30</td><td>{{path}}</td><td>17</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>31</td><td>{{path}}</td><td>18</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>32</td><td>{{path}}</td><td>18</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>33</td><td>{{path}}</td><td>18</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>34</td><td>{{path}}</td><td>19</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>35</td><td>{{path}}</td><td>19</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>36</td><td>{{path}}</td><td>19</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>37</td><td>{{path}}</td><td>20</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>38</td><td>{{path}}</td><td>20</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>39</td><td>{{path}}</td><td>20</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>40</td><td>{{path}}</td><td>21</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>41</td><td>{{path}}</td><td>21</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>42</td><td>{{path}}</td><td>21</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>43</td><td>{{path}}</td><td>22</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>44</td><td>{{path}}</td><td>22</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>45</td><td>{{path}}</td><td>22</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>46</td><td>{{path}}</td><td>23</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>47</td><td>{{path}}</td><td>23</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>48</td><td>{{path}}</td><td>23</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>49</td><td>{{path}}</td><td>24</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>50</td><td>{{path}}</td><td>24</td><td>The value of attribute [ bad ] must be in double quotes.</td></tr><tr><td>51</td><td>{{path}}</td><td>24</td><td>Duplicate of attribute name [ bad ] was found.</td></tr><tr><td>52</td><td>{{path}}</td><td>25</td><td>Tag must be paired, no start tag: [ </input> ]</td></tr><tr><td>53</td><td>{{path}}</td><td>26</td><td>Special characters must be escaped : [ < ].</td></tr><tr><td>54</td><td>{{path}}</td><td>26</td><td>Special characters must be escaped : [ > ].</td></tr><tr><td>55</td><td>{{path}}</td><td>28</td><td>Tag must be paired, no start tag: [ </div> ]</td></tr><tr><td>56</td><td>{{path}}</td><td>29</td><td>Tag must be paired, no start tag: [ </div> ]</td></tr><tr><td>57</td><td>{{path}}</td><td>30</td><td>Tag must be paired, no start tag: [ </hello> ]</td></tr><tr><td>58</td><td>{{path}}</td><td>31</td><td>Tag must be paired, no start tag: [ </test> ]</td></tr><tr><td>59</td><td>{{path}}</td><td>32</td><td>Tag must be paired, no start tag: [ </div> ]</td></tr><tr><td>60</td><td>{{path}}</td><td>38</td><td>The value of attribute [ class ] must be in double quotes.</td></tr><tr><td>61</td><td>{{path}}</td><td>38</td><td>The value of attribute [ what ] must be in double quotes.</td></tr><tr><td>62</td><td>{{path}}</td><td>38</td><td>The value of attribute [ something ] must be in double quotes.</td></tr><tr><td>63</td><td>{{path}}</td><td>44</td><td>Tag must be paired, no start tag: [ </div> ]</td></tr><tr><td>64</td><td>{{path}}</td><td>50</td><td>The value of attribute [ class ] must be in double quotes.</td></tr><tr><td>65</td><td>{{path}}</td><td>50</td><td>The value of attribute [ what ] must be in double quotes.</td></tr><tr><td>66</td><td>{{path}}</td><td>50</td><td>The value of attribute [ something ] must be in double quotes.</td></tr><tr><td>67</td><td>{{path}}</td><td>56</td><td>Tag must be paired, no start tag: [ </div> ]</td></tr><tr><td>68</td><td>{{path}}</td><td>62</td><td>The value of attribute [ class ] must be in double quotes.</td></tr><tr><td>69</td><td>{{path}}</td><td>62</td><td>The value of attribute [ what ] must be in double quotes.</td></tr><tr><td>70</td><td>{{path}}</td><td>62</td><td>The value of attribute [ something ] must be in double quotes.</td></tr><tr><td>71</td><td>{{path}}</td><td>68</td><td>Tag must be paired, no start tag: [ </div> ]</td></tr><tr><td>72</td><td>{{path}}</td><td>74</td><td>The value of attribute [ class ] must be in double quotes.</td></tr><tr><td>73</td><td>{{path}}</td><td>74</td><td>The value of attribute [ what ] must be in double quotes.</td></tr><tr><td>74</td><td>{{path}}</td><td>74</td><td>The value of attribute [ something ] must be in double quotes.</td></tr><tr><td>75</td><td>{{path}}</td><td>80</td><td>Tag must be paired, no start tag: [ </div> ]</td></tr><tr><td>76</td><td>{{path}}</td><td>81</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>77</td><td>{{path}}</td><td>82</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>78</td><td>{{path}}</td><td>83</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>79</td><td>{{path}}</td><td>84</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>80</td><td>{{path}}</td><td>85</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>81</td><td>{{path}}</td><td>86</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>82</td><td>{{path}}</td><td>87</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>83</td><td>{{path}}</td><td>88</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>84</td><td>{{path}}</td><td>89</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>85</td><td>{{path}}</td><td>90</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>86</td><td>{{path}}</td><td>91</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>87</td><td>{{path}}</td><td>92</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>88</td><td>{{path}}</td><td>93</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>89</td><td>{{path}}</td><td>94</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>90</td><td>{{path}}</td><td>95</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>91</td><td>{{path}}</td><td>96</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr><tr><td>92</td><td>{{path}}</td><td>97</td><td>Tag must be paired, no start tag: [ </bad> ]</td></tr></table></body></html>
53 changes: 53 additions & 0 deletions test/cli/formatters/html.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const fs = require('fs')
const path = require('path')

describe('CLI', () => {
describe('Formatter: html', () => {
it('should have stdout output with formatter html', (done) => {
const expected = fs
.readFileSync(path.resolve(__dirname, 'html.html'), 'utf8')
.replace(
/\{\{path\}\}/g,
path
.resolve(__dirname, '../../html/executable.html')
// TODO: we need to fix windows backslash
.replace('html\\executable.html', 'html/executable.html')
)

const expectedParts = expected.split('\n')

ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
'--format',
'html',
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).not.to.equal('')

const stdoutParts = stdout.split('\n')

expect(stdoutParts.length).to.be.equal(expectedParts.length)

for (let i = 0; i < stdoutParts.length; i++) {
const lineIndicator = `[L${i + 1}]: `
expect(`${lineIndicator}${stdoutParts[i]}`).to.be.equal(
`${lineIndicator}${expectedParts[i]}`
)
}

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
1,204 changes: 1,204 additions & 0 deletions test/cli/formatters/json.json

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions test/cli/formatters/json.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const fs = require('fs')
const path = require('path')

describe('CLI', () => {
describe('Formatter: json', () => {
it('should have stdout output with formatter json', (done) => {
const expectedFileContent = fs
.readFileSync(path.resolve(__dirname, 'json.json'), 'utf8')
.replace(
/\{\{path\}\}/g,
path
.resolve(__dirname, '../../html/executable.html')
.replace(/\\/g, '\\\\')
// TODO: we need to fix windows backslash
.replace('html\\\\executable.html', 'html/executable.html')
)

const expected = JSON.parse(expectedFileContent)

ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
'--format',
'json',
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).not.to.be.equal('')

const jsonStdout = JSON.parse(stdout)
expect(jsonStdout[0]).to.be.an('object')
expect(jsonStdout[0].file).to.contain('executable.html')

const stdoutMessages = jsonStdout[0].messages

expect(stdoutMessages).to.be.an(Array)
expect(stdoutMessages.length).to.be.equal(expected[0].messages.length)

for (let i = 0; i < stdoutMessages.length; i++) {
expect(stdoutMessages[i]).to.be.eql(expected[0].messages[i])
}

expect(jsonStdout[0].time).to.be.a('number')

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
34 changes: 34 additions & 0 deletions test/cli/formatters/junit.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const fs = require('fs')
const path = require('path')

describe('CLI', () => {
describe('Formatter: junit', () => {
it('should have stdout output with formatter junit', (done) => {
ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
'--format',
'junit',
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).to.contain('<?xml version="1.0" encoding="UTF-8"?>')
expect(stdout).to.contain('Found 92 errors')
expect(stdout).to.contain(
'^ Tag must be paired, no start tag: [ </bad> ] (tag-pair)'
)

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
34 changes: 34 additions & 0 deletions test/cli/formatters/markdown.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const path = require('path')

describe('CLI', () => {
describe('Formatter: markdown', () => {
it('should have stdout output with formatter markdown', (done) => {
ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
'--format',
'markdown',
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).to.contain('# TOC')
expect(stdout).to.contain('Found 92 errors, 0 warnings')
expect(stdout).to.contain('executable.html')
expect(stdout).to.contain(
'^ Tag must be paired, no start tag: [ </bad> ] (tag-pair)'
)

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
54 changes: 54 additions & 0 deletions test/cli/formatters/unix.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const expect = require('expect.js')

const ChildProcess = require('child_process')
const fs = require('fs')
const path = require('path')

describe('CLI', () => {
describe('Formatter: unix', () => {
it('should have stdout output with formatter unix', (done) => {
const expected = fs
.readFileSync(path.resolve(__dirname, 'unix.txt'), 'utf8')
.replace(
/\{\{path\}\}/g,
path
.resolve(__dirname, '../../html/executable.html')
// TODO: we need to fix windows backslash
.replace('html\\executable.html', 'html/executable.html')
)
.replace(/\\u001b/g, '\u001b')

const expectedParts = expected.split('\n')

ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../../../bin/htmlhint'),
path.resolve(__dirname, '../../html/executable.html'),
'--format',
'unix',
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)

expect(stdout).not.to.equal('')

const stdoutParts = stdout.split('\n')

expect(stdoutParts.length).to.be.equal(expectedParts.length)

for (let i = 0; i < stdoutParts.length; i++) {
const lineIndicator = `[L${i + 1}]: `
expect(`${lineIndicator}${stdoutParts[i]}`).to.be.equal(
`${lineIndicator}${expectedParts[i]}`
)
}

expect(stderr).to.be.equal('')
done()
}
)
})
})
})
94 changes: 94 additions & 0 deletions test/cli/formatters/unix.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{{path}}:8:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:8:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:8:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:9:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:9:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:9:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:10:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:10:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:10:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:11:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:11:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:11:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:12:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:12:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:12:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:13:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:13:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:13:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:14:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:14:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:14:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:15:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:15:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:15:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:16:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:16:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:16:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:17:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:17:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:17:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:18:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:18:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:18:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:19:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:19:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:19:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:20:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:20:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:20:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:21:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:21:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:21:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:22:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:22:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:22:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:23:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:23:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:23:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:24:7: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:24:14: The value of attribute [ bad ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:24:14: Duplicate of attribute name [ bad ] was found. [error/attr-no-duplication]
{{path}}:25:22: Tag must be paired, no start tag: [ </input> ] [error/tag-pair]
{{path}}:26:3: Special characters must be escaped : [ < ]. [error/spec-char-escape]
{{path}}:26:18: Special characters must be escaped : [ > ]. [error/spec-char-escape]
{{path}}:28:11: Tag must be paired, no start tag: [ </div> ] [error/tag-pair]
{{path}}:29:9: Tag must be paired, no start tag: [ </div> ] [error/tag-pair]
{{path}}:30:7: Tag must be paired, no start tag: [ </hello> ] [error/tag-pair]
{{path}}:31:5: Tag must be paired, no start tag: [ </test> ] [error/tag-pair]
{{path}}:32:3: Tag must be paired, no start tag: [ </div> ] [error/tag-pair]
{{path}}:38:19: The value of attribute [ class ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:38:28: The value of attribute [ what ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:38:36: The value of attribute [ something ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:44:3: Tag must be paired, no start tag: [ </div> ] [error/tag-pair]
{{path}}:50:19: The value of attribute [ class ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:50:28: The value of attribute [ what ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:50:36: The value of attribute [ something ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:56:3: Tag must be paired, no start tag: [ </div> ] [error/tag-pair]
{{path}}:62:19: The value of attribute [ class ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:62:28: The value of attribute [ what ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:62:36: The value of attribute [ something ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:68:3: Tag must be paired, no start tag: [ </div> ] [error/tag-pair]
{{path}}:74:19: The value of attribute [ class ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:74:28: The value of attribute [ what ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:74:36: The value of attribute [ something ] must be in double quotes. [error/attr-value-double-quotes]
{{path}}:80:3: Tag must be paired, no start tag: [ </div> ] [error/tag-pair]
{{path}}:81:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:82:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:83:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:84:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:85:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:86:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:87:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:88:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:89:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:90:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:91:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:92:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:93:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:94:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:95:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:96:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]
{{path}}:97:1: Tag must be paired, no start tag: [ </bad> ] [error/tag-pair]

\u001b[31m92 problems\u001b[39m
37 changes: 29 additions & 8 deletions test/core.spec.js
Original file line number Diff line number Diff line change
@@ -2,29 +2,30 @@ const expect = require('expect.js')

const HTMLHint = require('../dist/htmlhint.js').HTMLHint

describe('Core', function () {
it('Set false to rule no effected should result in an error', function () {
describe('Core', () => {
it('Set false to rule no effected should result in an error', () => {
const code = '<img src="test.gif" />'
const messages = HTMLHint.verify(code, { 'alt-require': false })
expect(messages.length).to.be(0)
})

it('Not load default ruleset when use undefined ruleset should result in an error', function () {
it('Not load default ruleset when use undefined ruleset should result in an error', () => {
const code =
'<P ATTR=\'1\' id="a">><div id="a"><img src="" a="1" a="2"/></div>'
const messages = HTMLHint.verify(code)
expect(messages.length).to.be(9)
})

it('Not load default ruleset when use empty ruleset should result in an error', function () {
it('Not load default ruleset when use empty ruleset should result in an error', () => {
const code =
'<P ATTR=\'1\' id="a">><div id="a"><img src="" a="1" a="2"/></div>'
const messages = HTMLHint.verify(code, {})
expect(messages.length).to.be(9)
})

it('Inline ruleset not worked should result in an error', function () {
let code = '<!-- htmlhint alt-require:true-->\r\n<img src="test.gif" />'
it('Inline ruleset not worked should result in an error', () => {
// With value = 'true'
let code = '<!-- htmlhint alt-require:true -->\r\n<img src="test.gif" />'
let messages = HTMLHint.verify(code, {
'alt-require': false,
})
@@ -34,14 +35,34 @@ describe('Core', function () {
expect(messages[0].line).to.be(2)
expect(messages[0].col).to.be(5)

code = '<!-- htmlhint alt-require:false-->\r\n<img src="test.gif" />'
// Without value
code = '<!-- htmlhint alt-require -->\r\n<img src="test.gif" />'
messages = HTMLHint.verify(code, {
'alt-require': false,
})

expect(messages.length).to.be(1)
expect(messages[0].rule.id).to.be('alt-require')
expect(messages[0].line).to.be(2)
expect(messages[0].col).to.be(5)

// With value = 'false'
code = '<!-- htmlhint alt-require:false -->\r\n<img src="test.gif" />'
messages = HTMLHint.verify(code, {
'alt-require': true,
})
expect(messages.length).to.be(0)

// Without rule
code = '<!-- htmlhint -->\r\n<img src="test.gif" />'
messages = HTMLHint.verify(code, {
'alt-require': false,
})

expect(messages.length).to.be(0)
})

it('Show formated result should not result in an error', function () {
it('Show formated result should not result in an error', () => {
const code =
'tttttttttttttttttttttttttttttttttttt<div>中文<img src="test.gif" />tttttttttttttttttttttttttttttttttttttttttttttt'
const messages = HTMLHint.verify(code, {
34 changes: 32 additions & 2 deletions test/executable.spec.js
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@ const expect = require('expect.js')
const ChildProcess = require('child_process')
const path = require('path')

describe('Executable', function () {
it('should close stream before exit', function (done) {
describe('Executable', () => {
it('should close stream before exit', (done) => {
const c = ChildProcess.spawn('node', [
path.resolve(__dirname, '../bin/htmlhint'),
'--format',
@@ -36,4 +36,34 @@ describe('Executable', function () {
expect(stdoutEnd || processEnd).to.be(false)
})
})

for (const format of [
'checkstyle',
'compact',
'default',
'html',
'json',
'junit',
'markdown',
'unix',
]) {
it(`should have stdout output with formatter ${format}`, (done) => {
ChildProcess.exec(
[
'node',
path.resolve(__dirname, '../bin/htmlhint'),
path.resolve(__dirname, './html/executable.html'),
'--format',
format,
].join(' '),
(error, stdout, stderr) => {
expect(error).to.be.an('object')
expect(error.code).to.be.equal(1)
expect(stdout).not.to.be.equal('')
expect(stderr).to.be.equal('')
done()
}
)
})
}
})
63 changes: 31 additions & 32 deletions test/htmlparser.spec.js
Original file line number Diff line number Diff line change
@@ -3,8 +3,7 @@ const expect = require('expect.js')
const HTMLParser = require('../dist/htmlhint.js').HTMLParser

expect.Assertion.prototype.event = function (type, attr) {
const self = this
const obj = self.obj
const obj = this.obj

if (attr !== undefined) {
attr.type = type
@@ -15,7 +14,7 @@ expect.Assertion.prototype.event = function (type, attr) {
attr = type
}
}
self.assert(
this.assert(
eqlEvent(obj, attr),
() =>
`expected "${JSON.stringify(obj)}" to event "${JSON.stringify(attr)}"`,
@@ -42,8 +41,8 @@ function getAllEvents(parser, arrEvents, callback) {
})
}

describe('HTMLParser: Base parse', function () {
it('should parse html code1', function (done) {
describe('HTMLParser: Base parse', () => {
it('should parse html code1', (done) => {
const parser = new HTMLParser()
const arrEvents = []
const targetEvents = [
@@ -234,8 +233,8 @@ describe('HTMLParser: Base parse', function () {
})
})

describe('HTMLParser: Object parse', function () {
it('should parse doctype: HTML Strict DTD', function (done) {
describe('HTMLParser: Object parse', () => {
it('should parse doctype: HTML Strict DTD', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -251,7 +250,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse doctype: HTML Transitional DTD', function (done) {
it('should parse doctype: HTML Transitional DTD', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -267,7 +266,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse doctype: HTML Frameset DTD', function (done) {
it('should parse doctype: HTML Frameset DTD', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -283,7 +282,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse doctype: XHTML 1.0 Strict', function (done) {
it('should parse doctype: XHTML 1.0 Strict', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -299,7 +298,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse doctype: XHTML 1.0 Transitional', function (done) {
it('should parse doctype: XHTML 1.0 Transitional', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -315,7 +314,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse doctype: XHTML 1.0 Frameset', function (done) {
it('should parse doctype: XHTML 1.0 Frameset', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -331,7 +330,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse doctype: XHTML 1.1', function (done) {
it('should parse doctype: XHTML 1.1', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -347,7 +346,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse doctype: html5', function (done) {
it('should parse doctype: html5', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -360,7 +359,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<!DOCTYPE HTML>')
})

it('should parse start tag: <p>', function (done) {
it('should parse start tag: <p>', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -372,7 +371,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<p>')
})

it('should not parse start tag: <div class"foo">', function (done) {
it('should not parse start tag: <div class"foo">', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -384,7 +383,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<div class"foo">')
})

it('should not parse start tag: <div class="foo>', function (done) {
it('should not parse start tag: <div class="foo>', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -396,7 +395,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<div class="foo>')
})

it('should not parse start tag: <div class=foo">', function (done) {
it('should not parse start tag: <div class=foo">', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -408,7 +407,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<div class=foo">')
})

it('should not parse start tag: <div class="foo"">', function (done) {
it('should not parse start tag: <div class="foo"">', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -420,7 +419,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<div class="foo"">')
})

it('should not parse start tag: <div class="foo""><span">', function (done) {
it('should not parse start tag: <div class="foo""><span">', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -432,7 +431,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<div class="foo""><span">')
})

it('should not parse start tag: <div class="foo""><a><span">', function (done) {
it('should not parse start tag: <div class="foo""><a><span">', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -444,7 +443,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<div class="foo""><a><span">')
})

it('should parse tag attrs', function (done) {
it('should parse tag attrs', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -476,7 +475,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<img width="200" height=\'300\' alt=abc a.b=ccc c*d=ddd>')
})

it('should parse end tag', function (done) {
it('should parse end tag', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -488,7 +487,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('</p>')
})

it('should parse selfclose tag', function (done) {
it('should parse selfclose tag', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -501,7 +500,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<br />')
})

it('should parse text', function (done) {
it('should parse text', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -513,7 +512,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<span>abc</span>')
})

it('should parse text in last', function (done) {
it('should parse text in last', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -525,7 +524,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<p>bbb')
})

it('should parse comment', function (done) {
it('should parse comment', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -538,7 +537,7 @@ describe('HTMLParser: Object parse', function () {
parser.parse('<!--comment\r\ntest-->')
})

it('should parse cdata: script', function (done) {
it('should parse cdata: script', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -564,7 +563,7 @@ describe('HTMLParser: Object parse', function () {
)
})

it('should parse cdata: style', function (done) {
it('should parse cdata: style', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -588,7 +587,7 @@ describe('HTMLParser: Object parse', function () {
})

describe('HTMLParser: Case parse', () => {
it('should parse special end tag', function (done) {
it('should parse special end tag', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -600,7 +599,7 @@ describe('HTMLParser: Case parse', () => {
parser.parse('</p >')
})

it('should parse special no quotes tags', function (done) {
it('should parse special no quotes tags', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
@@ -615,7 +614,7 @@ describe('HTMLParser: Case parse', () => {
parser.parse('<link rel=icon /><link rel=icon />')
})

it('should parse special empty attr', function (done) {
it('should parse special empty attr', (done) => {
const parser = new HTMLParser()
const arrEvents = []
getAllEvents(parser, arrEvents, () => {
28 changes: 14 additions & 14 deletions test/rules/alt-require.spec.js
Original file line number Diff line number Diff line change
@@ -7,20 +7,20 @@ const ruleOptions = {}

ruleOptions[ruldId] = true

describe(`Rules: ${ruldId}`, function () {
it('Img tag have empty alt attribute should not result in an error', function () {
describe(`Rules: ${ruldId}`, () => {
it('Img tag have empty alt attribute should not result in an error', () => {
const code = '<img width="200" height="300" alt="">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Img tag have non empty alt attribute should not result in an error', function () {
it('Img tag have non empty alt attribute should not result in an error', () => {
const code = '<img width="200" height="300" alt="test">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Img tag have not alt attribute should result in an error', function () {
it('Img tag have not alt attribute should result in an error', () => {
const code = '<img width="200" height="300">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(1)
@@ -31,19 +31,19 @@ describe(`Rules: ${ruldId}`, function () {
})

/* A tag can have shape and coords attributes and not have alt attribute */
it('A tag have not alt attribute should not result in an error', function () {
it('A tag have not alt attribute should not result in an error', () => {
const code = '<a>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Area tag have not href and alt attributes should not result in an error', function () {
it('Area tag have not href and alt attributes should not result in an error', () => {
const code = '<area>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Area[href] tag have not alt attribute should result in an error', function () {
it('Area[href] tag have not alt attribute should result in an error', () => {
const code = '<area href="#test">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(1)
@@ -53,7 +53,7 @@ describe(`Rules: ${ruldId}`, function () {
expect(messages[0].type).to.be('warning')
})

it('Area[href] tag have empty alt attribute should result in an error', function () {
it('Area[href] tag have empty alt attribute should result in an error', () => {
const code = '<area href="#test" alt="">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(1)
@@ -63,25 +63,25 @@ describe(`Rules: ${ruldId}`, function () {
expect(messages[0].type).to.be('warning')
})

it('Area[href] tag have non emtpy alt attribute should not result in an error', function () {
it('Area[href] tag have non emtpy alt attribute should not result in an error', () => {
const code = '<area href="#test" alt="test">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Input tag have not type and alt attributes should not result in an error', function () {
it('Input tag have not type and alt attributes should not result in an error', () => {
const code = '<input>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Input[type="text"] tag have not alt attribute should not result in an error', function () {
it('Input[type="text"] tag have not alt attribute should not result in an error', () => {
const code = '<input type="text">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Input[type="image"] tag have not alt attribute should result in an error', function () {
it('Input[type="image"] tag have not alt attribute should result in an error', () => {
const code = '<input type="image">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(1)
@@ -91,7 +91,7 @@ describe(`Rules: ${ruldId}`, function () {
expect(messages[0].type).to.be('warning')
})

it('Input[type="image"] tag have empty alt attribute should result in an error', function () {
it('Input[type="image"] tag have empty alt attribute should result in an error', () => {
const code = '<input type="image" alt="">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(1)
@@ -101,7 +101,7 @@ describe(`Rules: ${ruldId}`, function () {
expect(messages[0].type).to.be('warning')
})

it('Input[type="image"] tag have non emtpy alt attribute should not result in an error', function () {
it('Input[type="image"] tag have non emtpy alt attribute should not result in an error', () => {
const code = '<input type="image" alt="test">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
14 changes: 7 additions & 7 deletions test/rules/attr-lowercase.spec.js
Original file line number Diff line number Diff line change
@@ -7,8 +7,8 @@ const ruleOptions = {}

ruleOptions[ruldId] = true

describe(`Rules: ${ruldId}`, function () {
it('Not all lowercase attr should result in an error', function () {
describe(`Rules: ${ruldId}`, () => {
it('Not all lowercase attr should result in an error', () => {
let code = '<p TEST="abc">'
let messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(1)
@@ -27,34 +27,34 @@ describe(`Rules: ${ruldId}`, function () {
expect(messages[1].col).to.be(13)
})

it('Lowercase attr should not result in an error', function () {
it('Lowercase attr should not result in an error', () => {
const code = '<p test="abc">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Set is false should not result in an error', function () {
it('Set is false should not result in an error', () => {
const code = '<p TEST="abc">'
ruleOptions[ruldId] = false
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Set to array list should not result in an error', function () {
it('Set to array list should not result in an error', () => {
const code = '<p testBox="abc" tttAAA="ccc">'
ruleOptions[ruldId] = ['testBox', 'tttAAA']
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Set to array list with RegExp should not result in an error', function () {
it('Set to array list with RegExp should not result in an error', () => {
const code = '<p testBox="abc" bind:tapTop="ccc">'
ruleOptions[ruldId] = ['testBox', /bind:.*/]
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
})

it('Set to array list with regex string should not result in an error', function () {
it('Set to array list with regex string should not result in an error', () => {
const code = '<p testBox="abc" [ngFor]="ccc">'
ruleOptions[ruldId] = ['testBox', '/\\[.*\\]/']
const messages = HTMLHint.verify(code, ruleOptions)
6 changes: 3 additions & 3 deletions test/rules/attr-no-duplication.spec.js
Original file line number Diff line number Diff line change
@@ -7,8 +7,8 @@ const ruleOptions = {}

ruleOptions[ruldId] = true

describe(`Rules: ${ruldId}`, function () {
it('Attribute name been duplication should result in an error', function () {
describe(`Rules: ${ruldId}`, () => {
it('Attribute name been duplication should result in an error', () => {
const code = '<a href="a" href="b">bbb</a>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(1)
@@ -17,7 +17,7 @@ describe(`Rules: ${ruldId}`, function () {
expect(messages[0].col).to.be(12)
})

it('Attribute name not been duplication should not result in an error', function () {
it('Attribute name not been duplication should not result in an error', () => {
const code = '<a href="a">bbb</a>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).to.be(0)
31 changes: 0 additions & 31 deletions test/rules/attr-no-unnecessary-whitespace.js

This file was deleted.

33 changes: 33 additions & 0 deletions test/rules/attr-no-unnecessary-whitespace.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const expect = require('expect.js')

const HTMLHint = require('../../dist/htmlhint.js').HTMLHint

const ruldId = 'attr-no-unnecessary-whitespace'
const ruleOptions = {}

ruleOptions[ruldId] = true

describe(`Rules: ${ruldId}`, () => {
it('Attribute with spaces should result in an error', () => {
const codes = [
'<div title = "a" />',
'<div title= "a" />',
'<div title ="a" />',
]
for (let i = 0; i < codes.length; i++) {
const messages = HTMLHint.verify(codes[i], ruleOptions)
expect(messages.length).to.be(1)
expect(messages[0].rule.id).to.be(ruldId)
expect(messages[0].line).to.be(1)
expect(messages[0].col).to.be(5)
}
})

it('Attribute without spaces should not result in an error', () => {
const codes = ['<div title="a" />', '<div title="a = a" />']
for (let i = 0; i < codes.length; i++) {
const messages = HTMLHint.verify(codes[i], ruleOptions)
expect(messages.length).to.be(0)
}
})
})
8 changes: 4 additions & 4 deletions test/rules/attr-sort.spec.js
8 changes: 4 additions & 4 deletions test/rules/attr-unsafe-chars.spec.js
6 changes: 3 additions & 3 deletions test/rules/attr-value-double-quotes.spec.js
8 changes: 4 additions & 4 deletions test/rules/attr-value-not-empty.spec.js
14 changes: 7 additions & 7 deletions test/rules/attr-value-single-quotes.spec.js
20 changes: 10 additions & 10 deletions test/rules/attr-whitespace.spec.js
4 changes: 2 additions & 2 deletions test/rules/default.spec.js
6 changes: 3 additions & 3 deletions test/rules/doctype-first.spec.js
6 changes: 3 additions & 3 deletions test/rules/doctype-html5.spec.js
12 changes: 6 additions & 6 deletions test/rules/head-require.spec.js
12 changes: 6 additions & 6 deletions test/rules/head-script-disabled.spec.js
10 changes: 5 additions & 5 deletions test/rules/href-abs-or-rel.spec.js
8 changes: 4 additions & 4 deletions test/rules/id-class-ad-disabled.spec.js
64 changes: 32 additions & 32 deletions test/rules/id-class-value.spec.js
6 changes: 3 additions & 3 deletions test/rules/id-unique.spec.js
8 changes: 4 additions & 4 deletions test/rules/inline-script-disabled.spec.js
4 changes: 2 additions & 2 deletions test/rules/inline-style-disabled.spec.js
44 changes: 23 additions & 21 deletions test/rules/input-requires-label.spec.js
26 changes: 15 additions & 11 deletions test/rules/script-disabled.spec.js
24 changes: 12 additions & 12 deletions test/rules/space-tab-mixed-disabled.spec.js
34 changes: 17 additions & 17 deletions test/rules/spec-char-escape.spec.js
8 changes: 4 additions & 4 deletions test/rules/src-not-empty.spec.js
6 changes: 3 additions & 3 deletions test/rules/style-disabled.spec.js
8 changes: 4 additions & 4 deletions test/rules/tag-pair.spec.js
6 changes: 3 additions & 3 deletions test/rules/tag-self-close.spec.js
6 changes: 3 additions & 3 deletions test/rules/tagname-lowercase.spec.js
14 changes: 7 additions & 7 deletions test/rules/tagname-specialchars.spec.js
44 changes: 22 additions & 22 deletions test/rules/tags-check.spec.js
10 changes: 5 additions & 5 deletions test/rules/title-require.spec.js
7 changes: 7 additions & 0 deletions tsconfig.cli.json
4 changes: 4 additions & 0 deletions tsconfig.core.json
15 changes: 15 additions & 0 deletions tsconfig.json
14 changes: 14 additions & 0 deletions tsconfig.lint.json
24 changes: 16 additions & 8 deletions website/docusaurus.config.js
2 changes: 2 additions & 0 deletions website/package.json