add markdownlint-rules-grav-pages files

This commit is contained in:
OniriCorpe 2024-03-02 02:09:33 +01:00
parent e122cb7dd1
commit 0ff8dc5c03
4 changed files with 347 additions and 0 deletions

View 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"
]
}

View 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,
});
}
}
}
});
});
},
};

View 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,
);
}
}
}
});
});
},
};

View 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,
});
});
},
};