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