Code Monkey home page Code Monkey logo

layout-linter's Introduction

Layout-Linter CircleCI

Create your own custom rules, using CSS selectors, to lint an HTML page and make sure it follows your HTML/CSS conventions (classes, attributes, tags, structure etc.)

Screenshot Of A Linted Page

demo screenshot

How To Use

Install The Linter In Your App

go to your app's folder and run npm i layout-linter

Set Up Your Linting Rules

  • create a file containing your own custom linting rules as a json
  • these linting rules are based on CSS selectors (read more about them here), and can be as simple as the name of a tag (e.g. div) or very elaborate.
  • here is a guide on how to set up the contents of your json file:
{
  "rules": [{
    "selector": /*
                  a CSS selector indicating the element(s) that will be tested
                  as to whether they adhere to the rules below,

                  e.g. ".some-class"
                */

    "is": /*
            (rule)

            a tag name indicating what type the element must be,

            e.g. "img"
          */,

    "direct": /*
                (rule)

                an array of CSS selectors indicating direct children the element must contain,

                e.g. ["#id-x", ".class-b"]
              */,

    "contains": /*
                  (rule)

                  an array of CSS selectors indicating children (direct or indirect, it doesn't matter)
                  the element must contain,

                  e.g. ["#id-x", ".class-b"]
                */,

    "attrs": /*
              (rule)

              an array indicating attributes the element must have,

              e.g. ['attr-a', 'attr-b="xx"']
            */,

    "parent": /*
                (rule)

                a CSS selector indicating a direct parent that the linted element must have

                e.g. '.some-parent'
              */,

    "parents": /*
                (rule)

                an array of CSS selectors indicating a set of (direct or indirect) parents that the linted element must have

                e.g. ['.some-parent-a', '.some-parent-b']
              */,

     "siblings": /*
                (rule)

                an array of CSS selectors indicating a set of siblings that the linted element must have

                e.g. ['.some-sibling-a', '.some-sibling-b']
              */,

    "not": {

      /*
        any of the above rules (is, direct, contains, attrs, parent, parents, siblings), wrapped inside this `not`,
        indicating that these must NOT be true this time
      */

    }

  }, {

    "selector": "....",
    /* rule */,
    /* rule */,
    .....
    ..

  }]
}

Using The Linting Function

  • require the linting function anywhere your like by doing const lintLayout = require('layout-linter');
  • use the function as follows:
const lintLayout = require('layout-linter');

let result = lintLayout({


  /*
    - optional
    - [String]
    - an absolute (or relative to the curret directory) path to a json file
      containing the linting rules
    - if omitted, the linter will look for a `.layoutrc` file in your app's
      root folder only (it will not look for it in any of its subfolders)
  */

  layoutrc: '/some/custom/rules.config',



  /*
    - optional
    - refers to the CSS that will be used to style the linting errors that
      will appear in the linted HTML
    - [String]
    - an absolute (or relative to the curret directory) path to a .css file
    - will use the default CSS (provided by layout-linter internally) if omitted
    - can alse be set to false, in which case no CSS will be used
  */

  css: '/some/custom.css',




  /*
    - mandatory
    - [String]
    - an absolute (or relative to the curret directory) path to the .html file you want to lint
    - OR a valid HTML string (e.g. "<div>....</div>" or "<html>....</html>" etc..) that you want to lint
  */

  source: './source.html',
;



  /*
    - optional
    - [Boolean]
    - the `html` key, of the object returned by this function, will always consist of a complete HTML document,
      as a string, whether the function is passed an HTML fragment or a complete HTML document
    - if the function is passed an HTML fragment and this property is set to true,
      the `html` key, of the object returned by this function, will consist of the linted HTML fragment (as a string)
      and not of a complete HTML document containing that fragment
    - if the function is passed a complete HTML document setting this property to true or false will have no effect
  */

  fragment: true


});

The function will return an object containing the following keys:

