| import { addError } from 'markdownlint-rule-helpers' |
| import Ajv from 'ajv' |
|
|
| import { convertOldCTAUrl } from '@/content-render/scripts/cta-builder' |
| import ctaSchemaDefinition from '@/data-directory/lib/data-schemas/ctas' |
| import type { RuleParams, RuleErrorCallback, Rule } from '../../types' |
|
|
| const ajv = new Ajv({ strict: false, allErrors: true }) |
| const validateCTASchema = ajv.compile(ctaSchemaDefinition) |
|
|
| export const ctasSchema: Rule = { |
| names: ['GHD057', 'ctas-schema'], |
| description: 'CTA URLs must conform to the schema', |
| tags: ['ctas', 'schema', 'urls'], |
| function: (params: RuleParams, onError: RuleErrorCallback) => { |
| |
| |
| const urlRegex = /https?:\/\/[^\s)\]{}'">]+/g |
| const content = params.lines.join('\n') |
|
|
| let match |
| while ((match = urlRegex.exec(content)) !== null) { |
| const url = match[0] |
|
|
| |
| if (!url.includes('ref_')) continue |
|
|
| |
| let hostname: string |
| try { |
| hostname = new URL(url).hostname |
| } catch { |
| |
| continue |
| } |
| const allowedHosts = ['github.com', 'desktop.github.com'] |
| if (!allowedHosts.includes(hostname)) continue |
|
|
| |
| const isPlaceholderUrl = |
| /[A-Z_]+/.test(url) && |
| (url.includes('DESTINATION') || |
| url.includes('CTA+NAME') || |
| url.includes('LOCATION') || |
| url.includes('PRODUCT')) |
|
|
| if (isPlaceholderUrl) continue |
|
|
| try { |
| const urlObj = new URL(url) |
| const searchParams = urlObj.searchParams |
|
|
| |
| const refParams: Record<string, string> = {} |
| const hasRefParams = Array.from(searchParams.keys()).some((key) => key.startsWith('ref_')) |
|
|
| if (!hasRefParams) continue |
|
|
| |
| for (const [key, value] of searchParams.entries()) { |
| if (key.startsWith('ref_')) { |
| refParams[key] = value |
| } |
| } |
|
|
| |
| const hasOldParams = |
| 'ref_cta' in refParams || 'ref_loc' in refParams || 'ref_page' in refParams |
|
|
| if (hasOldParams) { |
| const result = convertOldCTAUrl(url) |
| if (result && result.newUrl !== url) { |
| |
| const lineIndex = params.lines.findIndex((line) => line.includes(url)) |
| const lineNumber = lineIndex >= 0 ? lineIndex + 1 : 1 |
| const line = lineIndex >= 0 ? params.lines[lineIndex] : '' |
| const urlStartInLine = line.indexOf(url) |
|
|
| const fixInfo = { |
| editColumn: urlStartInLine + 1, |
| deleteCount: url.length, |
| insertText: result.newUrl, |
| } |
|
|
| addError( |
| onError, |
| lineNumber, |
| 'CTA URL uses old parameter format (ref_cta, ref_loc, ref_page). Use new schema format (ref_product, ref_type, ref_style, ref_plan).', |
| line, |
| [urlStartInLine + 1, url.length], |
| fixInfo, |
| ) |
| } |
| } else { |
| |
| const isValid = validateCTASchema(refParams) |
|
|
| if (!isValid) { |
| const lineIndex = params.lines.findIndex((line) => line.includes(url)) |
| const lineNumber = lineIndex >= 0 ? lineIndex + 1 : 1 |
| const line = lineIndex >= 0 ? params.lines[lineIndex] : '' |
|
|
| |
| const errors = validateCTASchema.errors || [] |
| for (const error of errors) { |
| let message = '' |
| if (error.keyword === 'required') { |
| message = `Missing required parameter: ${(error.params as any)?.missingProperty}` |
| } else if (error.keyword === 'enum') { |
| const paramName = error.instancePath.substring(1) |
| |
| const invalidValue = refParams[paramName] |
| const allowedValues = (error.params as any)?.allowedValues || [] |
| message = `Invalid value for ${paramName}: "${invalidValue}". Valid values are: ${allowedValues.join(', ')}` |
| } else if (error.keyword === 'additionalProperties') { |
| message = `Unexpected parameter: ${(error.params as any)?.additionalProperty}` |
| } else { |
| message = `CTA URL validation error: ${error.message}` |
| } |
|
|
| addError( |
| onError, |
| lineNumber, |
| message, |
| line, |
| null, |
| null, |
| ) |
| } |
| } |
| } |
| } catch { |
| |
| continue |
| } |
| } |
| }, |
| } |
|
|