Add [W3C] Markup Validation Service Badge (#3833) (#4148)

* Add W3C Markup Validation Service Badge (#3833)

* Move helper functions into different file and added unit tests

* Remove unnecessary comments from spec file

* pr changes move code into transform method and validation of messages

* use joi.string().regex instead of custom validator

* Simplify the fetch, handle methods. Make Joi validation for string

* Remove empty parameter from tests and label from render method

* encodeUri on the doc and schema properties send to API

* Documentation and remove unnecessary Object.keys call

* Use regular expressions to make tests less brittle

* made service less for message and color more generic and less brittle

* Throw standard NoFound exception for invalid URL. Use w3c endpoint

* use sazerac for w3c-validation-helper.spec.js

* Replace documentation API url and API documentation url

* Switch back to https://validator.nu endpoint. Remove html4 assertions

* Increase strictness of NotFound checks
This commit is contained in:
seetd
2019-10-21 19:40:14 -04:00
committed by Caleb Cartwright
parent f82f7b798d
commit 54bcedc0f4
4 changed files with 657 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
'use strict'
const html5Expression =
'^HTML\\s?,\\s?SVG\\s?1\\.1\\s?,\\s?MathML\\s?3\\.0(\\s?,\\s?((ITS\\s?2\\.0)|(RDFa\\s?Lite\\s?1\\.1)))?$'
const html4Expression =
'^HTML\\s?4\\.01\\s?(Strict|Transitional|Frameset)\\s?,\\s?URL\\s?\\/\\s?XHTML\\s?1\\.0\\s?(Strict|Transitional|Frameset)\\s?,\\s?URL$'
const xhtmlExpression =
'^(XHTML\\s?,\\s?SVG\\s?1\\.1\\s?,\\s?MathML\\s?3\\.0(\\s?,\\s?RDFa\\s?Lite\\s?1\\.1)?)|(XHTML\\s?1\\.0\\s?Strict\\s?,\\s?URL\\s?,\\s?Ruby\\s?,\\s?SVG\\s?1\\.1\\s?,\\s?MathML\\s?3\\.0)$'
const svgExpression =
'^SVG\\s?1\\.1\\s?,\\s?URL\\s?,\\s?XHTML\\s?,\\s?MathML\\s?3\\.0$'
const presetRegex = new RegExp(
`(${html5Expression})|(${html4Expression})|(${xhtmlExpression})|(${svgExpression})`,
'i'
)
const getMessage = messageTypes => {
const messageTypeKeys = Object.keys(messageTypes)
messageTypeKeys.sort() // Sort to make the order error, warning for display
if (messageTypeKeys.length === 0) {
return 'validated'
}
const messages = messageTypeKeys.map(
key => `${messageTypes[key]} ${key}${messageTypes[key] > 1 ? 's' : ''}`
)
return messages.join(', ')
}
const getColor = messageTypes => {
if ('error' in messageTypes) {
return 'red'
}
if ('warning' in messageTypes) {
return 'yellow'
}
return 'brightgreen'
}
const getSchema = preset => {
if (!preset) return undefined
const decodedPreset = decodeURI(preset)
const schema = []
if (new RegExp(html4Expression, 'i').test(decodedPreset)) {
if (/Strict/i.test(decodedPreset)) {
schema.push('http://s.validator.nu/xhtml10/xhtml-strict.rnc')
} else if (/Transitional/i.test(decodedPreset)) {
schema.push('http://s.validator.nu/xhtml10/xhtml-transitional.rnc')
} else {
schema.push('http://s.validator.nu/xhtml10/xhtml-frameset.rnc')
}
schema.push('http://c.validator.nu/all-html4/')
} else if (/1\.0 Strict, URL, Ruby, SVG 1\.1/i.test(decodedPreset)) {
schema.push('http://s.validator.nu/xhtml1-ruby-rdf-svg-mathml.rnc')
schema.push('http://c.validator.nu/all-html4/')
} else {
if (new RegExp(html5Expression, 'i').test(decodedPreset)) {
if (/ITS 2\.0/i.test(decodedPreset)) {
schema.push('http://s.validator.nu/html5-its.rnc')
} else if (/RDFa Lite 1\.1/i.test(decodedPreset)) {
schema.push('http://s.validator.nu/html5-rdfalite.rnc')
} else {
schema.push('http://s.validator.nu/html5.rnc')
}
} else if (new RegExp(xhtmlExpression, 'i').test(decodedPreset)) {
if (/RDFa Lite 1\.1/i.test(decodedPreset)) {
schema.push('http://s.validator.nu/xhtml5-rdfalite.rnc')
} else {
schema.push('http://s.validator.nu/xhtml5.rnc')
}
} else if (new RegExp(svgExpression, 'i').test(decodedPreset)) {
schema.push('http://s.validator.nu/svg-xhtml5-rdf-mathml.rnc')
}
schema.push('http://s.validator.nu/html5/assertions.sch')
schema.push('http://c.validator.nu/all/')
}
return schema.map(url => encodeURI(url)).join(' ')
}
const documentation = `
<style>
.box {
display: flex;
justify-content: space-between;
}
.note {
font-size: smaller;
text-align: left;
}
</style>
<p>
The W3C validation badge performs validation of the HTML, SVG, MathML, ITS, RDFa Lite, XHTML documents.
The badge uses the type property of each message found in the messages from the validation results to determine to be an error or warning.
The rules are as follows:
<ul class="note">
<li>info: These messages are counted as warnings</li>
<li>error: These messages are counted as errors</li>
<li>non-document-error: These messages are counted as errors</li>
</ul>
</p>
<p>
This badge relies on the https://validator.nu/ service to perform the validation. Please refer to https://about.validator.nu/ for the full documentation and Terms of service.
The following are required from the consumer for the badge to function.
<ul class="note">
<li>
Path:
<ul>
<li>
parser: The parser that is used for validation. This is a passthru value to the service
<ul>
<li>default <i>(This will not pass a parser to the API and make the API choose the parser based on the validated content)</i></li>
<li>html <i>(HTML)</i></li>
<li>xml <i>(XML; dont load external entities)</i></li>
<li>xmldtd <i>(XML; load external entities)</i></li>
</ul>
</li>
</ul>
</li>
<li>
Query string:
<ul>
<li>
targetUrl (Required): This is the path for the document to be validated
</li>
<li>
preset (Optional can be left as blank): This is used to determine the schema for the document to be valdiated against.
The following are the allowed values
<ul>
<li>HTML, SVG 1.1, MathML 3.0</li>
<li>HTML, SVG 1.1, MathML 3.0, ITS 2.0</li>
<li>HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1</li>
<li>HTML 4.01 Strict, URL / XHTML 1.0 Strict, URL</li>
<li>HTML 4.01 Transitional, URL / XHTML 1.0 Transitional, URL</li>
<li>HTML 4.01 Frameset, URL / XHTML 1.0 Frameset, URL</li>
<li>XHTML, SVG 1.1, MathML 3.0</li>
<li>XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1</li>
<li>XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0</li>
<li>SVG 1.1, URL, XHTML, MathML 3.0</li>
</ul>
</li>
</ul>
</li>
</ul>
</p>
`
module.exports = {
documentation,
presetRegex,
getColor,
getMessage,
getSchema,
}

View File

@@ -0,0 +1,265 @@
'use strict'
const { expect } = require('chai')
const { test, given, forCases } = require('sazerac')
const {
presetRegex,
getMessage,
getColor,
getSchema,
} = require('./w3c-validation-helper')
describe('w3c-validation-helper', function() {
describe('presetRegex', function() {
function testing(preset) {
return presetRegex.test(preset)
}
test(testing, () => {
forCases([
given('html,svg 1.1,mathml 3.0'),
given('HTML,SVG 1.1,MathML 3.0'),
given('HTML, SVG 1.1, MathML 3.0'),
given('HTML , SVG 1.1 , MathML 3.0'),
given('HTML,SVG 1.1,MathML 3.0,ITS 2.0'),
given('HTML, SVG 1.1, MathML 3.0, ITS 2.0'),
given('HTML , SVG 1.1 , MathML 3.0 , ITS 2.0'),
given('HTML,SVG 1.1,MathML 3.0,RDFa Lite 1.1'),
given('HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1'),
given('HTML , SVG 1.1 , MathML 3.0 , RDFa Lite 1.1'),
given('HTML 4.01 Strict,URL/XHTML 1.0 Strict,URL'),
given('HTML 4.01 Strict, URL/ XHTML 1.0 Strict, URL'),
given('HTML 4.01 Strict , URL / XHTML 1.0 Strict , URL'),
given('HTML 4.01 Transitional,URL/XHTML 1.0 Transitional,URL'),
given('HTML 4.01 Transitional, URL/ XHTML 1.0 Transitional, URL'),
given('HTML 4.01 Transitional , URL / XHTML 1.0 Transitional , URL'),
given('HTML 4.01 Frameset,URL/XHTML 1.0 Frameset,URL'),
given('HTML 4.01 Frameset, URL/ XHTML 1.0 Frameset, URL'),
given('HTML 4.01 Frameset , URL / XHTML 1.0 Frameset , URL'),
given('XHTML,SVG 1.1,MathML 3.0'),
given('XHTML, SVG 1.1, MathML 3.0'),
given('XHTML , SVG 1.1 , MathML 3.0'),
given('XHTML,SVG 1.1,MathML 3.0,RDFa Lite 1.1'),
given('XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1'),
given('XHTML , SVG 1.1 , MathML 3.0 , RDFa Lite 1.1'),
given('XHTML 1.0 Strict,URL,Ruby,SVG 1.1,MathML 3.0'),
given('XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0'),
given('XHTML 1.0 Strict , URL , Ruby , SVG 1.1 , MathML 3.0'),
given('SVG 1.1,URL,XHTML,MathML 3.0'),
given('SVG 1.1, URL, XHTML, MathML 3.0'),
given('SVG 1.1 , URL , XHTML , MathML 3.0'),
]).expect(true)
})
test(testing, () => {
forCases([
given(undefined),
given(null),
given(''),
given(' '),
given('HTML'),
]).expect(false)
})
})
describe('getColor', function() {
it('returns "brightgreen" if no messages are provided', function() {
const messageTypes = {}
const actualResult = getColor(messageTypes)
expect(actualResult).to.equal('brightgreen')
})
it('returns "yellow" if only warning messages are provided', function() {
const messageTypes = { warning: 1 }
const actualResult = getColor(messageTypes)
expect(actualResult).to.equal('yellow')
})
it('returns "red" if only error messages are provided', function() {
const messageTypes = { error: 1 }
const actualResult = getColor(messageTypes)
expect(actualResult).to.equal('red')
})
it('returns "red" if both warning and error messages are provided', function() {
const messageTypes = { warning: 3, error: 4 }
const actualResult = getColor(messageTypes)
expect(actualResult).to.equal('red')
})
})
describe('getMessage', function() {
it('returns "validate" if no messages are provided', function() {
const messageTypes = {}
const actualResult = getMessage(messageTypes)
expect(actualResult).to.equal('validated')
})
it('returns "1 error" if 1 error message is provided', function() {
const messageTypes = { error: 1 }
const actualResult = getMessage(messageTypes)
expect(actualResult).to.equal('1 error')
})
it('returns "2 errors" if 2 error messages are provided', function() {
const messageTypes = { error: 2 }
const actualResult = getMessage(messageTypes)
expect(actualResult).to.equal('2 errors')
})
it('returns "1 warning" if 1 warning message is provided', function() {
const messageTypes = { warning: 1 }
const actualResult = getMessage(messageTypes)
expect(actualResult).to.equal('1 warning')
})
it('returns "2 warnings" if 2 warning messages are provided', function() {
const messageTypes = { warning: 2 }
const actualResult = getMessage(messageTypes)
expect(actualResult).to.equal('2 warnings')
})
it('returns "1 error, 1 warning" if 1 error and 1 warning message is provided', function() {
const messageTypes = { warning: 1, error: 1 }
const actualResult = getMessage(messageTypes)
expect(actualResult).to.equal('1 error, 1 warning')
})
it('returns "2 errors, 2 warnings" if 2 error and 2 warning message is provided', function() {
const messageTypes = { error: 2, warning: 2 }
const actualResult = getMessage(messageTypes)
expect(actualResult).to.equal('2 errors, 2 warnings')
})
})
describe('getSchema', function() {
function execution(preset) {
return getSchema(preset)
}
test(execution, () => {
forCases([given(undefined), given(null), given('')]).expect(undefined)
})
it('returns 3 schemas associated to the "HTML,SVG 1.1,MathML 3.0" preset', function() {
const preset = 'HTML,SVG 1.1,MathML 3.0'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/html5.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/'
)
})
it('returns 3 schemas associated to the "HTML,SVG 1.1,MathML 3.0,ITS 2.0" preset', function() {
const preset = 'HTML,SVG 1.1,MathML 3.0,ITS 2.0'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/html5-its.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/'
)
})
it('returns 3 schemas associated to the "HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1" preset', function() {
const preset = 'HTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/html5-rdfalite.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/'
)
})
it('returns 3 schemas associated to the "HTML 4.01 Strict, URL/ XHTML 1.0 Strict, URL" preset', function() {
const preset = 'HTML 4.01 Strict, URL/ XHTML 1.0 Strict, URL'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/xhtml10/xhtml-strict.rnc http://c.validator.nu/all-html4/'
)
})
it('returns 3 schemas associated to the "HTML 4.01 Transitional, URL/ XHTML 1.0 Transitional, URL" preset', function() {
const preset = 'HTML 4.01 Transitional, URL/ XHTML 1.0 Transitional, URL'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/xhtml10/xhtml-transitional.rnc http://c.validator.nu/all-html4/'
)
})
it('returns 3 schemas associated to the "HTML 4.01 Frameset, URL/ XHTML 1.0 Frameset, URL" preset', function() {
const preset = 'HTML 4.01 Frameset, URL/ XHTML 1.0 Frameset, URL'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/xhtml10/xhtml-frameset.rnc http://c.validator.nu/all-html4/'
)
})
it('returns 3 schemas associated to the "XHTML, SVG 1.1, MathML 3.0" preset', function() {
const preset = 'XHTML, SVG 1.1, MathML 3.0'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/xhtml5.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/'
)
})
it('returns 3 schemas associated to the "XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1" preset', function() {
const preset = 'XHTML, SVG 1.1, MathML 3.0, RDFa Lite 1.1'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/xhtml5-rdfalite.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/'
)
})
it('returns 3 schemas associated to the "XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0" preset', function() {
const preset = 'XHTML 1.0 Strict, URL, Ruby, SVG 1.1, MathML 3.0'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/xhtml1-ruby-rdf-svg-mathml.rnc http://c.validator.nu/all-html4/'
)
})
it('returns 3 schemas associated to the "SVG 1.1, URL, XHTML, MathML 3.0" preset', function() {
const preset = 'SVG 1.1, URL, XHTML, MathML 3.0'
const actualResult = getSchema(preset)
expect(actualResult).to.equal(
'http://s.validator.nu/svg-xhtml5-rdf-mathml.rnc http://s.validator.nu/html5/assertions.sch http://c.validator.nu/all/'
)
})
})
})

