├── .travis.yml ├── lib ├── doctests.js ├── anchor-exists.js ├── mixed-content.js ├── http-links.js ├── summary-heading.js ├── old-urls.js ├── font-elements.js ├── shell-prompts.js ├── alert-print-in-code.js ├── name-attribute.js ├── style-attribute.js ├── code-in-pre.js ├── link-count.js ├── example-colon-heading.js ├── html-comments.js ├── article-length.js ├── span-count.js ├── line-length-in-pre.js ├── absolute-urls-for-internal-links.js ├── different-locale-links.js ├── empty-brackets.js ├── pre-without-class.js ├── url-in-link-title.js ├── incorrectly-wrapped-sidebar-macros.js ├── index.js ├── data-macro-note.js ├── unnecessary-macro-params.js ├── empty-elements.js ├── wrong-syntax-class.js ├── api-syntax-headlines.js ├── macro-syntax-error.js ├── wrong-highlighted-line.js └── invalid-macros.js ├── README.md ├── CODE_OF_CONDUCT.md ├── tests ├── test-old-urls.js ├── test-font-elements.js ├── test-anchor-exists.js ├── test-empty-brackets.js ├── test-http-links.js ├── test-mixed-content.js ├── test-html-comments.js ├── test-unnecessary-macro-params.js ├── test-name-attribute.js ├── test-shell-prompts.js ├── test-alert-print-in-code.js ├── test-example-colon-heading.js ├── test-span-count.js ├── test-code-in-pre.js ├── test-style-attribute.js ├── test-summary-heading.js ├── test-line-length-in-pre.js ├── test-absolute-urls-for-internal-links.js ├── test-empty-elements.js ├── test-different-locale-links.js ├── test-data-macro-note.js ├── test-pre-without-class.js ├── test-invalid-macros.js ├── test-url-in-link-title.js ├── test-article-length.js ├── test-wrong-syntax-class.js ├── test-link-count.js ├── test-api-syntax-headlines.js ├── test-wrong-highlighted-line.js ├── test-macro-syntax-error.js └── test-incorrectly-wrapped-sidebar-macros.js ├── CONTRIBUTING.md ├── package.json ├── karma.conf.js └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | sudo: false 5 | dist: trusty 6 | 7 | before_script: 8 | - export DISPLAY=:99.0 9 | - sh -e /etc/init.d/xvfb start 10 | 11 | install: 12 | - npm install -G npm-run 13 | - npm install --dev 14 | 15 | script: 16 | - npm test 17 | -------------------------------------------------------------------------------- /lib/doctests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Utilities for all tests 3 | 4 | const ERROR = 1; 5 | const WARNING = 2; 6 | const INFO = 3; 7 | 8 | exports.ERROR = ERROR; 9 | exports.WARNING = WARNING; 10 | exports.INFO = INFO; 11 | 12 | exports.mapMatches = function mapMatches(matches, type) { 13 | return matches.map(match => ({msg: match, type})); 14 | } 15 | 16 | exports.isNewParagraphHelper = function isNewParagraphHelper(element) { 17 | if (!element || element.localName !== "span") { 18 | return false; 19 | } 20 | 21 | let style = element.getAttribute("style"); 22 | return style && /z-index:\s*9999;/.test(style); 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MDN Documentation linter rules 2 | 3 | This repository contains the rules used to lint an MDN Document. 4 | Those rules are made to avoid common mistakes around writing documentation on [MDN Web Docs](http://developer.mozilla.org). 5 | 6 | ## Used by 7 | 8 | Those rules are used by the following projects: 9 | - MDN Doc Linter Webextension (https://github.com/mdn/doc-linter-webextension) 10 | 11 | ## Contributors 12 | 13 | The rules were originally written by Sebastian Zartner [(@SebastianZ)](https://github.com/SebastianZ) and Florian Scholz [(@fscholz)](https://github.com/Elchi3) in [mdn-doc-tests](https://github.com/Elchi3/mdn-doc-tests) 14 | -------------------------------------------------------------------------------- /lib/anchor-exists.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title : Test that each internal links match with an anchor 3 | */ 4 | 5 | const ERROR = require('./doctests.js').ERROR; 6 | 7 | const REGEX_ID = /#(.*)/ 8 | 9 | exports.anchorExists = { 10 | name: 'anchor_exists', 11 | desc: 'anchor_exists_desc', 12 | check: function checkAnchorExists(rootElement) { 13 | let matches = []; 14 | let anchors = rootElement.querySelectorAll('a[href^="#"]'); 15 | for(let anchor of anchors) { 16 | let id = REGEX_ID.exec(anchor.href)[0]; 17 | let link = rootElement.querySelector(`${id}`); 18 | 19 | if(link === null) matches.push({msg: id, type: ERROR}); 20 | } 21 | 22 | return matches; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /lib/mixed-content.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title : Prevent non https content inclusion 3 | * 4 | * Implementation notes : We check that every is using https:// 5 | */ 6 | 7 | const ERROR = require('./doctests.js').ERROR; 8 | 9 | // The string must start by https:// 10 | const HTTPS_URL = /^https:\/\// 11 | 12 | exports.mixedContent = { 13 | name: "mixed_content", 14 | desc: "mixed_content_desc", 15 | check: function checkMixedContent(rootElement) { 16 | let images = rootElement.getElementsByTagName("img"); 17 | let matches = []; 18 | 19 | for(let index = 0; index < images.length; index++){ 20 | if(!images[index].getAttribute("src").match(HTTPS_URL)) { 21 | matches.push({node: images[index], msg: images[index].outerHTML, type: ERROR}); 22 | } 23 | } 24 | 25 | return matches; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /tests/test-old-urls.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const oldURLs = require('../lib/old-urls.js').oldURLs; 4 | 5 | describe('oldURLs', function() { 6 | it('Should return 2 errors regarding usage of old URLs format', function(done) { 7 | const str = 'Web' + 8 | 'Mozilla'; 9 | 10 | const expected = [ 11 | {msg: 'Web', type: ERROR}, 12 | {msg: 'Mozilla', type: ERROR} 13 | ]; 14 | 15 | let rootElement = document.createElement("body"); 16 | rootElement.innerHTML = str; 17 | 18 | let results = oldURLs.check(rootElement); 19 | 20 | results.forEach((element, index) => { 21 | delete element.node; 22 | assert.deepEqual(expected[index], element); 23 | }); 24 | 25 | done(); 26 | }); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /tests/test-font-elements.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const fontElements = require('../lib/font-elements.js').fontElements; 4 | 5 | describe('fontElments', function() { 6 | it('Should return 2 errors regarding ', function(done) { 7 | const str = 'Some text' + 8 | 'Another text'; 9 | 10 | const expected = [ 11 | {msg: 'Some text', type: ERROR}, 12 | {msg: 'Another text', type: ERROR} 13 | ]; 14 | 15 | let rootElement = document.createElement("body"); 16 | rootElement.innerHTML = str; 17 | 18 | let results = fontElements.check(rootElement); 19 | 20 | results.forEach((element, index) => { 21 | assert.deepEqual(expected[index], element); 22 | }); 23 | 24 | done(); 25 | }); 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /lib/http-links.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for http link usages where https should be used. 3 | * 4 | * Example 1: Mozilla should rather be 5 | * Mozilla. 6 | * 7 | * Implementation notes: This test checks whether links refer to HTTP URLs. Because some URLs 8 | * don't provide HTTPS access, matches are only output as warnings. 9 | */ 10 | 11 | const WARNING = require('./doctests.js').WARNING; 12 | 13 | exports.httpLinks = { 14 | name: "http_links", 15 | desc: "http_links_desc", 16 | check: function checkHTTPLinks(rootElement) { 17 | let httpLinks = rootElement.querySelectorAll("a[href^='http://']"); 18 | let matches = []; 19 | 20 | for (let i = 0; i < httpLinks.length; i++) { 21 | matches.push({ 22 | msg: httpLinks[i].outerHTML, 23 | type: WARNING 24 | }); 25 | } 26 | 27 | return matches; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/test-anchor-exists.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const anchorExists = require('../lib/anchor-exists.js').anchorExists; 3 | const ERROR = require('../lib/doctests.js').ERROR; 4 | 5 | describe('anchorExists', function() { 6 | it('Should return 1 Error regarding a link anchor without the related link', function(done) { 7 | const str = '

Valid anchor

' + 8 | 'Link to a valid anchor' + 9 | 'Link to an invalid anchor'; 10 | 11 | const expected = [ 12 | {msg: '#invalid', type: ERROR} 13 | ]; 14 | 15 | let rootElement = document.createElement("body"); 16 | rootElement.innerHTML = str; 17 | 18 | let results = anchorExists.check(rootElement); 19 | 20 | results.forEach((element, index) => { 21 | delete element.node; 22 | assert.deepEqual(expected[index], element); 23 | }); 24 | 25 | done(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/summary-heading.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for obsolete 'Summary' heading. 3 | * 4 | * Example 1:

Summary

is redundant, because the page title is shown above the article, 5 | * so it should be removed. 6 | * 7 | */ 8 | 9 | 10 | const ERROR = require('./doctests.js').ERROR; 11 | 12 | exports.summaryHeading = { 13 | name: "summary_heading", 14 | desc: "summary_heading_desc", 15 | 16 | check: function checkSummaryHeading(rootElement) { 17 | let headlines = rootElement.querySelectorAll("h1, h2, h3, h4, h5, h6"); 18 | let matches = []; 19 | 20 | if (headlines[0].textContent.match(/^\s*Summary\s*$/)) { 21 | matches.push({ 22 | node: headlines[0], 23 | msg: headlines[0].outerHTML, 24 | type: ERROR 25 | }); 26 | } 27 | 28 | return matches; 29 | }, 30 | 31 | fix: function fixSummaryHeading(matches) { 32 | matches.forEach(match => match.node.remove()); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /tests/test-empty-brackets.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const emptyBrackets = require('../lib/empty-brackets.js').emptyBrackets; 4 | 5 | describe('emptyBrackets', function() { 6 | it('Should return 2 errors regarding emptyBrackets', function(done) { 7 | const str = '{{ foo() }}' + 8 | '{{bar()}}' + 9 | '{{foobar("abc")}}' + 10 | '{{baz}}'; 11 | 12 | const expected = [ 13 | {msg: '{{ foo() }}', type: ERROR}, 14 | {msg: '{{bar()}}', type: ERROR} 15 | ]; 16 | 17 | const expectedAfterFixing = []; 18 | 19 | let rootElement = document.createElement("body"); 20 | rootElement.innerHTML = str; 21 | 22 | let results = emptyBrackets.check(rootElement); 23 | 24 | results.forEach((element, index) => { 25 | delete element.node; 26 | assert.deepEqual(element, expected[index]); 27 | }); 28 | 29 | done(); 30 | }); 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MDN Documentation Linter rules 2 | 3 | # How to install 4 | The code is not yet proposed as an npm package, you need to manually clone the repository 5 | ```shell 6 | git clone git@github.com:mdn/doc-linter-rules.git 7 | ``` 8 | Then you have to install the dependencies using npm 9 | ```shell 10 | npm install --dev 11 | ``` 12 | Finally, you can run the tests to ensure that everything is working as expected. 13 | ```shell 14 | npm test 15 | ``` 16 | 17 | Note: The tests are running on NightmareJS an headless Webkit based browser and on Firefox. The tests are expecting firefox to be installed. 18 | 19 | # How to add a rule 20 | The process to add a rule is done in three steps: 21 | 1. Create a test for the rule in tests/ the name of the test file must be the name of the rule prepend by test- (e.g test-article-length.js). 22 | 2. Create a linting file for the rule in lib/ (e.g article-length.js). 23 | 3. Register those two files into karma.conf.js in the files array. 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/test-http-links.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const WARNING = require('../lib/doctests.js').WARNING; 3 | const httpLinks = require('../lib/http-links.js').httpLinks; 4 | 5 | describe('httpLinks', function() { 6 | it('Should return 1 warning regarding http links', function(done) { 7 | const str = 'some page' + 8 | 'some page' + 9 | 'some page' + 10 | 'some page'; 11 | 12 | const expected = [ 13 | {msg: 'some page', type: WARNING} 14 | ]; 15 | 16 | let rootElement = document.createElement("body"); 17 | rootElement.innerHTML = str; 18 | 19 | let results = httpLinks.check(rootElement); 20 | 21 | results.forEach((element, index) => { 22 | assert.deepEqual(expected[index], element); 23 | }); 24 | 25 | done(); 26 | }); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /tests/test-mixed-content.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const mixedContent = require('../lib/mixed-content.js').mixedContent; 3 | const ERROR = require('../lib/doctests.js').ERROR; 4 | 5 | describe('mixedContent', function() { 6 | it('Should return 2 Errors regarding mixed content', function(done) { 7 | const str = '' + 8 | '' + 9 | ''; 10 | 11 | const expected = [ 12 | {msg: '', type: ERROR}, 13 | {msg: '', type: ERROR} 14 | ]; 15 | 16 | let rootElement = document.createElement("body"); 17 | rootElement.innerHTML = str; 18 | 19 | let results = mixedContent.check(rootElement); 20 | 21 | results.forEach((element, index) => { 22 | delete element.node; 23 | assert.deepEqual(expected[index], element); 24 | }); 25 | 26 | done(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/old-urls.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for old /en/ MDN URLs. 3 | * 4 | * Example 1: All URLs using MDN links, which contain "/en/" as locale should be replaced by 5 | * "/en-US/" URLs. E.g. CSS should rather be 6 | * CSS. 7 | * 8 | * Implementation notes: This test checks whether a link's 'href' attribute starts with "/en/". 9 | * It does not check whether the link is an internal MDN link, nor does it check different 10 | * locales than the English one. 11 | */ 12 | 13 | const ERROR = require('./doctests.js').ERROR; 14 | 15 | exports.oldURLs = { 16 | name: "old_en_urls", 17 | desc: "old_en_urls_desc", 18 | check: function checkOldURLs(rootElement) { 19 | let links = rootElement.querySelectorAll("a[href^='/en/' i]"); 20 | let matches = []; 21 | 22 | for (let i = 0; i < links.length; i++) { 23 | matches.push({ 24 | msg: links[i].outerHTML, 25 | type: ERROR 26 | }); 27 | } 28 | 29 | return matches; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /tests/test-html-comments.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const htmlComments = require('../lib/html-comments.js').htmlComments; 4 | 5 | describe('htmlComments', function() { 6 | it('Should return 3 errors regarding HTML comments tag', function(done) { 7 | const str = '' + 8 | '' + 9 | ''; 10 | 11 | const expected = [ 12 | {msg: '', type: ERROR}, 13 | {msg: '', type: ERROR}, 14 | {msg: '', type: ERROR} 15 | ]; 16 | 17 | let rootElement = document.createElement("body"); 18 | rootElement.innerHTML = str; 19 | 20 | let results = htmlComments.check(rootElement); 21 | 22 | results.forEach((element, index) => { 23 | delete element.node; 24 | assert.deepEqual(expected[index], element); 25 | }); 26 | 27 | done(); 28 | }); 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /lib/font-elements.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for deprecated elements that should be removed or replaced by other 3 | * elements. 4 | * 5 | * Example 1: Emphasized text should be replaced by 6 | * Emphasized text or Emphasized text. 7 | * 8 | * Example 2:

Heading

should be replaced by 9 | *

Heading

. 10 | * 11 | * Implementation notes: This test searches for all elements, but doesn't provide 12 | * a hint whether they should be removed or replaced by other elements. 13 | */ 14 | 15 | const ERROR = require('./doctests.js').ERROR; 16 | 17 | exports.fontElements = { 18 | name: "font_elements", 19 | desc: "font_elements_desc", 20 | check: function checkFontElements(rootElement) { 21 | let fontElements = rootElement.getElementsByTagName("font"); 22 | let matches = []; 23 | 24 | for (let i = 0; i < fontElements.length; i++) { 25 | matches.push({ 26 | msg: fontElements[i].outerHTML, 27 | type: ERROR 28 | }); 29 | } 30 | 31 | return matches; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /lib/shell-prompts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for shell prompts, i.e. lines starting with '>' or '$' in code blocks. 3 | * 4 | * Example 1:
$user: 
should rather be replaced by 5 | *
.
 6 |  *
 7 |  *  Implementation notes: This test checks whether lines within 
 elements start with '$' or
 8 |  *  '>'.
 9 |  */
10 | 
11 | const ERROR = require('./doctests.js').ERROR;
12 | 
13 | exports.shellPrompts = {
14 |   name: "shell_prompts",
15 |   desc: "shell_prompts_desc",
16 |   check: function checkShellPrompts(rootElement) {
17 |     let pres = rootElement.querySelectorAll("pre");
18 |     let matches = [];
19 | 
20 |     for (let i = 0; i < pres.length; i++) {
21 |       let code = pres[i].innerHTML.replace(//g, "\n").replace(" ", " ");
22 |       let shellPrompts = code.match(/^(?:\$|>).*/gm);
23 |       if (shellPrompts) {
24 |         shellPrompts.forEach(function addMatch(shellPrompt) {
25 |           matches.push({
26 |             msg: shellPrompt.replace(/<.+?>/g, ""),
27 |             type: ERROR
28 |           });
29 |         });
30 |       }
31 |     }
32 | 
33 |     return matches;
34 |   }
35 | };
36 | 


--------------------------------------------------------------------------------
/lib/alert-print-in-code.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  *  Title: Test whether discouraged statements are used in example code.
 3 |  *
 4 |  *  Example 1: 
alert("Some message");
should be avoided and 5 | *
console.log("Some message");
be used instead. 6 | * 7 | * Implementation notes: This test checks all
 blocks for the usage of discouraged functions.
 8 |  *  In some cases their usage may not be avoided, like for example on their description pages.
 9 |  *  The test does not account for those cases, though.
10 |  */
11 | 
12 | const mapMatches = require('./doctests.js').mapMatches;
13 | const ERROR = require('./doctests.js').ERROR;
14 | 
15 | exports.alertPrintInCode = {
16 |   name: "alert_print_in_code",
17 |   desc: "alert_print_in_code_desc",
18 |   check: function checkAlertPrintInCode(rootElement) {
19 |     let pres = rootElement.getElementsByTagName("pre");
20 |     let matches = [];
21 |     for (let i = 0; i < pres.length; i++) {
22 |       let preMatches = pres[i].textContent.match(/(?:alert|print|eval|document\.write)\s*\((?:.|\n)+?\)/gi) || [];
23 |       matches = matches.concat(mapMatches(preMatches, ERROR));
24 |     }
25 | 
26 |     return matches;
27 |   }
28 | };
29 | 


--------------------------------------------------------------------------------
/tests/test-unnecessary-macro-params.js:
--------------------------------------------------------------------------------
 1 | const assert = require('assert');
 2 | const ERROR = require('../lib/doctests.js').ERROR;
 3 | const unnecessaryMacroParams = require('../lib/unnecessary-macro-params.js').unnecessaryMacroParams;
 4 | 
 5 | describe('unnecessaryMacroParams', function() {
 6 |   it('Should return 2 errors regarding unnecessary parameters in macro call', function(done) {
 7 |     const str = '{{JSRef}}' +
 8 |       '{{JSRef()}}' +
 9 |       '{{JSRef("Global_Objects")}}' +
10 |       '{{ JSRef("Global_Objects", "Math") }}' +
11 |       '{{csssyntax("font-family")}}';
12 | 
13 |     const expected = [
14 |       {msg: "macro_with_unused_params", msgParams: ['{{JSRef("Global_Objects")}}'], type: ERROR},
15 |       {msg: "macro_with_unused_params", msgParams: ['{{ JSRef("Global_Objects", "Math") }}'], type: ERROR}
16 |     ];
17 | 
18 |     let rootElement = document.createElement("body");
19 |     rootElement.innerHTML = str;
20 | 
21 |     let results = unnecessaryMacroParams.check(rootElement);
22 | 
23 |     results.forEach((element, index) => {
24 |       delete element.node;
25 |       assert.deepEqual(expected[index], element);
26 |     });
27 | 
28 |     done();
29 |   });
30 | });
31 | 
32 | 


--------------------------------------------------------------------------------
/lib/name-attribute.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  *  Title: Test for elements with 'name' attributes.
 3 |  *
 4 |  *  Example 1: 

Syntax

should rather be

. 5 | * 6 | * Example 2: The name="" attribute in

paragraph

should rather be 7 | * removed. 8 | * 9 | * Implementation notes: This test checks all elements containing 'name' attributes. 10 | */ 11 | 12 | const ERROR = require('./doctests.js').ERROR; 13 | 14 | exports.nameAttribute = { 15 | name: "name_attributes", 16 | desc: "name_attributes_desc", 17 | check: function checkNameAttribute(rootElement) { 18 | let elementsWithNameAttribute = rootElement.querySelectorAll("[name]"); 19 | let matches = []; 20 | 21 | for (let i = 0; i < elementsWithNameAttribute.length; i++) { 22 | matches.push({ 23 | node: elementsWithNameAttribute[i], 24 | msg: `name="${elementsWithNameAttribute[i].getAttribute("name")}"`, 25 | type: ERROR 26 | }); 27 | } 28 | 29 | return matches; 30 | }, 31 | fix: function fixNameAttribute(matches) { 32 | matches.forEach(match => { 33 | match.node.removeAttribute("name"); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /tests/test-name-attribute.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const nameAttribute = require('../lib/name-attribute.js').nameAttribute; 4 | 5 | describe('nameAttribute', function() { 6 | it('Should return 5 errors regarding name= attribute', function(done) { 7 | const str = '' + 8 | '
' + 9 | '

foo

' + 10 | '

foo bar

' + 11 | '

baz

'; 12 | 13 | const expected = [ 14 | {msg: 'name=""', type: ERROR}, 15 | {msg: 'name="foo"', type: ERROR}, 16 | {msg: 'name="foo"', type: ERROR}, 17 | {msg: 'name="foo_bar"', type: ERROR}, 18 | {msg: 'name="baz"', type: ERROR} 19 | ]; 20 | 21 | const expectedAfterFixing = []; 22 | 23 | let rootElement = document.createElement("body"); 24 | rootElement.innerHTML = str; 25 | 26 | let results = nameAttribute.check(rootElement); 27 | 28 | results.forEach((element, index) => { 29 | delete element.node; 30 | assert.deepEqual(expected[index], element); 31 | }); 32 | 33 | done(); 34 | }); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /lib/style-attribute.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for incorrectly 'style' attributes. 3 | * 4 | * Example 1:

Emphasized text/

should rather be replaced 5 | * by

Emphasized text

. 6 | * 7 | * Implementation notes: This test searches for all 'style' attributes, which are not part of 8 | * CKEditor's new paragraph helper. 9 | */ 10 | 11 | const ERROR = require('./doctests.js').ERROR; 12 | 13 | const isNewParagraphHelper = require('./doctests.js').isNewParagraphHelper; 14 | 15 | exports.styleAttribute = { 16 | name: "style_attributes", 17 | desc: "style_attributes_desc", 18 | check: function checkStyleAttribute(rootElement) { 19 | let elementsWithStyleAttribute = rootElement.querySelectorAll("[style]"); 20 | let matches = []; 21 | 22 | for (let i = 0; i < elementsWithStyleAttribute.length; i++) { 23 | let node = elementsWithStyleAttribute[i]; 24 | 25 | // Exclude new paragraph helper 26 | if (isNewParagraphHelper(node) || isNewParagraphHelper(node.firstElementChild)) { 27 | continue; 28 | } 29 | 30 | matches.push({ 31 | msg: `style="${node.getAttribute("style")}"`, 32 | type: ERROR 33 | }); 34 | } 35 | 36 | return matches; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /tests/test-shell-prompts.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const shellPrompts = require('../lib/shell-prompts.js').shellPrompts; 4 | 5 | describe('shellPrompts', function() { 6 | it('Should return 6 errors regarding shell prompts', function(done) { 7 | const str = '
somecommand
' + 8 | '
$somecommand
' + 9 | '
$ somecommand
' + 10 | '
>somecommand
' + 11 | '
> somecommand
' + 12 | '
$ somecommand\noutput
$ anothercommand
'; 13 | 14 | const expected = [ 15 | {msg: '$somecommand', type: ERROR}, 16 | {msg: '$ somecommand', type: ERROR}, 17 | {msg: '>somecommand', type: ERROR}, 18 | {msg: '> somecommand', type: ERROR}, 19 | {msg: '$ somecommand', type: ERROR}, 20 | {msg: '$ anothercommand', type: ERROR} 21 | ] 22 | 23 | let rootElement = document.createElement("body"); 24 | rootElement.innerHTML = str; 25 | 26 | let results = shellPrompts.check(rootElement); 27 | 28 | results.forEach((element, index) => { 29 | delete element.node; 30 | assert.deepEqual(expected[index], element); 31 | }); 32 | 33 | done(); 34 | }); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /tests/test-alert-print-in-code.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const alertPrintInCode = require('../lib/alert-print-in-code.js').alertPrintInCode; 3 | const ERROR = require('../lib/doctests.js').ERROR; 4 | 5 | describe('alertPrintInCode', function() { 6 | it('Should return 4 alert or print error', function(done) { 7 | const str = '
alert("foo")
' + 8 | '
print("bar")
' + 9 | '
let someOthercode = baz; ' +
10 |       'alert("hello world"); \n let moreCode;
' + 11 | '
document.write("foobar");
'; 12 | 13 | const expected = [ 14 | {msg: 'alert("foo")', type: ERROR}, 15 | {msg: 'print("bar")', type: ERROR}, 16 | {msg: 'alert("hello world")', type: ERROR}, 17 | {msg: 'document.write("foobar")', type: ERROR} 18 | ]; 19 | 20 | 21 | let rootElement = document.createElement("body"); 22 | rootElement.innerHTML = str; 23 | 24 | let results = alertPrintInCode.check(rootElement); 25 | 26 | results.forEach((element, index) => { 27 | if(element.msg != expected[index].msg || element.type != expected[index].type) 28 | done(Error('Expected : ' + JSON.stringify(expected[index]) + ' got : ' + JSON.stringify(element))); 29 | }); 30 | 31 | done(); 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdn-doc-linter-rules", 3 | "version": "0.0.1", 4 | "description": "Rules to lint MDN documents", 5 | "author": "Mozilla Developer Network", 6 | "homepage": "https://github.com/mdn/doc-linter-rules#readme", 7 | "license": "MPL-2.0", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/mdn/doc-linter-rules.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/mdn/doc-linter-rules/issues" 14 | }, 15 | "keywords": [ 16 | "linting", 17 | "rules", 18 | "documentation", 19 | "mdn" 20 | ], 21 | "main": "index.js", 22 | "scripts": { 23 | "test": "./node_modules/.bin/karma start --browsers Firefox,Nightmare --single-run", 24 | "bundle": "./node_modules/.bin/browserify lib/index.js --s linter -o dist/bundle.js" 25 | }, 26 | "devDependencies": { 27 | "browserify": "^14.4.0", 28 | "karma": "^1.7.0", 29 | "karma-browserify": "^5.1.1", 30 | "karma-chrome-launcher": "^2.1.1", 31 | "karma-firefox-launcher": "^1.0.1", 32 | "karma-mocha": "^1.3.0", 33 | "karma-mocha-reporter": "^2.2.3", 34 | "karma-nightmare": "^0.4.8", 35 | "karma-phantomjs-launcher": "^1.0.4", 36 | "mocha": "^3.4.2", 37 | "nightmare": "^2.10.0", 38 | "watchify": "^3.9.0" 39 | }, 40 | "dependencies": {} 41 | } 42 | -------------------------------------------------------------------------------- /tests/test-example-colon-heading.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const exampleColonHeading = require('../lib/example-colon-heading.js').exampleColonHeading; 4 | 5 | describe('exampleColonHeading', function() { 6 | it('Sould return 3 ERROR regarding example colon heading', function(done) { 7 | const str = '

Example

' + 8 | '

Example

' + 9 | '

Example: Foo

' + 10 | '

Example: Using Math.sin

' + 11 | '

Example: Foo

'; 12 | 13 | const expected = [ 14 | {msg: '

Example: Foo

', type: ERROR}, 15 | {msg: '

Example: Using Math.sin

', type: ERROR}, 16 | {msg: '

Example: Foo

', type: ERROR} 17 | ]; 18 | 19 | const expectedAfterFixing = []; 20 | 21 | let rootElement = document.createElement("body"); 22 | rootElement.innerHTML = str; 23 | 24 | let results = exampleColonHeading.check(rootElement); 25 | results.forEach((element, index) => { 26 | delete element.node 27 | assert.deepEqual(expected[index], element); 28 | }); 29 | 30 | done(); 31 | }); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /lib/code-in-pre.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test whether code blocks unexpectedly contain elements, which break the syntax 3 | * highlighting. 4 | * 5 | * Example 1:
var x = 1;
is considered invalid and 6 | * should rather be written as
var x = 1;
. 7 | * 8 | * Implementation notes: This test checks whether
 elements contain  elements.
 9 |  */
10 | 
11 | const ERROR = require('./doctests.js').ERROR;
12 | 
13 | exports.codeInPre = {
14 |   name: "code_in_pre",
15 |   desc: "code_in_pre_desc",
16 |   check: function checkCodeInPre(rootElement) {
17 |     let codesInPres = rootElement.querySelectorAll("pre code");
18 |     let matches = [];
19 | 
20 |     for (let i = 0; i < codesInPres.length; i++) {
21 |       matches.push({
22 |         node: codesInPres[i],
23 |         msg: codesInPres[i].outerHTML,
24 |         type: ERROR
25 |       });
26 |     }
27 | 
28 |     return matches;
29 |   },
30 |   fix: function fixCodeInPre(matches) {
31 |     matches.forEach(match => {
32 |       let children = new DocumentFragment();
33 |       for (let i = 0; i < match.node.childNodes.length; i++) {
34 |         children.appendChild(match.node.childNodes[i].cloneNode(true));
35 |       }
36 |       match.node.parentNode.replaceChild(children, match.node);
37 |     });
38 |   }
39 | };
40 | 


--------------------------------------------------------------------------------
/lib/link-count.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Title : Test whether there are too many links in a page
 3 |  * This is done for SEO reasons, based on the assumption that in the case of a lot of links not all of them would be followed and indexed.
 4 |  *
 5 |  *  A Warning is emitted between 100 and 250 links
 6 |  *  An Error is emitted for more than 250 links
 7 |  *  An Info is emitted with the amount of links
 8 |  */
 9 | 
10 | const WARNING = require('./doctests.js').WARNING;
11 | const ERROR = require('./doctests.js').ERROR;
12 | const INFO = require('./doctests.js').INFO;
13 | 
14 | exports.linkCount = {
15 |   name: "link_count",
16 |   desc: "link_count_desc",
17 |   check: function checkLinkCost(rootElement) {
18 |     let links = rootElement.getElementsByTagName("a");
19 | 
20 |     if(links.length >= 100 && links.length < 250) {
21 |       return [{
22 |         msg: "count_link_warning",
23 |         msgParams: [links.length],
24 |         type: WARNING
25 |       }];
26 |     }
27 | 
28 |     if(links.length >= 250) {
29 |       return [{
30 |         msg: "count_link_error",
31 |         msgParams: [links.length],
32 |         type: ERROR
33 |       }];
34 |     }
35 | 
36 |     return [{
37 |       msg: "count_link_info",
38 |       msgParams: [links.length],
39 |       type: INFO
40 |     }];
41 |   },
42 | 
43 |   fix: function fixLinkCost(matches) {
44 |     return;
45 |   }
46 | };
47 | 


--------------------------------------------------------------------------------
/tests/test-span-count.js:
--------------------------------------------------------------------------------
 1 | const assert = require('assert');
 2 | const spanCount = require('../lib/span-count.js').spanCount;
 3 | const ERROR = require('../lib/doctests.js').ERROR;
 4 | 
 5 | describe('SpanCount', function() {
 6 | 	it('Should return 3 span in errors', function(done) {
 7 | 		const str = 'what?' +
 8 | 			'

nope

' + 9 | 'bar' + 10 | '
foobar
' + 11 | 'seoseoseo' + 12 | ' '; // Simulates new paragraph helper 13 | const expected = [ 14 | {msg: 'what?', type: ERROR}, 15 | {msg: 'bar', type: ERROR}, 16 | {msg: '
foobar
', type: ERROR} 17 | ]; 18 | 19 | const expectedAfterFixing = [ 20 | {msg: 'bar', type: ERROR} 21 | ]; 22 | 23 | let rootElement = document.createElement("body"); 24 | rootElement.innerHTML = str; 25 | 26 | let results = spanCount.check(rootElement); 27 | 28 | results.forEach((element, index) => { 29 | if(element.msg != expected[index].msg || element.type != expected[index].type) done(Error('Expected : ' + JSON.stringify(expected[index]) + ' got : ' + JSON.stringify(element))); 30 | }); 31 | 32 | done(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /lib/example-colon-heading.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for example headings starting with 'Example:'. 3 | * 4 | * Example 1:

Example: Simple example

should rather be written as

Simple example

5 | * 6 | * Implementation notes: This test checks whether the text of heading elements start with 7 | * 'Example:'. 8 | */ 9 | 10 | const ERROR = require('./doctests.js').ERROR; 11 | 12 | const reExample = /^\s*Example:[\s_]*/; 13 | 14 | exports.exampleColonHeading = { 15 | name: "example_headings", 16 | desc: "example_headings_desc", 17 | 18 | check: function checkExampleColonHeading(rootElement) { 19 | let headlines = rootElement.querySelectorAll("h1, h2, h3, h4, h5, h6"); 20 | let matches = []; 21 | 22 | for (let i = 0; i < headlines.length; i++) { 23 | if (headlines[i].textContent.match(reExample)) { 24 | matches.push({ 25 | node: headlines[i], 26 | msg: headlines[i].outerHTML, 27 | type: ERROR 28 | }); 29 | } 30 | } 31 | 32 | return matches; 33 | }, 34 | 35 | fix: function fixExampleColonHeading(matches) { 36 | matches.forEach(match => { 37 | match.node.textContent = match.node.textContent.replace(reExample, ""); 38 | let id = match.node.getAttribute("id"); 39 | id = id.replace(reExample, ""); 40 | match.node.setAttribute("id", id); 41 | }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /tests/test-code-in-pre.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const codeInPre = require('../lib/code-in-pre.js').codeInPre; 4 | 5 | describe('codeInPre', function() { 6 | it('Should return 5 errors regarding code in pre', function(done) { 7 | const str = '
no code
' + 8 | '
no code
' + 9 | '
some code
' + 10 | '
some codesome more inline code
' + 11 | '
foo\nsome code\nbar
\nsome code with\nline break\nbaz
'; 12 | 13 | const expected = [ 14 | {msg: 'some code', type: ERROR}, 15 | {msg: 'some code', type: ERROR}, 16 | {msg: 'some more inline code', type: ERROR}, 17 | {msg: 'some code',type: ERROR}, 18 | {msg: 'some code with\nline break',type: ERROR} 19 | ]; 20 | 21 | const expectedAfterFixing = []; 22 | 23 | let rootElement = document.createElement("body"); 24 | rootElement.innerHTML = str; 25 | 26 | let results = codeInPre.check(rootElement); 27 | 28 | results.forEach((element, index) => { 29 | delete element.node 30 | assert.deepEqual(element, expected[index]); 31 | }); 32 | 33 | done(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/html-comments.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for HTML comments (i.e. ). 3 | * 4 | * Example 1: Because an HTML comment like 5 | * 6 | * is only visible to article authors, it should rather either be made visible to readers, 7 | * e.g. by replacing it by 8 | *

This is a simple example for how the API is used.

9 | * or just be removed. 10 | * 11 | * Implementation notes: This test searches for all HTML comments. Because CKEditor escapes them 12 | * for security reasons, they need to be decoded first before displaying them. 13 | */ 14 | 15 | const ERROR = require('./doctests.js').ERROR; 16 | 17 | exports.htmlComments = { 18 | name: "html_comments", 19 | desc: "html_comments_desc", 20 | check: function checkHTMLComments(rootElement) { 21 | let treeWalker = document.createTreeWalker( 22 | rootElement, 23 | NodeFilter.SHOW_COMMENT 24 | ); 25 | let matches = []; 26 | 27 | while (treeWalker.nextNode()) { 28 | let comment = treeWalker.currentNode.data.replace(/\s*\{cke_protected\}\{C\}(\S+)\s*/, 29 | (match, data) => decodeURIComponent(data)); 30 | matches.push({ 31 | node: treeWalker.currentNode, 32 | msg: comment, 33 | type: ERROR 34 | }); 35 | } 36 | 37 | return matches; 38 | }, 39 | fix: function fixHTMLComments(matches) { 40 | matches.forEach(match => { 41 | match.node.remove(); 42 | }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /tests/test-style-attribute.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const styleAttribute = require('../lib/style-attribute.js').styleAttribute; 4 | 5 | describe('styleAttribute', function() { 6 | it('Should return 5 errors regarding style= attribute', function(done) { 7 | const str = '' + 8 | '
' + 9 | '
' + 10 | 'test' + 11 | '' + 12 | ' '; // Simulates new paragraph helper 13 | 14 | const expected = [ 15 | {msg: 'style=""', type: ERROR}, 16 | {msg: 'style="margin-top:5%"', type: ERROR}, 17 | {msg: 'style="background:#fff; color: rgb(234, 234, 234);"', type: ERROR}, 18 | {msg: 'style="padding: 5px !important"', type: ERROR}, 19 | {msg: 'style="font-family: \'Open Sans\', serif; line-height: 1.5"', type: ERROR} 20 | ]; 21 | 22 | let rootElement = document.createElement("body"); 23 | rootElement.innerHTML = str; 24 | 25 | let results = styleAttribute.check(rootElement); 26 | 27 | results.forEach((element, index) => { 28 | delete element.node; 29 | assert.deepEqual(expected[index], element); 30 | }); 31 | 32 | done(); 33 | }); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /lib/article-length.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test the length and read time of the article. 3 | * 4 | * Example 1: An article length of 1000 words will result in a read time estimation of 4 minutes. 5 | * 6 | * Implementation notes: This test expects a reading speed of 275 words per minute. The text is 7 | * roughly split by word bounderies using a regular expression. An article exceeding some length 8 | * threshold (2000 words by default) is considered long. 9 | */ 10 | 11 | 12 | const WARNING = require('./doctests.js').WARNING; 13 | const INFO = require('./doctests.js').INFO; 14 | 15 | // TODO: Implement the preferences for LONG_ARTICLE_WORD_COUNT_THRESHOLD 16 | const LONG_ARTICLE_WORD_COUNT_THRESHOLD = 2000; 17 | const WORDS_PER_MINUTE = 275; 18 | 19 | exports.articleLength = { 20 | name: "article_length", 21 | desc: "article_length_desc", 22 | check: function checkArticleLength(rootElement) { 23 | let text = rootElement.textContent; 24 | let wordCount = text.match(/\w+/g).length; 25 | let readTimeEstimation = Math.round(wordCount / WORDS_PER_MINUTE); 26 | if (readTimeEstimation === 0) { 27 | readTimeEstimation = "< 1"; 28 | } 29 | let matches = [{ 30 | msg: "article_length_info", 31 | msgParams: [String(wordCount), String(readTimeEstimation)], 32 | type: INFO 33 | }]; 34 | if (wordCount > LONG_ARTICLE_WORD_COUNT_THRESHOLD) { 35 | matches.push({ 36 | msg: "long_article", 37 | type: WARNING 38 | }); 39 | } 40 | return matches; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/span-count.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for incorrectly used elements. 3 | * 4 | * Example 1: Emphasized text/ should rather be replaced 5 | * by Emphasized text. 6 | * 7 | * Implementation notes: This test searches for all elements, which don't hold the SEO 8 | * summary and are not part of CKEditor's new paragraph helper. 9 | */ 10 | 11 | const ERROR = require('./doctests.js').ERROR; 12 | 13 | const isNewParagraphHelper = require('./doctests.js').isNewParagraphHelper; 14 | 15 | exports.spanCount = { 16 | name: "span_elements", 17 | desc: "span_elements_desc", 18 | 19 | check: function checkSpanCount(rootElement) { 20 | let spanElements = rootElement.querySelectorAll("span:not(.seoSummary)"); 21 | let matches = []; 22 | 23 | for (let i = 0; i < spanElements.length; i++) { 24 | let node = spanElements[i]; 25 | 26 | // Exclude new paragraph helper 27 | if (isNewParagraphHelper(node) || isNewParagraphHelper(node.firstElementChild)) { 28 | continue; 29 | } 30 | 31 | matches.push({ 32 | node, 33 | msg: node.outerHTML, 34 | type: ERROR 35 | }); 36 | } 37 | 38 | return matches; 39 | }, 40 | 41 | fix: function fixSpanCount(matches) { 42 | matches.forEach(match => { 43 | // Remove element in case it is unstyled 44 | if (!match.node.getAttribute("id") && !match.node.getAttribute("class") && !match.node.getAttribute("style")) { 45 | match.node.remove(); 46 | } 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /tests/test-summary-heading.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const summaryHeading = require('../lib/summary-heading.js').summaryHeading; 4 | 5 | describe('summaryHeading', function() { 6 | it('Should return 6 errors regarding Summary heading', function(done) { 7 | const str = '

Summary

' + 8 | '

Summary

' 9 | '

Summary

' + 10 | '

Summary

' 11 | '

Summary

' + 12 | '

Summary

' + 13 | '

Summary

' + 14 | '

Summary

' + 15 | '
Summary
' + 16 | '
Summary
' + 17 | '
Summary
' + 18 | '
Summary
'; 19 | 20 | const expected = [ 21 | {msg: '

Summary

', type: ERROR}, 22 | {msg: '

Summary

', type: ERROR}, 23 | {msg: '

Summary

', type: ERROR}, 24 | {msg: '

Summary

', type: ERROR}, 25 | {msg: '
Summary
', type: ERROR}, 26 | {msg: '
Summary
', type: ERROR} 27 | ]; 28 | 29 | const expectedAfterFixing = []; 30 | 31 | let rootElement = document.createElement("body"); 32 | rootElement.innerHTML = str; 33 | 34 | let results = summaryHeading.check(rootElement); 35 | 36 | results.forEach((element, index) => { 37 | delete element.node; 38 | assert.deepEqual(expected[index], element); 39 | }); 40 | 41 | done(); 42 | }); 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /tests/test-line-length-in-pre.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const WARNING = require('../lib/doctests.js').WARNING; 3 | const lineLengthInPre = require('../lib/line-length-in-pre.js').lineLengthInPre; 4 | 5 | describe('lineLengthInPre', function() { 6 | it('Should return 2 warning regarding too long
', function(done) {
 7 |     const str =  '
11111111111111111111111 11111111111111111111111 111111111111 111111111111111 1
' + 8 | '
11111111111111111111111 11111111111111111111111
111111111111 111111111111111 1
' + 9 | '
short\nstuff
' + 10 | '
Code having some link.
' + 11 | '
foo\nsome code\nbar
\n' + 12 | 'some code with\nline break\nbaz' + 13 | '11111111111 111111111111 function{ foo(); 11111111111111 bar 1111111111111111 111
'; 14 | 15 | const expected = [ 16 | { 17 | msg: '11111111111111111111111 11111111111111111111111 111111111111 111111111111111 1', 18 | type: WARNING 19 | }, 20 | { 21 | msg: 'baz11111111111 111111111111 function{ foo(); 11111111111111 bar 1111111111111111 111', 22 | type: WARNING 23 | } 24 | ]; 25 | 26 | let rootElement = document.createElement("body"); 27 | rootElement.innerHTML = str; 28 | 29 | let results = lineLengthInPre.check(rootElement); 30 | 31 | results.forEach((element, index) => { 32 | delete element.node; 33 | assert.deepEqual(expected[index], element); 34 | }); 35 | 36 | done(); 37 | }); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /lib/line-length-in-pre.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for the line length in code blocks. 3 | * 4 | * Example 1: Code blocks with very long lines like 5 | *
This is some code block with a long line exceeding the maximum of 78 characters.
6 | * should either be shortened or split into several lines to avoid the display of horizontal 7 | * scrollbars. 8 | * 9 | * Implementation notes: This test uses a threshold of 78 characters for the maximum length of 10 | * a line.
tags added while editing are replaced by line breaks and all other HTML tags 11 | * are removed. 12 | */ 13 | 14 | 15 | const WARNING = require('./doctests.js').WARNING; 16 | const mapMatches = require('./doctests.js').mapMatches; 17 | 18 | exports.lineLengthInPre = { 19 | name: "pre_line_too_long", 20 | desc: "pre_line_too_long_desc", 21 | check: function checkLineLengthInPre(rootElement) { 22 | let pres = rootElement.getElementsByTagName("pre"); 23 | let matches = []; 24 | 25 | for (let i = 0; i < pres.length; i++) { 26 | // While editing it happens that there are
s added instead of line break characters 27 | // Those need to be replaced by line breaks to correctly recognize long lines 28 | let codeBlock = pres[i].innerHTML.replace(//g, "\n"); 29 | 30 | // Remove all other HTML tags and only display the plain text 31 | codeBlock = codeBlock.replace(/<.+?>/g, ""); 32 | 33 | let longLines = codeBlock.match(/^(?:[^\r\n]|\r(?!\n)){78,}$/gm); 34 | if (longLines) { 35 | matches = matches.concat(longLines); 36 | } 37 | } 38 | 39 | return mapMatches(matches, WARNING); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /tests/test-absolute-urls-for-internal-links.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const absoluteURLsForInternalLinks = require('../lib/absolute-urls-for-internal-links.js').absoluteURLsForInternalLinks; 3 | const WARNING = require('../lib/doctests.js').WARNING; 4 | 5 | describe('absoluteURLsForInternalLinks', function() { 6 | it('Should return 3 internal links warning', function(done) { 7 | const str = 'Page' + 8 | 'Anchor' + 9 | 'Anchor' + 10 | 'Anchor' + 11 | 'Anchor' + 12 | 'Anchor'; 13 | 14 | const expected = [ 15 | {msg: 'Anchor', type: WARNING}, 16 | {msg: 'Anchor', type: WARNING}, 17 | {msg: 'Anchor', type: WARNING} 18 | ]; 19 | 20 | const expectedAfterFixing = []; 21 | 22 | let rootElement = document.createElement("body"); 23 | rootElement.innerHTML = str; 24 | 25 | let results = absoluteURLsForInternalLinks.check(rootElement); 26 | 27 | results.forEach((element, index) => { 28 | if(element.msg != expected[index].msg || element.type != expected[index].type) 29 | done(Error('Expected : ' + JSON.stringify(expected[index]) + ' got : ' + JSON.stringify(element))); 30 | }); 31 | 32 | done(); 33 | }); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /tests/test-empty-elements.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const WARNING = require('../lib/doctests.js').WARNING; 4 | const emptyElements = require('../lib/empty-elements.js').emptyElements; 5 | 6 | describe('emptyElements', function() { 7 | it('Should return 5 errors and 1 warnings', function(done) { 8 | const str = '

' + 9 | '

\n\r

' + 10 | '

 

' + 11 | '



' + 12 | '

' + 13 | '
foo
' + 14 | '' + 15 | '

' + 16 | '' + 17 | '

some text

' + 18 | '

some text

' + 19 | ' ' + 20 | ''; 21 | 22 | const expected = [ 23 | {msg: '

', type: ERROR}, 24 | {msg: '

\n\n

', type: ERROR}, 25 | {msg: '

 

', type: ERROR}, 26 | {msg: '



',type: ERROR}, 27 | {msg: '

',type: ERROR}, 28 | {msg: '
', type: WARNING} 29 | ]; 30 | const expectedAfterFixing = [ 31 | {msg: '
',type: WARNING} 32 | ]; 33 | 34 | let rootElement = document.createElement("body"); 35 | rootElement.innerHTML = str; 36 | 37 | let results = emptyElements.check(rootElement); 38 | 39 | results.forEach((element, index) => { 40 | delete element.node 41 | assert.deepEqual(element, expected[index]); 42 | }); 43 | 44 | done(); 45 | }); 46 | }); 47 | 48 | -------------------------------------------------------------------------------- /tests/test-different-locale-links.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const differentLocaleLinks = require('../lib/different-locale-links.js').differentLocaleLinks; 4 | 5 | describe('differentLocaleLinks', function() { 6 | it('Should return 3 errors regarding link using the wrong locale', function(done) { 7 | const str = 'Page' + 8 | 'Page' + 9 | 'Page' + 10 | 'Page' + 11 | 'Page' + 12 | 'Page' + 13 | 'Page'; 14 | 15 | const expected = [ 16 | {msg: "link_using_wrong_locale", msgParams: ["/xx-YY/docs/some/page", "en-US"], type: ERROR}, 17 | {msg: "link_using_wrong_locale", msgParams: ["http://developer.mozilla.org/xx-YY/docs/some/page", "en-US"], type: ERROR}, 18 | {msg: "link_using_wrong_locale", msgParams: ["https://developer.mozilla.org/xx-YY/docs/some/page", "en-US"], type: ERROR} 19 | ]; 20 | 21 | let rootElement = document.createElement("body"); 22 | rootElement.innerHTML = str; 23 | 24 | let results = differentLocaleLinks.check(rootElement); 25 | 26 | results.forEach((element, index) => { 27 | // TODO: Figure out the issues with msgParams as it's not normal 28 | delete element.msgParams; 29 | delete expected[index].msgParams; 30 | assert.deepEqual(element, expected[index]); 31 | }); 32 | 33 | done(); 34 | }); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /tests/test-data-macro-note.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const dataMacroNote = require('../lib/data-macro-note.js').dataMacroNote; 4 | 5 | describe('dataMacroNote', function() { 6 | it('Should return 2 errors regarding Data Macros Notes', function(done) { 7 | const str = '

{{nondatamacro}}

' + 8 | '

{{compat}}

' + 9 | '

{{css_ref}}

' + 10 | '

{{cssanimatedproperties}}

' + 11 | '

{{cssinfo}}

' + 12 | '

{{csssyntax}}

' + 13 | '

{{WebExtBrowserCompat}}

' + 14 | '

{{Compat}}

' + 15 | '

{{Compat}}

'; 16 | 17 | const expected = [ 18 | {msg: "data_macro_note_missing", msgParams: ["{{Compat}}"], type: ERROR}, 19 | {msg: "data_macro_source_link_missing", msgParams: ["{{Compat}}"], type: ERROR} 20 | ]; 21 | 22 | let rootElement = document.createElement("body"); 23 | rootElement.innerHTML = str; 24 | 25 | let results = dataMacroNote.check(rootElement); 26 | 27 | results.forEach((element, index) => { 28 | assert.deepEqual(element, expected[index]); 29 | }); 30 | 31 | done(); 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /lib/absolute-urls-for-internal-links.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test whether internal MDN links have absolute URLs. 3 | * 4 | * Example 1: An MDN link to the 'display' CSS property should use a the relative URL 5 | * /en-US/docs/Web/CSS/display and not the absolute URL 6 | * https://developer.mozilla.org/en-US/docs/Web/CSS/display. 7 | * 8 | * Implementation notes: This test checks whether the URL begins with the link 9 | * "https://developer.mozilla.org/", this means that allizom.org links are not covered. 10 | * window.location cannot be used for this, because that would break the unit test, which uses 11 | * about:blank as URL. 12 | */ 13 | 14 | const WARNING = require('./doctests.js').WARNING; 15 | 16 | const reAbsoluteURL = /^(?:https?:)?\/\/developer\.mozilla\.org(?=\/)/i; 17 | 18 | exports.absoluteURLsForInternalLinks = { 19 | name: "absolute_urls_for_internal_links", 20 | desc: "absolute_urls_for_internal_links_desc", 21 | check: function checkAbsoluteURLsForInternalLinks(rootElement) { 22 | let links = rootElement.getElementsByTagName("a"); 23 | let matches = []; 24 | for (let i = 0; i < links.length; i++) { 25 | let href = links[i].getAttribute("href"); 26 | if (href && href.match(reAbsoluteURL)) { 27 | matches.push({ 28 | node: links[i], 29 | msg: links[i].outerHTML, 30 | type: WARNING 31 | }); 32 | } 33 | } 34 | 35 | return matches; 36 | }, 37 | 38 | fix: function fixAbsoluteURLsForInternalLinks(matches) { 39 | matches.forEach(match => { 40 | let href = match.node.getAttribute("href"); 41 | let relativeURL = href.replace(reAbsoluteURL, ""); 42 | match.node.setAttribute("href", relativeURL); 43 | match.node.setAttribute("data-cke-saved-href", relativeURL); 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /lib/different-locale-links.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for wrong locales in links (). 3 | * 4 | * Example 1: If you are in a German document, internal links should 5 | * contain the "de" locale and not e.g. "en-US". 6 | * 7 | * Implementation notes: This test compares the current locale in the slug (document.URL) 8 | * with the locale used in internal links () 9 | */ 10 | 11 | const ERROR = require('./doctests.js').ERROR; 12 | 13 | exports.differentLocaleLinks = { 14 | name: "different_locale_links", 15 | desc: "different_locale_links_desc", 16 | check: function checkDifferentLocaleLinks(rootElement) { 17 | let [, pageDomain, pageLocale] = document.URL.match(/^(?:https?:\/\/)(.+?)\/([^/]+)/i) || 18 | ["", "developer.mozilla.org", "en-US"]; 19 | let links = rootElement.getElementsByTagName("a"); 20 | let matches = []; 21 | for (let i = 0; i < links.length; i++) { 22 | let href = links[i].getAttribute("href"); 23 | if (href) { 24 | let [, linkDomain, linkLocale] = href.match(/^(?:https?:\/\/(.+?))?\/([^/]+)/i) || 25 | [null, null, null]; 26 | let oldAttachmentLink = false; 27 | let compareLocale = false; 28 | if(linkLocale !== null) { 29 | oldAttachmentLink = !linkLocale.startsWith("@"); 30 | compareLocale = linkLocale.toLowerCase() !== pageLocale.toLowerCase(); 31 | } 32 | let internalLinks = !linkDomain || linkDomain === pageDomain; 33 | 34 | if(linkLocale !== null && linkLocale && oldAttachmentLink && compareLocale && internalLinks) { 35 | matches.push({ 36 | msg: "link_using_wrong_locale", 37 | msgParams: [href, pageLocale], 38 | type: ERROR 39 | }); 40 | } 41 | } 42 | } 43 | 44 | return matches; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /lib/empty-brackets.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for macros with empty brackets. 3 | * 4 | * Example 1: The {{CompatNo}} macro does not expect any parameters, so the parameter brackets 5 | * are redundant and should be avoided, i.e. it should not be written as {{CompatNo()}}. 6 | * 7 | * Implementation notes: This test checks for macros written with empty brackets and requests to 8 | * remove them. It does not check whether the macros actually require parameters. 9 | */ 10 | 11 | const ERROR = require('./doctests.js').ERROR; 12 | 13 | const reMacroWithEmptyBrackets = /\{\{\s*(.*?)\(\)\s*\}\}/gi; 14 | 15 | exports.emptyBrackets = { 16 | name: "empty_brackets", 17 | desc: "empty_brackets_desc", 18 | 19 | check: function checkEmptyBrackets(rootElement) { 20 | let treeWalker = document.createTreeWalker( 21 | rootElement, 22 | NodeFilter.SHOW_TEXT, 23 | // eslint-disable-next-line 24 | {acceptNode: node => reMacroWithEmptyBrackets.test(node.textContent) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT} 25 | ); 26 | let matches = []; 27 | 28 | while (treeWalker.nextNode()) { 29 | let textNodeMatches = treeWalker.currentNode.textContent.match(reMacroWithEmptyBrackets) || []; 30 | textNodeMatches.forEach(match => { 31 | matches.push({ 32 | node: treeWalker.currentNode, 33 | msg: match, 34 | type: ERROR 35 | }); 36 | }); 37 | } 38 | 39 | return matches; 40 | }, 41 | 42 | fix: function fixEmptyBrackets(matches) { 43 | let previousNode = null; 44 | matches.forEach(match => { 45 | if (match.node !== previousNode) { 46 | match.node.textContent = match.node.textContent 47 | .replace(reMacroWithEmptyBrackets, "{{$1}}"); 48 | } 49 | previousNode = match.node; 50 | }); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /lib/pre-without-class.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for code blocks without 'class' attribute specifying the syntax highlighting. 3 | * 4 | * Example 1:
var x = 1
should rather be replaced by 5 | *
var x = 1
. 6 | * 7 | * Implementation notes: This test checks all
 elements that have either an empty 'class'
 8 |  *  attribute or none at all. It also checks for elements that have the class 'eval'
 9 |  */
10 | 
11 | const WARNING = require('./doctests.js').WARNING;
12 | const ERROR = require('./doctests.js').ERROR;
13 | 
14 | exports.preWithoutClass = {
15 |   name: "pre_without_class",
16 |   desc: "pre_without_class_desc",
17 |   check: function checkPreWithoutClass(rootElement) {
18 |     let presWithoutClass = rootElement.querySelectorAll("pre:not([class]), pre[class='']");
19 |     let presWithEvalClass = rootElement.querySelectorAll("pre[class='eval']");
20 |     let matches = [];
21 | 
22 |     for (let i = 0; i < presWithoutClass.length; i++) {
23 |       // If the content is recognized as folder structure, don't add a warning for empty 
24 |       if (presWithoutClass[i].textContent.match(/^\S[^\n*]*\/\n/)) {
25 |         continue;
26 |       }
27 | 
28 |       let type = WARNING;
29 | 
30 |       // If the content is recognized as code or {{csssyntax}} macro, mark it as error
31 |       if (presWithoutClass[i].textContent.match(/^\s*(?:\/\*.+?\*\/|<.+?>|@[^\s\n]+[^\n]*\{\n|\{\{\s*csssyntax(?:\(\))?\s*\}\})/)) {
32 |         type = ERROR;
33 |       }
34 | 
35 |       matches.push({
36 |         msg: presWithoutClass[i].outerHTML,
37 |         type
38 |       });
39 |     }
40 | 
41 |     for(let i = 0; i < presWithEvalClass.length; i++) {
42 |       matches.push({
43 |         msg: presWithEvalClass[i].outerHTML,
44 |         type: ERROR
45 |       });
46 |     }
47 | 
48 |     return matches;
49 |   }
50 | };
51 | 


--------------------------------------------------------------------------------
/tests/test-pre-without-class.js:
--------------------------------------------------------------------------------
 1 | const assert = require('assert');
 2 | const ERROR = require('../lib/doctests.js').ERROR;
 3 | const WARNING = require('../lib/doctests.js').WARNING;
 4 | const preWithoutClass = require('../lib/pre-without-class.js').preWithoutClass;
 5 | 
 6 | describe('preWithoutClass', function() {
 7 |   it('Should return 4 errors and 4 warnings regarding pre lacking a class', function(done) {
 8 |     const str = '
' +
 9 |       '
' +
10 |       '
folder/\n  file
' + 11 | '
foobar;
' + 12 | '
/* comment */\nvar code;
' + 13 | '
@rule param {\n  descriptor: value;\n}
' + 14 | '
<tag>
' + 15 | '
' +
16 |       '
foo
' + 17 | '
 \n\r foo
' + 18 | '
Test on non pre
' + 19 | '
bar
' + 20 | '
Test
'; 21 | 22 | const expected = [ 23 | {msg: '
foobar;
', type: WARNING}, 24 | {msg: '
/* comment */\nvar code;
', type: ERROR}, 25 | {msg: '
@rule param {\n  descriptor: value;\n}
', type: ERROR}, 26 | {msg: '
<tag>
', type: ERROR}, 27 | {msg: '
', type: WARNING},
28 |       {msg: '
foo
', type: WARNING}, 29 | {msg: '
 \n\n foo
', type: WARNING}, 30 | {msg: '
bar
', type: ERROR} 31 | ]; 32 | 33 | 34 | let rootElement = document.createElement("body"); 35 | rootElement.innerHTML = str; 36 | 37 | let results = preWithoutClass.check(rootElement); 38 | 39 | results.forEach((element, index) => { 40 | delete element.node; 41 | assert.deepEqual(expected[index], element); 42 | }); 43 | 44 | done(); 45 | }); 46 | }); 47 | 48 | -------------------------------------------------------------------------------- /lib/url-in-link-title.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for incorrectly used URLs in link titles. 3 | * 4 | * Example 1: The 'title' attribute on 5 | *
CSS 6 | * should be removed, because it's redundant. 7 | * 8 | * Example 2: The 'title' attribute on 9 | * CSS 10 | * should be removed, because it's redundant and misleading. 11 | * 12 | * Implementation notes: This test checks whether the 'title' attribute of an element 13 | * contains the same URL or a part of it as within its 'href' attribute. It also handles URLs 14 | * using two-character locales vs. four character locales, e.g. "/en-US/" and "/en/". 15 | */ 16 | 17 | const ERROR = require('./doctests.js').ERROR; 18 | 19 | exports.urlInLinkTitle = { 20 | name: "url_in_link_title", 21 | desc: "url_in_link_title_desc", 22 | check: function checkURLsInTitleAttributes(rootElement) { 23 | let linkElements = rootElement.getElementsByTagName("a"); 24 | let matches = []; 25 | 26 | for (let i = 0; i < linkElements.length; i++) { 27 | let href = (linkElements[i].getAttribute("href") || "").toLowerCase(); 28 | let title = (linkElements[i].getAttribute("title") || "").toLowerCase(); 29 | if (title !== "" && (href.indexOf(title) !== -1 || 30 | (title.match(/[a-z]{2}(?:-[A-Z]{2})?\/docs\/.*?\//) || 31 | title === href.replace(/([a-z]{2})(?:-[a-z]{2})?\/docs\/(.*)/, "$1/$2")))) { 32 | matches.push({ 33 | node: linkElements[i], 34 | msg: linkElements[i].outerHTML, 35 | type: ERROR 36 | }); 37 | } 38 | } 39 | 40 | return matches; 41 | }, 42 | fix: function fixURLsInTitleAttributes(matches) { 43 | matches.forEach(match => { 44 | match.node.removeAttribute("title"); 45 | }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /tests/test-invalid-macros.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const WARNING = require('../lib/doctests.js').WARNING; 3 | const ERROR = require('../lib/doctests.js').ERROR; 4 | const invalidMacros = require('../lib/invalid-macros.js').invalidMacros; 5 | 6 | describe('invalidMacros', function() { 7 | it('Should return 4 warnings and 1 error regarding invalid macros', function(done) { 8 | const str = '{{apiref}}' + 9 | '{{bug(123456)}}' + 10 | '{{previous("some page"}}' + 11 | '{{cssinfo(\'font-weight\', \'@font\')}}' + 12 | '{{invalidmacroname}}' + 13 | '{{invalidmacroname(123456)}}' + 14 | '{{invalidmacroname("some page")}}' + 15 | '{{invalidmacroname(\'font-weight\', \'@font\')}}' + 16 | '{{ languages( { "ja": "Ja/Browser_chrome_tests" } ) }}'; 17 | 18 | const expected = [ 19 | {msg: '{{invalidmacroname}}', type: WARNING}, 20 | {msg: '{{invalidmacroname(123456)}}', type: WARNING}, 21 | {msg: '{{invalidmacroname("some page")}}', type: WARNING}, 22 | {msg: '{{invalidmacroname(\'font-weight\', \'@font\')}}', type: WARNING}, 23 | {msg: 'obsolete_macro', msgParams: ['{{ languages( { "ja": "Ja/Browser_chrome_tests" } ) }}'], type: ERROR} 24 | ]; 25 | 26 | const expectedAfterFixing = [ 27 | {msg: '{{invalidmacroname}}', type: WARNING}, 28 | {msg: '{{invalidmacroname(123456)}}', type: WARNING}, 29 | {msg: '{{invalidmacroname("some page")}}', type: WARNING}, 30 | {msg: '{{invalidmacroname(\'font-weight\', \'@font\')}}', type: WARNING} 31 | ]; 32 | 33 | let rootElement = document.createElement("body"); 34 | rootElement.innerHTML = str; 35 | 36 | let results = invalidMacros.check(rootElement); 37 | 38 | results.forEach((element, index) => { 39 | delete element.node; 40 | assert.deepEqual(expected[index], element); 41 | }); 42 | 43 | done(); 44 | }); 45 | }); 46 | 47 | -------------------------------------------------------------------------------- /tests/test-url-in-link-title.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const urlInLinkTitle = require('../lib/url-in-link-title.js').urlInLinkTitle; 4 | 5 | describe('urlInLinkTitle', function() { 6 | it('Should return 6 errors regarding HTTP Links', function(done) { 7 | const str = 'Test' + 8 | 'Test' + 9 | 'Test' + 10 | 'Test' + 11 | 'Test' + 12 | 'Test' + 13 | 'Test' + 14 | 'Test' + 15 | 'Web' + 16 | 'Mozilla'; 17 | 18 | const expected = [ 19 | {msg: 'Test', type: ERROR}, 20 | {msg: 'Test', type: ERROR}, 21 | {msg: 'Test', type: ERROR}, 22 | {msg: 'Test', type: ERROR}, 23 | {msg: 'Web', type: ERROR}, 24 | {msg: 'Mozilla', type: ERROR} 25 | ]; 26 | 27 | const expectedAfterFixing = []; 28 | 29 | let rootElement = document.createElement("body"); 30 | rootElement.innerHTML = str; 31 | 32 | let results = urlInLinkTitle.check(rootElement); 33 | 34 | results.forEach((element, index) => { 35 | delete element.node; 36 | assert.deepEqual(expected[index], element); 37 | }); 38 | 39 | done(); 40 | }); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /tests/test-article-length.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const INFO = require('../lib/doctests.js').INFO; 3 | const WARNING = require('../lib/doctests.js').WARNING; 4 | const articleLength = require('../lib/article-length.js').articleLength; 5 | 6 | describe('articleLength', function() { 7 | it('Should return 1 INFO regarding article length (100 words)', function(done) { 8 | const str = Array(100).fill("foo").join(" "); 9 | const expected = [ 10 | {msg: "article_length_info", msgParams: ["100", "< 1"], type: INFO} 11 | ]; 12 | 13 | let rootElement = document.createElement("body"); 14 | rootElement.innerHTML = str; 15 | 16 | let results = articleLength.check(rootElement); 17 | 18 | results.forEach((element, index) => { 19 | assert.deepEqual(expected[index], element); 20 | }); 21 | 22 | done(); 23 | }); 24 | 25 | it('Should return 1 INFO regarding article length (500 words)', function(done) { 26 | const str = Array(500).fill("foo").join(" "); 27 | const expected = [ 28 | {msg: "article_length_info", msgParams: ["500", "2"], type: INFO} 29 | ]; 30 | 31 | let rootElement = document.createElement("body"); 32 | rootElement.innerHTML = str; 33 | 34 | let results = articleLength.check(rootElement); 35 | 36 | results.forEach((element, index) => { 37 | assert.deepEqual(expected[index], element); 38 | }); 39 | 40 | done(); 41 | }); 42 | 43 | it('Should return 1 INFO and 1 WARNING (3000 words)', function(done) { 44 | const str = Array(3000).fill("foo").join(" "); 45 | const expected = [ 46 | {msg: "article_length_info", msgParams: ["3000", "11"], type: INFO}, 47 | {msg: "long_article", type: WARNING} 48 | ]; 49 | 50 | let rootElement = document.createElement("body"); 51 | rootElement.innerHTML = str; 52 | 53 | let results = articleLength.check(rootElement); 54 | 55 | results.forEach((element, index) => { 56 | assert.deepEqual(expected[index], element); 57 | }); 58 | 59 | done(); 60 | }); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /tests/test-wrong-syntax-class.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const wrongSyntaxClass = require('../lib/wrong-syntax-class.js').wrongSyntaxClass; 4 | 5 | describe('wrongSyntaxClass', function() { 6 | it('Should return 0 errors regarding wrong syntax in class= (syntaxbox)', function(done) { 7 | const str = 'foo

Syntax

\\n
syntax
bar'; 8 | const expected = []; 9 | 10 | let rootElement = document.createElement("body"); 11 | rootElement.innerHTML = str; 12 | 13 | let results = wrongSyntaxClass.check(rootElement); 14 | 15 | results.forEach((element, index) => { 16 | delete element.node; 17 | assert.deepEqual(expected[index], element); 18 | }); 19 | 20 | done(); 21 | }); 22 | 23 | it('Should return 0 errors regarding class brushjs', function(done) { 24 | const str = 'foo

Syntax

\\n
syntax examples
bar

Formal syntax

\\n
syntax
'; 25 | const expected = []; 26 | 27 | let rootElement = document.createElement("body"); 28 | rootElement.innerHTML = str; 29 | 30 | let results = wrongSyntaxClass.check(rootElement); 31 | 32 | results.forEach((element, index) => { 33 | delete element.node; 34 | assert.deepEqual(expected[index], element); 35 | }); 36 | 37 | done(); 38 | }); 39 | 40 | it('Should return 1 error regarding eval', function(done) { 41 | const str = 'foo

Syntax

\\n
syntax examples
bar

Formal syntax

\\n
syntax
baz

Other section

'; 42 | const expected = [ 43 | {msg: "wrong_syntax_class_used", msgParams: ["eval"], type: ERROR} 44 | ]; 45 | 46 | let rootElement = document.createElement("body"); 47 | rootElement.innerHTML = str; 48 | 49 | let results = wrongSyntaxClass.check(rootElement); 50 | 51 | results.forEach((element, index) => { 52 | delete element.node; 53 | assert.deepEqual(expected[index], element); 54 | }); 55 | 56 | done(); 57 | }); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /tests/test-link-count.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const WARNING = require('../lib//doctests.js').WARNING; 4 | const INFO = require('../lib/doctests.js').INFO; 5 | const linkCost = require('../lib/link-count.js').linkCount; 6 | 7 | describe('linkCount', function() { 8 | it('Should return 1 error if there is more than 249 links in a page', function(done) { 9 | const str = Array(250).fill('').join(' '); 10 | const expected = [ 11 | {msg: "count_link_error", msgParams: [250], type: ERROR} 12 | ]; 13 | 14 | let rootElement = document.createElement("body"); 15 | rootElement.innerHTML = str; 16 | 17 | let results = linkCost.check(rootElement); 18 | 19 | results.forEach((element, index) => { 20 | delete element.node; 21 | assert.deepEqual(expected[index], element); 22 | }); 23 | 24 | done(); 25 | }); 26 | 27 | it('Should return 1 warning if there is more than 99 links but less than 250', function(done) { 28 | const str = Array(100).fill('').join(' '); 29 | const expected = [ 30 | {msg: "count_link_warning", msgParams: [100], type: WARNING} 31 | ]; 32 | 33 | let rootElement = document.createElement("body"); 34 | rootElement.innerHTML = str; 35 | 36 | let results = linkCost.check(rootElement); 37 | 38 | results.forEach((element, index) => { 39 | delete element.node; 40 | assert.deepEqual(expected[index], element); 41 | }); 42 | 43 | done(); 44 | }); 45 | 46 | it('Should return 1 info if it doesn\'t exceed any threshold', function(done) { 47 | const str = Array(3).fill('').join(' '); 48 | const expected = [ 49 | {msg: 'count_link_info', msgParams: [3], type: INFO} 50 | ]; 51 | 52 | let rootElement = document.createElement("body"); 53 | rootElement.innerHTML = str; 54 | 55 | let results = linkCost.check(rootElement); 56 | 57 | results.forEach((element, index) => { 58 | delete element.node; 59 | assert.deepEqual(expected[index], element); 60 | }); 61 | 62 | done(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /lib/incorrectly-wrapped-sidebar-macros.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for sidebar macros that are not wrapped in
elements 3 | * 4 | * Example 1:

{{APIRef}}

should be replaced by
{{APIRef}}
. 5 | * 6 | * Implementation notes: This test checks whether some named macros are wrapped in other elements 7 | * than
s. 8 | */ 9 | 10 | const ERROR = require('./doctests.js').ERROR; 11 | 12 | exports.incorrectlyWrappedSidebarMacros = { 13 | name: "incorrectly_wrapped_sidebar_macros", 14 | desc: "incorrectly_wrapped_sidebar_macros_desc", 15 | 16 | check: function checkIncorrectlyWrappedSidebarMacros(rootElement) { 17 | const allowedMacros = /^(?:apiref|cssref|htmlref|jsref|makesimplequicklinks|mathmlref|svgrefelem)$|sidebar$/i; 18 | 19 | let treeWalker = document.createTreeWalker( 20 | rootElement, 21 | NodeFilter.SHOW_TEXT, 22 | // eslint-disable-next-line 23 | {acceptNode: node => node.textContent.match(/\{\{.*?\}\}/) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT} 24 | ); 25 | let matches = []; 26 | 27 | while (treeWalker.nextNode()) { 28 | let reMacroName = /\{\{\s*([^(}\s]+).*?\}\}/g; 29 | let macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent); 30 | while (macroNameMatch) { 31 | if (macroNameMatch[1].match(allowedMacros) !== null && 32 | treeWalker.currentNode.parentElement.localName !== "div") { 33 | matches.push({ 34 | node: treeWalker.currentNode.parentElement, 35 | msg: "wrong_element_wrapping_sidebar_macro", 36 | msgParams: [macroNameMatch[0], treeWalker.currentNode.parentElement.localName], 37 | type: ERROR 38 | }); 39 | } 40 | macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent); 41 | } 42 | } 43 | 44 | return matches; 45 | }, 46 | 47 | fix: function fixIncorrectlyWrappedSidebarMacros(matches) { 48 | matches.forEach(match => { 49 | let divElement = document.createElement("div"); 50 | let childNodes = match.node.childNodes; 51 | for (let i = 0; i < childNodes.length; i++) { 52 | divElement.appendChild(childNodes[i].cloneNode(true)); 53 | } 54 | 55 | match.node.parentNode.replaceChild(divElement, match.node); 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const docTests = { 2 | absoluteURLsForInternalLinks : require('./absolute-urls-for-internal-links.js').absoluteURLsForInternalLinks, 3 | alertPrintInCode : require('./alert-print-in-code.js').alertPrintInCode, 4 | anchorExists : require('./anchor-exists.js').anchorExists, 5 | apiSyntaxHeadlines : require('./api-syntax-headlines.js').apiSyntaxHeadlines, 6 | articleLength : require('./article-length.js').articleLength, 7 | codeInPre : require('./code-in-pre.js').codeInPre, 8 | dataMacroNote : require('./data-macro-note.js').dataMacroNote, 9 | differentLocaleLinks : require('./different-locale-links.js').differentLocaleLinks, 10 | emptyBrackets : require('./empty-brackets.js').emptyBrackets, 11 | emptyElements : require('./empty-elements.js').emptyElements, 12 | exampleColonHeading : require('./example-colon-heading.js').exampleColonHeading, 13 | fontElements : require('./font-elements.js').fontElements, 14 | htmlComments : require('./html-comments.js').htmlComments, 15 | httpLinks : require('./http-links.js').httpLinks, 16 | incorrectlyWrappedSidebarMacros : require('./incorrectly-wrapped-sidebar-macros.js').incorrectlyWrappedSidebarMacros, 17 | invalidMacros : require('./invalid-macros.js').invalidMacros, 18 | lineLengthInPre : require('./line-length-in-pre.js').lineLengthInPre, 19 | linkCount : require('./link-count.js').linkCount, 20 | macroSyntaxError : require('./macro-syntax-error.js').macroSyntaxError, 21 | mixedContent : require('./mixed-content.js').mixedContent, 22 | nameAttribute : require('./name-attribute.js').nameAttribute, 23 | oldURLs : require('./old-urls.js').oldURLs, 24 | preWithoutClass : require('./pre-without-class.js').preWithoutClass, 25 | shellPrompts : require('./shell-prompts.js').shellPrompts, 26 | spanCount : require('./span-count.js').spanCount, 27 | styleAttribute : require('./style-attribute.js').styleAttribute, 28 | summaryHeading : require('./summary-heading.js').summaryHeading, 29 | unnecessaryMacroParams : require('./unnecessary-macro-params.js').unnecessaryMacroParams, 30 | urlInLinkTitle : require('./url-in-link-title.js').urlInLinkTitle, 31 | wrongHighlightedLine : require('./wrong-highlighted-line.js').wrongHighlightedLine, 32 | wrongSyntaxClass : require('./wrong-syntax-class.js').wrongSyntaxClass 33 | }; 34 | 35 | module.exports = docTests; 36 | -------------------------------------------------------------------------------- /tests/test-api-syntax-headlines.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const apiSyntaxHeadlines = require('../lib/api-syntax-headlines.js').apiSyntaxHeadlines; 3 | const ERROR = require('../lib/doctests.js').ERROR; 4 | 5 | describe('apiSyntaxHeadlines', function() { 6 | it('Should return 5 apiSyntaxHeadlines errors', function(done) { 7 | const str = '

Syntax

' + 8 | '

Exceptions thrown

' + 9 | '

Returns

' + 10 | '

Arguments

'; 11 | 12 | const expected = [ 13 | {msg: "invalid_headline_name", msgParams: ["Exception thrown"], type: ERROR}, 14 | {msg: "invalid_headline_name", msgParams: ["Returns"], type: ERROR}, 15 | {msg: "invalid_headline_name", msgParams: ["Arguments"], type: ERROR}, 16 | {msg: "invalid_headline_order", type: ERROR}, 17 | {msg: "invalid_headline_order", type: ERROR} 18 | ]; 19 | 20 | const expectedAfterFixing = [ 21 | {msg: "invalid_headline_order", type: ERROR}, 22 | {msg: "invalid_headline_order", type: ERROR} 23 | ]; 24 | 25 | 26 | let rootElement = document.createElement("body"); 27 | rootElement.innerHTML = str; 28 | 29 | let results = apiSyntaxHeadlines.check(rootElement); 30 | 31 | results.forEach((element, index) => { 32 | if(element.msg != expected[index].msg || element.type != expected[index].type) 33 | done(Error('Expected : ' + JSON.stringify(expected[index]) + ' got : ' + JSON.stringify(element))); 34 | }); 35 | 36 | done(); 37 | }); 38 | 39 | it('Should return 1 Error regarding apiSyntaxHeadlines errors', function(done) { 40 | const str = '

Syntax

' + 41 | '

Return value

' + 42 | '

Errors thrown

'; 43 | 44 | const expected = [ 45 | {msg: "invalid_headline_name", msgParams: ["Errors thrown"], type: ERROR} 46 | ]; 47 | 48 | const expectedAfterFixing = []; 49 | 50 | let rootElement = document.createElement("body"); 51 | rootElement.innerHTML = str; 52 | 53 | let results = apiSyntaxHeadlines.check(rootElement); 54 | 55 | results.forEach((element, index) => { 56 | if(element.msg != expected[index].msg || element.type != expected[index].type) 57 | done(Error('Expected : ' + JSON.stringify(expected[index]) + ' got : ' + JSON.stringify(element))); 58 | }); 59 | 60 | done(); 61 | }); 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /lib/data-macro-note.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test that data macros have a note above them pointing to their data sources. 3 | * 4 | * Example 1: If you are using e.g. the {{compat}} or {{cssinfo}} macros, there should 5 | * be a hidden note for contributors in edit-mode, so that they know where the data comes 6 | * from and how to change it. 7 | * 8 | * Implementation notes: This test checks if there is a note for each macro 9 | * (by checking whether the sibling element before the element containing the macro 10 | * has class="hidden") and whether that note contains a link (i.e. an 11 | * element; it doesn't check whether the link is valid). 12 | */ 13 | 14 | const ERROR = require('./doctests.js').ERROR; 15 | 16 | const dataMacros = /^(?:compat|css_ref|cssanimatedproperties|cssinfo|csssyntax|webextbrowsercompat)$/i; 17 | 18 | exports.dataMacroNote = { 19 | name: "data_macro_note", 20 | desc: "data_macro_note_desc", 21 | check: function checkDataMacroNote(rootElement) { 22 | let treeWalker = document.createTreeWalker( 23 | rootElement, 24 | NodeFilter.SHOW_TEXT, 25 | { 26 | // eslint-disable-next-line 27 | acceptNode: node => node.textContent.match(/\{\{.*?\}\}/) ? 28 | NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT 29 | } 30 | ); 31 | let matches = []; 32 | 33 | while (treeWalker.nextNode()) { 34 | let reMacroName = /\{\{\s*([^(}\s]+).*?\}\}/g; 35 | let macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent); 36 | while (macroNameMatch) { 37 | let noteElement = treeWalker.currentNode.parentNode.previousSibling; 38 | if (dataMacros.test(macroNameMatch[1])) { 39 | if (!noteElement || !noteElement.classList.contains("hidden")) { 40 | matches.push({ 41 | msg: "data_macro_note_missing", 42 | msgParams: [macroNameMatch[0]], 43 | type: ERROR 44 | }); 45 | } else if (!noteElement.querySelector("[href^='http']")) { 46 | matches.push({ 47 | msg: "data_macro_source_link_missing", 48 | msgParams: [macroNameMatch[0]], 49 | type: ERROR 50 | }); 51 | } 52 | } 53 | macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent); 54 | } 55 | } 56 | 57 | return matches; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /lib/unnecessary-macro-params.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for obsolete macro parameters. 3 | * 4 | * Example 1: Some macros like {{JSRef}} don't require parameters anymore, so they should be 5 | * removed. 6 | * 7 | * Example 2: Some macros like {{cssinfo}} don't require parameters when the related information 8 | * can be read from the page's slug, so they should be removed in those cases. 9 | * 10 | * Implementation notes: This test checks for a specific list of macros, which either have no 11 | * parameters at all or their parameters are redundant. It uses the page title for comparison, so 12 | * the unit test doesn't break while working on about:blank. 13 | */ 14 | 15 | 16 | const ERROR = require('./doctests.js').ERROR; 17 | 18 | const reMacrosNotRequiringParams = /\{\{\s*(?:JSRef|csssyntax|cssinfo|svginfo)\([^)]+?\)\s*\}\}/i; 19 | const reMacrosNotRequiringParamsGlobal = new RegExp(reMacrosNotRequiringParams.source, "gi"); 20 | 21 | exports.unnecessaryMacroParams = { 22 | name: "unnecessary_macro_params", 23 | desc: "unnecessary_macro_params_desc", 24 | check: function checkUnnecessaryMacroParams(rootElement) { 25 | let treeWalker = document.createTreeWalker( 26 | rootElement, 27 | NodeFilter.SHOW_TEXT, 28 | { 29 | // eslint-disable-next-line 30 | acceptNode: node => node.textContent.match(reMacrosNotRequiringParams) ? 31 | NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT 32 | } 33 | ); 34 | let matches = []; 35 | 36 | while (treeWalker.nextNode()) { 37 | let textNodeMatches = treeWalker.currentNode.textContent.match( 38 | reMacrosNotRequiringParamsGlobal) || []; 39 | textNodeMatches.forEach(match => { 40 | let paramMatch = match.match(/(?:csssyntax|cssinfo|svginfo)\((["'])(.+?)\1/i); 41 | if (paramMatch) { 42 | let param = paramMatch[2]; 43 | if (param === document.title.replace(/^(.+?) \| Edit.*$/, "$1")) { 44 | matches.push({ 45 | msg: "macro_with_unnecessary_params_equalling_slug", 46 | msgParams: [match], 47 | type: ERROR 48 | }); 49 | } 50 | } else { 51 | matches.push({ 52 | msg: "macro_with_unused_params", 53 | msgParams: [match], 54 | type: ERROR 55 | }); 56 | } 57 | }); 58 | } 59 | 60 | return matches; 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /lib/empty-elements.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for empty elements. 3 | * 4 | * Example 1: Paragraphs only containing a non-breaking space (

 

) should be avoided. 5 | * 6 | * Implementation notes: This test checks for elements containing no text or only space 7 | * characters excluding the new paragraph helper of CKEditor and self-closing elements except 8 | *
and elements. 9 | */ 10 | 11 | const ERROR = require('./doctests.js').ERROR; 12 | const WARNING = require('./doctests.js').WARNING; 13 | 14 | const isNewParagraphHelper = require('./doctests.js').isNewParagraphHelper; 15 | 16 | exports.emptyElements = { 17 | name: "empty_elements", 18 | desc: "empty_elements_desc", 19 | check: function checkEmptyElements(rootElement) { 20 | let treeWalker = document.createTreeWalker( 21 | rootElement, 22 | NodeFilter.SHOW_ELEMENT, 23 | { 24 | acceptNode: node => { 25 | // matching self-closing elements and td elements and excluding them 26 | if (!node.localName.match(/^link|track|param|area|command|col|base|meta|hr|source|img|keygen|br|wbr|input$/i) && 27 | !node.localName.match(/^td$/i) && node.textContent.match(/^(?: |\s|\n)*$/)) { 28 | // Exclude new paragraph helper 29 | if (isNewParagraphHelper(node.firstElementChild)) { 30 | return NodeFilter.FILTER_REJECT; 31 | } 32 | 33 | // Elements containing self-closing elements except
and are considered non-empty 34 | let descendantSelfClosingElements = node.querySelectorAll( 35 | "link,track,param,area,command,col,base,meta,hr,source,img,keygen,input"); 36 | return descendantSelfClosingElements.length === 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; 37 | } 38 | return NodeFilter.FILTER_SKIP; 39 | } 40 | } 41 | ); 42 | let matches = []; 43 | 44 | while (treeWalker.nextNode()) { 45 | matches.push({ 46 | node: treeWalker.currentNode, 47 | msg: treeWalker.currentNode.outerHTML, 48 | type: treeWalker.currentNode.localName === "td" ? WARNING : ERROR 49 | }); 50 | } 51 | 52 | return matches; 53 | }, 54 | 55 | fix: function fixEmptyElements(matches) { 56 | matches.forEach(match => { 57 | if (match.type === ERROR) { 58 | match.node.remove(); 59 | } 60 | }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /lib/wrong-syntax-class.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for whether the 'syntax' class is properly used on a syntax block. 3 | * 4 | * Example 1:
 elements following a 'Formal syntax' heading are expected to contain a syntax
 5 |  *  definition, which needs to be styled using class="syntaxbox".
 6 |  *
 7 |  *  Example 2: 
 elements following a 'Syntax' heading where there is no 'Formal syntax'
 8 |  *  section are expected to contain a syntax definition, which needs to be styled using
 9 |  *  class="syntaxbox".
10 |  *
11 |  *  Implementation notes: This test first searches for an 

Formal syntax

heading. If none 12 | * is found, it searches for a

Syntax

heading. If one of those is found, the following 13 | *
 element is expected to hold a syntax definition, which needs to be styled using
14 |  *  class="syntaxbox".
15 |  */
16 | 
17 | const ERROR = require('./doctests.js').ERROR;
18 | 
19 | exports.wrongSyntaxClass = {
20 |   name: "wrong_syntax_class",
21 |   desc: "wrong_syntax_class_desc",
22 |   check: function checkWrongSyntaxClass(rootElement) {
23 |     function checkPre(heading) {
24 |       let element = heading.nextSibling;
25 |       while (element && element.localName !== "h2") {
26 |         if (element.localName === "pre" && element.className !== "syntaxbox") {
27 |           return {
28 |             node: element,
29 |             msg: "wrong_syntax_class_used",
30 |             msgParams: [element.className],
31 |             type: ERROR
32 |           };
33 |         }
34 |         element = element.nextElementSibling;
35 |       }
36 |       return undefined;
37 |     }
38 | 
39 |     let subHeadings = rootElement.getElementsByTagName("h3");
40 |     let formalSyntaxSection = null;
41 |     for (let i = 0; !formalSyntaxSection && i < subHeadings.length; i++) {
42 |       if (subHeadings[i].textContent.match(/Formal syntax/i)) {
43 |         formalSyntaxSection = subHeadings[i];
44 |       }
45 |     }
46 | 
47 |     let matches = [];
48 |     if (formalSyntaxSection) {
49 |       let match = checkPre(formalSyntaxSection);
50 |       if (match) {
51 |         matches.push(match);
52 |       }
53 |     } else {
54 |       let headings = rootElement.getElementsByTagName("h2");
55 |       let syntaxSection = null;
56 |       for (let i = 0; !syntaxSection && i < headings.length; i++) {
57 |         if (headings[i].textContent.toLowerCase() === "syntax") {
58 |           syntaxSection = headings[i];
59 |         }
60 |       }
61 | 
62 |       if (syntaxSection) {
63 |         let match = checkPre(syntaxSection);
64 |         if (match) {
65 |           matches.push(match);
66 |         }
67 |       }
68 |     }
69 | 
70 |     return matches;
71 |   },
72 |   fix: function fixWrongSyntaxClass(matches) {
73 |     matches.forEach(match => {
74 |       match.node.className = "syntaxbox";
75 |     });
76 |   }
77 | };
78 | 


--------------------------------------------------------------------------------
/tests/test-wrong-highlighted-line.js:
--------------------------------------------------------------------------------
 1 | const assert = require('assert');
 2 | const ERROR = require('../lib/doctests.js').ERROR;
 3 | const wrongHighlightedLine = require('../lib/wrong-highlighted-line.js').wrongHighlightedLine;
 4 | 
 5 | describe('wrongHighlightedLine', function() {
 6 |   it('Should return 10 errors related to wrong highlighted line', function(done) {
 7 |     const str = '
foo\nbar
' + 8 | '
foo\nbar
' + 9 | '
foo\nbar
' + 10 | '
foo\nbar\nbaz
' + 11 | '
foo\nbar\nbaz
' + 12 | '
foo\nbar\nbaz\nbax\nbix
' + 13 | '
foo\nbar\nbaz\nbax\nbix
' + 14 | '
foo\nbar
' + 15 | '
foo\nbar
' + 16 | '
foo\nbar
' + 17 | '
foo
bar
' + 18 | '
foo
bar
' + 19 | '
foo
bar
' + 20 | '
foo\nbar\nbaz
' + 21 | '
foo\nbar\nbaz
'; 22 | 23 | const expected = [ 24 | {msg: "highlighted_line_number_not_positive", msgParams: ["0", "0"], type: ERROR}, 25 | {msg: "highlighted_line_number_not_positive", msgParams: ["-1", "-1"], type: ERROR}, 26 | {msg: "highlighted_line_number_too_big", msgParams: ["3", "2", "3"], type: ERROR}, 27 | {msg: "highlighted_line_number_too_big", msgParams: ["3", "2", "3"], type: ERROR}, 28 | {msg: "highlighted_line_number_too_big", msgParams: ["3", "2", "3"], type: ERROR}, 29 | {msg: "highlighted_line_number_too_big", msgParams: ["3", "2", "3"], type: ERROR}, 30 | {msg: "highlighted_line_number_not_positive", msgParams: ["-3", "1,-3--5,3"], type: ERROR}, 31 | {msg: "highlighted_line_number_not_positive", msgParams: ["-5", "1,-3--5,3"], type: ERROR}, 32 | {msg: "invalid_highlighted_range", msgParams: ["-3", "-5", "1,-3--5,3"], type: ERROR}, 33 | {msg: "highlighted_line_number_too_big", msgParams: ["6", "3", " 1, 3 - 6 ,2 "], type: ERROR} 34 | ]; 35 | 36 | let rootElement = document.createElement("body"); 37 | rootElement.innerHTML = str; 38 | 39 | let results = wrongHighlightedLine.check(rootElement); 40 | 41 | results.forEach((element, index) => { 42 | delete element.node; 43 | assert.deepEqual(expected[index], element); 44 | }); 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /lib/api-syntax-headlines.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test whether the API syntax headlines are named correctly, i.e. 'Parameters', 3 | * 'Return value' and 'Exceptions', and whether they appear in the correct order. 4 | * 5 | * Example 1: The return value should have 'Return value' as headline and not 'Returns'. 6 | * 7 | * Example 2: Exceptions should have 'Exceptions' as headline and not 'Errors' or 'Errors thrown'. 8 | * 9 | * Example 3: Having an 'Exceptions' section before the 'Return value' section should be avoided. 10 | * The correct order of the sections is 'Parameters', 'Return value' and 'Exceptions'. 11 | * 12 | * Implementation notes: This test searches for specific keywords like 'returns' or 'errors' and 13 | * expects the headlines to be

elements under a

Syntax

section. 14 | */ 15 | 16 | const ERROR = require('./doctests.js').ERROR; 17 | 18 | const disallowedNames = new Map([ 19 | ["arguments", "Parameters"], 20 | ["returns", "Return value"], 21 | ["errors", "Exceptions"], 22 | ["errors thrown", "Exceptions"], 23 | ["exceptions thrown", "Exceptions"] 24 | ]); 25 | const validOrder = [ 26 | new Set(["parameters", "arguments"]), 27 | new Set(["return value", "returns"]), 28 | new Set(["exceptions", "exceptions thrown", "errors", "errors thrown"]) 29 | ]; 30 | 31 | exports.apiSyntaxHeadlines = { 32 | name: "api_syntax_headlines", 33 | desc: "api_syntax_headlines_desc", 34 | 35 | check: function checkAPISyntaxHeadlines(rootElement) { 36 | let headlines = rootElement.getElementsByTagName("h2"); 37 | let syntaxSection = null; 38 | let order = []; 39 | let matches = []; 40 | for (let i = 0; !syntaxSection && i < headlines.length; i++) { 41 | if (headlines[i].textContent === "Syntax") { 42 | syntaxSection = headlines[i]; 43 | } 44 | } 45 | 46 | if (syntaxSection) { 47 | let subHeadingElements = []; 48 | let element = syntaxSection.nextSibling; 49 | while (element && element.localName !== "h2") { 50 | if (element.localName === "h3") { 51 | subHeadingElements.push(element); 52 | } 53 | element = element.nextSibling; 54 | } 55 | for (let i = 0; i < subHeadingElements.length; i++) { 56 | let subHeading = subHeadingElements[i].textContent.toLowerCase(); 57 | for (let j = 0; j < validOrder.length; j++) { 58 | let heading = validOrder[j]; 59 | if (heading.has(subHeading)) { 60 | order.push(j); 61 | } 62 | } 63 | if (disallowedNames.has(subHeading)) { 64 | matches.push({ 65 | node: subHeadingElements[i], 66 | msg: "invalid_headline_name", 67 | msgParams: [subHeadingElements[i].textContent], 68 | type: ERROR 69 | }); 70 | } 71 | } 72 | 73 | // Check the order of the headlines 74 | for (let i = 1; i < order.length; i++) { 75 | if (order[i] < order[i - 1]) { 76 | matches.push({ 77 | msg: "invalid_headline_order", 78 | type: ERROR 79 | }); 80 | } 81 | } 82 | } 83 | 84 | return matches; 85 | }, 86 | 87 | fix: function fixAPISyntaxHeadlines(matches) { 88 | matches.forEach(match => { 89 | switch (match.msg) { 90 | case "invalid_headline_name": 91 | match.node.textContent = disallowedNames.get(match.node.textContent.toLowerCase()); 92 | break; 93 | } 94 | }); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Jun 19 2017 18:08:59 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'browserify'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'lib/doctests.js', 19 | 'lib/*.js', 20 | 'tests/test-absolute-urls-for-internal-links.js', 21 | 'tests/test-alert-print-in-code.js', 22 | 'tests/test-api-syntax-headlines.js', 23 | 'tests/test-article-length.js', 24 | 'tests/test-code-in-pre.js', 25 | 'tests/test-data-macro-note.js', 26 | 'tests/test-different-locale-links.js', 27 | 'tests/test-empty-brackets.js', 28 | 'tests/test-empty-elements.js', 29 | 'tests/test-example-colon-heading.js', 30 | 'tests/test-font-elements.js', 31 | 'tests/test-html-comments.js', 32 | 'tests/test-http-links.js', 33 | 'tests/test-incorrectly-wrapped-sidebar-macros.js', 34 | 'tests/test-invalid-macros.js', 35 | 'tests/test-line-length-in-pre.js', 36 | 'tests/test-macro-syntax-error.js', 37 | 'tests/test-name-attribute.js', 38 | 'tests/test-old-urls.js', 39 | 'tests/test-pre-without-class.js', 40 | 'tests/test-shell-prompts.js', 41 | 'tests/test-span-count.js', 42 | 'tests/test-style-attribute.js', 43 | 'tests/test-summary-heading.js', 44 | 'tests/test-unnecessary-macro-params.js', 45 | 'tests/test-url-in-link-title.js', 46 | 'tests/test-wrong-highlighted-line.js', 47 | 'tests/test-wrong-syntax-class.js', 48 | 'tests/test-link-count.js', 49 | 'tests/test-anchor-exists.js', 50 | 'tests/test-mixed-content.js' 51 | ], 52 | 53 | 54 | // list of files to exclude 55 | exclude: [ 56 | ], 57 | 58 | 59 | // preprocess matching files before serving them to the browser 60 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 61 | preprocessors: { 62 | 'lib/*.js': ['browserify'], 63 | 'tests/*.js' : ['browserify'], 64 | }, 65 | 66 | 67 | // test results reporter to use 68 | // possible values: 'dots', 'progress' 69 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 70 | reporters: ['mocha'], 71 | 72 | 73 | // web server port 74 | port: 9876, 75 | 76 | 77 | // enable / disable colors in the output (reporters and logs) 78 | colors: true, 79 | 80 | 81 | // level of logging 82 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 83 | logLevel: config.LOG_INFO, 84 | 85 | 86 | // enable / disable watching file and executing tests whenever any file changes 87 | autoWatch: true, 88 | 89 | 90 | // start these browsers 91 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 92 | browsers: ['Chrome', 'Firefox', 'Nightmare'], 93 | 94 | 95 | // Continuous Integration mode 96 | // if true, Karma captures browsers, runs the tests and exits 97 | singleRun: false, 98 | 99 | // Concurrency level 100 | // how many browser should be started simultaneous 101 | concurrency: Infinity 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /lib/macro-syntax-error.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for syntax errors in macro calls. 3 | * 4 | * Example 1: {{macro} misses a closing curly brace, so it will be recognized as error. 5 | * 6 | * Example 2: {{macro('param'}} misses a closing bracket, so it will be recognized as error. 7 | * 8 | * Example 3: {{macro("param"))}} has an additional closing bracket, so it will be recognized as error. 9 | * 10 | * Example 4: {{macro('param)}} and {{macro(param")}} have incorrectly quoted string parameters, 11 | * so they will be recognized as errors. 12 | * 13 | * Implementation notes: This test uses regular expressions to recognize invalid macros. 14 | * It currently fails to properly validate macros containing JSON parameters (see issue #139). 15 | */ 16 | 17 | const ERROR = require('./doctests.js').ERROR; 18 | 19 | exports.macroSyntaxError = { 20 | name: "macro_syntax_error", 21 | desc: "macro_syntax_error_desc", 22 | check: function checkMacroSyntaxError(rootElement) { 23 | function validateStringParams(macro) { 24 | let paramListStartIndex = macro.indexOf("(") + 1; 25 | let paramListEndMatch = macro.match(/\)*\s*\}{1,2}$/); 26 | let paramListEndIndex = macro.length - paramListEndMatch[0].length; 27 | let stringParamQuote = ""; 28 | for (let i = paramListStartIndex; i < paramListEndIndex; i++) { 29 | if (macro[i] === "\"") { 30 | if (stringParamQuote === "") { 31 | stringParamQuote = "\""; 32 | } else if (stringParamQuote === "\"" && macro[i - 1] !== "\\") { 33 | stringParamQuote = ""; 34 | } 35 | } else if (macro[i] === "'") { 36 | if (stringParamQuote === "") { 37 | stringParamQuote = "'"; 38 | } else if (stringParamQuote === "'" && macro[i - 1] !== "\\") { 39 | stringParamQuote = ""; 40 | } 41 | } else if (stringParamQuote === "" && macro[i].match(/[^\s,\d\-.]/)) { 42 | return false; 43 | } 44 | } 45 | return stringParamQuote === ""; 46 | } 47 | 48 | let treeWalker = document.createTreeWalker( 49 | rootElement, 50 | NodeFilter.SHOW_TEXT, 51 | // eslint-disable-next-line 52 | {acceptNode: node => node.textContent.match(/\{\{[^\(\}]*\([^\}]*\}\}|\{\{[^\}]*?\}(?:(?=[^\}])|$)/) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT} 53 | ); 54 | let matches = []; 55 | 56 | while (treeWalker.nextNode()) { 57 | let textNodeMatches = treeWalker.currentNode.textContent.match(/\{\{[^(}]*\([^}]*\}\}|\{\{[^}]*?\}(?:(?=[^}])|$)/gi) || []; 58 | textNodeMatches.forEach(macro => { 59 | if (macro.match(/[^}]\}$/)) { 60 | matches.push({ 61 | msg: "missing_closing_curly_brace", 62 | msgParams: [macro], 63 | type: ERROR 64 | }); 65 | } 66 | if (macro.match(/^\{\{[^(]+\(.+?[^)\s]\s*\}\}$/)) { 67 | matches.push({ 68 | msg: "missing_closing_bracket", 69 | msgParams: [macro], 70 | type: ERROR 71 | }); 72 | } 73 | if (!validateStringParams(macro)) { 74 | matches.push({ 75 | msg: "string_parameter_incorrectly_quoted", 76 | msgParams: [macro], 77 | type: ERROR 78 | }); 79 | } 80 | if (macro.match(/\){2,}\}{1,2}$/)) { 81 | matches.push({ 82 | msg: "additional_closing_bracket", 83 | msgParams: [macro], 84 | type: ERROR 85 | }); 86 | } 87 | }); 88 | } 89 | 90 | return matches; 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /tests/test-macro-syntax-error.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const macroSyntaxError = require('../lib/macro-syntax-error.js').macroSyntaxError; 4 | 5 | describe('macroSyntaxError', function() { 6 | it('Should return 19 errors regarding macro syntax', function(done) { 7 | const str = '{{macro}}' + 8 | '{{ macro }}' + 9 | '{{macro("param")}}' + 10 | '{{ macro("param") }}' + 11 | '{{macro(123)}}' + 12 | '{{macro(123, "param")}}' + 13 | '{{macro(\'param\', 123, "param")}}' + 14 | '{{macro("param)}}' + // Missing closing double quote 15 | '{{macro(\'param)}}' + // Missing closing single quote 16 | '{{macro(param)}}' + // Missing quotes 17 | '{{macro(param")}}' + // Missing opening double quote 18 | '{{macro(param\')}}' + // Missing opening single quote 19 | '{{macro(\'param\', 123, "param)}}' + // Missing closing double quote, multiple parameters 20 | '{{macro("param"))}}' + // Double closing parameter list bracket 21 | '{{macro("param")}' + // Missing closing macro curly brace after double quoted parameter 22 | '{{macro(\'param\')}' + // Missing closing macro curly brace after single quoted parameter 23 | '{{macro("param"}}' + // Missing closing parameter list bracket after double quoted parameter 24 | '{{macro(\'param\'}}' + // Missing closing parameter list bracket after single quoted parameter 25 | '{{macro(param"}}' + // Missing opening double quote and missing closing parameter list bracket 26 | '{{macro(param"))}}' + // Missing opening double quote and double closing parameter list bracket 27 | '{{macro(123, "param()"}}'; // Missing closing parameter list bracket after string parameter containing bracket 28 | 29 | const expected = [ 30 | {msg: "string_parameter_incorrectly_quoted", msgParams: ['{{macro("param)}}'], type: ERROR}, 31 | {msg: "string_parameter_incorrectly_quoted", msgParams: ["{{macro('param)}}"], type: ERROR}, 32 | {msg: "string_parameter_incorrectly_quoted", msgParams: ["{{macro(param)}}"], type: ERROR}, 33 | {msg: "string_parameter_incorrectly_quoted", msgParams: ['{{macro(param")}}'], type: ERROR}, 34 | {msg: "string_parameter_incorrectly_quoted", msgParams: ["{{macro(param')}}"], type: ERROR}, 35 | {msg: "string_parameter_incorrectly_quoted", msgParams: ["{{macro('param', 123, \"param)}}"], type: ERROR}, 36 | {msg: "additional_closing_bracket", msgParams: ['{{macro("param"))}}'], type: ERROR}, 37 | {msg: "missing_closing_curly_brace", msgParams: ['{{macro("param")}'], type: ERROR}, 38 | {msg: "missing_closing_curly_brace", msgParams: ["{{macro(\'param\')}"], type: ERROR}, 39 | {msg: "missing_closing_bracket", msgParams: ['{{macro("param"}}'], type: ERROR}, 40 | {msg: "missing_closing_bracket", msgParams: ["{{macro(\'param\'}}"], type: ERROR}, 41 | {msg: "missing_closing_bracket", msgParams: ['{{macro(param"}}'], type: ERROR}, 42 | {msg: "string_parameter_incorrectly_quoted", msgParams: ['{{macro(param"}}'], type: ERROR}, 43 | {msg: "string_parameter_incorrectly_quoted", msgParams: ['{{macro(param"))}}'], type: ERROR}, 44 | {msg: "additional_closing_bracket", msgParams: ['{{macro(param"))}}'], type: ERROR}, 45 | {msg: "missing_closing_bracket", msgParams: ['{{macro(123, "param()"}}'], type: ERROR} 46 | ]; 47 | 48 | let rootElement = document.createElement("body"); 49 | rootElement.innerHTML = str; 50 | 51 | let results = macroSyntaxError.check(rootElement); 52 | 53 | results.forEach((element, index) => { 54 | delete element.node; 55 | assert.deepEqual(expected[index], element); 56 | }); 57 | 58 | done(); 59 | }); 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /lib/wrong-highlighted-line.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Test for incorrect line highlights in code examples. 3 | * 4 | * Example 1: Negative highlights like in 5 | *
var x = 1;
6 | * are invalid. 7 | * 8 | * Example 2: Highlights exceeding the line count like in 9 | *
var x = 1;
10 | * are invalid. 11 | * 12 | * Example 3: Highlighted ranges of lines exceeding the line count like in 13 | *
var x = 1;
14 | * are invalid. 15 | * 16 | * Example 4: Highlighted ranges where the start line is bigger than the end line line in 17 | *
var x = 1;\nvar y = 2;
18 | * are invalid. 19 | * 20 | * Implementation notes: This test searches for all
 elements containing a 'highlight'
 21 |  *  class, then splits the numbers and ranges wrapped by square brackets following the 'highlight'
 22 |  *  class and finally checks each item whether its valid.
 23 |  */
 24 | 
 25 | const ERROR = require('./doctests.js').ERROR;
 26 | 
 27 | const reHighlighting = /highlight:?\s*\[(.+?)\]/i;
 28 | 
 29 | exports.wrongHighlightedLine = {
 30 |   name: "wrong_highlighted_line",
 31 |   desc: "wrong_highlighted_line_desc",
 32 | 
 33 |   check: function checkWrongHighlightedLine(rootElement) {
 34 |     let presWithHighlighting = rootElement.querySelectorAll("pre[class*='highlight']");
 35 |     let matches = [];
 36 | 
 37 |     for (let i = 0; i < presWithHighlighting.length; i++) {
 38 |       let match = presWithHighlighting[i].getAttribute("class").match(reHighlighting);
 39 |       if (match) {
 40 |         let numbersAndRanges = match[1].split(",");
 41 |         let lineCount = presWithHighlighting[i].innerHTML.split(/|\n/gi).length;
 42 | 
 43 |         numbersAndRanges.forEach(numberOrRange => {
 44 |           let start;
 45 |           let end;
 46 |           [, start, end] = numberOrRange.match(/^\s*(-?\d+)(?:\s*-\s*(-?\d+))?\s*$/);
 47 | 
 48 |           if (start === undefined) {
 49 |             return;
 50 |           }
 51 | 
 52 |           start = Number(start);
 53 |           end = Number(end);
 54 | 
 55 |           if (start <= 0) {
 56 |             matches.push({
 57 |               node: presWithHighlighting[i],
 58 |               msg: "highlighted_line_number_not_positive",
 59 |               msgParams: [String(start), match[1]],
 60 |               type: ERROR
 61 |             });
 62 |           }
 63 |           if (start > lineCount) {
 64 |             matches.push({
 65 |               node: presWithHighlighting[i],
 66 |               msg: "highlighted_line_number_too_big",
 67 |               msgParams: [String(start), String(lineCount), match[1]],
 68 |               type: ERROR
 69 |             });
 70 |           }
 71 |           if (!Number.isNaN(end)) {
 72 |             if (end > lineCount) {
 73 |               matches.push({
 74 |                 node: presWithHighlighting[i],
 75 |                 msg: "highlighted_line_number_too_big",
 76 |                 msgParams: [String(end), String(lineCount), match[1]],
 77 |                 type: ERROR
 78 |               });
 79 |             }
 80 |             if (end <= 0) {
 81 |               matches.push({
 82 |                 node: presWithHighlighting[i],
 83 |                 msg: "highlighted_line_number_not_positive",
 84 |                 msgParams: [String(end), match[1]],
 85 |                 type: ERROR
 86 |               });
 87 |             }
 88 |             if (start > end) {
 89 |               matches.push({
 90 |                 node: presWithHighlighting[i],
 91 |                 msg: "invalid_highlighted_range",
 92 |                 msgParams: [String(start), String(end), match[1]],
 93 |                 type: ERROR
 94 |               });
 95 |             }
 96 |           }
 97 |         });
 98 |       }
 99 |     }
100 | 
101 |     return matches;
102 |   },
103 | 
104 |   fix: function fixWrongHighlightedLine(matches) {
105 |     matches.forEach(match => {
106 |       match.node.className = match.node.className.replace(reHighlighting, "").replace(/;\s*$/, "");
107 |     });
108 |   }
109 | };
110 | 


--------------------------------------------------------------------------------
/lib/invalid-macros.js:
--------------------------------------------------------------------------------
  1 | /*
  2 |  *  Title: Test for the usage of invalid macros.
  3 |  *
  4 |  *  Example 1: The usage of 

{{SomeMacro}}

should rather be removed, replaced by a valid 5 | * macro or by static text and {{SomeMacro}} should be deleted. 6 | * 7 | * Implementation notes: This test uses an (incomprehensive) whitelist of allowed macros and a 8 | * list of obsolete macros. Obsolete macros are marked as errors, all others, which are not 9 | * whitelisted are marked as warnings. 10 | */ 11 | 12 | const ERROR = require('./doctests.js').ERROR; 13 | const WARNING = require('./doctests.js').WARNING; 14 | 15 | const obsoleteMacros = [ 16 | "languages" 17 | ]; 18 | 19 | exports.invalidMacros = { 20 | name: "invalid_macros", 21 | desc: "invalid_macros_desc", 22 | check: function checkInvalidMacros(rootElement) { 23 | const allowedMacros = [ 24 | "addonsidebar", 25 | "apiref", 26 | "anch", 27 | "availableinworkers", 28 | "bug", 29 | "canvassidebar", 30 | "chromebug", 31 | "communitybox", 32 | "compat", 33 | "cssdata", 34 | "cssinfo", 35 | "cssref", 36 | "csssyntax", 37 | "cssxref", 38 | "defaultapisidebar", 39 | "deprecated_header", 40 | "deprecated_inline", 41 | "discussionlist", 42 | "docstatus", 43 | "domxref", 44 | "draft", 45 | "edgebug", 46 | "embedghlivesample", 47 | "embedlivesample", 48 | "embedyoutube", 49 | "event", 50 | "experimental_inline", 51 | "firefox_for_developers", 52 | "fx_minversion_inline", 53 | "fxos_maxversion_inline", 54 | "fxos_minversion_inline", 55 | "gecko", 56 | "gecko_minversion_inline", 57 | "geckorelease", 58 | "glossary", 59 | "groupdata", 60 | "htmlattrdef", 61 | "htmlattrxref", 62 | "htmlelement", 63 | "htmlref", 64 | "httpheader", 65 | "httpmethod", 66 | "httpsidebar", 67 | "httpstatus", 68 | "includesubnav", 69 | "inheritancediagram", 70 | "interface", 71 | "interfacedata", 72 | "jsfiddlelink", 73 | "jsref", 74 | "jssidebar", 75 | "jsxref", 76 | "js_property_attributes", 77 | "l10n:common", 78 | "l10n:compattable", 79 | "l10n:css", 80 | "l10n:javascript", 81 | "l10n:svg", 82 | "localizationstatusinsection", 83 | "mathmlelement", 84 | "mathmlref", 85 | "next", 86 | "non-standard_header", 87 | "non-standard_inline", 88 | "noscript_inline", 89 | "obsolete_header", 90 | "obsolete_inline", 91 | "optional_inline", 92 | "page", 93 | "previous", 94 | "previousmenunext", 95 | "previousnext", 96 | "promote-mdn", 97 | "property_prefix", 98 | "readonlyinline", 99 | "releasegecko", 100 | "rfc", 101 | "seecompattable", 102 | "sidebarutilities", 103 | "sm_minversion_inline", 104 | "spec2", 105 | "specname", 106 | "svgattr", 107 | "svgdata", 108 | "svgelement", 109 | "svginfo", 110 | "svgref", 111 | "tb_minversion_inline", 112 | "webextapiembedtype", 113 | "webextapiref", 114 | "webextapisidebar", 115 | "webextchromecompat", 116 | "webextexamplesdata", 117 | "webextexamples", 118 | "webglsidebar", 119 | "webkitbug", 120 | "webrtcsidebar", 121 | "xref", 122 | "xulattr", 123 | "xulelem" 124 | ]; 125 | 126 | let treeWalker = document.createTreeWalker( 127 | rootElement, 128 | NodeFilter.SHOW_TEXT, 129 | // eslint-disable-next-line 130 | {acceptNode: node => node.textContent.match(/\{\{.*?\}\}/) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT} 131 | ); 132 | let matches = []; 133 | 134 | while (treeWalker.nextNode()) { 135 | let reMacroName = /\{\{\s*([^(}\s]+).*?\}\}/g; 136 | let macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent); 137 | while (macroNameMatch) { 138 | if (obsoleteMacros.includes(macroNameMatch[1].toLowerCase())) { 139 | matches.push({ 140 | node: treeWalker.currentNode, 141 | msg: "obsolete_macro", 142 | msgParams: [macroNameMatch[0]], 143 | type: ERROR 144 | }); 145 | } else if (!allowedMacros.includes(macroNameMatch[1].toLowerCase())) { 146 | matches.push({ 147 | msg: macroNameMatch[0], 148 | type: WARNING 149 | }); 150 | } 151 | macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent); 152 | } 153 | } 154 | 155 | return matches; 156 | }, 157 | 158 | fix: function fixInvalidMacros(matches) { 159 | let reObsoleteMacros = 160 | new RegExp(`\\{\\{\\s*(?:${obsoleteMacros.join("|")}).*?\\}\\}`, "gi"); 161 | 162 | matches.forEach(match => { 163 | if (!match.node) { 164 | return; 165 | } 166 | 167 | match.node.textContent = match.node.textContent.replace(reObsoleteMacros, ""); 168 | if (match.node.parentNode.textContent.match(/^(\s| )*$/)) { 169 | match.node.parentNode.remove(); 170 | } 171 | }); 172 | } 173 | }; 174 | -------------------------------------------------------------------------------- /tests/test-incorrectly-wrapped-sidebar-macros.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ERROR = require('../lib/doctests.js').ERROR; 3 | const incorrectlyWrappedSidebarMacros = require('../lib/incorrectly-wrapped-sidebar-macros.js').incorrectlyWrappedSidebarMacros; 4 | 5 | describe('incorrectlyWrappedSidebarMacros', function() { 6 | it('Should return 0 errors (CSSRef)', function(done) { 7 | const str = '
{{CSSRef}}
'; 8 | const expected = []; 9 | 10 | let rootElement = document.createElement("body"); 11 | rootElement.innerHTML = str; 12 | 13 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 14 | 15 | results.forEach((element, index) => { 16 | delete element.node; 17 | assert.deepEqual(expected[index], element); 18 | }); 19 | 20 | done(); 21 | }); 22 | 23 | it('Should return 0 errors (HTMLRef)', function(done) { 24 | 25 | const str = '
{{HTMLRef}}
'; 26 | const expected = []; 27 | 28 | let rootElement = document.createElement("body"); 29 | rootElement.innerHTML = str; 30 | 31 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 32 | 33 | results.forEach((element, index) => { 34 | delete element.node; 35 | assert.deepEqual(expected[index], element); 36 | }); 37 | 38 | done(); 39 | }); 40 | 41 | it('Should return 0 errors (APIRef)', function(done) { 42 | 43 | const str = '
{{APIRef}}
'; 44 | const expected = []; 45 | 46 | let rootElement = document.createElement("body"); 47 | rootElement.innerHTML = str; 48 | 49 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 50 | 51 | results.forEach((element, index) => { 52 | delete element.node; 53 | assert.deepEqual(expected[index], element); 54 | }); 55 | 56 | done(); 57 | }); 58 | 59 | it('Should return 0 errors (JSRef)', function(done) { 60 | 61 | const str = '
{{JSRef}}
'; 62 | const expected = []; 63 | 64 | let rootElement = document.createElement("body"); 65 | rootElement.innerHTML = str; 66 | 67 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 68 | 69 | results.forEach((element, index) => { 70 | delete element.node; 71 | assert.deepEqual(expected[index], element); 72 | }); 73 | 74 | done(); 75 | }); 76 | 77 | it('Should return 0 errors (SVGRefElem)', function(done) { 78 | 79 | const str = '
{{SVGRefElem}}
'; 80 | const expected = []; 81 | 82 | let rootElement = document.createElement("body"); 83 | rootElement.innerHTML = str; 84 | 85 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 86 | 87 | results.forEach((element, index) => { 88 | delete element.node; 89 | assert.deepEqual(expected[index], element); 90 | }); 91 | 92 | done(); 93 | }); 94 | 95 | it('Should return 0 errors (JSSidebar)', function(done) { 96 | 97 | const str = '
{{JSSidebar}}
'; 98 | const expected = []; 99 | 100 | let rootElement = document.createElement("body"); 101 | rootElement.innerHTML = str; 102 | 103 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 104 | 105 | results.forEach((element, index) => { 106 | delete element.node; 107 | assert.deepEqual(expected[index], element); 108 | }); 109 | 110 | done(); 111 | }); 112 | 113 | it('Should return 0 errors (AddonSidebar)', function(done) { 114 | 115 | const str = '
{{AddonSidebar}}
'; 116 | const expected = []; 117 | 118 | let rootElement = document.createElement("body"); 119 | rootElement.innerHTML = str; 120 | 121 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 122 | 123 | results.forEach((element, index) => { 124 | delete element.node; 125 | assert.deepEqual(expected[index], element); 126 | }); 127 | 128 | done(); 129 | }); 130 | 131 | it('Should return 0 errors (APIRef with parameters)', function(done) { 132 | 133 | const str = '
{{APIRef("some API")}}
'; 134 | const expected = []; 135 | 136 | let rootElement = document.createElement("body"); 137 | rootElement.innerHTML = str; 138 | 139 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 140 | 141 | results.forEach((element, index) => { 142 | assert.deepEqual(expected[index], element); 143 | }); 144 | 145 | done(); 146 | }); 147 | 148 | it('Should return 1 error regarding

wrapping a sidebar', function(done) { 149 | 150 | const str = '

{{CSSRef}}

'; 151 | const expected = [ 152 | {msg: "wrong_element_wrapping_sidebar_macro", msgParams: ["{{CSSRef}}", "p"], type: ERROR} 153 | ]; 154 | 155 | let rootElement = document.createElement("body"); 156 | rootElement.innerHTML = str; 157 | 158 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 159 | 160 | results.forEach((element, index) => { 161 | delete element.node; 162 | assert.deepEqual(expected[index], element); 163 | }); 164 | 165 | done(); 166 | }); 167 | 168 | 169 | it('Should return 1 error regarding wrapping a sidebar', function(done) { 170 | 171 | const str = '{{ APIRef("Some API") }}'; 172 | const expected = [ 173 | {msg: "wrong_element_wrapping_sidebar_macro", msgParams: ["{{ APIRef(\"Some API\") }}", "span"], type: ERROR} 174 | ]; 175 | 176 | let rootElement = document.createElement("body"); 177 | rootElement.innerHTML = str; 178 | 179 | let results = incorrectlyWrappedSidebarMacros.check(rootElement); 180 | 181 | results.forEach((element, index) => { 182 | delete element.node; 183 | assert.deepEqual(expected[index], element); 184 | }); 185 | 186 | done(); 187 | }); 188 | }); 189 | 190 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------