UNPKG

@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
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 }