mirror of
https://github.com/YunoHost/doc.git
synced 2024-09-03 20:06:26 +02:00
add markdownlint-rules-grav-pages files
This commit is contained in:
parent
e122cb7dd1
commit
0ff8dc5c03
4 changed files with 347 additions and 0 deletions
119
markdownlint-rules-grav-pages/frontmatter.schema.json
Normal file
119
markdownlint-rules-grav-pages/frontmatter.schema.json
Normal file
|
@ -0,0 +1,119 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "bool", "minLength": 2, "maxLength": 80 },
|
||||
"media_order": { "type": "string" },
|
||||
"body_classes": { "type": "string" },
|
||||
"published": { "type": "boolean" },
|
||||
"visible": { "type": "boolean" },
|
||||
"redirect": { "type": "string" },
|
||||
"cache_enable": { "type": "boolean" },
|
||||
"debugger": { "type": "boolean" },
|
||||
"never_cache_twig": { "type": "boolean" },
|
||||
"twig_first": { "type": "boolean" },
|
||||
"routable": { "type": "boolean" },
|
||||
"login_redirect_here": { "type": "boolean" },
|
||||
"last_modified": { "type": "boolean" },
|
||||
"http_response_code": { "type": "integer" },
|
||||
"lightbox": { "type": "boolean" },
|
||||
"etag": { "type": "boolean" },
|
||||
"template": { "type": "string" },
|
||||
"template_format": { "type": "string" },
|
||||
"date": { "type": "string" },
|
||||
"publish_date": { "type": "string" },
|
||||
"unpublish_date": { "type": "string" },
|
||||
"expires": { "type": "integer" },
|
||||
"menu": { "type": "string", "minLength": 2, "maxLength": 30 },
|
||||
"slug": { "type": "string" },
|
||||
"routes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": { "type": "string" },
|
||||
"canonical": { "type": "string" },
|
||||
"aliases": { "type": "array", "items": { "type": "string" } }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"taxonomy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": { "type": "array", "items": { "type": "string" } },
|
||||
"tag": { "type": "array", "items": { "type": "string" } }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"external_url": { "type": "string" },
|
||||
"external_links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"author": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"twitter": { "type": "string" },
|
||||
"bio": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"summary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"format": { "type": "string" },
|
||||
"size": { "type": "integer" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"sitemap": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changefreq": { "type": "string" },
|
||||
"priority": { "type": "number" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"process": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"markdown": { "type": "boolean" },
|
||||
"twig": { "type": "boolean" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"markdown": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extra": { "type": "boolean" },
|
||||
"auto_line_breaks": { "type": "boolean" },
|
||||
"auto_url_links": { "type": "boolean" },
|
||||
"escape_markup": { "type": "boolean" },
|
||||
"special_chars": { "type": "object", "additionalProperties": { "type": "string" } }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refresh": { "type": "integer" }
|
||||
},
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"page-toc": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": { "type": "boolean" },
|
||||
"start": { "type": "integer" },
|
||||
"depth": { "type": "integer" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"title"
|
||||
]
|
||||
}
|
31
markdownlint-rules-grav-pages/valid-images.js
Normal file
31
markdownlint-rules-grav-pages/valid-images.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const fs = require('fs');
|
||||
const flat = require('../lib/flat');
|
||||
|
||||
module.exports = {
|
||||
names: ['valid-images'],
|
||||
description: 'Rule that reports if a file has valid image references',
|
||||
tags: ['test'],
|
||||
function: function rule(params, onError) {
|
||||
flat(params.tokens).filter((token) => token.type === 'image').forEach((image) => {
|
||||
image.attrs.forEach((attr) => {
|
||||
if (attr[0] === 'src') {
|
||||
let imgSrc = attr[1];
|
||||
if (!imgSrc.match(/^(https?:)/)) {
|
||||
if (imgSrc.includes('?')) {
|
||||
imgSrc = imgSrc.slice(0, imgSrc.indexOf('?'));
|
||||
}
|
||||
const path = `${params.name.split('/').slice(0, -1).join('/')}/${imgSrc}`;
|
||||
|
||||
if (!fs.existsSync(path) || !fs.lstatSync(path).isFile()) {
|
||||
onError({
|
||||
lineNumber: image.lineNumber,
|
||||
detail: `Image src '${imgSrc}' does not link to a valid file.`,
|
||||
context: image.line,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
128
markdownlint-rules-grav-pages/valid-internal-links.js
Normal file
128
markdownlint-rules-grav-pages/valid-internal-links.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
const transliteration = require('transliteration');
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
const md = require('markdown-it')({ html: true });
|
||||
const helpers = require('markdownlint/helpers/helpers');
|
||||
const flat = require('../lib/flat');
|
||||
|
||||
const validateAnchor = (link, href, tokens, onError) => {
|
||||
let anchorFound = false;
|
||||
tokens.filter((token) => token.type === 'heading_open').forEach((heading) => {
|
||||
if (!heading.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headingAnchor = transliteration.slugify(heading.line.toLowerCase(), {
|
||||
replace: {
|
||||
ä: 'ae',
|
||||
ö: 'oe',
|
||||
ü: 'ue',
|
||||
},
|
||||
});
|
||||
|
||||
if (`#${headingAnchor}` === href) {
|
||||
anchorFound = true;
|
||||
}
|
||||
});
|
||||
if (!anchorFound) {
|
||||
onError({
|
||||
lineNumber: link.lineNumber,
|
||||
detail: `Did not find matching heading for anchor '${href}'`,
|
||||
context: link.line,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Annotate tokens with line/lineNumber, duplication from markdownlint because function
|
||||
// is not exposed
|
||||
const annotateTokens = (tokens, lines) => {
|
||||
let tbodyMap = null;
|
||||
return tokens.map((token) => {
|
||||
// Handle missing maps for table body
|
||||
if (token.type === 'tbody_open') {
|
||||
tbodyMap = token.map.slice();
|
||||
} else if ((token.type === 'tr_close') && tbodyMap) {
|
||||
tbodyMap[0] += 1;
|
||||
} else if (token.type === 'tbody_close') {
|
||||
tbodyMap = null;
|
||||
}
|
||||
const mappedToken = token;
|
||||
if (tbodyMap && !token.map) {
|
||||
mappedToken.map = tbodyMap.slice();
|
||||
}
|
||||
// Update token metadata
|
||||
if (token.map) {
|
||||
mappedToken.line = lines[token.map[0]];
|
||||
mappedToken.lineNumber = token.map[0] + 1;
|
||||
// Trim bottom of token to exclude whitespace lines
|
||||
while (token.map[1] && !(lines[token.map[1] - 1].trim())) {
|
||||
mappedToken.map[1] -= 1;
|
||||
}
|
||||
// Annotate children with lineNumber
|
||||
let childLineNumber = token.lineNumber;
|
||||
(token.children || []).map((child) => {
|
||||
const mappedChild = child;
|
||||
mappedChild.lineNumber = childLineNumber;
|
||||
mappedChild.line = lines[childLineNumber - 1];
|
||||
if ((child.type === 'softbreak') || (child.type === 'hardbreak')) {
|
||||
childLineNumber += 1;
|
||||
}
|
||||
return mappedChild;
|
||||
});
|
||||
}
|
||||
|
||||
return mappedToken;
|
||||
});
|
||||
};
|
||||
|
||||
const parseMarkdownContent = (fileContent) => {
|
||||
// Remove UTF-8 byte order marker (if present)
|
||||
let content = fileContent.replace(/^\ufeff/, '');
|
||||
// Ignore the content of HTML comments
|
||||
content = helpers.clearHtmlCommentText(content);
|
||||
// Parse content into tokens and lines
|
||||
const tokens = md.parse(content, {});
|
||||
const lines = content.split(helpers.newLineRe);
|
||||
return annotateTokens(tokens, lines);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
names: ['valid-internal-links'],
|
||||
description: 'Rule that reports if a file has an internal link to an invalid file',
|
||||
tags: ['test'],
|
||||
function: function rule(params, onError) {
|
||||
flat(params.tokens).filter((token) => token.type === 'link_open').forEach((link) => {
|
||||
link.attrs.forEach((attr) => {
|
||||
if (attr[0] === 'href') {
|
||||
const href = attr[1];
|
||||
if (href.match(/^#/)) {
|
||||
validateAnchor(link, href, params.tokens, onError);
|
||||
} else if (href && !href.match(/^(mailto:|https?:)/)) {
|
||||
const parsedHref = url.parse(href);
|
||||
let path;
|
||||
if (parsedHref.pathname.match(/^\//)) {
|
||||
path = `pages${parsedHref.pathname}`;
|
||||
} else {
|
||||
path = `${params.name.split('/').slice(0, -1).join('/')}/${parsedHref.pathname}`;
|
||||
}
|
||||
if (!fs.existsSync(path) || !fs.lstatSync(path).isFile()) {
|
||||
onError({
|
||||
lineNumber: link.lineNumber,
|
||||
detail: `Relative link '${href}' does not link to a valid file.`,
|
||||
context: link.line,
|
||||
});
|
||||
} else if (parsedHref.hash) {
|
||||
// console.log(md.parse(fs.readFileSync(path).toString()));
|
||||
validateAnchor(
|
||||
link,
|
||||
parsedHref.hash,
|
||||
parseMarkdownContent(fs.readFileSync(path).toString()),
|
||||
onError,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
69
markdownlint-rules-grav-pages/valid-metadata-block.js
Normal file
69
markdownlint-rules-grav-pages/valid-metadata-block.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
const YAML = require('yamljs');
|
||||
const { Validator } = require('jsonschema');
|
||||
// See https://learn.getgrav.org/content/headers
|
||||
const metadataSchema = require('./frontmatter.schema.json');
|
||||
|
||||
module.exports = {
|
||||
names: ['valid-metadata-block'],
|
||||
description: 'Rule that reports if a file does not have a valid grav metadata block',
|
||||
tags: ['test'],
|
||||
function: function rule(params, onError) {
|
||||
if (!params.frontMatterLines || params.frontMatterLines.length < 3) {
|
||||
onError({
|
||||
lineNumber: 1,
|
||||
detail: 'Missing grav metadata block',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const frontMatterLines = params.frontMatterLines.filter((line) => !!line);
|
||||
if (frontMatterLines[0] !== '---') {
|
||||
onError({
|
||||
lineNumber: 1,
|
||||
detail: "Grav metadata block has to start with a '---'",
|
||||
context: frontMatterLines[0],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (frontMatterLines[frontMatterLines.length - 1] !== '---') {
|
||||
onError({
|
||||
lineNumber: 1,
|
||||
detail: "Grav metadata block has to end with a '---'",
|
||||
context: frontMatterLines[frontMatterLines.length - 1],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const yamlString = frontMatterLines.slice(1, -1).join('\n');
|
||||
let yamlDocument;
|
||||
try {
|
||||
yamlDocument = YAML.parse(yamlString);
|
||||
} catch (err) {
|
||||
onError({
|
||||
lineNumber: 1,
|
||||
detail: 'Error parsing grav metadata block',
|
||||
context: err.toString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!yamlDocument) {
|
||||
onError({
|
||||
lineNumber: 1,
|
||||
detail: 'Grav metadata is not a valid yaml document',
|
||||
context: yamlString,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const v = new Validator();
|
||||
|
||||
const validation = v.validate(yamlDocument, metadataSchema);
|
||||
|
||||
validation.errors.forEach((validationError) => {
|
||||
onError({
|
||||
lineNumber: 1,
|
||||
detail: validationError.toString(),
|
||||
context: yamlString,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
Loading…
Add table
Reference in a new issue