UNPKG

rdme

Version:

ReadMe's official CLI and GitHub Action.

164 lines (163 loc) 7.53 kB
import fs from 'node:fs'; import path from 'node:path'; import { Ajv } from 'ajv'; import _addFormats from 'ajv-formats'; import grayMatter from 'gray-matter'; // workaround from here: https://github.com/ajv-validator/ajv-formats/issues/85#issuecomment-2262652443 const addFormats = _addFormats; /** * Validates the frontmatter data, fixes any issues, and returns the results. */ export function fix( /** frontmatter data to be validated */ data, /** schema to validate against */ schema, /** * mappings of object IDs to slugs * (e.g., category IDs to category URIs) */ mappings) { if (!Object.keys(data).length) { this.debug('no frontmatter attributes found, skipping validation'); return { fixableErrorCount: 0, errors: [], hasIssues: false, unfixableErrors: [], updatedData: data }; } const ajv = new Ajv({ allErrors: true, strictTypes: false, strictTuples: false }); addFormats(ajv); const ajvValidate = ajv.compile(schema); ajvValidate(data); let errors; if (ajvValidate?.errors) { errors = ajvValidate.errors; this.debug(`${errors.length} errors found: ${JSON.stringify(errors)}`); // if one of the errors is that the category uri doesn't match the pattern, // we can ignore it since we normalize it later const categoryUriPatternErrorIndex = ajvValidate.errors.findIndex(error => error.instancePath === '/category/uri' && error.keyword === 'pattern'); if (categoryUriPatternErrorIndex >= 0) { this.debug('removing category uri pattern error'); errors.splice(categoryUriPatternErrorIndex, 1); } // also do the same for the parent uri const parentUriPatternErrorIndex = ajvValidate.errors.findIndex(error => error.instancePath === '/parent/uri' && error.keyword === 'pattern'); if (parentUriPatternErrorIndex >= 0) { this.debug('removing category uri pattern error'); errors.splice(parentUriPatternErrorIndex, 1); } } let fixableErrorCount = 0; const unfixableErrors = []; const updatedData = structuredClone(data); if (typeof errors === 'undefined' || !errors.length) { return { errors: [], fixableErrorCount, hasIssues: false, unfixableErrors, updatedData }; } errors.forEach(error => { if (error.instancePath === '/category' && error.keyword === 'type' && (this.route === 'guides' || this.route === 'reference')) { const uri = mappings.categories[data.category]; updatedData.category = { uri: uri || `uri-that-does-not-map-to-${data.category}`, }; fixableErrorCount += 1; } else if (error.keyword === 'additionalProperties') { const badKey = error.params.additionalProperty; const extractedValue = data[badKey]; if (error.schemaPath === '#/additionalProperties') { // if the bad property is at the root level, delete it delete updatedData[badKey]; fixableErrorCount += 1; // hidden is the only attribute that is present in all page types if (badKey === 'hidden') { const hidden = typeof extractedValue === 'boolean' ? extractedValue : extractedValue === 'true'; updatedData.privacy = { view: hidden ? 'anyone_with_link' : 'public' }; } else { switch (this.route) { case 'custom_pages': switch (badKey) { case 'htmlmode': // if the `content` object exists, add to it. otherwise, create it if (typeof updatedData.content === 'object' && updatedData.content) { updatedData.content.type = extractedValue ? 'html' : 'markdown'; } else { updatedData.content = { type: extractedValue ? 'html' : 'markdown' }; } break; case 'fullscreen': updatedData.appearance = { fullscreen: extractedValue }; break; default: break; } break; case 'guides': case 'reference': switch (badKey) { case 'excerpt': // if the `content` object exists, add to it. otherwise, create it if (typeof updatedData.content === 'object' && updatedData.content) { updatedData.content.excerpt = extractedValue; } else { updatedData.content = { excerpt: extractedValue }; } break; case 'categorySlug': updatedData.category = { uri: extractedValue }; break; case 'parentDoc': { const uri = mappings.parentPages[extractedValue]; if (uri) { updatedData.parent = { uri }; } } break; case 'parentDocSlug': updatedData.parent = { uri: extractedValue }; break; case 'order': updatedData.position = extractedValue; break; default: break; } break; default: break; } } } else { unfixableErrors.push(error); } } else { unfixableErrors.push(error); } }); return { errors, fixableErrorCount, hasIssues: true, unfixableErrors, updatedData }; } export function writeFixes( /** all metadata for the page that will be written to */ metadata, /** frontmatter changes to be applied */ updatedData, /** output directory to write to */ outputDirArg) { const result = grayMatter.stringify(metadata.content, updatedData); const outputPath = outputDirArg ? path.join(outputDirArg, metadata.filePath) : metadata.filePath; this.debug(`writing fixes to ${outputPath}`); const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(outputPath, result, { encoding: 'utf-8' }); const updatedMetadata = { ...metadata, data: updatedData, }; return updatedMetadata; }