diff --git a/app/index.js b/app/index.js index aec98bc4..b3822e21 100644 --- a/app/index.js +++ b/app/index.js @@ -44,13 +44,18 @@ app.get('/example/', function (req, res) { app.post('/', upload.single('theme'), function (req, res, next) { - var zip = { + const zip = { path: req.file.path, name: req.file.originalname }; + const options = { + checkVersion: req.body.version || 'latest' + }; + debug('Uploaded: ' + zip.name + ' to ' + zip.path); + debug('Version to check: ' + options.checkVersion); - gscan.checkZip(zip) + gscan.checkZip(zip, options) .then(function processResult(theme) { debug('Checked: ' + zip.name); res.theme = theme; @@ -67,8 +72,11 @@ app.post('/', }); }, function doRender(req, res) { + const options = { + checkVersion: req.body.version || 'latest' + }; debug('Formatting result'); - var result = gscan.format(res.theme); + const result = gscan.format(res.theme, options); debug('Rendering result'); scanHbs.handlebars.logger.level = 0; res.render('result', result); diff --git a/app/public/gscan.css b/app/public/gscan.css index 9de20db1..640a02ac 100644 --- a/app/public/gscan.css +++ b/app/public/gscan.css @@ -1 +1 @@ -a,abbr,acronym,address,applet,article,aside,audio,big,blockquote,body,canvas,caption,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,ul,var,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}img{max-width:100%}html{box-sizing:border-box;font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}*,:after,:before{box-sizing:inherit}a{background-color:transparent}a:active,a:hover{outline:0}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;color:inherit;font:inherit}button{overflow:visible;border:none}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input:focus{outline:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}legend{padding:0;border:0}textarea{overflow:auto}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}html{overflow-y:scroll;font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body,html{overflow-x:hidden}body{color:#52636b;font-family:Whitney SSm A,Whitney SSm B,sans-serif;font-size:1.45rem;line-height:1.6em;font-weight:400;font-style:normal;letter-spacing:0;text-rendering:optimizeLegibility;background:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-moz-font-feature-settings:"liga" on}::-moz-selection{text-shadow:none;background:#cbeafb}::selection{text-shadow:none;background:#cbeafb}hr{display:block;margin:2em 0;padding:0;height:1px;border:0;border-top:1px solid #e5eff5}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{margin:0;padding:0;border:0}blockquote,dl,ol,p,ul{margin:0 0 1.5em}ol,ul{padding-left:2.5em}ol ol,ol ul,ul ol,ul ul{margin:.5em 0 1em;padding-left:2em}ul{list-style:disc}ol{list-style:decimal}li{margin:.5em 0;line-height:1.6em}dt{float:left;margin:0 20px 0 0;width:120px;color:#343f44;font-weight:500;text-align:right}dd{margin:0 0 5px;text-align:left}blockquote{margin:1.6em 0;padding:0 1.6em;border-left:.5em solid #e5eff5}blockquote p{margin:.8em 0;font-size:1.2em;font-weight:300}blockquote small{display:inline-block;margin:.8em 0 .8em 1.5em;font-size:.9em;opacity:.8}blockquote small:before{content:"\2014 \00A0"}blockquote cite{font-weight:700}blockquote cite a{font-weight:400}a{color:#26a8ed;text-decoration:none;transition:all .5s ease}a:hover{text-decoration:underline;transition:all .2s ease}.hidden{display:none!important}.visuallyhidden{position:absolute;overflow:hidden;clip:rect(0 0 0 0);margin:-1px;padding:0;width:1px;height:1px;border:0}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{position:static;overflow:visible;clip:auto;margin:0;width:auto;height:auto}.invisible{visibility:hidden}.sr-only{position:absolute;overflow:hidden;clip:rect(0,0,0,0);margin:-1px;padding:0;width:1px;height:1px;border:0}.sr-only-focusable:focus{z-index:4;overflow:visible;clip:auto;margin:0;padding:0 10px;width:auto;height:auto;color:#333;line-height:49px;font-weight:700;text-decoration:none;background-color:#f5f5f5}.right{float:right}.left{float:left}.clearfix:after,.clearfix:before{content:" ";display:table}.clearfix:after{clear:both}.gh-table{box-sizing:border-box;margin:1.6em 0;max-width:100%;width:100%;background-color:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.gh-table td,.gh-table th{padding:8px;border-top:1px solid #e2edf2;line-height:20px;text-align:left;vertical-align:top}.gh-table th{color:#7d878a}.gh-table caption+thead tr:first-child td,.gh-table caption+thead tr:first-child th,.gh-table colgroup+thead tr:first-child td,.gh-table colgroup+thead tr:first-child th,.gh-table thead:first-child tr:first-child td,.gh-table thead:first-child tr:first-child th{border-top:0}.gh-table tbody+tbody{border-top:2px solid #e2edf2}.gh-table .gh-table .gh-table{background-color:#fff}.gh-table tbody>tr:nth-child(odd)>td,.gh-table tbody>tr:nth-child(odd)>th{background-color:#f4f8fa}.gh-table.plain tbody>tr:nth-child(odd)>td,.gh-table.plain tbody>tr:nth-child(odd)>th{background:transparent}h1,h2,h3,h4,h5,h6{margin-top:0;color:#242628;line-height:1.15;font-weight:800;text-rendering:optimizeLegibility}h1{margin:0 0 .5em;font-size:2.6rem;font-weight:600}@media (max-width:500px){h1{font-size:2.2rem}}h2{margin:1.5em 0 .5em;font-size:2rem;font-weight:500}@media (max-width:500px){h2{font-size:1.8rem}}h3{margin:1.5em 0 .5em;font-size:1.8rem;font-weight:500}@media (max-width:500px){h3{font-size:1.7rem}}h4{margin:1.5em 0 .5em;font-size:1.6rem;font-weight:500}h5,h6{margin:1.5em 0 .5em;font-size:1.4rem;font-weight:500}.supermassive-h1{margin-top:40px;color:#fa3a57;font-size:12vw;font-weight:100}p code{padding:0 5px 2px;line-height:1;font-weight:400!important;background:#d0ecfb;border-radius:4px}.gh-anchor{color:inherit}.gh-anchor,.gh-anchor:hover{text-decoration:none}.gh-header{padding:30px 0 60px;text-align:center}.gh-header h1,.gh-mainhead{margin:0;font-size:4.4rem;font-weight:300}.gh-mainhead{color:inherit;letter-spacing:-2px}.gh-subhead{margin:.3em 0 0;font-size:2rem;line-height:1.3em;opacity:.7}@media (max-width:660px){.gh-header{padding:5vw 10vw}.gh-header h1,.gh-mainhead{font-size:6vw}.gh-subhead{font-size:1.6rem}}p+p>.gh-morelink{position:relative;top:-10px}.gh-morelink svg{margin-left:3px;height:.5em}.gh-badge{display:inline-block;padding:2px 3px 3px;border:1px solid #97bd38;color:#fff;font-size:.8em;line-height:1em;font-weight:600;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,.1);white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:linear-gradient(#a9d142,#9ec738);border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.15)}.gh-badge-blue{border:1px solid #3da4db;background:linear-gradient(#57baf0,#3dabe6)}.gh-badge-outline{border-color:#dae0e2;color:#bdc7cc;font-weight:400;background:transparent;box-shadow:none}.gh-customerlogos{opacity:.7}.gh-signupbox-section{border-top:1px solid #edf4f8;border-bottom:1px solid #edf4f8;background:linear-gradient(#f8fafc,#f4f8fb)}.gh-signupbox{-ms-flex-pack:justify;justify-content:space-between;margin:0 -20px;padding:20px;font-size:1.6rem;line-height:1.3em;background:#fff;border-radius:6px;box-shadow:0 0 1px rgba(0,0,0,.1),0 2px 6px rgba(0,0,0,.03)}.gh-signup-actions,.gh-signupbox{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-signup-actions{-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:0;flex-shrink:0;margin-left:20px;max-width:300px}.gh-signup-actions .gh-btn{-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0}.gh-signup-actions .gh-btn-blue{-ms-flex-positive:1;flex-grow:1;margin-right:10px}.gh-signup-actions .gh-btn-blue span{width:100%;text-align:center}@media (max-width:1040px){.gh-signupbox{margin:0}.gh-signupbox-text-extension{display:none}}@media (max-width:620px){.gh-signupbox{-ms-flex-direction:column;flex-direction:column;text-align:center}.gh-signup-actions{margin:15px 0 0;max-width:none;width:100%}}.gh-ghostpro{display:inline-block;white-space:nowrap}.gh-ghostpro svg{height:31px}.gh-ghostpro .gh-badge{margin-left:4px;font-size:1.2rem;text-transform:uppercase}.gh-btn{display:inline-block;outline:none;border:1px solid #ccdfeb;color:#738a94;text-decoration:none!important;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:#738a94;border-radius:5px;transition:none;-webkit-font-smoothing:subpixel-antialiased}.gh-btn span{display:block;padding:0 14px;height:37px;font-size:1.3rem;line-height:37px;font-weight:400;text-align:center;border-radius:4px}.gh-btn.disabled,.gh-btn[disabled],fieldset[disabled] .gh-btn{box-shadow:none;opacity:.8;cursor:not-allowed;pointer-events:none}.gh-btn-blue{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#3da1d6,#2288bf);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-blue span{background:linear-gradient(#4fb7f0,#29a0e0 60%,#29a0e0 90%,#36a6e2);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-blue:active,.gh-btn-blue:focus{background:#1e78a9}.gh-btn-blue:active span,.gh-btn-blue:focus span{background:#29a0e0;box-shadow:none}.gh-btn-green{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#99bf38,#83a333);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-green span{background:linear-gradient(#a9d145,#97bc38 60%,#97bc38 90%,#96ba3b);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-green:active,.gh-btn-green:focus{background:#83a333}.gh-btn-green:active span,.gh-btn-green:focus span{background:#97bc38;box-shadow:none}.gh-btn-red{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#d64f30,#b33a1e);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-red span{background:linear-gradient(#f06242,#dc411e 60%,#dc411e 90%,#e24a28);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-red:active,.gh-btn-red:focus{background:#9d331b}.gh-btn-red:active span,.gh-btn-red:focus span{background:#dc411e;box-shadow:none}.gh-btn-black{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#323232,#171717);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-black span{background:linear-gradient(#454545,#2a2c2d 60%,#2a2c2d 90%,#313435);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-black:active,.gh-btn-black:focus{background:#0a0a0a}.gh-btn-black:active span,.gh-btn-black:focus span{background:#2a2c2d;box-shadow:none}.gh-btn-grey{padding:1px;border:0;color:#343f44;text-shadow:0 1px 0 #fff;fill:#343f44;background:linear-gradient(#ced9df,#bdc7cc);box-shadow:0 1px 0 rgba(0,0,0,.05);transition:none!important}.gh-btn-grey span{background:linear-gradient(#f8fafc,#f6f8f9);box-shadow:inset 0 1px 0 #fff}.gh-btn-grey:active,.gh-btn-grey:focus{background:#bdc7cc}.gh-btn-grey:active span,.gh-btn-grey:focus span{background:#f3f5f7;box-shadow:none}.gh-btn-white{background:#fff}.gh-btn-white span{height:38px}.gh-btn-block{display:block;width:100%}.gh-btn-icon span{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-btn-icon svg{margin-right:10px;height:15px}.gh-spinner{position:relative;display:inline-block;width:18px;height:18px;border:4px solid rgba(0,0,0,.2);border-radius:100%;animation:a 1s linear infinite}.gh-spinner:before{content:"";display:block;margin-top:9px;width:4px;height:4px;background:rgba(0,0,0,.6);border-radius:100%}@keyframes a{0%{transform:rotate(0deg)}50%{transform:rotate(180deg)}to{transform:rotate(1turn)}}.gh-btn-group{margin-top:3rem}.gh-btn-group .gh-btn:first-of-type{margin-right:15px}.gh-btn-group .gh-btn span{font-size:1.4rem}.gh-form-group{position:relative;margin-bottom:1.6em;max-width:500px;width:100%;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.gh-form-group p{margin:4px 0 0;color:#b1b1b1;font-size:1.3rem}.gh-form-group h3{margin-bottom:1.6em;font-size:1.5rem}.gh-form-group label{margin-bottom:4px}.gh-form-group-horizontal{display:-ms-flexbox;display:flex}.gh-form-group-horizontal .gh-form-group+.gh-form-group{margin-left:15px}@media (max-width:550px){.gh-form-group{max-width:100%}}.gh-input-icon{position:relative}.gh-input-icon input{padding-left:35px}.gh-input-icon svg{position:absolute;top:50%;left:12px;z-index:2;height:14px;fill:#a6bac5;transform:translateY(-7px)}.gh-input-icon-lock svg{transform:translateY(-8px)}.gh-input,.gh-select,select{display:block;padding:12px;width:100%;border:1px solid #dae2e7;color:#738a94;font-size:1.3rem;line-height:1em;font-weight:400;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;border-radius:5px;transition:border-color .15s linear;-webkit-appearance:none}.gh-select,select{cursor:pointer}textarea{min-width:250px;min-height:10rem;max-width:500px;width:100%;height:auto;line-height:1.5;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;resize:vertical}textarea.gh-input{line-height:1.5em}.gh-input.focus,.gh-input:focus,.gh-select:focus,select:focus,textarea:focus{outline:0;border-color:#becdd5}.for-checkbox label,.for-radio label{display:block;padding-bottom:4px;cursor:pointer}.for-checkbox label p,.for-radio label p{overflow:auto;color:#000;font-weight:400}.for-checkbox label:hover p,.for-radio label:hover p{color:#738a94}.for-checkbox input,.for-radio input{position:absolute;top:0;right:0;bottom:0;left:-9999px}.for-checkbox .input-toggle-component,.for-radio .input-toggle-component{position:relative;top:1px;display:inline-block;float:left;margin-right:7px;width:18px;height:18px;border:1px solid #dfe1e3;background:#f7f7f7}.for-checkbox label:hover input:not(:checked)+.input-toggle-component,.for-radio label:hover input:not(:checked)+.input-toggle-component{border-color:#c5d2d9}.for-checkbox .input-toggle-component{border-radius:5px;transition:background .15s ease-in-out,border-color .15s ease-in-out}.for-checkbox .input-toggle-component:before{content:"";position:absolute;top:4px;left:3px;width:10px;height:6px;border:2px solid #fff;border-top:none;border-right:none;opacity:0;transition:opacity .15s ease-in-out;transform:rotate(-45deg)}.for-checkbox input:checked+.input-toggle-component{border-color:#88ae29;background:#a4d037}.for-checkbox input:checked+.input-toggle-component:before{opacity:1}.for-radio .input-toggle-component{border-radius:100px;transition:background .15s ease-in-out,border-color .15s ease-in-out}.for-radio .input-toggle-component:before{content:"";position:absolute;top:4px;left:4px;width:8px;height:8px;background:#fff;border-radius:100%;opacity:0;transition:opacity .15s ease-in-out}.for-radio input:checked+.input-toggle-component{border-color:#88ae29;background:#a4d037}.for-radio input:checked+.input-toggle-component:before{opacity:1}.gh-select{position:relative;display:block;overflow:hidden;padding:0;max-width:100%;width:100%;border-width:0}.gh-select select{padding:8px 10px;outline:none;line-height:normal;text-indent:.01px;text-overflow:"";background:#fff;appearance:none;-webkit-appearance:none;-moz-appearance:window}.gh-select select::-ms-expand{display:none}.gh-select select:-moz-focusring{color:transparent;text-shadow:0 0 0 #000}.gh-error .gh-input{border-color:#f05230}.gh-error .gh-input-icon svg{fill:#f05230}.gh-error-message{display:block;margin:10px 0 0;color:#f05230;font-size:1.2rem;line-height:1.4em}.gh-success .gh-input{border-color:#a4d037}.gh-success .gh-input-icon svg{fill:#8eb62b}.hamburger{display:-ms-flexbox;display:flex;overflow:visible;margin:0;padding:2px 0;border:0;color:inherit;font:inherit;text-transform:none;background-color:transparent;cursor:pointer;transition:opacity .15s linear,filter .15s linear}.hamburger-box{position:relative;display:inline-block;width:20px;height:13px}.hamburger-inner{top:50%;display:block;margin-top:-2px}.hamburger-inner,.hamburger-inner:after,.hamburger-inner:before{position:absolute;width:20px;height:1px;background-color:#738a94;border-radius:4px;transition:transform .15s ease}.hamburger-inner:after,.hamburger-inner:before{content:"";display:block}.hamburger-inner:before{top:-6px}.hamburger-inner:after{bottom:-6px}.hamburger--collapse .hamburger-inner{top:auto;bottom:0;transition-delay:.15s;transition-timing-function:cubic-bezier(.55,.055,.675,.19);transition-duration:.15s}.hamburger--collapse .hamburger-inner:after{top:-12px;transition:top .3s cubic-bezier(.33333,.66667,.66667,1) .3s,opacity .1s linear}.hamburger--collapse .hamburger-inner:before{transition:top .12s cubic-bezier(.33333,.66667,.66667,1) .3s,transform .15s cubic-bezier(.55,.055,.675,.19)}.gh-mobilehead-open .hamburger-inner,.gh-mobilehead-open .hamburger-inner:after,.gh-mobilehead-open .hamburger-inner:before{background-color:#343f44}.gh-mobilehead-open .hamburger-inner{transition-delay:.32s;transition-timing-function:cubic-bezier(.215,.61,.355,1);transform:translate3d(0,-6px,0) rotate(-45deg)}.gh-mobilehead-open .hamburger-inner:after{top:0;opacity:0;transition:top .3s cubic-bezier(.33333,0,.66667,.33333),opacity .1s linear .27s}.gh-mobilehead-open .hamburger-inner:before{top:0;transition:top .12s cubic-bezier(.33333,0,.66667,.33333) .18s,transform .15s cubic-bezier(.215,.61,.355,1) .42s;transform:rotate(-90deg)}.gh-viewport{position:relative}.inner{margin:0 auto;max-width:1030px;width:100%}.gh-head{position:relative;z-index:4;margin-bottom:40px;padding:0 4vw;width:100%;border-bottom:1px solid #e5eff5;background:hsla(0,0%,100%,.97)}@media (max-width:600px){.gh-head{padding-right:15px;padding-left:15px}}.gh-navbar{-ms-flex-pack:justify;justify-content:space-between;height:55px;font-size:13px}.gh-navbar,.gh-navbar-left,.gh-navbar-right{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-nav-logo{-ms-flex-negative:0;flex-shrink:0;display:-ms-flexbox!important;display:flex!important;-ms-flex-align:center;align-items:center;margin-left:-12px;color:#738a94}.gh-nav-item.gh-nav-logo:hover{color:inherit}.gh-nav-logo .ghost-svg{width:auto;height:25px}.gh-nav-logo-suffix{position:relative;margin:0 0 0 12px;padding:0 0 0 11px;font-size:1.6rem;font-weight:400;text-shadow:none;transition:none}.gh-nav-logo-suffix:before{content:"";position:absolute;top:0;left:0;display:block;width:1px;height:23px;background:#d3e4ee;transform:rotate(25deg)}.gh-navbar-item{-ms-flex-negative:0;flex-shrink:0;display:block;padding:10px 12px;color:#738a94;font-weight:500;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gh-drop.active .gh-drop-trigger,.gh-navbar-item-active,.gh-navbar-item:hover{color:#343f44;text-decoration:none}.gh-navbar-item:hover .gh-nav-logo-suffix{color:inherit}.gh-more-drop svg{margin-left:3px;width:7px;height:auto}.gh-more-drop .gh-dropdown{right:-73px;width:146px;font-size:1.2rem;text-shadow:none}.gh-more-drop .gh-dropdown:after{right:66px}.gh-more-drop .gh-dropdown:before{right:64px}.gh-more-drop .gh-badge{font-size:1rem}.gh-navbar-btn{margin-left:10px;color:#738a94;transition:all .5s ease}.gh-navbar-btn span{padding:8px 10px;min-width:66px;height:auto;line-height:1;text-align:center}.gh-navbar-btn:hover{border-color:#3eb0ef;color:#3da1d6;transition:all .2s ease}.gh-signin-drop svg{margin-left:5px;width:7px;height:auto}.gh-head-transparent .gh-head{border:none;text-shadow:rgba(0,0,0,.1) 0 1px 1px;background:transparent}.gh-head-transparent .gh-head .ghost-svg g,.gh-head-transparent .gh-head .ghost-svg path{fill:#fff}.gh-head-transparent .gh-head .gh-navbar-item{color:hsla(0,0%,100%,.85)}.gh-head-transparent .gh-head .gh-drop.active .gh-drop-trigger,.gh-head-transparent .gh-head .gh-navbar-item:hover{color:#fff}.gh-head-transparent .gh-head .gh-more-drop .gh-dropdown{top:110%;box-shadow:0 3px 8px rgba(0,0,0,.15),0 0 2px rgba(0,0,0,.2)}.gh-head-transparent .gh-head .gh-navbar-btn{border-color:hsla(0,0%,100%,.6);color:hsla(0,0%,100%,.8)}.gh-head-transparent .gh-head .gh-navbar-btn:hover{border-color:#fff;color:#fff}.gh-head-transparent .gh-hero{margin-top:calc(-6vw - 55px)}.gh-head-transparent .gh-hero .gh-btn-outline{border-color:hsla(0,0%,100%,.7);color:hsla(0,0%,100%,.8);transition:all .5s ease}.gh-head-transparent .gh-hero .gh-btn-outline:hover{border-color:#fff;color:#fff;transition:all .15s ease}.gh-mobilehead{position:absolute;top:0;left:0;z-index:3;display:none;overflow:hidden;width:100%;height:48px;border-bottom:1px solid rgba(0,0,0,.05);background:hsla(0,0%,100%,.97);box-shadow:inset 0 -1px 0 0 #fff,0 1px 5px rgba(0,0,0,.02);transition:all .5s ease-out,background 1s ease-out;transition-delay:.2s;-webkit-backdrop-filter:blur(3px)}.gh-mobilenavbar{position:relative;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;height:48px;font-size:13px}.gh-nav-burger{display:block;padding:15px;color:#738a94;font-size:25px;line-height:1}.gh-nav-burger,.gh-nav-burger:hover{text-decoration:none}.gh-mobilenavbar .gh-nav-logo{position:absolute;top:0;right:50%;display:block;margin-right:-33px;padding:13px 0}.gh-mobilenavbar .ghost-svg{width:auto;height:23px}.gh-mobilenavbar .gh-login-link{display:block;padding:14px 15px;font-size:1.5rem}.gh-mobilenavbar .gh-login-link:hover{text-decoration:none}.gh-mobilemenu{padding:2.7vw 14vw}.gh-mobilemenu a{display:block;border-bottom:1px solid #e5eff5;color:#738a94;font-size:1.8rem;line-height:4.8rem;font-weight:200;opacity:0;transition:transform .3s cubic-bezier(.4,.01,.165,.99),opacity .2s cubic-bezier(.4,.01,.165,.99);transition-delay:.4s;transform:scale(1.1) translateY(-25px)}.gh-mobilemenu a:first-child{transition-delay:.56s}.gh-mobilemenu a:nth-child(2){transition-delay:.49s}.gh-mobilemenu a:nth-child(3){transition-delay:.42s}.gh-mobilemenu a:nth-child(4){transition-delay:.35s}.gh-mobilemenu a:nth-child(5){transition-delay:.28s}.gh-mobilemenu a:nth-child(6){transition-delay:.21s}.gh-mobilemenu a:nth-child(7){transition-delay:.14s}.gh-mobilemenu a:nth-child(8){transition-delay:.07s}.gh-mobilemenu a:hover{color:#3eb0ef;text-decoration:none}.gh-mobilemenu a.active{color:#343f44;font-weight:400}.gh-mobilehead-open{position:fixed;top:0;right:0;left:0;z-index:5;overflow:hidden;margin:0;height:100vh;transition:all .3s ease-in,background .5s ease-in;transition-delay:.3s}.gh-mobilehead-open .gh-mobilemenu a{opacity:1;transition:transform .6s cubic-bezier(.4,.01,.165,.99),opacity .9s cubic-bezier(.4,.01,.165,.99);transform:scale(1) translateY(0)}.gh-mobilehead-open .gh-mobilemenu a:nth-child(8){transition-delay:.56s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(7){transition-delay:.49s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(6){transition-delay:.42s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(5){transition-delay:.35s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(4){transition-delay:.28s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(3){transition-delay:.21s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(2){transition-delay:.14s}.gh-mobilehead-open .gh-mobilemenu a:first-child{transition-delay:.07s}@media (max-width:660px){.gh-viewport{padding-top:68px}.gh-head{display:none}.gh-mobilehead{display:block}}.gh-main{padding:0 4vw}.gh-section{margin:0 -4vw;padding:60px 4vw}@media (max-width:600px){.gh-section{padding:10vw 4vw}}.gh-section-dark{color:#fff;text-shadow:rgba(0,0,0,.1) 0 1px 2px}.gh-section-dark h2{color:#fff}.gh-section-dark h3{color:#fff;font-size:1.8rem}.gh-section-dark p{opacity:.8}.gh-section-dark a{color:#5fbef2}.gh-section-dark a.gh-anchor{color:#fff}.gh-section h3 .gh-badge{margin-left:4px;padding:2px;padding-bottom:2px;font-size:.7em;vertical-align:top}.gh-section-head{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;margin-bottom:1em;text-align:center}.gh-section-head h2{margin:1em 0 .3em;font-size:2.6rem;font-weight:400}@media (max-width:660px){.gh-section-head h2{font-size:4.8vw}}.gh-section-head p{margin:0;font-size:2rem;line-height:1.4em;font-weight:300;opacity:.8}@media (max-width:660px){.gh-section-head p{font-size:1.6rem}}.gh-1col{margin:0 auto;max-width:720px;font-size:1.8rem;line-height:1.65em}@media (max-width:660px){.gh-1col{font-size:1.6rem}}.gh-grid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:justify;justify-content:space-between;margin:0 -40px -10px;padding:0 20px}.gh-grid-item{-ms-flex:1 0 300px;flex:1 0 300px;padding:20px}@media (max-width:934px){.gh-grid-item{padding:5px 20px}}@media (max-width:620px){.gh-grid{-ms-flex-direction:column;flex-direction:column;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.gh-grid-item{position:relative;-ms-flex:1 0 auto;flex:1 0 auto;padding:0 20px 0 65px}.gh-grid-item svg{position:absolute;top:9px;left:23px;width:24px;height:auto}}.gh-grid-item svg{height:30px;fill:#ad26b4}.gh-grid-blue .gh-grid-item svg{fill:#3eb0ef}.gh-grid-green .gh-grid-item svg{fill:#a4d037}.gh-grid-backups svg{margin-bottom:-3px;height:33px}.gh-grid-seo svg{margin-top:2px;margin-bottom:2px;height:26px}.gh-grid-item h3{margin:1em 0 .3em;font-size:1.6rem}.gh-biggrid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:justify;justify-content:space-between;margin:4vw auto 0;padding:0 20px}.gh-biggrid-item{position:relative;-ms-flex:1 0 300px;flex:1 0 300px;margin:15px 30px;padding:0 0 0 70px;font-size:1.6rem}.gh-biggrid-item h3{margin:0 0 5px;color:#fecd35}.gh-biggrid-item svg{position:absolute;top:0;left:0;width:40px;height:auto;fill:#fff}.gh-biggrid-item a{color:#fff;font-weight:500;text-decoration:underline}.gh-foot{padding:3vw 4vw 5vw;font-size:1.3rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:#fff}.gh-foot a{color:#738a94;text-decoration:none}.gh-foot a:hover{color:#343f44;text-decoration:none}.gh-foot-content{padding-top:20px;border-top:1px solid #e5eff5}.gh-foot-content,.gh-foot-nav{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between}.gh-foot-nav{padding-left:60px;width:100%}.gh-foot-col{padding:0 10px}.gh-foot-col h4{margin:5px 0 10px;font-size:1.4rem}.gh-foot-col ul{margin:0;padding:0;list-style:none}.gh-foot-col ul a{display:block;padding:3px 0;min-width:90px}.gh-foot-col li{margin:0}.gh-foot-col .gh-badge{position:relative;top:-1px}.gh-foot-mast{position:relative;-ms-flex:0 0 300px;flex:0 0 300px;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start}.gh-foot-mast svg{display:inline-block;width:auto;height:25px}.gh-foot-mast-text{margin:10px 0;color:#738a94;line-height:1.5em}.gh-langswitch .gh-drop-trigger{display:inline-block;padding:4px 8px 4px 0;color:#26a8ed}.gh-langswitch .gh-drop-trigger span{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-langswitch svg{margin-left:4px;width:7px;height:auto}.gh-langswitch .gh-dropdown{right:-65px;width:130px}.gh-langswitch .gh-dropdown .gh-badge-outline{transition:all .4s ease}.gh-langswitch a:hover .gh-badge-outline{border-color:#576870;color:#576870}.gh-langswitch .active a{color:#3eb0ef}.gh-langswitch .active .gh-badge-outline,.gh-langswitch .active a:hover .gh-badge-outline{border-color:#3eb0ef;color:#3eb0ef}.gh-foot-social{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-foot-social a{display:block;padding:7px}.gh-foot-social a:first-of-type{padding-left:0}.gh-foot-social svg{width:auto;height:14px;fill:#92a3ab;transition:all .3s ease}.gh-foot-social a:hover svg{fill:#343f44}.gh-foot-copyright{position:absolute;bottom:3px;left:0;margin-top:5px;color:#738a94;font-size:1.1rem}@media (max-width:965px){.gh-foot-col:last-of-type{display:none}}@media (max-width:800px){.gh-foot{text-align:center}.gh-foot-content{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}.gh-foot-nav{-ms-flex-order:1;order:1;padding:0;max-width:none}.gh-foot-col{padding:0 10px}.gh-foot-col:last-of-type{display:block}.gh-foot-mast{-ms-flex-order:2;order:2;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center;margin:20px 0;padding:20px;max-width:450px;width:auto;border-top:1px solid #e5eff5;text-align:center}.gh-foot-copyright{position:static}}@media (max-width:550px){.gh-foot-content{padding:0;border:none}.gh-foot-mast{-ms-flex:0 0 auto;flex:0 0 auto;margin-top:0;padding:20px 20px 0}.gh-foot-nav{display:none}}.gh-foot-floating .gh-foot{padding-top:2vw}.gh-foot-floating .gh-foot-content,.gh-foot-floating .gh-foot-mast{border-top:none}.gh-foot-min{padding:0 4vw 4vw;border-top:1px solid #e5eff5;background:linear-gradient(#f8fafc,#f4f8fb)}.gh-foot-min .gh-foot-content{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:20px 0;border-top:none}.gh-foot-min-logo svg{width:auto;height:22px}.gh-foot-min-nav{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;margin-left:-15px}.gh-foot-min-back{margin-right:-15px}.gh-foot-min-item{padding:0 15px}@media (max-width:660px){.gh-foot-min .gh-foot-content{-ms-flex-direction:row;flex-direction:row}.gh-foot-min-item:nth-child(1n+2){display:none}}.gh-nav-logo,.gh-nav-logo:hover{color:#26a8ed}.gh-gscan-head{margin-bottom:6vw;text-align:center}.gh-subhead.warning{color:#f05230;margin-top:.5em}.report-body{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:start;align-items:flex-start}.gh-section.report{padding-top:0}.report-section{padding:0 60px 0 0}.report-section.results{-ms-flex-positive:1;flex-grow:1;width:60%}.report-section.meta{-ms-flex-positive:1;flex-grow:1;overflow:hidden;margin-top:25px;padding:20px;width:40%;border:1px solid #e5eff5;border-radius:5px}.image-container{margin:-20px -20px 20px;border-bottom:1px solid #e5eff5}.info{-ms-flex-positive:2;flex-grow:2;width:100%}.report-meta ul{margin:0;padding:0;list-style:none}.image-container img{max-width:100%}.report-heading{-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;margin-bottom:1.5em;font-size:36px}.report-heading,.result-summary{display:-ms-flexbox;display:flex}.result-summary .title{margin-left:10px}.report h3,.report h4{line-height:1.6}.report h3{display:block;margin:0 5px 6px;font-size:1.17em;font-weight:700}.report h4{margin:0;font-size:16px;font-weight:400;opacity:1}.toggle-details .hide{display:none}.toggle-details{position:relative;display:inline-block;overflow:hidden;margin-top:2px;padding:0;height:20px;font-size:15px;opacity:1;cursor:pointer}.level-group{margin-top:10px}.details.hide{display:none}.indicator{display:inline-block;margin-right:6px;width:30px;height:30px;color:#fff;line-height:1.5;font-weight:700;text-align:center;background:transparent;border-radius:100%}.score{padding:0 4px;color:#fff}.indicator.error:before,.indicator.warning:before{content:"!";margin-top:-2px}.indicator.recommendation:before{content:"?"}.indicator.passing:before{content:"\2713\0020";margin-left:-2px}.indicator.error{border:2px solid #f05230;color:#ee3e17}.score.error{background:#f05230}.indicator.warning,.recommendation{border:2px solid #fecd35;color:#febf01}.score.warning{background:#f2a925}.indicator.passing{border:2px solid #a4d037;color:#98c22e}.score.passing{background:#a4d037}.features li{list-style-type:none}.features li:before{content:"\2713\0020"}.tags li{display:inline;list-style-type:none}.tags li:after{content:","}.tags li:last-child:after{content:"."}button[type=submit].button-add:disabled,button[type=submit].button-add:disabled:hover{color:#b3b9ba;background:#eeefef;cursor:not-allowed}.rule-group{padding-bottom:30px;border-bottom:1px solid #e5eff5}.rule{padding:10px 0;transition:all .3s ease}.rule>p{margin:0}.features ul{padding:0}.rule.expanded{margin:0 0 10px -20px;padding:20px;background:#f8fafc;border-radius:4px}.gh-section-gscan-uploader{min-height:70vh}.gh-gscan-uploader{margin-bottom:6vw;text-align:center}@media (max-width:500px){.report-heading{-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-pack:center;justify-content:center}.result-summary{-ms-flex:1 1 380px;flex:1 1 380px;margin-bottom:1em}} +a,abbr,acronym,address,applet,article,aside,audio,big,blockquote,body,canvas,caption,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,ul,var,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}img{max-width:100%}html{box-sizing:border-box;font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}*,:after,:before{box-sizing:inherit}a{background-color:transparent}a:active,a:hover{outline:0}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;color:inherit;font:inherit}button{overflow:visible;border:none;background:transparent}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input:focus{outline:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}legend{padding:0;border:0}textarea{overflow:auto}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}html{overflow-y:scroll;font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body,html{overflow-x:hidden}body{color:#52636b;font-family:Whitney SSm A,Whitney SSm B,sans-serif;font-size:1.45rem;line-height:1.6em;font-weight:400;font-style:normal;letter-spacing:0;text-rendering:optimizeLegibility;background:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-moz-font-feature-settings:"liga" on}::-moz-selection{text-shadow:none;background:#cbeafb}::selection{text-shadow:none;background:#cbeafb}hr{display:block;margin:2em 0;padding:0;height:1px;border:0;border-top:1px solid #e5eff5}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{margin:0;padding:0;border:0}blockquote,dl,ol,p,ul{margin:0 0 1.5em}ol,ul{padding-left:2.5em}ol ol,ol ul,ul ol,ul ul{margin:.5em 0 1em;padding-left:2em}ul{list-style:disc}ol{list-style:decimal}li{margin:.5em 0;line-height:1.6em}dt{float:left;margin:0 20px 0 0;width:120px;color:#343f44;font-weight:500;text-align:right}dd{margin:0 0 5px;text-align:left}blockquote{margin:1.6em 0;padding:0 1.6em;border-left:.5em solid #e5eff5}blockquote p{margin:.8em 0;font-size:1.2em;font-weight:300}blockquote small{display:inline-block;margin:.8em 0 .8em 1.5em;font-size:.9em;opacity:.8}blockquote small:before{content:"\2014 \00A0"}blockquote cite{font-weight:700}blockquote cite a{font-weight:400}a{color:#26a8ed;text-decoration:none;transition:all .5s ease}a:hover{text-decoration:underline;transition:all .2s ease}.hidden{display:none!important}.visuallyhidden{position:absolute;overflow:hidden;clip:rect(0 0 0 0);margin:-1px;padding:0;width:1px;height:1px;border:0}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{position:static;overflow:visible;clip:auto;margin:0;width:auto;height:auto}.invisible{visibility:hidden}.sr-only{position:absolute;overflow:hidden;clip:rect(0,0,0,0);margin:-1px;padding:0;width:1px;height:1px;border:0}.sr-only-focusable:focus{z-index:900;overflow:visible;clip:auto;margin:0;padding:0 10px;width:auto;height:auto;color:#333;line-height:49px;font-weight:700;text-decoration:none;background-color:#f5f5f5}.right{float:right}.left{float:left}.clearfix:after,.clearfix:before{content:" ";display:table}.clearfix:after{clear:both}.gh-table{box-sizing:border-box;margin:1.6em 0;max-width:100%;width:100%;background-color:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.gh-table td,.gh-table th{padding:8px;border-top:1px solid #e2edf2;line-height:20px;text-align:left;vertical-align:top}.gh-table th{color:#7d878a}.gh-table caption+thead tr:first-child td,.gh-table caption+thead tr:first-child th,.gh-table colgroup+thead tr:first-child td,.gh-table colgroup+thead tr:first-child th,.gh-table thead:first-child tr:first-child td,.gh-table thead:first-child tr:first-child th{border-top:0}.gh-table tbody+tbody{border-top:2px solid #e2edf2}.gh-table .gh-table .gh-table{background-color:#fff}.gh-table tbody>tr:nth-child(odd)>td,.gh-table tbody>tr:nth-child(odd)>th{background-color:#f4f8fa}.gh-table.plain tbody>tr:nth-child(odd)>td,.gh-table.plain tbody>tr:nth-child(odd)>th{background:transparent}h1,h2,h3,h4,h5,h6{margin-top:0;color:#242628;line-height:1.15;font-weight:800;text-rendering:optimizeLegibility}h1{margin:0 0 .5em;font-size:2.6rem;font-weight:600}@media (max-width:500px){h1{font-size:2.2rem}}h2{margin:1.5em 0 .5em;font-size:2rem;font-weight:500}@media (max-width:500px){h2{font-size:1.8rem}}h3{margin:1.5em 0 .5em;font-size:1.8rem;font-weight:500}@media (max-width:500px){h3{font-size:1.7rem}}h4{margin:1.5em 0 .5em;font-size:1.6rem;font-weight:500}h5,h6{margin:1.5em 0 .5em;font-size:1.4rem;font-weight:500}.supermassive-h1{margin-top:40px;color:#fa3a57;font-size:12vw;font-weight:100}p code{padding:0 5px 2px;line-height:1;font-weight:400!important;background:#d0ecfb;border-radius:4px}.gh-anchor{color:inherit}.gh-anchor,.gh-anchor:hover{text-decoration:none}.gh-header{padding:80px 0;text-align:center}.gh-header h1{margin:0;font-size:4.4rem;font-weight:500}.gh-mainhead{margin:0;color:inherit;font-size:4.4rem;font-weight:300;letter-spacing:-2px}.gh-subhead{margin:.3em 0 0;font-size:2rem;line-height:1.3em;opacity:.7}@media (max-width:660px){.gh-header{padding:5vw 10vw}.gh-header h1,.gh-mainhead{font-size:6vw}.gh-subhead{font-size:1.6rem}}p+p>.gh-morelink{position:relative;top:-10px}.gh-morelink svg{margin-left:3px;height:.5em}.gh-badge{display:inline-block;padding:2px 3px 3px;border:1px solid #97bd38;color:#fff;font-size:.8em;line-height:1em;font-weight:600;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,.1);white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:linear-gradient(#a4d037,#97bd38);border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.15)}.gh-badge-blue{border:1px solid #3da4db;background:linear-gradient(#57baf0,#3dabe6)}.gh-badge-outline{border-color:#dae0e2;color:#bdc7cc;font-weight:400;background:transparent;box-shadow:none}.gh-customerlogos{display:block;opacity:.7}.gh-signupbox-section{border-top:1px solid #edf4f8;border-bottom:1px solid #edf4f8;background:linear-gradient(#f8fafc,#f4f8fb)}.gh-signupbox{-ms-flex-pack:justify;justify-content:space-between;margin:0 -20px;padding:20px;font-size:1.6rem;line-height:1.3em;background:#fff;border-radius:6px;box-shadow:0 0 1px rgba(0,0,0,.1),0 2px 6px rgba(0,0,0,.03)}.gh-signup-actions,.gh-signupbox{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-signup-actions{-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:0;flex-shrink:0;margin-left:20px;max-width:300px}.gh-signup-actions .gh-btn{-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0}.gh-signup-actions .gh-btn-blue{-ms-flex-positive:1;flex-grow:1;margin-right:10px}.gh-signup-actions .gh-btn-blue span{width:100%;text-align:center}@media (max-width:1040px){.gh-signupbox{margin:0}.gh-signupbox-text-extension{display:none}}@media (max-width:620px){.gh-signupbox{-ms-flex-direction:column;flex-direction:column;text-align:center}.gh-signup-actions{margin:15px 0 0;max-width:none;width:100%}}.gh-ghostpro{display:inline-block;white-space:nowrap}.gh-ghostpro svg{height:31px}.gh-ghostpro .gh-badge{margin-left:4px;padding-top:4px;font-size:1.2rem;text-transform:uppercase}.gh-btn{display:inline-block;outline:none;border:1px solid #ccdfeb;color:#738a94;text-decoration:none!important;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:#738a94;border-radius:5px;transition:none;-webkit-font-smoothing:subpixel-antialiased}.gh-btn span{display:block;padding:0 14px;height:37px;font-size:1.3rem;line-height:37px;font-weight:400;text-align:center;border-radius:4px}.gh-btn.disabled,.gh-btn[disabled],fieldset[disabled] .gh-btn{box-shadow:none;opacity:.8;cursor:not-allowed;pointer-events:none}.gh-btn-blue{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#3da1d6,#2288bf);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-blue span{background:linear-gradient(#4fb7f0,#29a0e0 60%,#29a0e0 90%,#36a6e2);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-blue:active,.gh-btn-blue:focus{background:#1e78a9}.gh-btn-blue:active span,.gh-btn-blue:focus span{background:#29a0e0;box-shadow:none}.gh-btn-blue-outline{color:#26a8ed;border-color:#3eb0ef}.gh-btn-green{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#99bf38,#83a333);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-green span{background:linear-gradient(#a9d145,#97bc38 60%,#97bc38 90%,#96ba3b);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-green:active,.gh-btn-green:focus{background:#83a333}.gh-btn-green:active span,.gh-btn-green:focus span{background:#97bc38;box-shadow:none}.gh-btn-green-outline{color:#98c22e;border-color:#a4d037}.gh-btn-red{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#d64f30,#b33a1e);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-red span{background:linear-gradient(#f06242,#dc411e 60%,#dc411e 90%,#e24a28);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-red:active,.gh-btn-red:focus{background:#9d331b}.gh-btn-red:active span,.gh-btn-red:focus span{background:#dc411e;box-shadow:none}.gh-btn-red-outline{color:#ee3e17;border-color:#f05230}.gh-btn-black{padding:1px;border:0;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.1);fill:#fff;background:linear-gradient(#323232,#171717);box-shadow:0 1px 0 rgba(0,0,0,.12);transition:none!important}.gh-btn-black span{background:linear-gradient(#454545,#2a2c2d 60%,#2a2c2d 90%,#313435);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.gh-btn-black:active,.gh-btn-black:focus{background:#0a0a0a}.gh-btn-black:active span,.gh-btn-black:focus span{background:#2a2c2d;box-shadow:none}.gh-btn-black-outline{color:#2a3337;border-color:#343f44}.gh-btn-grey{padding:1px;border:0;color:#343f44;text-shadow:0 1px 0 #fff;fill:#343f44;background:linear-gradient(#ced9df,#bdc7cc);box-shadow:0 1px 0 rgba(0,0,0,.05);transition:none!important}.gh-btn-grey span{background:linear-gradient(#f8fafc,#f6f8f9);box-shadow:inset 0 1px 0 #fff}.gh-btn-grey:active,.gh-btn-grey:focus{background:#bdc7cc}.gh-btn-grey:active span,.gh-btn-grey:focus span{background:#f3f5f7;box-shadow:none}.gh-btn-white{background:#fff}.gh-btn-white span{height:38px}.gh-btn-block{display:block;width:100%}.gh-btn-icon span{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.gh-btn-icon svg{margin-right:10px;height:15px}.gh-spinner{position:relative;display:inline-block;margin-right:8px;width:18px;height:18px;border:4px solid rgba(0,0,0,.2);border-radius:100%;vertical-align:text-bottom;animation:a 1s linear infinite}.gh-spinner:before{content:"";display:block;margin-top:9px;width:4px;height:4px;background:rgba(0,0,0,.6);border-radius:100%}.gh-btn-green .gh-spinner{border-color:hsla(0,0%,100%,.2)}.gh-btn-green .gh-spinner:before{background:hsla(0,0%,100%,.6)}@keyframes a{0%{transform:rotate(0deg)}50%{transform:rotate(180deg)}to{transform:rotate(1turn)}}svg.retry-animated{stroke:#fff;animation:b .5s ease-in-out forwards}@keyframes b{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.gh-btn-group{margin-top:3rem}.gh-btn-group .gh-btn:first-of-type{margin-right:15px}.gh-btn-group .gh-btn span{font-size:1.4rem}.gh-form-group{position:relative;margin-bottom:1.6em;max-width:500px;width:100%;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.gh-form-group p{margin:4px 0 0;color:#b1b1b1;font-size:1.3rem}.gh-form-group h3{margin-bottom:1.6em;font-size:1.5rem}.gh-form-group label{margin-bottom:4px}.gh-form-group-horizontal{display:-ms-flexbox;display:flex}.gh-form-group-horizontal .gh-form-group+.gh-form-group{margin-left:15px}@media (max-width:550px){.gh-form-group{max-width:100%}}.gh-input,.gh-select,select{display:block;padding:12px;width:100%;border:1px solid #dae2e7;color:#687e87;font-size:1.3rem;line-height:1em;font-weight:400;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;border-radius:5px;transition:border-color .15s linear;-webkit-appearance:none}.gh-select,select{cursor:pointer}::-webkit-input-placeholder{color:#a9b6bc;font-weight:200}:-ms-input-placeholder{color:#a9b6bc;font-weight:200}textarea{min-width:250px;min-height:10rem;max-width:500px;width:100%;height:auto;line-height:1.5;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;resize:vertical}textarea.gh-input{line-height:1.5em}.gh-input.focus,.gh-input.stripe-focus,.gh-input:focus,.gh-select:focus,select:focus,textarea:focus{outline:0;border-color:#becdd5}.gh-input:-webkit-autofill{transition:background-color 100000000s;-webkit-text-fill-color:#687e87;-webkit-animation:1ms void-animation-out}.gh-input.stripe-autofill{background-color:#fff!important}.gh-input-icon{position:relative}.gh-input-icon input{padding-left:35px}.gh-input-icon select{padding-right:35px}.gh-input-icon svg{position:absolute;top:50%;left:12px;z-index:100;height:14px;fill:#a6bac5;transform:translateY(-7px)}.gh-select.gh-input-icon svg{right:12px;left:auto}.gh-input-icon-lock svg{transform:translateY(-8px)}.gh-input-tooltip{position:relative}.gh-input-tooltip input{padding-right:35px}.gh-input-tooltip button{position:absolute;top:0;right:5px;z-index:100;overflow:hidden;height:41px}.gh-input-tooltip button svg{height:14px;fill:#a6bac5}.for-checkbox label,.for-radio label{display:block;padding-bottom:4px;cursor:pointer}.for-checkbox label p,.for-radio label p{overflow:auto;color:#000;font-weight:400}.for-checkbox label:hover p,.for-radio label:hover p{color:#738a94}.for-checkbox input,.for-radio input{position:absolute;top:0;right:0;bottom:0;left:-9999px}.for-checkbox .input-toggle-component,.for-radio .input-toggle-component{position:relative;top:1px;display:inline-block;float:left;margin-right:7px;width:18px;height:18px;border:1px solid #dfe1e3;background:#f7f7f7}.for-checkbox label:hover input:not(:checked)+.input-toggle-component,.for-radio label:hover input:not(:checked)+.input-toggle-component{border-color:#c5d2d9}.for-checkbox .input-toggle-component{border-radius:5px;transition:background .15s ease-in-out,border-color .15s ease-in-out}.for-checkbox .input-toggle-component:before{content:"";position:absolute;top:4px;left:3px;width:10px;height:6px;border:2px solid #fff;border-top:none;border-right:none;opacity:0;transition:opacity .15s ease-in-out;transform:rotate(-45deg)}.for-checkbox input:checked+.input-toggle-component{border-color:#88ae29;background:#a4d037}.for-checkbox input:checked+.input-toggle-component:before{opacity:1}.for-radio .input-toggle-component{border-radius:100px;transition:background .15s ease-in-out,border-color .15s ease-in-out}.for-radio .input-toggle-component:before{content:"";position:absolute;top:4px;left:4px;width:8px;height:8px;background:#fff;border-radius:100%;opacity:0;transition:opacity .15s ease-in-out}.for-radio input:checked+.input-toggle-component{border-color:#88ae29;background:#a4d037}.for-radio input:checked+.input-toggle-component:before{opacity:1}.gh-select{position:relative;display:block;overflow:hidden;padding:0;max-width:100%;width:100%;border-width:0}.gh-select select{padding:8px 10px;outline:none;line-height:normal;text-indent:.01px;text-overflow:"";background:#fff;appearance:none;-webkit-appearance:none;-moz-appearance:window}.gh-select select::-ms-expand{display:none}.gh-select select:-moz-focusring{color:transparent;text-shadow:0 0 0 #000}.gh-error .gh-input,.gh-error select{border-color:#f05230}.gh-error .gh-input-icon svg,.gh-error .gh-input-tooltip button svg{fill:#f05230}.gh-error-message{display:block;margin:10px 0 0;color:#f05230;font-size:1.2rem;line-height:1.4em}.gh-form-group .gh-response{position:absolute;top:-4px;right:0;margin:0;color:#a6b0b3;font-size:1.1rem;font-weight:400;text-align:right}.gh-form-group.gh-error .gh-response,.gh-main-error.gh-error{color:#f05230}.gh-flow-loading{margin-top:5px;font-size:1.4rem}.gh-success .gh-input,.gh-success select{border-color:#a4d037}.gh-success .gh-input-icon svg{fill:#8eb62b}.gh-success .gh-input-tooltip button{display:none}.hamburger{display:-ms-flexbox;display:flex;overflow:visible;margin:0;padding:2px 0;border:0;color:inherit;font:inherit;text-transform:none;background-color:transparent;cursor:pointer;transition:opacity .15s linear,filter .15s linear}.hamburger-box{position:relative;display:inline-block;width:20px;height:13px}.hamburger-inner{top:50%;display:block;margin-top:-2px}.hamburger-inner,.hamburger-inner:after,.hamburger-inner:before{position:absolute;width:20px;height:1px;background-color:#738a94;border-radius:4px;transition:transform .15s ease}.hamburger-inner:after,.hamburger-inner:before{content:"";display:block}.hamburger-inner:before{top:-6px}.hamburger-inner:after{bottom:-6px}.hamburger--collapse .hamburger-inner{top:auto;bottom:0;transition-delay:.15s;transition-timing-function:cubic-bezier(.55,.055,.675,.19);transition-duration:.15s}.hamburger--collapse .hamburger-inner:after{top:-12px;transition:top .3s cubic-bezier(.33333,.66667,.66667,1) .3s,opacity .1s linear}.hamburger--collapse .hamburger-inner:before{transition:top .12s cubic-bezier(.33333,.66667,.66667,1) .3s,transform .15s cubic-bezier(.55,.055,.675,.19)}.gh-mobilehead-open .hamburger-inner,.gh-mobilehead-open .hamburger-inner:after,.gh-mobilehead-open .hamburger-inner:before{background-color:#343f44}.gh-mobilehead-open .hamburger-inner{transition-delay:.32s;transition-timing-function:cubic-bezier(.215,.61,.355,1);transform:translate3d(0,-6px,0) rotate(-45deg)}.gh-mobilehead-open .hamburger-inner:after{top:0;opacity:0;transition:top .3s cubic-bezier(.33333,0,.66667,.33333),opacity .1s linear .27s}.gh-mobilehead-open .hamburger-inner:before{top:0;transition:top .12s cubic-bezier(.33333,0,.66667,.33333) .18s,transform .15s cubic-bezier(.215,.61,.355,1) .42s;transform:rotate(-90deg)}.gh-viewport{position:relative;min-height:100%}.inner{margin:0 auto;max-width:1030px;width:100%}.gh-head{position:relative;z-index:900;margin-bottom:40px;padding:0 4vw;width:100%;border-bottom:1px solid #e5eff5;background:hsla(0,0%,100%,.97)}@media (max-width:600px){.gh-head{padding-right:15px;padding-left:15px}}.gh-navbar{-ms-flex-pack:justify;justify-content:space-between;height:55px;font-size:13px}.gh-navbar,.gh-navbar-left,.gh-navbar-right{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-nav-logo{-ms-flex-negative:0;flex-shrink:0;display:-ms-flexbox!important;display:flex!important;-ms-flex-align:center;align-items:center;margin-left:-15px;color:#738a94}.gh-nav-item.gh-nav-logo:hover{color:inherit}.gh-nav-logo .ghost-svg{width:auto;height:25px}.gh-nav-logo-suffix{position:relative;margin:0 0 0 12px;padding:0 0 0 11px;font-size:1.6rem;font-weight:400;text-shadow:none;transition:none}.gh-nav-logo-suffix:before{content:"";position:absolute;top:0;left:0;display:block;width:1px;height:23px;background:#d3e4ee;transform:rotate(25deg)}.gh-navbar-item{-ms-flex-negative:0;flex-shrink:0;display:block;padding:10px 15px;color:#738a94;font-weight:500;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gh-drop.active .gh-drop-trigger,.gh-navbar-item-active,.gh-navbar-item:hover{color:#343f44;text-decoration:none}.gh-navbar-item:hover .gh-nav-logo-suffix{color:inherit}.gh-more-drop svg{margin-left:3px;width:7px;height:auto}.gh-more-drop .gh-dropdown{right:-73px;width:146px;font-size:1.2rem;text-shadow:none}.gh-more-drop .gh-dropdown:after{right:66px}.gh-more-drop .gh-dropdown:before{right:64px}.gh-more-drop .gh-badge{font-size:1rem}.gh-navbar-btn{margin-left:12px;color:#738a94;transition:all .5s ease}.gh-navbar-btn span{padding:8px 10px;min-width:66px;height:auto;line-height:1;text-align:center}.gh-navbar-btn:hover{border-color:#3eb0ef;color:#3da1d6;transition:all .2s ease}.gh-signin-drop svg{margin-left:5px;width:7px;height:auto}.gh-head-transparent .gh-head{border:none;text-shadow:rgba(0,0,0,.1) 0 1px 1px;background:transparent}.gh-head-transparent .gh-head .ghost-svg g,.gh-head-transparent .gh-head .ghost-svg path{fill:#fff}.gh-head-transparent .gh-head .gh-navbar-item{color:hsla(0,0%,100%,.85)}.gh-head-transparent .gh-head .gh-drop.active .gh-drop-trigger,.gh-head-transparent .gh-head .gh-navbar-item:hover{color:#fff}.gh-head-transparent .gh-head .gh-more-drop .gh-dropdown{top:110%;box-shadow:0 3px 8px rgba(0,0,0,.15),0 0 2px rgba(0,0,0,.2)}.gh-head-transparent .gh-head .gh-navbar-btn{border-color:hsla(0,0%,100%,.6);color:hsla(0,0%,100%,.8)}.gh-head-transparent .gh-head .gh-navbar-btn:hover{border-color:#fff;color:#fff}.gh-head-transparent .gh-hero{margin-top:calc(-6vw - 55px)}@media (max-width:660px){.gh-head-transparent .gh-hero{margin-top:-20px}}.gh-head-transparent .gh-hero .gh-btn-outline{border-color:hsla(0,0%,100%,.7);color:hsla(0,0%,100%,.8);transition:all .5s ease}.gh-head-transparent .gh-hero .gh-btn-outline:hover{border-color:#fff;color:#fff;transition:all .15s ease}.gh-mobilehead{position:absolute;top:0;left:0;z-index:200;display:none;overflow:hidden;width:100%;height:48px;border-bottom:1px solid rgba(0,0,0,.05);background:hsla(0,0%,100%,.97);box-shadow:inset 0 -1px 0 0 #fff,0 1px 5px rgba(0,0,0,.02);transition:all .5s ease-out,background 1s ease-out;transition-delay:.2s;-webkit-backdrop-filter:blur(3px)}.gh-mobilenavbar{position:relative;z-index:10;display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;height:48px;font-size:13px}.gh-nav-burger{display:block;padding:15px;color:#738a94;font-size:25px;line-height:1}.gh-nav-burger,.gh-nav-burger:hover{text-decoration:none}.gh-mobilenavbar .gh-nav-logo{position:absolute;top:0;right:50%;display:block;padding:13px 0;transform:translateX(50%)}.gh-mobilenavbar .ghost-svg{width:auto;height:23px}.gh-mobilenavbar .gh-login-link{display:block;padding:14px 15px;font-size:1.5rem}.gh-mobilenavbar .gh-login-link:hover{text-decoration:none}.gh-mobilemenu{padding:2.7vw 14vw}.gh-mobilemenu a{display:block;border-bottom:1px solid #e5eff5;color:#738a94;font-size:1.8rem;line-height:4.8rem;font-weight:200;opacity:0;transition:transform .3s cubic-bezier(.4,.01,.165,.99),opacity .2s cubic-bezier(.4,.01,.165,.99);transition-delay:.4s;transform:scale(1.1) translateY(-25px)}.gh-mobilemenu a:first-child{transition-delay:.56s}.gh-mobilemenu a:nth-child(2){transition-delay:.49s}.gh-mobilemenu a:nth-child(3){transition-delay:.42s}.gh-mobilemenu a:nth-child(4){transition-delay:.35s}.gh-mobilemenu a:nth-child(5){transition-delay:.28s}.gh-mobilemenu a:nth-child(6){transition-delay:.21s}.gh-mobilemenu a:nth-child(7){transition-delay:.14s}.gh-mobilemenu a:nth-child(8){transition-delay:.07s}.gh-mobilemenu a:hover{color:#3eb0ef;text-decoration:none}.gh-mobilemenu a.active{color:#343f44;font-weight:400}.gh-mobilehead-open{position:fixed;top:0;right:0;left:0;z-index:9999;overflow:hidden;margin:0;height:100vh;transition:all .3s ease-in,background .5s ease-in;transition-delay:.3s}.gh-mobilehead-open .gh-mobilemenu a{opacity:1;transition:transform .6s cubic-bezier(.4,.01,.165,.99),opacity .9s cubic-bezier(.4,.01,.165,.99);transform:scale(1) translateY(0)}.gh-mobilehead-open .gh-mobilemenu a:nth-child(8){transition-delay:.56s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(7){transition-delay:.49s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(6){transition-delay:.42s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(5){transition-delay:.35s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(4){transition-delay:.28s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(3){transition-delay:.21s}.gh-mobilehead-open .gh-mobilemenu a:nth-child(2){transition-delay:.14s}.gh-mobilehead-open .gh-mobilemenu a:first-child{transition-delay:.07s}@media (max-width:660px){.gh-viewport{padding-top:68px}.gh-head{display:none}.gh-mobilehead{display:block}}.gh-main{padding:0 4vw}.gh-section{margin:0 -4vw;padding:60px 4vw}@media (max-width:600px){.gh-section{padding:10vw 4vw}}.gh-section-white{background:#fff}.gh-section-dark{color:#fff;text-shadow:rgba(0,0,0,.1) 0 1px 2px}.gh-section-dark h2{color:#fff}.gh-section-dark h3{color:#fff;font-size:1.8rem}.gh-section-dark p{opacity:.8}.gh-section-dark a{color:#5fbef2}.gh-section-dark a.gh-anchor{color:#fff}.gh-section h3 .gh-badge{margin-left:4px;padding:2px;padding-bottom:2px;font-size:.7em;vertical-align:top}.gh-section-head{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;margin-bottom:1em;text-align:center}.gh-section-head h2{margin:1em 0 .3em;font-size:2.6rem;font-weight:400}@media (max-width:660px){.gh-section-head h2{font-size:4.8vw}}.gh-section-head p{margin:0;font-size:2rem;line-height:1.4em;font-weight:300;opacity:.8}@media (max-width:660px){.gh-section-head p{font-size:1.6rem}}.gh-1col{margin:0 auto;max-width:720px;font-size:1.8rem;line-height:1.65em}@media (max-width:660px){.gh-1col{font-size:1.6rem}}.gh-grid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:justify;justify-content:space-between;margin:0 -40px -10px;padding:0 20px}.gh-grid-item{-ms-flex:1 0 300px;flex:1 0 300px;padding:20px}@media (max-width:934px){.gh-grid-item{padding:5px 20px}}@media (max-width:620px){.gh-grid{-ms-flex-direction:column;flex-direction:column;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.gh-grid-item{position:relative;-ms-flex:1 0 auto;flex:1 0 auto;padding:0 20px 0 65px}.gh-grid-item svg{position:absolute;top:9px;left:23px;width:24px;height:auto}}.gh-grid-item svg{height:30px;fill:#ad26b4}.gh-grid-blue .gh-grid-item svg{fill:#3eb0ef}.gh-grid-green .gh-grid-item svg{fill:#a4d037}.gh-grid-backups svg{margin-bottom:-3px;height:33px}.gh-grid-seo svg{margin-top:2px;margin-bottom:2px;height:26px}.gh-grid-item h3{margin:1em 0 .3em;font-size:1.6rem}.gh-biggrid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:justify;justify-content:space-between;margin:4vw auto 0;padding:0 20px}.gh-biggrid-item{position:relative;-ms-flex:1 0 300px;flex:1 0 300px;margin:15px 30px;padding:0 0 0 70px;font-size:1.6rem}.gh-biggrid-item h3{margin:0 0 5px;color:#fecd35}.gh-biggrid-item svg{position:absolute;top:0;left:0;width:40px;height:auto;fill:#fff}.gh-biggrid-item a{color:#fff;font-weight:500;text-decoration:underline}@media (max-width:620px){.gh-biggrid-item{margin:15px auto;padding:0 20px 0 65px}.gh-biggrid-item svg{width:auto;height:30px}}.gh-foot{padding:3vw 4vw 5vw;font-size:1.3rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:#fff}.gh-foot a{color:#738a94;text-decoration:none}.gh-foot a:hover{color:#343f44;text-decoration:none}.gh-foot-content{padding-top:20px;border-top:1px solid #e5eff5}.gh-foot-content,.gh-foot-nav{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between}.gh-foot-nav{padding-left:60px;width:100%}.gh-foot-col{padding:0 10px}.gh-foot-col h4{margin:5px 0 10px;font-size:1.4rem}.gh-foot-col ul{margin:0;padding:0;list-style:none}.gh-foot-col ul a{display:block;padding:3px 0;min-width:90px}.gh-foot-col li{margin:0}.gh-foot-col .gh-badge{position:relative;top:-1px}.gh-foot-mast{position:relative;-ms-flex:0 0 300px;flex:0 0 300px;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start}.gh-foot-mast svg{display:inline-block;width:auto;height:25px}.gh-foot-mast-text{margin:10px 0;color:#738a94;line-height:1.5em}.gh-langswitch .gh-drop-trigger{display:inline-block;padding:4px 8px 4px 0;color:#26a8ed}.gh-langswitch .gh-drop-trigger span{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-langswitch svg{margin-left:4px;width:7px;height:auto}.gh-langswitch .gh-dropdown{right:-65px;width:130px}.gh-langswitch .gh-dropdown .gh-badge-outline{transition:all .4s ease}.gh-langswitch a:hover .gh-badge-outline{border-color:#576870;color:#576870}.gh-langswitch .active a{color:#3eb0ef}.gh-langswitch .active .gh-badge-outline,.gh-langswitch .active a:hover .gh-badge-outline{border-color:#3eb0ef;color:#3eb0ef}.gh-foot-social{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.gh-foot-social a{display:block;padding:7px}.gh-foot-social a:first-of-type{padding-left:0}.gh-foot-social svg{width:auto;height:14px;fill:#92a3ab;transition:all .3s ease}.gh-foot-social a:hover svg{fill:#343f44}.gh-foot-copyright{position:absolute;bottom:3px;left:0;margin-top:5px;color:#738a94;font-size:1.1rem}@media (max-width:965px){.gh-foot-col:last-of-type{display:none}}@media (max-width:800px){.gh-foot{text-align:center}.gh-foot-content{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}.gh-foot-nav{-ms-flex-order:1;order:1;padding:0;max-width:none}.gh-foot-col{padding:0 10px}.gh-foot-col:last-of-type{display:block}.gh-foot-mast{-ms-flex-order:2;order:2;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center;margin:20px 0;padding:20px;max-width:450px;width:auto;border-top:1px solid #e5eff5;text-align:center}.gh-foot-copyright{position:static}}@media (max-width:550px){.gh-foot-content{padding:0;border:none}.gh-foot-mast{-ms-flex:0 0 auto;flex:0 0 auto;margin-top:0;padding:20px 20px 0}.gh-foot-nav{display:none}}.gh-foot-floating .gh-foot{padding-top:2vw}.gh-foot-floating .gh-foot-content,.gh-foot-floating .gh-foot-mast{border-top:none}.gh-foot-min{padding:0 4vw 4vw;border-top:1px solid #e5eff5;background:linear-gradient(#f8fafc,#f4f8fb)}.gh-foot-min .gh-foot-content{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:20px 0;border-top:none}.gh-foot-min-logo svg{width:auto;height:22px}.gh-foot-min-nav{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;margin-left:-15px}.gh-foot-min-back{margin-right:-15px}.gh-foot-min-item{padding:0 15px}@media (max-width:660px){.gh-foot-min .gh-foot-content{-ms-flex-direction:row;flex-direction:row}.gh-foot-min-item:nth-child(1n+2){display:none}}.gh-nav-logo,.gh-nav-logo:hover{color:#26a8ed}.gh-gscan-head{margin-bottom:6vw;text-align:center}.gh-subhead.warning{margin-top:.5em;color:#f05230}.report-body{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:start;align-items:flex-start}.gh-section.report{padding-top:0}.report-section{padding:0 60px 0 0}.report-section.results{-ms-flex-positive:1;flex-grow:1;width:60%}.report-section.meta{-ms-flex-positive:1;flex-grow:1;overflow:hidden;margin-top:25px;padding:20px;width:40%;border:1px solid #e5eff5;border-radius:5px}.image-container{margin:-20px -20px 20px;border-bottom:1px solid #e5eff5}.info{-ms-flex-positive:2;flex-grow:2;width:100%}.report-meta ul{margin:0;padding:0;list-style:none}.image-container img{max-width:100%}.report-heading{-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;margin-bottom:1.5em;font-size:36px}.report-heading,.result-summary{display:-ms-flexbox;display:flex}.result-summary .title{margin-left:10px}.report h3,.report h4{line-height:1.6}.report h3{display:block;margin:0 5px 6px;font-size:1.17em;font-weight:700}.report h4{margin:0;font-size:16px;font-weight:400;opacity:1}.toggle-details .hide{display:none}.toggle-details{position:relative;display:inline-block;overflow:hidden;margin-top:2px;padding:0;height:20px;font-size:15px;opacity:1;cursor:pointer}.level-group{margin-top:10px}.details.hide{display:none}.indicator{display:inline-block;margin-right:6px;width:30px;height:30px;color:#fff;line-height:1.5;font-weight:700;text-align:center;background:transparent;border-radius:100%}.score{padding:0 4px;color:#fff}.indicator.error:before,.indicator.warning:before{content:"!";margin-top:-2px}.indicator.recommendation:before{content:"?"}.indicator.passing:before{content:"\2713\0020";margin-left:-2px}.indicator.error{border:2px solid #f05230;color:#ee3e17}.score.error{background:#f05230}.indicator.warning,.recommendation{border:2px solid #fecd35;color:#febf01}.score.warning{background:#f2a925}.indicator.passing{border:2px solid #a4d037;color:#98c22e}.score.passing{background:#a4d037}.features li{list-style-type:none}.features li:before{content:"\2713\0020"}.tags li{display:inline;list-style-type:none}.tags li:after{content:","}.tags li:last-child:after{content:"."}button[type=submit].button-add:disabled,button[type=submit].button-add:disabled:hover{color:#b3b9ba;background:#eeefef;cursor:not-allowed}.rule-group{padding-bottom:30px;border-bottom:1px solid #e5eff5}.rule{padding:10px 0;transition:all .3s ease}.rule>p{margin:0}.features ul{padding:0}.rule.expanded{margin:0 0 10px -20px;padding:20px;background:#f8fafc;border-radius:4px}.gh-section-gscan-uploader{min-height:70vh}.gh-gscan-uploader{margin-bottom:6vw;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.gh-gscan-uploader-form{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.gh-gscan-uploader-form label{display:block;margin:0 0 8px;font-size:1.3rem;line-height:1.3em;font-weight:400}.gh-gscan-uploader-form .gh-select{line-height:1em;padding:12px}@media (max-width:500px){.report-heading{-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-pack:center;justify-content:center}.result-summary{-ms-flex:1 1 380px;flex:1 1 380px;margin-bottom:1em}} \ No newline at end of file diff --git a/app/tpl/index.hbs b/app/tpl/index.hbs index 840cd70f..2735ab35 100644 --- a/app/tpl/index.hbs +++ b/app/tpl/index.hbs @@ -3,16 +3,28 @@

Scan Your Theme

Upload a zip to check for errors, deprecations and other compatibility issues.

-

GScan validates your theme based on the Ghost 1.0 requirements.

- - See the changelog for Ghost 1.0 themes +

GScan validates your theme based on the Ghost 2.0 requirements by default.

+

You can select a different version if you need to check your theme for a previous major version.

+ +
+ See the changelog for Ghost 2.0 themes
-
- - + +
+ +
+
+ + +
+ +
diff --git a/bin/cli.js b/bin/cli.js index ddbce5e4..579dd545 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -6,6 +6,7 @@ var pkgJson = require('../package.json'), chalk = require('chalk'), gscan = require('../lib'), + options = {}, themePath = '', levels; @@ -15,6 +16,7 @@ program .arguments('cmd ') .option('-p, --pre', 'Run a pre-check only') .option('-z, --zip', 'Theme path points to a zip file') + .option('-1, --v1', 'Check theme for Ghost 1.0 compatibility, instead of 2.0') .action(function (theme) { themePath = theme; }) @@ -32,8 +34,8 @@ function outputResult(result) { console.log('-', levels[result.level](result.level), result.rule); } -function outputResults(theme) { - theme = gscan.format(theme); +function outputResults(theme, options) { + theme = gscan.format(theme, options); console.log(chalk.bold.underline('\nRule Report:')); @@ -62,21 +64,30 @@ function outputResults(theme) { if (!program.args.length) { program.help(); } else { + if (program.v1) { + options.checkVersion = 'v1'; + } else { + // CASE: set default value + options.checkVersion = 'latest'; + } + if (program.zip) { console.log('Checking zip file...'); - gscan.checkZip(themePath) - .then(outputResults) - .catch(function (error) { + gscan.checkZip(themePath, options) + .then(theme => outputResults(theme, options)) + .catch((error) => { console.error(error); }); } else { console.log('Checking directory...'); - gscan.check(themePath).then(outputResults).catch(function ENOTDIRPredicate(err) { - return err.code === 'ENOTDIR'; - }, function (err) { - console.error(err.message); - console.error('Did you mean to add the -z flag to read a zip file?'); + gscan.check(themePath, options) + .then(theme => outputResults(theme, options)) + .catch(function ENOTDIRPredicate(err) { + return err.code === 'ENOTDIR'; + }, function (err) { + console.error(err.message); + console.error('Did you mean to add the -z flag to read a zip file?'); /* eslint-enable no-console */ - }); + }); } } diff --git a/lib/checker.js b/lib/checker.js index b49869a2..7b92ae08 100644 --- a/lib/checker.js +++ b/lib/checker.js @@ -1,11 +1,9 @@ -var Promise = require('bluebird'), - _ = require('lodash'), - requireDir = require('require-dir'), - readTheme = require('./read-theme'), - checker, - checks; +const Promise = require('bluebird'); +const _ = require('lodash'); +const requireDir = require('require-dir'); +const readTheme = require('./read-theme'); -checks = requireDir('./checks'); +const checks = requireDir('./checks'); /** * Check theme @@ -13,16 +11,18 @@ checks = requireDir('./checks'); * Takes a theme path, reads the theme, and checks it for issues. * Returns a theme object. * @param themePath + * @param options * @returns {Object} */ -checker = function checkAll(themePath, options) { +const checker = function checkAll(themePath, options) { options = options || {}; - return readTheme(themePath).then(function (theme) { - return Promise.reduce(_.values(checks), function (theme, check) { - return check(theme, themePath, options); - }, theme); - }); + return readTheme(themePath) + .then(function (theme) { + return Promise.reduce(_.values(checks), function (theme, check) { + return check(theme, options, themePath); + }, theme); + }); }; module.exports = checker; diff --git a/lib/checks/001-deprecations.js b/lib/checks/001-deprecations.js index 718e3f62..a6e5401b 100644 --- a/lib/checks/001-deprecations.js +++ b/lib/checks/001-deprecations.js @@ -1,176 +1,86 @@ -var _ = require('lodash'), - path = require('path'), - fs = require('fs-extra'), - checkDeprecations; +const _ = require('lodash'); +const path = require('path'); +const fs = require('fs-extra'); +const spec = require('../specs'); +const debug = require('ghost-ignition').debug('checks:deprecations'); -checkDeprecations = function checkDeprecations(theme, themePath) { - var checks = [ - { - helperRegEx: /{{\s*?pageUrl\b[\w\s='"]*?}}/ig, - helperName: '{{pageUrl}}', - ruleCode: 'GS001-DEPR-PURL' - }, - { - helperRegEx: / 0) { - theme.results.fail[ruleCode] = {failures: failures}; + theme.results.fail[ruleToCheck] = {failures: failures}; } else { - theme.results.pass.push(ruleCode); + theme.results.pass.push(ruleToCheck); } return theme; diff --git a/lib/checks/010-package-json.js b/lib/checks/010-package-json.js index 6a3e96a6..feff022c 100644 --- a/lib/checks/010-package-json.js +++ b/lib/checks/010-package-json.js @@ -1,29 +1,35 @@ -var _ = require('lodash'), - path = require('path'), - semver = require('semver'), - validator = require('validator'), - pfs = require('../promised-fs'), - _private = {}, - packageJSONFile = 'package.json', - packageJSONConditionalRules = { - configPPIsInteger: 'GS010-PJ-CONF-PPP-INT' - }, - packageJSONValidationRules = _.extend({ - isPresent: 'GS010-PJ-REQ', - canBeParsed: 'GS010-PJ-PARSE', - nameIsRequired: 'GS010-PJ-NAME-REQ', - nameIsLowerCase: 'GS010-PJ-NAME-LC', - nameIsHyphenated: 'GS010-PJ-NAME-HY', - versionIsSemverCompliant: 'GS010-PJ-VERSION-SEM', - versionIsRequired: 'GS010-PJ-VERSION-REQ', - authorEmailIsValid: 'GS010-PJ-AUT-EM-VAL', - authorEmailIsRequired: 'GS010-PJ-AUT-EM-REQ', - configPPPIsRequired: 'GS010-PJ-CONF-PPP' - }, packageJSONConditionalRules); - -_private.validatePackageJSONFields = function validatePackageJSONFields(packageJSON, theme) { - var failed = [], - passedRulesToOmit = []; +const _ = require('lodash'); +const path = require('path'); +const semver = require('semver'); +const validator = require('validator'); +const pfs = require('../promised-fs'); +const _private = {}; +const packageJSONFile = 'package.json'; +// const debug = require('ghost-ignition').debug('checks:package-json'); + +const v1PackageJSONConditionalRules = { + configPPIsInteger: 'GS010-PJ-CONF-PPP-INT' +}; +const v1PackageJSONValidationRules = _.extend({ + isPresent: 'GS010-PJ-REQ', + canBeParsed: 'GS010-PJ-PARSE', + nameIsRequired: 'GS010-PJ-NAME-REQ', + nameIsLowerCase: 'GS010-PJ-NAME-LC', + nameIsHyphenated: 'GS010-PJ-NAME-HY', + versionIsSemverCompliant: 'GS010-PJ-VERSION-SEM', + versionIsRequired: 'GS010-PJ-VERSION-REQ', + authorEmailIsValid: 'GS010-PJ-AUT-EM-VAL', + authorEmailIsRequired: 'GS010-PJ-AUT-EM-REQ', + configPPPIsRequired: 'GS010-PJ-CONF-PPP' +}, v1PackageJSONConditionalRules); + +const latestPackageJSONConditionalRules = {}; + +const latestPackageJSONValidationRules = _.extend({}, latestPackageJSONConditionalRules); + +_private.validatePackageJSONFields = function validatePackageJSONFields(packageJSON, theme, packageJSONValidationRules) { + let failed = []; + const passedRulesToOmit = []; if (!packageJSON.name) { failed.push(packageJSONValidationRules.nameIsRequired); @@ -85,8 +91,11 @@ _private.getFailedRules = function getFailedRules(keys) { })); }; -module.exports = function checkPackageJSON(theme) { - var packageJSONPath = path.join(theme.path, packageJSONFile); +module.exports = function checkPackageJSON(theme, options) { + const packageJSONPath = path.join(theme.path, packageJSONFile); + const checkVersion = _.get(options, 'checkVersion', 'latest'); + const packageJSONValidationRules = checkVersion === 'v1' ? v1PackageJSONValidationRules : _.merge(v1PackageJSONValidationRules, latestPackageJSONValidationRules); + const packageJSONConditionalRules = checkVersion === 'v1' ? v1PackageJSONConditionalRules : _.merge(v1PackageJSONConditionalRules, latestPackageJSONConditionalRules); // CASE: package.json must be present (if not, all validation rules fail) if (!_.some(theme.files, {file: packageJSONFile})) { @@ -96,10 +105,10 @@ module.exports = function checkPackageJSON(theme) { // CASE: package.json must be valid return pfs.readFile(packageJSONPath, 'utf8') - .then(function (packageJSON) { + .then((packageJSON) => { try { packageJSON = JSON.parse(packageJSON); - theme = _private.validatePackageJSONFields(packageJSON, theme); + theme = _private.validatePackageJSONFields(packageJSON, theme, packageJSONValidationRules); theme.name = packageJSON.name; theme.version = packageJSON.version; @@ -117,7 +126,7 @@ module.exports = function checkPackageJSON(theme) { return theme; } }) - .catch(function (err) { + .catch((err) => { _.extend(theme.results.fail, _private.getFailedRules(_.values(_.omit(packageJSONValidationRules, ['isPresent'].concat(_.keys(packageJSONConditionalRules)))))); theme.results.fail[packageJSONValidationRules.canBeParsed].failures = [ diff --git a/lib/checks/020-theme-structure.js b/lib/checks/020-theme-structure.js index ea810700..0e77ac91 100644 --- a/lib/checks/020-theme-structure.js +++ b/lib/checks/020-theme-structure.js @@ -1,5 +1,8 @@ -var _ = require('lodash'), - checkThemeStructure; +const _ = require('lodash'); +const spec = require('../specs'); +const debug = require('ghost-ignition').debug('checks:theme-structure'); + +let checkThemeStructure; // TODO: template inspection //_.each(spec.templates, function (template) { @@ -16,28 +19,29 @@ var _ = require('lodash'), // } //}); -checkThemeStructure = function checkThemeStructure(theme) { - var checks = [ - { - path: 'index.hbs', - ruleCode: 'GS020-INDEX-REQ' - }, - { - path: 'post.hbs', - ruleCode: 'GS020-POST-REQ' - }, - { - path: 'default.hbs', - ruleCode: 'GS020-DEF-REC' +const v1RulesToCheck = ['GS020-INDEX-REQ', 'GS020-POST-REQ', 'GS020-DEF-REC']; + +const latestRulesToCheck = []; + +checkThemeStructure = function checkThemeStructure(theme, options) { + const checkVersion = _.get(options, 'checkVersion', 'latest'); + const ruleSet = spec.get([checkVersion]); + + const rulesToCheck = checkVersion === 'v1' ? v1RulesToCheck : _.union(v1RulesToCheck, latestRulesToCheck); + + _.each(rulesToCheck, function (ruleCode) { + let check = ruleSet.rules[ruleCode]; + + if (!check) { + debug(`Rule '${ruleCode}' is not defined in rulesest for version '${checkVersion}'`); + return; } - ]; - _.each(checks, function (check) { if (!_.some(theme.files, {file: check.path})) { // file doesn't exist - theme.results.fail[check.ruleCode] = {}; + theme.results.fail[ruleCode] = {}; } else { - theme.results.pass.push(check.ruleCode); + theme.results.pass.push(ruleCode); } }); diff --git a/lib/checks/030-assets.js b/lib/checks/030-assets.js index c02d438a..a16d7780 100644 --- a/lib/checks/030-assets.js +++ b/lib/checks/030-assets.js @@ -1,29 +1,35 @@ -var _ = require('lodash'), - path = require('path'), - fs = require('fs'), - checkAssets; - -checkAssets = function checkAssets(theme, themePath) { - var ruleCode = 'GS030-ASSET-REQ', - failures = [], - defaultHbs = _.filter(theme.files, {file: 'default.hbs'}), - assetRegex = /(src|href)=['"](.*?\/assets\/.*?)['"]/gmi, - assetMatch, stats; +const _ = require('lodash'); +const path = require('path'); +const fs = require('fs'); +const spec = require('../specs'); +// const debug = require('ghost-ignition').debug('checks:assets'); + +let checkAssets; + +checkAssets = function checkAssets(theme, options, themePath) { + const checkVersion = _.get(options, 'checkVersion', 'latest'); + const ruleSet = spec.get([checkVersion]); + + let ruleToCheck = 'GS030-ASSET-REQ'; + let failures = []; + let defaultHbs = _.filter(theme.files, {file: 'default.hbs'}); + let assetMatch; + let stats; if (!_.isEmpty(defaultHbs)) { defaultHbs = defaultHbs[0]; - while ((assetMatch = assetRegex.exec(defaultHbs.content)) !== null) { + while ((assetMatch = ruleSet.rules[ruleToCheck].regex.exec(defaultHbs.content)) !== null) { failures.push({ref: assetMatch[2]}); } } if (failures.length > 0) { - theme.results.fail[ruleCode] = {failures: failures}; + theme.results.fail[ruleToCheck] = {failures: failures}; } else { - theme.results.pass.push(ruleCode); + theme.results.pass.push(ruleToCheck); } - ruleCode = 'GS030-ASSET-SYM'; + ruleToCheck = 'GS030-ASSET-SYM'; failures = []; theme.files.forEach(function (theme) { @@ -39,9 +45,9 @@ checkAssets = function checkAssets(theme, themePath) { }); if (failures.length > 0) { - theme.results.fail[ruleCode] = {failures: failures}; + theme.results.fail[ruleToCheck] = {failures: failures}; } else { - theme.results.pass.push(ruleCode); + theme.results.pass.push(ruleToCheck); } return theme; diff --git a/lib/checks/040-ghost-head-foot.js b/lib/checks/040-ghost-head-foot.js index 65e9e0e2..4e8d6cef 100644 --- a/lib/checks/040-ghost-head-foot.js +++ b/lib/checks/040-ghost-head-foot.js @@ -1,23 +1,31 @@ -var _ = require('lodash'), - checkGhostHeadFoot; - -checkGhostHeadFoot = function checkGhostHeadFoot(theme) { - var checks = [ - { - helper: 'ghost_head', - ruleCode: 'GS040-GH-REQ' - }, - { - helper: 'ghost_foot', - ruleCode: 'GS040-GF-REQ' +const _ = require('lodash'); +const spec = require('../specs'); +const debug = require('ghost-ignition').debug('checks:ghost-head-foot'); + +let checkGhostHeadFoot; + +const v1RulesToCheck = ['GS040-GH-REQ', 'GS040-GF-REQ']; + +const latestRulesToCheck = []; + +checkGhostHeadFoot = function checkGhostHeadFoot(theme, options) { + const checkVersion = _.get(options, 'checkVersion', 'latest'); + const ruleSet = spec.get([checkVersion]); + + const rulesToCheck = checkVersion === 'v1' ? v1RulesToCheck : _.union(v1RulesToCheck, latestRulesToCheck); + + _.each(rulesToCheck, function (ruleCode) { + let check = ruleSet.rules[ruleCode]; + + if (!check) { + debug(`Rule '${ruleCode}' is not defined in rulesest for version '${checkVersion}'`); + return; } - ]; - _.each(checks, function (check) { if (!theme.helpers || !theme.helpers.hasOwnProperty(check.helper)) { - theme.results.fail[check.ruleCode] = {}; + theme.results.fail[ruleCode] = {}; } else { - theme.results.pass.push(check.ruleCode); + theme.results.pass.push(ruleCode); } }); diff --git a/lib/format.js b/lib/format.js index 6eadd0a0..e8542e22 100644 --- a/lib/format.js +++ b/lib/format.js @@ -1,12 +1,13 @@ -var _ = require('lodash'), - spec = require('./spec'), - format, - calcScore, - levelWeights = { - error: 10, - warning: 3, - recommendation: 1 - }; +const _ = require('lodash'); +const spec = require('./specs'); +const levelWeights = { + error: 10, + warning: 3, + recommendation: 1 +}; + +let format; +let calcScore; calcScore = function calcScore(results, stats) { var maxScore, actualScore, balancedScore; @@ -32,6 +33,8 @@ calcScore = function calcScore(results, stats) { */ format = function format(theme, options) { options = _.extend({onlyFatalErrors: false}, options); + const checkVersion = _.get(options, 'checkVersion', 'latest'); + const ruleSet = spec.get([checkVersion]); var processedCodes = [], hasFatalErrors = false, @@ -47,7 +50,7 @@ format = function format(theme, options) { theme.results.missing = []; _.each(theme.results.fail, function (info, code) { - var rule = spec.rules[code]; + const rule = ruleSet.rules[code]; if (rule.fatal && options.onlyFatalErrors || options.onlyFatalErrors === false) { if (rule.fatal) { @@ -63,14 +66,15 @@ format = function format(theme, options) { delete theme.results.fail; _.each(theme.results.pass, function (code, index) { - var rule = spec.rules[code]; + const rule = ruleSet.rules[code]; + theme.results.pass[index] = _.extend({}, rule, {code: code}); stats[rule.level] += 1; processedCodes.push(code); }); - _.each(_.difference(_.keys(spec.rules), processedCodes), function (code) { - theme.results.missing.push(spec.rules[code]); + _.each(_.difference(_.keys(ruleSet.rules), processedCodes), function (code) { + theme.results.missing.push(ruleSet.rules[code]); }); theme.results.score = calcScore(theme.results, stats); diff --git a/lib/specs/index.js b/lib/specs/index.js new file mode 100644 index 00000000..29593cfc --- /dev/null +++ b/lib/specs/index.js @@ -0,0 +1,5 @@ +module.exports = { + get: function get(key) { + return require(`./${key}`); + } +}; \ No newline at end of file diff --git a/lib/specs/latest.js b/lib/specs/latest.js new file mode 100644 index 00000000..cc3c2003 --- /dev/null +++ b/lib/specs/latest.js @@ -0,0 +1,28 @@ +const _ = require('lodash'); +const previousSpec = require('./v1'); + +const previousKnownHelpers = previousSpec.knownHelpers; +const previousTemplates = previousSpec.templates; +const previousRules = previousSpec.rules; + +let knownHelpers; +let templates; +let rules; + +knownHelpers = []; + +knownHelpers = _.union(previousKnownHelpers, knownHelpers); + +templates = []; + +templates = _.union(previousTemplates, templates); + +rules = {}; + +rules = _.merge(previousRules, rules); + +module.exports = { + knownHelpers: knownHelpers, + templates: templates, + rules: rules +}; diff --git a/lib/spec.js b/lib/specs/v1.js similarity index 78% rename from lib/spec.js rename to lib/specs/v1.js index 1c488a8d..e34a4f98 100644 --- a/lib/spec.js +++ b/lib/specs/v1.js @@ -80,14 +80,18 @@ rules = { rule: 'Replace {{pageUrl}} with {{page_url}}', fatal: true, details: 'The helper {{pageUrl}} was replaced with {{page_url}}.
' + - 'Find more information about the {{page_url}} helper here.' + 'Find more information about the {{page_url}} helper here.', + regex: /{{\s*?pageUrl\b[\w\s='"]*?}}/ig, + helper: '{{pageUrl}}' }, 'GS001-DEPR-MD': { level: 'error', rule: 'The usage of {{meta_description}} in HTML head is no longer required', details: 'The usage of {{meta_description}} in the HTML head tag is no longer required because Ghost outputs this for you automatically in {{ghost_head}}.
' + - 'Check out the documentation for {{meta_description}} here.
' + - 'To see, what else is rendered with the {{ghost_head}} helper, look here.' + 'Check out the documentation for {{meta_description}} here.
' + + 'To see, what else is rendered with the {{ghost_head}} helper, look here.', + regex: /{{image}} helper was replaced with the {{img_url}} helper.
' + 'Depending on the context of the {{img_url}} helper you would need to use e. g.

{{#post}}
    {{img_url feature_image}}
{{/post}}


to render the feature image of the blog post.
' + '
If you are using {{if image}}, then you have to replace it with e.g. {{if feature_image}}.' + - '

Find more information about the {{img_url}} helper here and ' + - 'read more about Ghost\'s usage of contexts here.' + '

Find more information about the {{img_url}} helper here and ' + + 'read more about Ghost\'s usage of contexts here.', + regex: /{{\s*?image\b[\w\s='"]*?}}/g, + helper: '{{image}}' }, 'GS001-DEPR-COV': { level: 'error', @@ -105,8 +111,10 @@ rules = { fatal: true, details: 'The cover attribute was replaced with cover_image. To render the cover image in author context, you need to use

' + '{{#author}}
    {{cover_image}}
{{/author}}


' + - 'See the object attributes of author here.
' + - 'To render the cover image of your blog, just use {{@blog.cover_image}}. See here.' + 'See the object attributes of author here.
' + + 'To render the cover image of your blog, just use {{@blog.cover_image}}. See here.', + regex: /{{\s*?cover\s*?}}/g, + helper: '{{cover}}' }, 'GS001-DEPR-AIMG': { level: 'error', @@ -114,7 +122,9 @@ rules = { fatal: true, details: 'The image attribute in author context was replaced with profile_image.
' + 'Instead of {{author.image}} you need to use {{author.profile_image}}.
' + - 'See the object attributes of author here.' + 'See the object attributes of author here.', + regex: /{{\s*?author\.image\s*?}}/g, + helper: '{{author.image}}' }, 'GS001-DEPR-PIMG': { level: 'error', @@ -122,7 +132,9 @@ rules = { fatal: true, details: 'The image attribute in post context was replaced with feature_image.
' + 'Instead of {{post.image}} you need to use {{post.feature_image}}.
' + - 'See the object attributes of post here.' + 'See the object attributes of post here.', + regex: /{{\s*?post\.image\s*?}}/g, + helper: '{{post.image}}' }, 'GS001-DEPR-BC': { level: 'error', @@ -130,7 +142,9 @@ rules = { fatal: true, details: 'The cover attribute was replaced with cover_image.
' + 'Instead of {{@blog.cover}} you need to use {{@blog.cover_image}}.
' + - 'See here.' + 'See here.', + regex: /{{\s*?@blog\.cover\s*?}}/g, + helper: '{{@blog.cover}}' }, 'GS001-DEPR-AC': { level: 'error', @@ -138,7 +152,9 @@ rules = { fatal: true, details: 'The cover attribute was replaced with cover_image.
' + 'Instead of {{author.cover}} you need to use {{author.cover_image}}.
' + - 'See the object attributes of author here.' + 'See the object attributes of author here.', + regex: /{{\s*?author\.cover\s*?}}/g, + helper: '{{author.cover}}' }, 'GS001-DEPR-TIMG': { level: 'error', @@ -146,7 +162,9 @@ rules = { fatal: true, details: 'The image attribute in tag context was replaced with feature_image.
' + 'Instead of {{tag.image}} you need to use {{tag.feature_image}}.
' + - 'See the object attributes of tags here.' + 'See the object attributes of tags here.', + regex: /{{\s*?tag\.image\s*?}}/g, + helper: '{{tag.image}}' }, 'GS001-DEPR-PAIMG': { level: 'error', @@ -154,7 +172,9 @@ rules = { fatal: true, details: 'The image attribute in author context was replaced with feature_image.
' + 'Instead of {{post.author.image}} you need to use {{post.author.feature_image}}.
' + - 'See the object attributes of author here.' + 'See the object attributes of author here.', + regex: /{{\s*?post\.author\.image\s*?}}/g, + helper: '{{post.author.image}}' }, 'GS001-DEPR-PAC': { level: 'error', @@ -162,7 +182,9 @@ rules = { fatal: true, details: 'The cover attribute in author context was replaced with cover_image.
' + 'Instead of {{post.author.cover}} you need to use {{post.author.cover_image}}.
' + - 'See the object attributes of author here.' + 'See the object attributes of author here.', + regex: /{{\s*?post\.author\.cover\s*?}}/g, + helper: '{{post.author.cover}}' }, 'GS001-DEPR-PTIMG': { level: 'error', @@ -170,7 +192,9 @@ rules = { fatal: true, details: 'The image attribute in tag context was replaced with feature_image.
' + 'Instead of {{post.tags.[#].image}} you need to use {{post.tags.[#].feature_image}}.
' + - 'See the object attributes of tags here.' + 'See the object attributes of tags here.', + regex: /{{\s*?post\.tags\.\[[0-9]+\]\.image\s*?}}/g, + helper: '{{post.tags.[#].image}}' }, 'GS001-DEPR-TSIMG': { level: 'error', @@ -178,7 +202,9 @@ rules = { fatal: true, details: 'The image attribute in tag context was replaced with feature_image.
' + 'Instead of {{tags.[#].image}} you need to use {{tags.[#].feature_image}}.
' + - 'See the object attributes of tags here.' + 'See the object attributes of tags here.', + regex: /{{\s*?tags\.\[[0-9]+\]\.image\s*?}}/g, + helper: '{{tags.[#].image}}' }, 'GS001-DEPR-CON-IMG': { level: 'error', @@ -198,7 +224,9 @@ rules = { '{{#tag}}
' + '    {{#if feature_image}}
        {{feature_image}}
    {{/if}}
' + '{{/tag}}


' + - 'See the object attributes of tags here.' + 'See the object attributes of tags here.', + regex: /{{\s*?#if\s*?image\s*?}}/g, + helper: '{{#if image}}' }, 'GS001-DEPR-CON-COV': { level: 'error', @@ -206,8 +234,10 @@ rules = { fatal: true, details: 'The cover attribute was replaced with cover_image. To check for the cover image in author context, you need to use

' + '{{#if cover_image}}
    {{cover_image}}
{{/if}}


' + - 'See the object attributes of author here.
' + - 'To check for the cover image of your blog, just use {{#if @blog.cover_image}}. See here.' + 'See the object attributes of author here.
' + + 'To check for the cover image of your blog, just use {{#if @blog.cover_image}}. See here.', + regex: /{{\s*?#if\s*?cover\s*?}}/g, + helper: '{{#if cover}}' }, 'GS001-DEPR-CON-BC': { level: 'error', @@ -215,7 +245,9 @@ rules = { fatal: true, details: 'The cover attribute was replaced with cover_image.
' + 'Instead of {{#if @blog.cover}} you need to use {{#if @blog.cover_image}}.
' + - 'See here.' + 'See here.', + regex: /{{\s*?#if\s*?@blog\.cover\s*?}}/g, + helper: '{{#if @blog.cover}}' }, 'GS001-DEPR-CON-AC': { level: 'error', @@ -223,7 +255,9 @@ rules = { fatal: true, details: 'The cover attribute was replaced with cover_image.
' + 'Instead of {{#if author.cover}} you need to use {{#if author.cover_image}}.
' + - 'See the object attributes of author here.' + 'See the object attributes of author here.', + regex: /{{\s*?#if\s*?author\.cover\s*?}}/g, + helper: '{{#if author.cover}}' }, 'GS001-DEPR-CON-AIMG': { level: 'error', @@ -231,7 +265,9 @@ rules = { fatal: true, details: 'The image attribute in author context was replaced with profile_image.
' + 'Instead of {{#if author.image}} you need to use {{#if author.profile_image}}.
' + - 'See the object attributes of author here.' + 'See the object attributes of author here.', + regex: /{{\s*?#if\s*?author\.image\s*?}}/g, + helper: '{{#if author.image}}' }, 'GS001-DEPR-CON-TIMG': { level: 'error', @@ -239,7 +275,9 @@ rules = { fatal: true, details: 'The image attribute in tag context was replaced with feature_image.
' + 'Instead of {{#if tag.image}} you need to use {{#if tag.feature_image}}.
' + - 'See the object attributes of tags here.' + 'See the object attributes of tags here.', + regex: /{{\s*?#if\s*?tag\.image\s*?}}/g, + helper: '{{#if tag.image}}' }, 'GS001-DEPR-CON-PTIMG': { level: 'error', @@ -247,7 +285,9 @@ rules = { fatal: true, details: 'The image attribute in tag context was replaced with feature_image.
' + 'Instead of {{#if post.tags.[#].image}} you need to use {{#if post.tags.[#].feature_image}}.
' + - 'See the object attributes of tags here.' + 'See the object attributes of tags here.', + regex: /{{\s*?#if\s*?post\.tags\.\[[0-9]+\].image\s*?}}/g, + helper: '{{#if posts.tags.[#].image}}' }, 'GS001-DEPR-CON-TSIMG': { level: 'error', @@ -255,38 +295,53 @@ rules = { fatal: true, details: 'The image attribute in tag context was replaced with feature_image.
' + 'Instead of {{#if tags.[#].image}} you need to use {{#if tags.[#].feature_image}}.
' + - 'See the object attributes of tags here.' + 'See the object attributes of tags here.', + regex: /{{\s*?#if\s*?tags\.\[[0-9]+\].image\s*?}}/g, + helper: '{{#if tags.[#].image}}' }, 'GS001-DEPR-PPP': { level: 'error', rule: 'Replace {{@blog.posts_per_page}} with {{@config.posts_per_page}}', details: 'The global {{@blog.posts_per_page}} property was replaced with {{@config.posts_per_page}}.
' + - 'Read here about the attribute and ' + - 'check here where you can customise the posts per page setting, as this is now adjustable in your theme.' + 'Read here about the attribute and ' + + 'check here where you can customise the posts per page setting, as this is now adjustable in your theme.', + regex: /{{\s*?@blog\.posts_per_page\s*?}}/g, + helper: '{{@blog.posts_per_page}}' }, 'GS001-DEPR-C0H': { level: 'error', rule: 'Replace {{content words="0"}} with the {{img_url}} helper', details: 'The {{content words="0"}} hack doesn\'t work anymore (and was never supported). Please use the {{img_url}} helper to render images.
' + - 'Find more information about the {{img_url}} helper here and ' + 'Find more information about the {{img_url}} helper here and ', + regex: /{{\s*?content words=("|')0("|')\s*?}}/g, + helper: '{{content words="0"}}' }, 'GS001-DEPR-CSS-AT': { level: 'error', rule: 'Replace .archive-template with the .paged CSS class', details: 'The .archive-template CSS class was replaced with the .paged. Please replace this in your stylesheet.
' + - 'See the context table to check which classes Ghost uses for each context.' + 'See the context table to check which classes Ghost uses for each context.', + regex: /\.archive-template[\s{]/g, + className: '.archive-template', + css: true }, 'GS001-DEPR-CSS-PA': { level: 'error', rule: 'Replace .page with the .page-template css class', details: 'The .page CSS class was replaced with the .page-template. Please replace this in your stylesheet.
' + - 'See the context table to check which classes Ghost uses for each context.' + 'See the context table to check which classes Ghost uses for each context.', + regex: /\.page[\s{]/g, + className: '.page', + css: true }, 'GS001-DEPR-CSS-PATS': { level: 'error', rule: 'Replace .page-template-slug with the .page-slug css class', details: 'The .page-template-slug CSS class was replaced with the .page-slug. Please replace this in your stylesheet.
' + - 'See the context table to check which classes Ghost uses for each context.' + 'See the context table to check which classes Ghost uses for each context.', + regex: /\.page-template-\w+[\s{]/g, + className: '.page-template-slug', + css: true }, 'GS002-DISQUS-ID': { level: 'error', @@ -297,10 +352,12 @@ rules = { 'To resolve this, we\'ve added a new {{comment_id}} helper that will output the old ID ' + 'for posts that have been imported from LTS, and the new ID for new posts. ' + 'The Disqus embed must be updated from this.page.identifier = \'ghost-{{id}}\'; to ' + - 'this.page.identifier = \'ghost-{{comment_id}}\'; to ensure Disqus continues to work.' + 'this.page.identifier = \'ghost-{{comment_id}}\'; to ensure Disqus continues to work.', + regex: /(page\.|disqus_)identifier\s?=\s?['"].*?({{\s*?id\s*?}}).*?['"];?/g }, 'GS002-ID-HELPER': { level: 'recommendation', + version: 'v1', rule: 'The output of {{id}} changed between Ghost LTS and 1.0.0, you may need to use {{comment_id}} instead.', details: 'The output of {{id}} has changed between Ghost LTS and 1.0.0. ' + @@ -308,7 +365,8 @@ rules = { 'We\'ve added a new {{comment_id}} helper that will output the old ID ' + 'for posts that have been imported from LTS, and the new ID for new posts. ' + 'If you need the old ID to be output on imported posts, then you will need to use ' + - '{{comment_id}} rather than {{id}}.' + '{{comment_id}} rather than {{id}}.', + regex: /({{\s*?id\s*?}})/g }, 'GS005-TPL-ERR': { level: 'error', @@ -395,26 +453,30 @@ rules = { rule: 'A template file called index.hbs must be present', fatal: true, details: 'Your theme must have a template file called index.hbs.
' + - 'Read here more about the required template structure and index.hbs in particular.' + 'Read here more about the required template structure and index.hbs in particular.', + path: 'index.hbs' }, 'GS020-POST-REQ': { level: 'error', rule: 'A template file called post.hbs must be present', fatal: true, details: 'Your theme must have a template file called index.hbs.
' + - 'Read here more about the required template structure and post.hbs in particular.' + 'Read here more about the required template structure and post.hbs in particular.', + path: 'post.hbs' }, 'GS020-DEF-REC': { level: 'recommendation', rule: 'Provide a default layout template called default.hbs', details: 'It is recommended that your theme has a template file called default.hbs.
' + - 'Read here more about the recommended template structure and default.hbs in particular.' + 'Read here more about the recommended template structure and default.hbs in particular.', + path: 'default.hbs' }, 'GS030-ASSET-REQ': { level: 'warning', rule: 'Assets such as CSS & JS must use the {{asset}} helper', details: 'The listed files should be included using the {{asset}} helper.
' + - 'For more information, please see the {{asset}} helper documentation.' + 'For more information, please see the {{asset}} helper documentation.', + regex: /(src|href)=['"](.*?\/assets\/.*?)['"]/gmi }, 'GS030-ASSET-SYM': { level: 'error', @@ -428,14 +490,16 @@ rules = { rule: 'The helper {{ghost_head}} should be present', details: 'The {{ghost_head}} helper should be present in your theme. It outputs many useful things, such as "code injection" scripts, structured data, canonical links, meta description etc.
' + 'The helper belongs just before the tag in your default.hbs template.
' + - 'For more details, please see the {{ghost_head}} helper documentation.' + 'For more details, please see the {{ghost_head}} helper documentation.', + helper: 'ghost_head' }, 'GS040-GF-REQ': { level: 'warning', rule: 'The helper {{ghost_foot}} should be present', details: 'The {{ghost_foot}} helper should be present in your theme. It outputs scripts as saved in "code injection" scripts.
' + 'The helper belongs just before the tag in your default.hbs template.
' + - 'For more details, please see the {{ghost_foot}} helper documentation.' + 'For more details, please see the {{ghost_foot}} helper documentation.', + helper: 'ghost_foot' } }; diff --git a/test/001-deprecations.test.js b/test/001-deprecations.test.js index 162d6b47..fc77ad94 100644 --- a/test/001-deprecations.test.js +++ b/test/001-deprecations.test.js @@ -3,168 +3,342 @@ var should = require('should'), // eslint-disable-line no-unused-vars utils = require('./utils'); describe('001 Deprecations', function () { - it('[failure] theme is invalid', function (done) { - utils.testCheck(thisCheck, '001-deprecations/invalid').then(function (output) { - output.should.be.a.ValidThemeObject(); - - output.results.fail.should.be.an.Object().with.keys( - 'GS001-DEPR-PURL', - 'GS001-DEPR-MD', - 'GS001-DEPR-IMG', - 'GS001-DEPR-COV', - 'GS001-DEPR-AIMG', - 'GS001-DEPR-PIMG', - 'GS001-DEPR-PAIMG', - 'GS001-DEPR-PAC', - 'GS001-DEPR-PTIMG', - 'GS001-DEPR-TSIMG', - 'GS001-DEPR-PPP', - 'GS001-DEPR-C0H', - 'GS001-DEPR-BC', - 'GS001-DEPR-CON-BC', - 'GS001-DEPR-AC', - 'GS001-DEPR-CON-AC', - 'GS001-DEPR-CON-AIMG', - 'GS001-DEPR-CON-PTIMG', - 'GS001-DEPR-CON-TSIMG', - 'GS001-DEPR-CON-IMG', - 'GS001-DEPR-CON-COV', - 'GS001-DEPR-CON-TIMG', - 'GS001-DEPR-TIMG', - 'GS001-DEPR-CSS-AT', - 'GS001-DEPR-CSS-PA', - 'GS001-DEPR-CSS-PATS' - ); - - // pageUrl - output.results.fail['GS001-DEPR-PURL'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-PURL'].failures.length.should.eql(3); - - // meta_description in - output.results.fail['GS001-DEPR-MD'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-MD'].failures.length.should.eql(1); - - // {{image}} - output.results.fail['GS001-DEPR-IMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-IMG'].failures.length.should.eql(2); - - // {{cover}} - output.results.fail['GS001-DEPR-COV'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-COV'].failures.length.should.eql(3); - - // {{author.image}} - output.results.fail['GS001-DEPR-AIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-AIMG'].failures.length.should.eql(2); - - // {{post.image}} - output.results.fail['GS001-DEPR-PIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-PIMG'].failures.length.should.eql(1); - - // {{@blog.cover}} - output.results.fail['GS001-DEPR-BC'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-BC'].failures.length.should.eql(1); - - // {{author.cover}} - output.results.fail['GS001-DEPR-AC'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-AC'].failures.length.should.eql(2); - - // {{post.author.cover}} - output.results.fail['GS001-DEPR-PAC'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-PAC'].failures.length.should.eql(1); - - // {{post.author.image}} - output.results.fail['GS001-DEPR-PAIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-PAIMG'].failures.length.should.eql(1); - - // {{tag.image}} - output.results.fail['GS001-DEPR-TIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-TIMG'].failures.length.should.eql(1); - - // {{posts.tags.[4].image}} - output.results.fail['GS001-DEPR-PTIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-PTIMG'].failures.length.should.eql(1); - - // {{tags.[4].image}} - output.results.fail['GS001-DEPR-TSIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-TSIMG'].failures.length.should.eql(1); - - // {{#if image}} - output.results.fail['GS001-DEPR-CON-IMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CON-IMG'].failures.length.should.eql(1); - - // {{#if cover}} - output.results.fail['GS001-DEPR-CON-COV'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CON-COV'].failures.length.should.eql(1); - - // {{#if tag.image}} - output.results.fail['GS001-DEPR-CON-TIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CON-TIMG'].failures.length.should.eql(1); - - // {{#if tags.[#].image}} - output.results.fail['GS001-DEPR-CON-TSIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CON-TSIMG'].failures.length.should.eql(1); - - // {{#if post.tags.[#].image}} - output.results.fail['GS001-DEPR-CON-PTIMG'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CON-PTIMG'].failures.length.should.eql(1); - - // {{@blog.posts_per_page}} - output.results.fail['GS001-DEPR-PPP'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-PPP'].failures.length.should.eql(1); - - // {{content word="0"}} - output.results.fail['GS001-DEPR-C0H'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-C0H'].failures.length.should.eql(2); - - // css class .page - output.results.fail['GS001-DEPR-CSS-PA'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CSS-PA'].failures.length.should.eql(2); - - // css class .page-template-{slug} - output.results.fail['GS001-DEPR-CSS-PATS'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CSS-PATS'].failures.length.should.eql(2); - - // css class .achive-template - output.results.fail['GS001-DEPR-CSS-AT'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-CSS-AT'].failures.length.should.eql(1); - - output.results.pass.should.be.an.Object().which.is.empty(); - - done(); - }).catch(done); - }); - - it('[success] should show no error if no deprecated helpers used', function (done) { - utils.testCheck(thisCheck, '001-deprecations/valid').then(function (output) { - output.should.be.a.ValidThemeObject(); - - output.results.fail.should.be.an.Object().which.is.empty(); - output.results.pass.should.be.an.Array().with.lengthOf(26); - - done(); - }).catch(done); + describe('older version passed', function () { + const options = {checkVersion: 'v1'}; + + it('[failure] theme is invalid', function (done) { + utils.testCheck(thisCheck, '001-deprecations/invalid', options).then(function (output) { + output.should.be.a.ValidThemeObject(); + + output.results.fail.should.be.an.Object().with.keys( + 'GS001-DEPR-PURL', + 'GS001-DEPR-MD', + 'GS001-DEPR-IMG', + 'GS001-DEPR-COV', + 'GS001-DEPR-AIMG', + 'GS001-DEPR-PIMG', + 'GS001-DEPR-PAIMG', + 'GS001-DEPR-PAC', + 'GS001-DEPR-PTIMG', + 'GS001-DEPR-TSIMG', + 'GS001-DEPR-PPP', + 'GS001-DEPR-C0H', + 'GS001-DEPR-BC', + 'GS001-DEPR-CON-BC', + 'GS001-DEPR-AC', + 'GS001-DEPR-CON-AC', + 'GS001-DEPR-CON-AIMG', + 'GS001-DEPR-CON-PTIMG', + 'GS001-DEPR-CON-TSIMG', + 'GS001-DEPR-CON-IMG', + 'GS001-DEPR-CON-COV', + 'GS001-DEPR-CON-TIMG', + 'GS001-DEPR-TIMG', + 'GS001-DEPR-CSS-AT', + 'GS001-DEPR-CSS-PA', + 'GS001-DEPR-CSS-PATS' + ); + + // pageUrl + output.results.fail['GS001-DEPR-PURL'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PURL'].failures.length.should.eql(3); + + // meta_description in + output.results.fail['GS001-DEPR-MD'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-MD'].failures.length.should.eql(1); + + // {{image}} + output.results.fail['GS001-DEPR-IMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-IMG'].failures.length.should.eql(2); + + // {{cover}} + output.results.fail['GS001-DEPR-COV'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-COV'].failures.length.should.eql(3); + + // {{author.image}} + output.results.fail['GS001-DEPR-AIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-AIMG'].failures.length.should.eql(2); + + // {{post.image}} + output.results.fail['GS001-DEPR-PIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PIMG'].failures.length.should.eql(1); + + // {{@blog.cover}} + output.results.fail['GS001-DEPR-BC'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-BC'].failures.length.should.eql(1); + + // {{author.cover}} + output.results.fail['GS001-DEPR-AC'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-AC'].failures.length.should.eql(2); + + // {{post.author.cover}} + output.results.fail['GS001-DEPR-PAC'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PAC'].failures.length.should.eql(1); + + // {{post.author.image}} + output.results.fail['GS001-DEPR-PAIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PAIMG'].failures.length.should.eql(1); + + // {{tag.image}} + output.results.fail['GS001-DEPR-TIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-TIMG'].failures.length.should.eql(1); + + // {{posts.tags.[4].image}} + output.results.fail['GS001-DEPR-PTIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PTIMG'].failures.length.should.eql(1); + + // {{tags.[4].image}} + output.results.fail['GS001-DEPR-TSIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-TSIMG'].failures.length.should.eql(1); + + // {{#if image}} + output.results.fail['GS001-DEPR-CON-IMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-IMG'].failures.length.should.eql(1); + + // {{#if cover}} + output.results.fail['GS001-DEPR-CON-COV'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-COV'].failures.length.should.eql(1); + + // {{#if tag.image}} + output.results.fail['GS001-DEPR-CON-TIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-TIMG'].failures.length.should.eql(1); + + // {{#if tags.[#].image}} + output.results.fail['GS001-DEPR-CON-TSIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-TSIMG'].failures.length.should.eql(1); + + // {{#if post.tags.[#].image}} + output.results.fail['GS001-DEPR-CON-PTIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-PTIMG'].failures.length.should.eql(1); + + // {{@blog.posts_per_page}} + output.results.fail['GS001-DEPR-PPP'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PPP'].failures.length.should.eql(1); + + // {{content word="0"}} + output.results.fail['GS001-DEPR-C0H'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-C0H'].failures.length.should.eql(2); + + // css class .page + output.results.fail['GS001-DEPR-CSS-PA'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CSS-PA'].failures.length.should.eql(2); + + // css class .page-template-{slug} + output.results.fail['GS001-DEPR-CSS-PATS'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CSS-PATS'].failures.length.should.eql(2); + + // css class .achive-template + output.results.fail['GS001-DEPR-CSS-AT'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CSS-AT'].failures.length.should.eql(1); + + output.results.pass.should.be.an.Object().which.is.empty(); + + done(); + }).catch(done); + }); + + it('[success] should show no error if no deprecated helpers used', function (done) { + utils.testCheck(thisCheck, '001-deprecations/valid', options).then(function (output) { + output.should.be.a.ValidThemeObject(); + + output.results.fail.should.be.an.Object().which.is.empty(); + output.results.pass.should.be.an.Array().with.lengthOf(26); + + done(); + }).catch(done); + }); + + it('[mixed] should pass and fail when some rules pass and others fail', function (done) { + utils.testCheck(thisCheck, '001-deprecations/mixed', options).then(function (output) { + output.should.be.a.ValidThemeObject(); + + output.results.fail.should.be.an.Object().with.keys( + 'GS001-DEPR-PURL', + 'GS001-DEPR-MD', + 'GS001-DEPR-IMG', + 'GS001-DEPR-COV', + 'GS001-DEPR-PIMG', + 'GS001-DEPR-BC', + 'GS001-DEPR-TIMG', + 'GS001-DEPR-C0H' + ); + + output.results.fail['GS001-DEPR-PURL'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PURL'].failures.length.should.eql(2); + output.results.pass.should.be.an.Array().with.lengthOf(18); + + done(); + }).catch(done); + }); + + // TODO: write this as soon as we have new rules + it('does not check for latest versions checks'); }); - it('[mixed] should pass and fail when some rules pass and others fail', function (done) { - utils.testCheck(thisCheck, '001-deprecations/mixed').then(function (output) { - output.should.be.a.ValidThemeObject(); - - output.results.fail.should.be.an.Object().with.keys( - 'GS001-DEPR-PURL', - 'GS001-DEPR-MD', - 'GS001-DEPR-IMG', - 'GS001-DEPR-COV', - 'GS001-DEPR-PIMG', - 'GS001-DEPR-BC', - 'GS001-DEPR-TIMG', - 'GS001-DEPR-C0H' - ); - - output.results.fail['GS001-DEPR-PURL'].should.be.a.ValidFailObject(); - output.results.fail['GS001-DEPR-PURL'].failures.length.should.eql(2); - output.results.pass.should.be.an.Array().with.lengthOf(18); - - done(); - }).catch(done); + describe('latest version', function () { + it('[failure] theme is invalid', function (done) { + utils.testCheck(thisCheck, '001-deprecations/invalid').then(function (output) { + output.should.be.a.ValidThemeObject(); + + output.results.fail.should.be.an.Object().with.keys( + 'GS001-DEPR-PURL', + 'GS001-DEPR-MD', + 'GS001-DEPR-IMG', + 'GS001-DEPR-COV', + 'GS001-DEPR-AIMG', + 'GS001-DEPR-PIMG', + 'GS001-DEPR-PAIMG', + 'GS001-DEPR-PAC', + 'GS001-DEPR-PTIMG', + 'GS001-DEPR-TSIMG', + 'GS001-DEPR-PPP', + 'GS001-DEPR-C0H', + 'GS001-DEPR-BC', + 'GS001-DEPR-CON-BC', + 'GS001-DEPR-AC', + 'GS001-DEPR-CON-AC', + 'GS001-DEPR-CON-AIMG', + 'GS001-DEPR-CON-PTIMG', + 'GS001-DEPR-CON-TSIMG', + 'GS001-DEPR-CON-IMG', + 'GS001-DEPR-CON-COV', + 'GS001-DEPR-CON-TIMG', + 'GS001-DEPR-TIMG', + 'GS001-DEPR-CSS-AT', + 'GS001-DEPR-CSS-PA', + 'GS001-DEPR-CSS-PATS' + ); + + // pageUrl + output.results.fail['GS001-DEPR-PURL'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PURL'].failures.length.should.eql(3); + + // meta_description in + output.results.fail['GS001-DEPR-MD'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-MD'].failures.length.should.eql(1); + + // {{image}} + output.results.fail['GS001-DEPR-IMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-IMG'].failures.length.should.eql(2); + + // {{cover}} + output.results.fail['GS001-DEPR-COV'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-COV'].failures.length.should.eql(3); + + // {{author.image}} + output.results.fail['GS001-DEPR-AIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-AIMG'].failures.length.should.eql(2); + + // {{post.image}} + output.results.fail['GS001-DEPR-PIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PIMG'].failures.length.should.eql(1); + + // {{@blog.cover}} + output.results.fail['GS001-DEPR-BC'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-BC'].failures.length.should.eql(1); + + // {{author.cover}} + output.results.fail['GS001-DEPR-AC'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-AC'].failures.length.should.eql(2); + + // {{post.author.cover}} + output.results.fail['GS001-DEPR-PAC'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PAC'].failures.length.should.eql(1); + + // {{post.author.image}} + output.results.fail['GS001-DEPR-PAIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PAIMG'].failures.length.should.eql(1); + + // {{tag.image}} + output.results.fail['GS001-DEPR-TIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-TIMG'].failures.length.should.eql(1); + + // {{posts.tags.[4].image}} + output.results.fail['GS001-DEPR-PTIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PTIMG'].failures.length.should.eql(1); + + // {{tags.[4].image}} + output.results.fail['GS001-DEPR-TSIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-TSIMG'].failures.length.should.eql(1); + + // {{#if image}} + output.results.fail['GS001-DEPR-CON-IMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-IMG'].failures.length.should.eql(1); + + // {{#if cover}} + output.results.fail['GS001-DEPR-CON-COV'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-COV'].failures.length.should.eql(1); + + // {{#if tag.image}} + output.results.fail['GS001-DEPR-CON-TIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-TIMG'].failures.length.should.eql(1); + + // {{#if tags.[#].image}} + output.results.fail['GS001-DEPR-CON-TSIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-TSIMG'].failures.length.should.eql(1); + + // {{#if post.tags.[#].image}} + output.results.fail['GS001-DEPR-CON-PTIMG'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CON-PTIMG'].failures.length.should.eql(1); + + // {{@blog.posts_per_page}} + output.results.fail['GS001-DEPR-PPP'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PPP'].failures.length.should.eql(1); + + // {{content word="0"}} + output.results.fail['GS001-DEPR-C0H'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-C0H'].failures.length.should.eql(2); + + // css class .page + output.results.fail['GS001-DEPR-CSS-PA'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CSS-PA'].failures.length.should.eql(2); + + // css class .page-template-{slug} + output.results.fail['GS001-DEPR-CSS-PATS'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CSS-PATS'].failures.length.should.eql(2); + + // css class .achive-template + output.results.fail['GS001-DEPR-CSS-AT'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-CSS-AT'].failures.length.should.eql(1); + + output.results.pass.should.be.an.Object().which.is.empty(); + + done(); + }).catch(done); + }); + + it('[success] should show no error if no deprecated helpers used', function (done) { + utils.testCheck(thisCheck, '001-deprecations/valid').then(function (output) { + output.should.be.a.ValidThemeObject(); + + output.results.fail.should.be.an.Object().which.is.empty(); + output.results.pass.should.be.an.Array().with.lengthOf(26); + + done(); + }).catch(done); + }); + + it('[mixed] should pass and fail when some rules pass and others fail', function (done) { + utils.testCheck(thisCheck, '001-deprecations/mixed').then(function (output) { + output.should.be.a.ValidThemeObject(); + + output.results.fail.should.be.an.Object().with.keys( + 'GS001-DEPR-PURL', + 'GS001-DEPR-MD', + 'GS001-DEPR-IMG', + 'GS001-DEPR-COV', + 'GS001-DEPR-PIMG', + 'GS001-DEPR-BC', + 'GS001-DEPR-TIMG', + 'GS001-DEPR-C0H' + ); + + output.results.fail['GS001-DEPR-PURL'].should.be.a.ValidFailObject(); + output.results.fail['GS001-DEPR-PURL'].failures.length.should.eql(2); + output.results.pass.should.be.an.Array().with.lengthOf(18); + + done(); + }).catch(done); + }); }); }); diff --git a/test/005-template-compile.test.js b/test/005-template-compile.test.js index eb06ae1f..cc8c4da0 100644 --- a/test/005-template-compile.test.js +++ b/test/005-template-compile.test.js @@ -3,8 +3,10 @@ var should = require('should'), // eslint-disable-line no-unused-vars thisCheck = require('../lib/checks/005-template-compile'); describe('Template compile', function () { + const options = {checkVersion: 'v1'}; + it('should output empty array for a theme with no templates', function (done) { - utils.testCheck(thisCheck, 'is-empty').then(function (output) { + utils.testCheck(thisCheck, 'is-empty', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.fail.should.be.an.Object().which.is.empty(); @@ -16,7 +18,7 @@ describe('Template compile', function () { }); it('should output empty array for a theme with valid templates', function (done) { - utils.testCheck(thisCheck, '005-compile/valid').then(function (output) { + utils.testCheck(thisCheck, '005-compile/valid', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.fail.should.be.an.Object().which.is.empty(); @@ -29,7 +31,7 @@ describe('Template compile', function () { }); it('should output errors for a theme with invalid templates', function (done) { - utils.testCheck(thisCheck, '005-compile/invalid').then(function (output) { + utils.testCheck(thisCheck, '005-compile/invalid', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().which.is.empty(); @@ -57,7 +59,7 @@ describe('Template compile', function () { }); it('theme with partials and unknown helper', function (done) { - utils.testCheck(thisCheck, 'theme-with-partials').then(function (output) { + utils.testCheck(thisCheck, 'theme-with-partials', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.fail.should.be.an.Object().with.keys('GS005-TPL-ERR'); diff --git a/test/010-package-json.test.js b/test/010-package-json.test.js index 72222072..e9793dac 100644 --- a/test/010-package-json.test.js +++ b/test/010-package-json.test.js @@ -3,8 +3,10 @@ var should = require('should'), // eslint-disable-line no-unused-vars thisCheck = require('../lib/checks/010-package-json'); describe('010: package.json', function () { + const options = {checkVersion: 'v1'}; + it('should output error for missing package.json', function (done) { - utils.testCheck(thisCheck, 'is-empty').then(function (output) { + utils.testCheck(thisCheck, 'is-empty', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().with.lengthOf(0); @@ -29,7 +31,7 @@ describe('010: package.json', function () { }); it('should output error for missing package.json', function (done) { - utils.testCheck(thisCheck, 'is-empty').then(function (output) { + utils.testCheck(thisCheck, 'is-empty', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().with.lengthOf(0); @@ -54,7 +56,7 @@ describe('010: package.json', function () { }); it('should output error for invalid package.json (parsing)', function (done) { - utils.testCheck(thisCheck, '010-packagejson/parse-error').then(function (output) { + utils.testCheck(thisCheck, '010-packagejson/parse-error', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().with.lengthOf(0); @@ -82,7 +84,7 @@ describe('010: package.json', function () { }); it('valid fields', function (done) { - utils.testCheck(thisCheck, '010-packagejson/fields-are-valid').then(function (theme) { + utils.testCheck(thisCheck, '010-packagejson/fields-are-valid', options).then(function (theme) { theme.should.be.a.ValidThemeObject(); theme.results.pass.should.eql([ @@ -105,7 +107,7 @@ describe('010: package.json', function () { }); it('invalid fields', function (done) { - utils.testCheck(thisCheck, '010-packagejson/fields-are-invalid').then(function (theme) { + utils.testCheck(thisCheck, '010-packagejson/fields-are-invalid', options).then(function (theme) { theme.should.be.a.ValidThemeObject(); theme.results.pass.should.eql([ @@ -129,7 +131,7 @@ describe('010: package.json', function () { }); it('missing fields', function (done) { - utils.testCheck(thisCheck, '010-packagejson/fields-are-missing').then(function (theme) { + utils.testCheck(thisCheck, '010-packagejson/fields-are-missing', options).then(function (theme) { theme.should.be.a.ValidThemeObject(); theme.results.pass.should.eql([ @@ -153,7 +155,7 @@ describe('010: package.json', function () { }); it('bad config (ppp: -3 > 0)', function (done) { - utils.testCheck(thisCheck, '010-packagejson/bad-config').then(function (theme) { + utils.testCheck(thisCheck, '010-packagejson/bad-config', options).then(function (theme) { theme.should.be.a.ValidThemeObject(); theme.results.pass.should.eql([ @@ -177,7 +179,7 @@ describe('010: package.json', function () { }); it('bad config 2 (ppp: 0 > 0)', function (done) { - utils.testCheck(thisCheck, '010-packagejson/bad-config-2').then(function (theme) { + utils.testCheck(thisCheck, '010-packagejson/bad-config-2', options).then(function (theme) { theme.should.be.a.ValidThemeObject(); theme.results.pass.should.eql([ diff --git a/test/020-theme-structure.test.js b/test/020-theme-structure.test.js index 8a5a5dc4..87cf1af2 100644 --- a/test/020-theme-structure.test.js +++ b/test/020-theme-structure.test.js @@ -4,8 +4,10 @@ var should = require('should'), // eslint-disable-line no-unused-vars thisCheck = require('../lib/checks/020-theme-structure'); describe('Theme structure', function () { + const options = {checkVersion: 'v1'}; + it('should fail all rules if no files present', function (done) { - utils.testCheck(thisCheck, 'is-empty').then(function (output) { + utils.testCheck(thisCheck, 'is-empty', options).then(function (output) { output.should.be.a.ValidThemeObject(); // Should not pass any rules @@ -21,7 +23,7 @@ describe('Theme structure', function () { }); it('should pass and fail when some rules pass and others fail', function (done) { - utils.testCheck(thisCheck, '020-structure/mixed').then(function (output) { + utils.testCheck(thisCheck, '020-structure/mixed', options).then(function (output) { output.should.be.a.ValidThemeObject(); // Should pass the index rule @@ -38,7 +40,7 @@ describe('Theme structure', function () { }); it('should still fail with just a recommendation', function (done) { - utils.testCheck(thisCheck, '020-structure/recommendation').then(function (output) { + utils.testCheck(thisCheck, '020-structure/recommendation', options).then(function (output) { output.should.be.a.ValidThemeObject(); // Should not pass any rules diff --git a/test/030-assets.test.js b/test/030-assets.test.js index 9bb72ce0..f5cd5b5a 100644 --- a/test/030-assets.test.js +++ b/test/030-assets.test.js @@ -3,8 +3,10 @@ var should = require('should'), // eslint-disable-line no-unused-vars thisCheck = require('../lib/checks/030-assets'); describe('Assets', function () { + const options = {checkVersion: 'v1'}; + it('should show a warning for missing asset helper when an asset is detected', function (done) { - utils.testCheck(thisCheck, '030-assets/missing').then(function (output) { + utils.testCheck(thisCheck, '030-assets/missing', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().with.lengthOf(1); @@ -22,7 +24,7 @@ describe('Assets', function () { }); it('should pass when asset helper is present', function (done) { - utils.testCheck(thisCheck, '030-assets/valid').then(function (output) { + utils.testCheck(thisCheck, '030-assets/valid', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.fail.should.be.an.Object().which.is.empty(); @@ -36,7 +38,7 @@ describe('Assets', function () { }); it('should show error when symlink is present', function (done) { - utils.testCheck(thisCheck, '030-assets/symlink').then(function (output) { + utils.testCheck(thisCheck, '030-assets/symlink', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().with.lengthOf(1); diff --git a/test/040-ghost-head-foot.test.js b/test/040-ghost-head-foot.test.js index ed12b205..1fd46b83 100644 --- a/test/040-ghost-head-foot.test.js +++ b/test/040-ghost-head-foot.test.js @@ -3,8 +3,10 @@ var should = require('should'), // eslint-disable-line no-unused-vars thisCheck = require('../lib/checks/040-ghost-head-foot'); describe('Ghost head & foot', function () { + const options = {checkVersion: 'v1'}; + it('should show warnings for missing ghost head & foot helpers when no .hbs files are present', function (done) { - utils.testCheck(thisCheck, 'is-empty').then(function (output) { + utils.testCheck(thisCheck, 'is-empty', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().which.is.empty(); @@ -19,7 +21,7 @@ describe('Ghost head & foot', function () { }); it('should show warnings for missing ghost head & foot helpers when they are not in any .hbs file', function (done) { - utils.testCheck(thisCheck, '040-head-foot/missing').then(function (output) { + utils.testCheck(thisCheck, '040-head-foot/missing', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().which.is.empty(); @@ -34,7 +36,7 @@ describe('Ghost head & foot', function () { }); it('should output nothing when ghost head & foot helpers are present', function (done) { - utils.testCheck(thisCheck, '040-head-foot/valid').then(function (output) { + utils.testCheck(thisCheck, '040-head-foot/valid', options).then(function (output) { output.should.be.a.ValidThemeObject(); output.results.pass.should.be.an.Array().with.lengthOf(2); diff --git a/test/general.test.js b/test/general.test.js index 5e3a75d7..5196d652 100644 --- a/test/general.test.js +++ b/test/general.test.js @@ -1,17 +1,17 @@ -var should = require('should'), - sinon = require('sinon'), - path = require('path'), - rewire = require('rewire'), - _ = require('lodash'), - pfs = require('../lib/promised-fs'), - checkZip = require('../lib').checkZip, - themePath = require('./utils').themePath, - readZip = require('../lib/read-zip'), - readTheme = rewire('../lib/read-theme'), - checker = require('../lib/checker'), - format = require('../lib/format'), - - sandbox = sinon.sandbox.create(); +const should = require('should'); +const sinon = require('sinon'); +const path = require('path'); +const rewire = require('rewire'); +const _ = require('lodash'); +const pfs = require('../lib/promised-fs'); +const checkZip = require('../lib').checkZip; +const themePath = require('./utils').themePath; +const readZip = require('../lib/read-zip'); +const readTheme = rewire('../lib/read-theme'); +const checker = require('../lib/checker'); +const format = require('../lib/format'); + +const sandbox = sinon.sandbox.create(); process.env.NODE_ENV = 'testing'; @@ -29,7 +29,7 @@ function testReadZip(name) { } describe('Zip file handler can read zip files', function () { - after(function (done) { + after((done) => { pfs.remove('./test/tmp', function (err) { done(err); }); @@ -40,90 +40,96 @@ describe('Zip file handler can read zip files', function () { }); it('Flat example: zip without folder should unzip and callback with a path', function (done) { - testReadZip('flat-example.zip').then(function (zip) { - zip.path.should.be.a.String; - zip.origPath.should.be.a.String; - zip.name.should.be.a.String; - zip.origName.should.be.a.String; - zip.path.should.not.match(/flat-example$/); - zip.path.should.eql(zip.origPath); - zip.origName.should.eql('flat-example'); - done(); - }).catch(done); + testReadZip('flat-example.zip') + .then((zip) => { + zip.path.should.be.a.String; + zip.origPath.should.be.a.String; + zip.name.should.be.a.String; + zip.origName.should.be.a.String; + zip.path.should.not.match(/flat-example$/); + zip.path.should.eql(zip.origPath); + zip.origName.should.eql('flat-example'); + done(); + }).catch(done); }); it('Simple example: zip with same-name folder should unzip and callback with a path, resolving base dir', function (done) { - testReadZip('example.zip').then(function (zip) { - zip.path.should.be.a.String; - zip.origPath.should.be.a.String; - zip.name.should.be.a.String; - zip.origName.should.be.a.String; - zip.path.should.match(/\/example$/); - zip.path.should.not.eql(zip.origPath); - zip.origName.should.eql('example'); - done(); - }).catch(done); + testReadZip('example.zip') + .then((zip) => { + zip.path.should.be.a.String; + zip.origPath.should.be.a.String; + zip.name.should.be.a.String; + zip.origName.should.be.a.String; + zip.path.should.match(/\/example$/); + zip.path.should.not.eql(zip.origPath); + zip.origName.should.eql('example'); + done(); + }).catch(done); }); it('Bad example: zip with dif-name folder should unzip and callback with a path, resolving base dir', function (done) { - testReadZip('bad-example.zip').then(function (zip) { - zip.path.should.be.a.String; - zip.origPath.should.be.a.String; - zip.name.should.be.a.String; - zip.origName.should.be.a.String; - zip.path.should.match(/\/bad-example-folder/); - zip.path.should.not.eql(zip.origPath); - zip.origName.should.eql('bad-example'); - done(); - }).catch(done); + testReadZip('bad-example.zip') + .then((zip) => { + zip.path.should.be.a.String; + zip.origPath.should.be.a.String; + zip.name.should.be.a.String; + zip.origName.should.be.a.String; + zip.path.should.match(/\/bad-example-folder/); + zip.path.should.not.eql(zip.origPath); + zip.origName.should.eql('bad-example'); + done(); + }).catch(done); }); it('Nested example: zip with nested folders should unzip and callback with a path, resolving base dir', function (done) { - testReadZip('nested-example.zip').then(function (zip) { - zip.path.should.be.a.String; - zip.origPath.should.be.a.String; - zip.name.should.be.a.String; - zip.origName.should.be.a.String; - zip.path.should.match(/\/nested-example\/bad-example-folder$/); - zip.path.should.not.eql(zip.origPath); - zip.origName.should.eql('nested-example'); - done(); - }).catch(done); + testReadZip('nested-example.zip') + .then((zip) => { + zip.path.should.be.a.String; + zip.origPath.should.be.a.String; + zip.name.should.be.a.String; + zip.origName.should.be.a.String; + zip.path.should.match(/\/nested-example\/bad-example-folder$/); + zip.path.should.not.eql(zip.origPath); + zip.origName.should.eql('nested-example'); + done(); + }).catch(done); }); it('Multi example: complex zip should unzip and callback with a path, resolving base dir', function (done) { - testReadZip('multi-example.zip').then(function (zip) { - zip.path.should.be.a.String; - zip.origPath.should.be.a.String; - zip.name.should.be.a.String; - zip.origName.should.be.a.String; - zip.path.should.match(/\/multi-example\/theme\/theme-name/); - zip.path.should.not.eql(zip.origPath); - zip.origName.should.eql('multi-example'); - done(); - }).catch(done); + testReadZip('multi-example.zip') + .then((zip) => { + zip.path.should.be.a.String; + zip.origPath.should.be.a.String; + zip.name.should.be.a.String; + zip.origName.should.be.a.String; + zip.path.should.match(/\/multi-example\/theme\/theme-name/); + zip.path.should.not.eql(zip.origPath); + zip.origName.should.eql('multi-example'); + done(); + }).catch(done); }); // If the zip file does not contain index.hbs, we return the standard path, and our checks will report the errors it('No index.hbs example: zip should unzip and callback with a path', function (done) { - testReadZip('not-a-theme.zip').then(function (zip) { - zip.path.should.be.a.String; - zip.origPath.should.be.a.String; - zip.name.should.be.a.String; - zip.origName.should.be.a.String; - zip.path.should.not.match(/not-a-theme$/); - zip.path.should.eql(zip.origPath); - zip.origName.should.eql('not-a-theme'); - done(); - }).catch(done); + testReadZip('not-a-theme.zip') + .then((zip) => { + zip.path.should.be.a.String; + zip.origPath.should.be.a.String; + zip.name.should.be.a.String; + zip.origName.should.be.a.String; + zip.path.should.not.match(/not-a-theme$/); + zip.path.should.eql(zip.origPath); + zip.origName.should.eql('not-a-theme'); + done(); + }).catch(done); }); }); describe('check zip', function () { describe('ensure ignored assets are getting ignored', function () { it('default', function () { - return checkZip(themePath('030-assets/ignored.zip'), {keepExtractedDir: true}) - .then(function (theme) { + return checkZip(themePath('030-assets/ignored.zip'), {keepExtractedDir: true, checkVersion: 'v1'}) + .then((theme) => { theme.files.length.should.eql(1); theme.files[0].file.should.match(/default\.hbs/); @@ -135,8 +141,8 @@ describe('check zip', function () { }); it('Don\'t remove files if theme not in tmp directory', function () { - return checker(themePath('030-assets/ignored')) - .then(function (theme) { + return checker(themePath('030-assets/ignored'), {checkVersion: 'v1'}) + .then((theme) => { theme.files.length.should.eql(1); theme.files[0].file.should.match(/default\.hbs/); @@ -151,7 +157,7 @@ describe('check zip', function () { describe('Read theme', function () { it('returns correct result', function (done) { - readTheme(themePath('is-empty')).then(function (theme) { + readTheme(themePath('is-empty')).then((theme) => { theme.should.be.a.ValidThemeObject(); theme.files.should.eql([ @@ -163,12 +169,12 @@ describe('Read theme', function () { }); it('Can read partials', function (done) { - readTheme(themePath('theme-with-partials')).then(function (theme) { + readTheme(themePath('theme-with-partials')).then((theme) => { theme.should.be.a.ValidThemeObject(); theme.files.should.be.an.Array().with.lengthOf(7); - var fileNames = _.map(theme.files, function (file) { + const fileNames = _.map(theme.files, function (file) { return _.pickBy(file, function (value, key) { return key === 'file' || key === 'ext'; }); @@ -187,7 +193,7 @@ describe('Read theme', function () { }); it('Can extract custom templates', function (done) { - readTheme(themePath('theme-with-custom-templates')).then(function (theme) { + readTheme(themePath('theme-with-custom-templates')).then((theme) => { theme.should.be.a.ValidThemeObject(); theme.files.should.be.an.Array().with.lengthOf(11); @@ -262,7 +268,7 @@ describe('Read Hbs Files', function () { it('can read partials with POSIX paths', function (done) { // This roughly matches Example I - var exampleI = [ + const exampleI = [ {file: 'index.hbs', ext: '.hbs'}, {file: 'package.json', ext: '.json'}, {file: 'partialsbroke.hbs', ext: '.hbs'}, @@ -276,7 +282,7 @@ describe('Read Hbs Files', function () { readTheme.__get__('readHbsFiles')({ files: exampleI, path: 'fake/example-i' - }).then(function (result) { + }).then((result) => { result.partials.should.be.an.Array().with.lengthOf(2); result.partials.should.eql(['mypartial', 'subfolder/test']); done(); @@ -285,7 +291,7 @@ describe('Read Hbs Files', function () { it('can read partials with windows paths', function (done) { // This matches Example I, but on Windows - var exampleI = [ + const exampleI = [ {file: 'index.hbs', ext: '.hbs'}, {file: 'package.json', ext: '.json'}, {file: 'partialsbroke.hbs', ext: '.hbs'}, @@ -298,7 +304,7 @@ describe('Read Hbs Files', function () { files: exampleI, path: 'fake\\example-i' }) - .then(function (result) { + .then((result) => { result.partials.should.be.an.Array().with.lengthOf(2); result.partials.should.eql(['mypartial', 'subfolder\\test']); done(); @@ -308,7 +314,42 @@ describe('Read Hbs Files', function () { describe('Checker', function () { it('returns a valid theme when running all checks', function (done) { - checker(themePath('is-empty')).then(function (theme) { + checker(themePath('is-empty')).then((theme) => { + theme.should.be.a.ValidThemeObject(); + + theme.files.should.eql([ + {file: '.gitkeep', ext: '.gitkeep'}, + {file: 'README.md', ext: '.md'} + ]); + + theme.results.pass.should.be.an.Array().with.lengthOf(31); + theme.results.pass.should.containEql('GS005-TPL-ERR', 'GS030-ASSET-REQ', 'GS030-ASSET-SYM'); + + theme.results.fail.should.be.an.Object().with.keys( + 'GS010-PJ-REQ', + 'GS010-PJ-PARSE', + 'GS010-PJ-NAME-REQ', + 'GS010-PJ-NAME-LC', + 'GS010-PJ-NAME-HY', + 'GS010-PJ-VERSION-SEM', + 'GS010-PJ-VERSION-REQ', + 'GS010-PJ-AUT-EM-VAL', + 'GS010-PJ-AUT-EM-REQ', + 'GS010-PJ-CONF-PPP', + 'GS020-INDEX-REQ', + 'GS020-POST-REQ', + 'GS020-DEF-REC', + 'GS040-GH-REQ', + 'GS040-GF-REQ' + ); + + done(); + }); + }); + + // TODO: this needs to be activated as soon as we have new checks for the ruleset + it.skip('checks for an older version if passed', function (done) { + checker(themePath('is-empty'), {checkVersion: 'v1'}).then((theme) => { theme.should.be.a.ValidThemeObject(); theme.files.should.eql([ @@ -342,7 +383,7 @@ describe('Checker', function () { }); it('should not follow symlinks', function (done) { - checker(themePath('030-assets/symlink2')).then(function (theme) { + checker(themePath('030-assets/symlink2')).then((theme) => { theme.should.be.a.ValidThemeObject(); theme.files.should.containEql({file: 'assets/mysymlink', ext: undefined}); theme.results.fail.should.containEql('GS030-ASSET-SYM'); @@ -354,7 +395,7 @@ describe('Checker', function () { describe('format', function () { it('assert sorting', function (done) { - checker(themePath('005-compile/invalid')).then(function (theme) { + checker(themePath('005-compile/invalid')).then((theme) => { theme = format(theme); theme.results.error[0].fatal.should.eql(true); @@ -366,7 +407,7 @@ describe('format', function () { }); it('assert sorting', function (done) { - checker(themePath('is-empty')).then(function (theme) { + checker(themePath('is-empty')).then((theme) => { theme = format(theme); theme.results.error[0].fatal.should.eql(true); diff --git a/test/utils.js b/test/utils.js index a767b0f4..f2687911 100644 --- a/test/utils.js +++ b/test/utils.js @@ -56,7 +56,7 @@ testCheck = function testCheck(checkLib, themeId, options) { var themePath = getThemePath(themeId); return readTheme(themePath).then(function runCheck(theme) { - return checkLib.call(this, theme, themePath, options); + return checkLib.call(this, theme, options, themePath); }); };