{
  html: /* the linted HTML as a string */,

  errors: /* the total number of errors found in HTML */,

  log: /*
        an array of objects, each object consisting of:
          - a string, indicating the problematic element
          - an array of error messages for that element

        e.g.  [{
                element: '<p class="this"></p>',
                errors: ['this element has this error', 'this element has this other error', ...]
              }, {
                element: '<div class="that"></div>',
                errors: ['that element has this error', 'that element has this other error', ...]
              }]
       */
}

How To Contribute

git clone [email protected]:peopledoc/layout-linter.git
cd ./layout-linter/
make install

Read how to write tests for your rules.

How To Try The Demo

In layout-linter/ project directory, run:

make run-demo

You will see an HTML page linted using the following set of rules (based on the Bootstrap framework HTML rules).

โ„น๏ธ You can use DEBUG=true to show more information.

DEBUG=true make run-demo

How To Run Tests

In layout-linter/ project directory, run:

make test

License

This project is licensed under the MIT License.

layout-linter's People

Contributors

edouard-lopez avatar pixelik avatar saintsebastian avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

layout-linter's Issues

generate warning-rules automatically out of "good" HTML files

some people might feel that writing their own custom rules is too much work ๐Ÿคทโ€โ™‚๏ธ

why not generate rules automatically using a correct HTML file as a reference?

  1. parse an HTML file that already abides by the correct layout structure
  2. generate rules automatically according to what the parser comes across in the above file
  3. these "rules" cannot become "breaking" rules as there could be exceptions to them and the parser can't be intelligent enough to figure that out, however it could generate helpful warning-rules that could then be used by the linter in order to lint other files and produce "warnings" (in the terminal and in the linter-HTML file)

for example, given the HTML below:

<div class="row">
  <div class="row__column">
    <button class="button"></button>
  </div>
  <div class="row__column"></div>
</div>

the parser could automatically generate the following warnings:

[
  {
    "selector": ".row",
    "direct": [".row__column"]
  },
  {
    "selector": ".row__column",
    "siblings": [".row__column"]
  },
  {
    "selector": "button",
    "attrs": ["class='button'"]
  }
]

ping @saintsebastian @xcambar

Where/How should the linting rules be declared

It could be a .jsonthat looks something like this:

{
  selector: '.some-selector-1',
  contains: ['h1', 'h2'],
  notContains: ['div'],
  attr: ['alt'],
  notAttr: ['href']
},
{
  selector: '.some-selector-2',
  contains: ['h1', 'h2'],
  notContains: ['div'],
  attr: ['alt'],
  notAttr: ['href']
},
...etc.

I can't think of any other rules other than those four:

  • contains
  • notContains
  • attr
  • notAttr

but should we be even more explicit like :

  • contains
  • notContains
  • containsDirect
  • notContainsDirect
  • attr
  • notAttr

?

rule: <img> has @alt attribute

This will help improve accessibility.

Can be either implemented

As an instance of hasAttributes rule:

{
  selector: 'img',
  hasAttributes: 'alt'
}
  • pros:
    • reuse existing rule ;
    • so just config ;
    • can be part of a preset.
  • cons:
    • up to the user to add it

By itself

{
  selector: 'img',
  imageIsAccessible: true
}
  • pros:
    • allow more complex logic
  • cons:
    • what does accessible mean? It might lead to a complex rule that overlap with other.

fix tooltips positioning method

use

(function() {
  var w = window,
    d = document,
    e = d.documentElement,
    g = d.getElementsByTagName('body')[0],
    windowWidth = w.innerWidth || e.clientWidth || g.clientWidth,
    windowHeight = w.innerHeight|| e.clientHeight|| g.clientHeight;

  var tooltipMaxWidth = 240;
  var tooltipMaxHeight = 100;

  var tooltipClassName = 'layout-linter-tooltip';
  var tooltipTargetAttrName = 'layout-linter-tooltip-id';

  function positionTooltips() {
    document.querySelectorAll('.'+tooltipClassName).forEach(function(tooltip) {
      var el = document.querySelector('['+tooltipTargetAttrName+'="'+tooltip.id+'"]');
      var de = document.documentElement;
      var box = el.getBoundingClientRect();
      var top = box.top + window.pageYOffset - de.clientTop;
      var left = box.left + window.pageXOffset - de.clientLeft;
      if (top < 10) {
        top = 10; // negative top used in CSS
      }
      if (top + tooltipMaxHeight > windowHeight) {
        top = windowHeight - tooltipMaxHeight;
      }

      // use diff method to account for horizontal overflow
      // if (left + tooltipMaxWidth > windowWidth) {
      //   left = windowWidth - tooltipMaxWidth;
      // }

      tooltip.style.top = top + 'px';
      tooltip.style.left = left + 'px';
    });
  };

  window.addEventListener('resize', function() {
    positionTooltips();
  });

  positionTooltips();

})();

