import { RuleParams, RuleOnError } from "markdownlint" import * as yaml from "js-yaml" export const enforceHeaderStructure = { names: ["enforce-header-structure"], description: "Proposal header structure should follow template", tags: ["structure"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) return const category: string = frontMatter.category if (!category) return if (["Meta"].includes(category)) return const filtered = params.tokens.filter(function filterToken(token) { return ( token.type === "heading_open" && (token.tag === "h1" || token.tag === "h2") ) }) let index = 0 let tempHeadings = expectedHeadings; while (index < expectedHeadings.length) { let token = filtered[index] tempHeadings = tempHeadings.filter(item => item !== token.line) if (index + 1 >= filtered.length) { onError({ lineNumber: token.lineNumber, detail: `Expected heading \`${expectedHeadings[index]}\` and none exists. Please follow the structure outlined in the Proposal Template.`, context: token.line, }) return } else { index++ } } tempHeadings.forEach(item => { onError({ lineNumber: 1, detail: `Expected heading \`${item}\` and none exists. Please follow the structure outlined in the Proposal Template.`, }) }) if (tempHeadings.length >= 0) { return } }, } const expectedHeadings = [ "## Summary", "## Motivation", "## Alternatives Considered", "## New Terminology", "## Detailed Design", "## Impact", "## Security Considerations", ] export const enforceMetadataStructure = { names: ["enforce-front-matter-structure"], description: "Proposal front matter should be YAML following template structure", tags: ["front-matter"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) { onError({ lineNumber: 1, detail: `Missing front matter metadata formatted as YAML`, }) return } Object.keys(requiredMetadata).forEach((meta) => { if (!frontMatter[meta]) { onError({ lineNumber: 1, detail: `Front matter metadata either doesn't contain \`${meta}\` or isn't formatted correctly`, }) } }) Object.keys(frontMatter).forEach((key) => { if (!(requiredMetadata as any)[key] && !(optionalMetadata as any)[key]) { onError({ lineNumber: 1, detail: `Front matter contains invalid metadata \`${key}\``, }) } }) }, } const requiredMetadata = { simd: {}, title: {}, authors: {}, category: {}, type: {}, status: {}, created: {}, } const optionalMetadata = { feature: {}, } export const metadataSimdIsValid = { names: ["front-matter-has-simd"], description: "Metadata `simd` is a 4 digit numerical string", tags: ["front-matter"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) return const simd: string = frontMatter.simd if (!simd) return if (isNaN(Number(simd))) { onError({ lineNumber: 1, detail: "Front matter `simd` must be a numerical string", }) } if (simd.length !== 4) { onError({ lineNumber: 1, detail: "Front matter `simd` must be 4 digits", }) } }, } export const metadataTitleIsValid = { names: ["front-matter-has-title"], description: "Proposal front matter should include a title no longer than 45 characters", tags: ["front-matter"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) return const title: string = frontMatter.title if (!title) return if (title.length > 45) { onError({ lineNumber: 1, detail: "Metadata `title` should be no longer than 45 characters", }) } }, } export const metadataAuthorsIsValid = { names: ["front-matter-has-authors"], description: "Proposal front matter should include authors", tags: ["front-matter"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) return const authors: string = frontMatter.authors if (!authors) return if (authors.length == 0) { onError({ lineNumber: 1, detail: "Metadata `authors` exists but doesn't include any values", }) } }, } export const metadataCategoryIsValid = { names: ["front-matter-has-valid-category"], description: "Proposal front matter should have a valid category", tags: ["front-matter"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) return const category: string = frontMatter.category if (!category) return if (!["Meta", "Standard"].includes(category)) { onError({ lineNumber: 1, detail: `\`${category}\` is not supported as a value for category`, }) } }, } export const metadataTypeIsValid = { names: ["front-matter-has-valid-type"], description: "Proposal front matter should have a valid type", tags: ["front-matter"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) return const type: string = frontMatter.type if (!type) return const validTypes = ["Core", "Networking", "Interface", "Meta"] if (!validTypes.some((validType) => type.includes(validType))) { onError({ lineNumber: 1, detail: `\`${type}\` is not supported as a value for type. Valid values for type are: ${validTypes.join( ", " )}`, }) } }, } export const metadataStatusIsValid = { names: ["front-matter-has-valid-status"], description: "Proposal front matter should have a valid status", tags: ["front-matter"], function: function rule(params: RuleParams, onError: RuleOnError) { const string = params.frontMatterLines .join("\n") .trim() .replace(/^-*$/gm, "") const frontMatter: any = yaml.load(string) if (!frontMatter) return const status: string = frontMatter.status if (!status) return const validStatus = [ "Idea", "Draft", "Review", "Accepted", "Stagnant", "Withdrawn", "Implemented", ] if (!validStatus.includes(status)) { onError({ lineNumber: 1, detail: `\`${status}\` is not supported as a value for status. Valid values for status are: ${validStatus.join( ", " )}`, }) } }, }