View File

@@ -0,0 +1,136 @@
'use strict'
const Joi = require('@hapi/joi')
const { optionalUrl } = require('../validators')
const {
documentation,
presetRegex,
getColor,
getMessage,
getSchema,
} = require('./w3c-validation-helper')
const { BaseJsonService, NotFound } = require('..')
const schema = Joi.object({
url: Joi.string().optional(),
messages: Joi.array()
.required()
.items(
Joi.object({
type: Joi.string()
.allow('info', 'error', 'non-document-error')
.required(),
subType: Joi.string().optional(),
message: Joi.string().required(),
})
),
}).required()
const queryParamSchema = Joi.object({
targetUrl: optionalUrl.required(),
preset: Joi.string()
.regex(presetRegex)
.allow(''),
}).required()
module.exports = class W3cValidation extends BaseJsonService {
static get category() {
return 'analysis'
}
static get route() {
return {
base: 'w3c-validation',
pattern: ':parser(default|html|xml|xmldtd)',
queryParamSchema,
}
}
static get examples() {
return [
{
title: 'W3C Validation',
namedParams: { parser: 'html' },
queryParams: {
targetUrl: 'https://validator.nu/',
preset: 'HTML, SVG 1.1, MathML 3.0',
},
staticPreview: this.render({ messageTypes: {} }),
documentation,
},
]
}
static get defaultBadgeData() {
return {
label: 'w3c',
}
}
static render({ messageTypes }) {
return {
message: getMessage(messageTypes),
color: getColor(messageTypes),
}
}
async fetch(targetUrl, preset, parser) {
return this._requestJson({
url: 'https://validator.nu/',
schema,
options: {
qs: {
schema: getSchema(preset),
parser: parser === 'default' ? undefined : parser,
doc: encodeURI(targetUrl),
out: 'json',
},
},
})
}
transform(url, messages) {
if (messages.length === 1) {
const { subType, type, message } = messages[0]
if (type === 'non-document-error' && subType === 'io') {
let notFound = false
if (
message ===
'HTTP resource not retrievable. The HTTP status from the remote server was: 404.'
) {
notFound = true
} else if (message.endsWith('Name or service not known')) {
const domain = message.split(':')[0].trim()
notFound = url.indexOf(domain) !== -1
}
if (notFound) {
throw new NotFound({ prettyMessage: 'target url not found' })
}
}
}
return messages.reduce((accumulator, message) => {
let { type } = message
if (type === 'info') {
type = 'warning'
} else {
// All messages are suppose to have a type and there can only be info, error or non-document
// If a new type gets introduce this will flag them as errors
type = 'error'
}
if (!(type in accumulator)) {
accumulator[type] = 0
}
accumulator[type] += 1
return accumulator
}, {})
}
async handle({ parser }, { targetUrl, preset }) {
const { url, messages } = await this.fetch(targetUrl, preset, parser)
return this.constructor.render({
messageTypes: this.transform(url, messages),
})
}
}