to position tooltips

Runing demo fail

Hi @Pixelik,
Glad to see you are still working on the project โค๏ธ

I'm trying to run the demo and had some issues. I'm on Linux Mint so some are probably related to that.

โฏ make run-demo                                                                                                                                                                                                                                               
clear                                                                                                                                                                                                                                                         
node ./demo/build                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
========= LAYOUT-LINTER | DEMO (start) =========                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
LAYOUT-LINTER | errors found =  10                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
LAYOUT-LINTER | log (see below)                                                                                                                                                                                                                               
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
[ { element: '<div class="list-group mb-3"></div>',                                                                                                                                                                                                           
    errors:                                                                                                                                                                                                                                                   
     [ 'tag must be a &lt;ul&gt;',                                                                                                                                                                                                                            
       'element must not contain this direct child: li:not(.list-group-item)' ] },                                                                                                                                                                            
  { element: '<p class="input-group"></p>',                                                                                                                                                                                                                       errors: [ 'tag must be a &lt;div&gt;' ] },                                                                                                                                                                                                                
  { element: '<div class="input-group"></div>',                                                                                                                                                                                                               
    errors: [ 'element must contain this direct child: input' ] },                                                                                                                                                                                            
  { element: '<div class="input-group-prepend"></div>',                                                                                                                                                                                                       
    errors: [ 'element must have this direct parent: .input-group' ] },                                                                                                                                                                                       
  { element: '<input type="text" class="form-control" id="email" placeholder="[email protected]">',                                                                                                                                                             
    errors: [ 'element must have this attribute: type=\'email\'' ] },                                                                                                                                                                                         
  { element: '<input type="text" class="form-control" id="address" required="">',                                                                                                                                                                             
    errors: [ 'element must have this attribute: placeholder' ] },                                                                                                                                                                                            
  { element: '<input type="text" class="form-control" id="zip">',                                                                                                                                                                                             
    errors: [ 'element must have this attribute: placeholder' ] },                                                                                                                                                                                            
  { element: '<input type="text" class="form-control" placeholder="Promo code">',                                                                                                                                                                                 errors: [ 'element must have this sibling: label' ] },                                                                                                                                                                                                    
  { element: '<a></a>',                                                                                                                                                                                                                                       
    errors: [ 'element must have this attribute: href' ] } ]                                                                                                                                                                                                  
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
=========  LAYOUT-LINTER | DEMO (end)  =========                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                              
open -na "Google Chrome" --args --incognito http://127.0.0.1:8080/                                                                                                                                                                                            
open: invalid option -- 'n'                                                                                                                                                                                                                                   
Usage: open [OPTIONS] -- command

Debug mode

The help is overwhelming the output, you could use an environment variable to trigger this display:

+ if (!!process.env.DEBUG === true) {
  /* eslint-disable no-console */
  console.log("\n\n\n");
  console.log("========= LAYOUT-LINTER | DEMO (start) =========");
  โ€ฆ
  console.log("=========  LAYOUT-LINTER | DEMO (end)  =========");
  console.log("\n\n\n");
+}

Default behavior won't show the help, if you want it then do:

DEBUG=true make run-demo

Browser to launch

open is not available same on Linux. Here is a simple solution to support various OS/Browser:

