@ietf-tools/idnits
Version:
Library / CLI to inspect Internet-Draft documents for a variety of conditions to conform with IETF policies.
326 lines (309 loc) • 15.1 kB
JavaScript
import { ALLOWED_DOMAINS_DEFAULT } from './config/externals.mjs'
import { MODES } from './config/modes.mjs'
import { ValidationComment, ValidationError, ValidationWarning } from './helpers/error.mjs'
import {
decodeBufferToUTF8,
validateContent,
validateEncoding
} from './modules/raw.mjs'
import { validateDocName, validateFilename } from './modules/filename.mjs'
import { validateFQDNs } from './modules/fqdn.mjs'
import { validateIPs } from './modules/ip.mjs'
import { validate2119Keywords, validateTermsStyle } from './modules/keywords.mjs'
import {
validateAbstractSection,
validateAuthorSection,
validateIANAConsiderationsSection,
validateIntroductionSection,
validateReferencesInText,
validateReferencesSection,
validateSecurityConsiderationsSection
} from './modules/sections.mjs'
import {
validateDate,
validateObsoleteUpdateRef,
validateCategory,
validateVersion
} from './modules/metadata.mjs'
import {
detectDeprecatedElements,
validateCodeBlocks,
validateIETFTLPBoilerplateXML,
validateIprAttribute,
validateIPRAutogeneratedBoilerplate,
validateSubmissionType,
validateTextLikeRefs,
validateXMLBoilerplate
} from './modules/xml.mjs'
import {
validateLineExtraSpacing,
validateLineLength,
validateIDIndicator,
validateCopyrightNoticeSectionIsNumbered,
validateCodeBlockLicenses,
validateCodeComments,
validatePages,
validateCopyrightDate,
validateLicenseDeclarations,
validateCopyrightSection,
validateStatusOfThisMemoSectionIsNumbered,
validateAbstractSectionIsNumbered,
validateLinksInText,
validateReferenceStyle,
validatePKorBM,
validateHyphenatedLineBreaks,
validateUpdatesAndObsoletesLines,
validateExpiresLine,
validateSaysWorkingDocuments,
validateAcceptableParagraphCallingOutSixMonthValidity,
validateDocumentName,
validateParagraphLinkingToIdsList,
validateMultipleAcceptableParagraphPointingListId,
validateTableOfContentsAndDocumentPages,
validateSubmissionComplianceLine,
validateSubmissionComplianceLinePage,
validateSeparatedFormfeeds,
validateFormFeedOnSeparateLine,
validateTitleUnexpectedIndentation,
validatePreviousCopyrightSection,
validatePageNumbering
} from './modules/txt.mjs'
import {
validateDownrefs,
validateInformativeReferences,
validateNormativeReferences,
validateUnclassifiedReferences,
validatePublishedDraftReferences
} from './modules/downref.mjs'
export { ALLOWED_DOMAINS_DEFAULT, MODES, ValidationComment, ValidationError, ValidationWarning }
/**
* Get a list of validations to run based on document type
*
* @param {String} docType Document Type
* @returns {Array} List of validation groups
*/
export function getAllValidations (docType) {
// -> Preparsing Validations Tasks
const validations = [
{
key: 'preparsing',
title: 'Pre-parsing validations',
tasks: [
{
key: 'filename',
title: 'Validating filename',
task: async (ctx) => validateFilename(ctx.filename, ctx.options)
},
{
key: 'encoding',
title: 'Validating encoding',
task: async (ctx) => validateEncoding(ctx.raw, ctx.options)
},
{
key: 'decodingutf8',
title: 'Decoding UTF-8',
task: async (ctx) => { ctx.data = await decodeBufferToUTF8(ctx.raw) },
isVoid: true
},
{
key: 'content',
title: 'Validating content',
task: async (ctx) => validateContent(ctx.data, ctx.options)
}
],
concurrent: false
}
]
// -> Document Parsing Task
switch (docType) {
case 'txt': {
validations.push({
key: 'parse',
title: 'Parsing document',
tasks: [
{
key: 'parsetxt',
title: 'Parsing TXT document',
task: async (ctx) => {
const { parse } = await import('./parsers/txt.mjs')
ctx.doc = await parse(ctx.data, ctx.filename)
},
isVoid: true
}
],
concurrent: false
})
break
}
case 'xml': {
validations.push({
key: 'parse',
title: 'Parsing document',
tasks: [
{
key: 'parsexml',
title: 'Parsing XML document',
task: async (ctx) => {
const { parse } = await import('./parsers/xml.mjs')
ctx.doc = await parse(ctx.data, ctx.filename)
},
isVoid: true
}
],
concurrent: false
})
break
}
default: {
throw new Error('Unsupported File Format')
}
}
// -> Common Validations Tasks
validations.push({
key: 'common',
title: 'Common validations',
tasks: [
{ key: 'docname', title: 'Document name', task: async (ctx) => validateDocName(ctx.doc, ctx.options) },
{ key: 'abstractsection', title: 'Abstract section', task: async (ctx) => validateAbstractSection(ctx.doc, ctx.options) },
{ key: 'introsection', title: 'Introduction section', task: async (ctx) => validateIntroductionSection(ctx.doc, ctx.options) },
{ key: 'securityconsiderations', title: 'Security considerations section', task: async (ctx) => validateSecurityConsiderationsSection(ctx.doc, ctx.options) },
{ key: 'authorsections', title: 'Author section(s)', task: async (ctx) => validateAuthorSection(ctx.doc, ctx.options) },
{ key: 'refsections', title: 'References section(s)', task: async (ctx) => validateReferencesSection(ctx.doc, ctx.options) },
{ key: 'ianaconsiderations', title: 'IANA considerations section', task: async (ctx) => validateIANAConsiderationsSection(ctx.doc, ctx.options) },
{ key: 'fqdns', title: 'FQDNs', task: async (ctx) => validateFQDNs(ctx.doc, ctx.options) },
{ key: 'ips', title: 'IPs', task: async (ctx) => validateIPs(ctx.doc, ctx.options) },
{ key: 'reqlevelkeywords', title: 'Requirement Level Keywords', task: async (ctx) => validate2119Keywords(ctx.doc, ctx.options) },
{ key: 'termsstyle', title: 'Terms Style', task: async (ctx) => validateTermsStyle(ctx.doc, ctx.options) },
{ key: 'date', title: 'Date', task: async (ctx) => validateDate(ctx.doc, ctx.options) },
{ key: 'updatesobsrefs', title: 'Updates / Obsoletes references', task: async (ctx) => validateObsoleteUpdateRef(ctx.doc, ctx.options) },
{ key: 'category', title: 'Category', task: async (ctx) => validateCategory(ctx.doc, ctx.options) },
{ key: 'version', title: 'Version', task: async (ctx) => validateVersion(ctx.doc, ctx.options) },
{ key: 'refsintext', title: 'References in text', task: async (ctx) => validateReferencesInText(ctx.doc, ctx.options) },
{ key: 'downrefs', title: 'Downrefs', task: async (ctx) => validateDownrefs(ctx.doc, ctx.options) },
{ key: 'normrefs', title: 'Normative references', task: async (ctx) => validateNormativeReferences(ctx.doc, ctx.options) },
{ key: 'unclassifiedrefs', title: 'Unclassified references', task: async (ctx) => validateUnclassifiedReferences(ctx.doc, ctx.options) }
],
concurrent: true
})
// -> Document Type Specific Validations Tasks
switch (docType) {
case 'xml': {
validations.push({
key: 'xml',
title: 'XML-specific validations',
tasks: [
{ key: 'xmldeprecatedels', title: 'Deprecated elements', task: async (ctx) => detectDeprecatedElements(ctx.doc, ctx.options) },
{ key: 'xmlsubmissiontype', title: 'Submission type', task: async (ctx) => validateSubmissionType(ctx.doc, ctx.options) },
{ key: 'xmlcodeblocks', title: 'Code blocks', task: async (ctx) => validateCodeBlocks(ctx.doc, ctx.options) },
{ key: 'xmltextlikerefs', title: 'Text-like references', task: async (ctx) => validateTextLikeRefs(ctx.doc, ctx.options) },
{ key: 'xmliprattr', title: 'IPR attribute', task: async (ctx) => validateIprAttribute(ctx.doc, ctx.options) },
{ key: 'xmlboilerplate', title: 'XML Boilerplate', task: async (ctx) => validateXMLBoilerplate(ctx.doc, ctx.options) },
{ key: 'xmlietftlpboilerplate', title: 'IETF TLP Boilerplate', task: async (ctx) => validateIETFTLPBoilerplateXML(ctx.doc, ctx.options) },
{ key: 'xmliprautogenboilerplate', title: 'IPR Autogenerated Boilerplate', task: async (ctx) => validateIPRAutogeneratedBoilerplate(ctx.doc, ctx.options) }
],
concurrent: true
})
break
}
case 'txt': {
validations.push({
key: 'txt',
title: 'TXT-specific validations',
tasks: [
{ key: 'txtlinelength', title: 'Line length', task: async (ctx) => validateLineLength(ctx.doc, ctx.options) },
{ key: 'txtcodecomments', title: 'Code comments', task: async (ctx) => validateCodeComments(ctx.doc, ctx.options) },
{ key: 'txtcopyrightnoticesection', title: 'Copyright notice section', task: async (ctx) => validateCopyrightNoticeSectionIsNumbered(ctx.doc, ctx.options) },
{ key: 'txtstatusofmemo', title: 'Status of this memo section', task: async (ctx) => validateStatusOfThisMemoSectionIsNumbered(ctx.doc, ctx.options) },
{ key: 'txtabstractsection', title: 'Abstract section numbered', task: async (ctx) => validateAbstractSectionIsNumbered(ctx.doc, ctx.options) },
{ key: 'txtreflinks', title: 'Reference links', task: async (ctx) => validateLinksInText(ctx.doc, ctx.options) },
{ key: 'txtrefstyle', title: 'Reference style', task: async (ctx) => validateReferenceStyle(ctx.doc, ctx.options) },
{ key: 'txtpkbmfirstline', title: 'PK or BM first line', task: async (ctx) => validatePKorBM(ctx.doc, ctx.options) },
{ key: 'txthyphlinebreaks', title: 'Hyphenated line breaks', task: async (ctx) => validateHyphenatedLineBreaks(ctx.doc, ctx.options) },
{ key: 'txtlineextraspacing', title: 'Line extra spacing', task: async (ctx) => validateLineExtraSpacing(ctx.doc, ctx.options) },
{ key: 'txtcodeblocklicenses', title: 'Code block licenses', task: async (ctx) => validateCodeBlockLicenses(ctx.doc, ctx.options) },
{ key: 'txtupdatesobslines', title: 'Updates and obsoletes lines', task: async (ctx) => validateUpdatesAndObsoletesLines(ctx.doc, ctx.options) },
{ key: 'txtdraftrefs', title: 'Draft references', task: async (ctx) => validatePublishedDraftReferences(ctx.doc, ctx.options) },
{ key: 'txtinforefs', title: 'Informational references', task: async (ctx) => validateInformativeReferences(ctx.doc, ctx.options) },
{ key: 'txtcopyrightdate', title: 'Copyright date', task: async (ctx) => validateCopyrightDate(ctx.doc, ctx.options) },
{ key: 'txtlicensedeclarations', title: 'License declarations', task: async (ctx) => validateLicenseDeclarations(ctx.doc, ctx.options) },
{ key: 'txtcopyrightsection', title: 'Copyright section', task: async (ctx) => validateCopyrightSection(ctx.doc, ctx.options) },
{ key: 'txtoverlongpages', title: 'Page length', task: async (ctx) => validatePages(ctx.doc, ctx.options) },
{ key: 'txtpreviouscopyrightsection', title: 'Previous copyright section', task: async (ctx) => validatePreviousCopyrightSection(ctx.doc, ctx.options) },
{ ket: 'txtpagenumbered', title: 'Pages numbering', task: async (ctx) => validatePageNumbering(ctx.doc, ctx.options) }
],
concurrent: true
})
validations.push({
title: 'TXT-specific - Internet-Draft validations',
tasks: [
{ key: 'txtidr', title: 'Internet-Draft indicator', task: async (ctx) => validateIDIndicator(ctx.doc, ctx.options) },
{ key: 'txtexpiration', title: 'Expires line', task: async (ctx) => validateExpiresLine(ctx.doc, ctx.options) },
{ key: 'txtworkdocument', title: 'Acceptable paragraph noting that the document is a draft', task: async (ctx) => validateSaysWorkingDocuments(ctx.doc, ctx.options) },
{ key: 'txtsixmonthvalid', title: 'Acceptable paragraph calling out six month validity', task: async (ctx) => validateAcceptableParagraphCallingOutSixMonthValidity(ctx.doc, ctx.options) },
{ key: 'txtdocname', title: 'Document name appears on first page', task: async (ctx) => validateDocumentName(ctx.doc, ctx.options) },
{ key: 'txtidsparagraph', title: 'Pointing to the list of current I-Ds', task: async (ctx) => validateParagraphLinkingToIdsList(ctx.doc, ctx.options) },
{ key: 'txtmultipleidsparagraph', title: 'Multiple pointing to the list of current I-Ds', task: async (ctx) => validateMultipleAcceptableParagraphPointingListId(ctx.doc, ctx.options) },
{ key: 'txtpagecount', title: 'Has more than page threshold and no TOC', task: async (ctx) => validateTableOfContentsAndDocumentPages(ctx.doc, ctx.options) },
{ key: 'txtsubmission', title: 'Disclosure text', task: async (ctx) => validateSubmissionComplianceLine(ctx.doc, ctx.options) },
{ key: 'txtsubmissionlinepage', title: 'submission compliance line page', task: async (ctx) => validateSubmissionComplianceLinePage(ctx.doc, ctx.options) },
{ key: 'txtformfeeds', title: 'Separated form feeds', task: async (ctx) => validateSeparatedFormfeeds(ctx.doc, ctx.options) },
{ key: 'txtformfeedsline', title: 'Form feed separate line with some text', task: async (ctx) => validateFormFeedOnSeparateLine(ctx.doc, ctx.options) },
{ key: 'txtindentation', title: 'Unexpected indentation', task: async (ctx) => validateTitleUnexpectedIndentation(ctx.doc, ctx.options) }
],
concurrent: true,
condition: (ctx) => ctx.doc.docKind !== 'rfc'
})
}
}
return validations
}
/**
* Perform validations on a document
*
* @param {Buffer} raw Buffer of the document contents
* @param {String} filename Filename of the document to validate
* @param {Object} options Validation options
* @returns {Array} Validation results
*/
export async function checkNits (raw, filename, {
year,
allowedDomains = ALLOWED_DOMAINS_DEFAULT,
mode = MODES.NORMAL,
offline = false
} = {}) {
const ext = filename.endsWith('.xml') ? 'xml' : 'txt'
const result = []
const ctx = {
raw,
filename,
options: {
allowedDomains,
mode,
offline,
year
}
}
const validations = getAllValidations(ext)
for (const valGroup of validations) {
// Skip validation group if condition is not met
if (valGroup.condition && !valGroup.condition(ctx)) {
continue
}
if (valGroup.concurrent) {
const valGroupResult = await Promise.all(valGroup.tasks.map(valTask => valTask.task(ctx)))
for (const taskResult of valGroupResult) {
if (Array.isArray(taskResult)) {
result.push(...taskResult)
}
}
} else {
for (const valTask of valGroup.tasks) {
const taskResult = await valTask.task(ctx)
if (!valTask.isVoid && Array.isArray(taskResult)) {
result.push(...taskResult)
}
}
}
}
return result
}