View File

@@ -0,0 +1,100 @@
'use strict'
const Joi = require('@hapi/joi')
const t = (module.exports = require('../tester').createServiceTester())
const isErrorOnly = Joi.string().regex(/^[0-9]+ errors?$/)
const isWarningOnly = Joi.string().regex(/^[0-9]+ warnings?$/)
const isErrorAndWarning = Joi.string().regex(
/^[0-9]+ errors?, [0-9]+ warnings?$/
)
const isW3CMessage = Joi.alternatives().try(
'validated',
isErrorOnly,
isWarningOnly,
isErrorAndWarning
)
const isW3CColors = Joi.alternatives().try('brightgreen', 'red', 'yellow')
t.create(
'W3C Validation page conforms to standards with no preset and parser with brightgreen badge'
)
.get(
'/default.json?targetUrl=https://hsivonen.com/test/moz/messages-types/no-message.html'
)
.expectBadge({
label: 'w3c',
message: isW3CMessage,
color: isW3CColors,
})
t.create(
'W3C Validation page conforms to standards with no HTML4 preset and HTML parser with brightgreen badge'
)
.get(
'/html.json?targetUrl=https://hsivonen.com/test/moz/messages-types/no-message.html&preset=HTML,%20SVG%201.1,%20MathML%203.0'
)
.expectBadge({
label: 'w3c',
message: isW3CMessage,
color: isW3CColors,
})
t.create('W3C Validation target url not found error')
.get(
'/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/404.html'
)
.expectBadge({
label: 'w3c',
message: 'target url not found',
})
t.create('W3C Validation target url host not found error')
.get('/default.json?targetUrl=https://adfasdfasdfasdfadfadfadfasdfadf.com')
.expectBadge({
label: 'w3c',
message: 'target url not found',
})
t.create('W3C Validation page has 1 validation error with red badge')
.get(
'/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/warning.html'
)
.expectBadge({
label: 'w3c',
message: isW3CMessage,
color: isW3CColors,
})
t.create(
'W3C Validation page has 3 validation error using HTML 4.01 Frameset preset with red badge'
)
.get(
'/html.json?targetUrl=http://hsivonen.com/test/moz/messages-types/warning.html&preset=HTML 4.01 Frameset, URL / XHTML 1.0 Frameset, URL'
)
.expectBadge({
label: 'w3c',
message: isW3CMessage,
color: isW3CColors,
})
t.create('W3C Validation page has 1 validation warning with yellow badge')
.get(
'/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/info.svg'
)
.expectBadge({
label: 'w3c',
message: isW3CMessage,
color: isW3CColors,
})
t.create('W3C Validation page has multiple of validation errors with red badge')
.get(
'/default.json?targetUrl=http://hsivonen.com/test/moz/messages-types/range-error.html'
)
.expectBadge({
label: 'w3c',
message: isW3CMessage,
color: isW3CColors,
})