+ OPEN_CHROMIUM_BROWSER:=chromium-browser --incognito http://127.0.0.1:8080/ 2>/dev/null
+ OPEN_CHROME_BROWSER:=open -na "Google Chrome" --args --incognito http://127.0.0.1:8080/ 2>/dev/null
+ OPEN_FIREFOX_BROWSER:=firefox --private-window http://127.0.0.1:8080/ 2>/dev/null

run-demo:
	clear
	node ./demo/build
-	open -na "Google Chrome" --args --incognito http://127.0.0.1:8080/
+ 	${OPEN_CHROMIUM_BROWSER} || ${OPEN_FIREFOX_BROWSER} || ${OPEN_CHROME_BROWSER} || echo 'No Browser available'
	http-server

Missing dev-dep

make: http-server: Command not found

Just install it as dev-dep:

npm install  --save-dev  http-server 
      "eslint": "^4.19.1",
+    "http-server": "^0.11.1",
      "husky": "^1.1.2",

Also, you can start it before opening the browser by sending it to background:

-	http-server
+	http-server &
	${OPEN_CHROMIUM_BROWSER} || ${OPEN_FIREFOX_BROWSER} || ${OPEN_CHROME_BROWSER} || echo 'No Browser available'

fix open tooltip alignment

some tooltips are not aligned correctly when opened, probably depending on the type/place of their target element

current state of layout-linter

  1. README contains all the information on how layout-linter is currently set up to work
  2. testing.md contains information about how testing is set up

layout-linter is not ready for production yet because most tests are missing.

I will resume writing tests (and optimising the overall code if needed) only after the current state of the module has been approved by @peopledoc/tribe-js.

By approved I mean:

We're aiming for an MVP (which is what the module is currently set up to do and nothing more) so I'm not looking for suggestions regarding enhancements, optimizations etc. (these will come later) but rather suggestions of the type "it must not go into production like this, this thing has the be changed into this, hold on from finalising code/tests unless this is changed into that etc..".

Thanks! ๐Ÿ™‚

Allow snippets

Currently expects only fully formed js. Since app accepts plainttext HTML snippets would be great, external module can be used to verify that js if valid before linting

Use as an executable

It'd be more portable to be able to use layout-linter as an executable:

$ layout-linter -i path/to/file.html

Interesting options:

  • select the file(s) to analyse
  • lint from STDIN instead of a file
  • select the rules' file
  • Select the type of output (human readable, json...)

Accept YAML and JSON

YAML is a less verbose file format than JSON and is becoming more and more popular.

It could be an option to support it.

Idea: accepts rules in a folder

In case the rules file become too large, it could be an option to merge every file in a folder as a set of rules.

.layout-linter.d/
  |_ layout.json
  |_ header.json
  |_ sidebar.json
  |_ card.json
  |_ ...

The name of the folder is left as an option to the user (with a default path)

Name rules and reference rules

We can build a more comprehensive set of rules if we can reference them from one place to the other.

As an example, one could have a @primary-button rule, as in:

{
  rule: 'primary-button',
  selector: 'button',
  is: '.button.button--primary'
}

and any other rule could use this rule to declare that a primary-button should be used in place. An example with a navigation:

{
  rule: 'main-navigation',
  selector: 'nav.main',
  direct: ['@primary-button']
}

... so as to compose rules and layouts, just as we do with components in the application code ;)

make search for .layoutrc more efficient

  1. only check for .layoutrc in root app folder: remove this check
  2. do not check for .layout across all folders under the root app folder as this is too heavy
  3. only check for .layout in the root app folder itself or in a path specified by the user

catch fs errors (like fs.statSync ENAMETOOLONG)

Tweet announcement

Suggestion:
We just released layout-linter, that will analyze your html and create visual feedback for it, based on your own html linting rules!
add screenshot

More examples

refs #40

I keep thinking that people will understand better what's at stake if they have examples to analyze.

The GIF makes a great job at the end result, but it doesn't guide in the process/implementation. Maybe the Bootstrap example is a bit too big to chew on for starters and smaller, more focused examples would help.

What's your take on this?

Run in browser

If that hasn't already been discussed, how about making the code run in browsers so that the linter can analyze anything even live apps.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.