UNPKG

@atlaskit/adf-utils

Version:

Set of utilities to traverse, modify and create ADF documents.

983 lines (966 loc) 36.9 kB
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure'; // Ignored via go/ees005 // eslint-disable-next-line import/no-namespace import * as specs from './specs'; import { copy, isBoolean, isDefined, isInteger, isNumber, isPlainObject, isString, makeArray } from './utils'; import { extractAllowedContent } from './extractAllowedContent'; import { validatorFnMap } from './rules'; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any function mapMarksItems(spec, fn = x => x) { if (spec.props && spec.props.marks) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { items, ...rest } = spec.props.marks; return { ...spec, props: { ...spec.props, marks: { ...rest, /** * `Text & MarksObject<Mark-1>` produces `items: ['mark-1']` * `Text & MarksObject<Mark-1 | Mark-2>` produces `items: [['mark-1', 'mark-2']]` */ items: items.length ? Array.isArray(items[0]) ? items.map(fn) : [fn(items)] : [[]] } } }; } else { return spec; } } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const partitionObject = (obj, predicate) => Object.keys(obj).reduce((acc, key) => { const result = predicate(key, obj[key], obj); acc[result ? 0 : 1].push(key); return acc; }, [[], []]); /** * Checks if a spec is a variant spec. * A variant spec is an array where the first element is a string (base spec name) * and the second element is a ValidatorSpec object { props: { ... } } * * @param spec - The spec to check * @returns true if the spec is a variant spec, false otherwise */ const isVariant = spec => typeof spec === 'object' && !!spec && 0 in spec && 1 in spec && typeof spec[0] === 'string' && typeof spec[1] === 'object'; /** * Normalizes the structure of files imported from './specs'. * We denormalised the spec to save bundle size. */ export function createSpec(nodes, marks) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any return Object.keys(specs).reduce((newSpecs, k) => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any let spec = { ...specs[k] }; if (expValEqualsNoExposure('platform_editor_flexible_list_schema', 'isEnabled', true) && isVariant(spec) && // Only apply to variants which are explicitly marked for override in `variantSpecOverrides` Object.values(variantSpecOverrides).includes(k)) { // This allows the variant spec to also have the content normalization applied to it // When the spec is a variant it will be in the form of ['base_spec_name', { props: { ... } }] // So the actual validator spec of the variant will be the second item in the array // We also need to shallow clone this to ensure we don't mutate the original spec spec = { ...spec[1] }; } if (spec.props) { spec.props = { ...spec.props }; if (spec.props.content) { // 'tableCell_content' => { type: 'array', items: [ ... ] } if (isString(spec.props.content)) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any spec.props.content = specs[spec.props.content]; } // ['inline', 'emoji'] if (Array.isArray(spec.props.content)) { /** * Flatten * * Input: * [ { type: 'array', items: [ 'tableHeader' ] }, { type: 'array', items: [ 'tableCell' ] } ] * * Output: * { type: 'array', items: [ [ 'tableHeader' ], [ 'tableCell' ] ] } */ spec.props.content = { type: 'array', items: (spec.props.content || []).map(arr => arr.items) }; } else { spec.props.content = { ...spec.props.content }; } spec.props.content.items = spec.props.content.items // ['inline'] => [['emoji', 'hr', ...]] // ['media'] => [['media']] .map(item => isString(item) ? // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any Array.isArray(specs[item]) ? // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any specs[item] : [item] : item) // [['emoji', 'hr', 'inline_code']] => [['emoji', 'hr', ['text', { marks: {} }]]] .map(item => item.map(subItem => // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any Array.isArray(specs[subItem]) ? // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any specs[subItem] : isString(subItem) ? subItem : // Now `NoMark` produces `items: []`, should be fixed in generator ['text', subItem]) // Remove unsupported nodes & marks // Filter nodes .filter(subItem => { if (nodes) { // Node with overrides // ['mediaSingle', { props: { content: { items: [ 'media', 'caption' ] } }}] if (Array.isArray(subItem)) { var _subItem$, _subItem$$props, _subItem$$props$conte; const isMainNodeSupported = nodes.indexOf(subItem[0]) > -1; if (isMainNodeSupported && (_subItem$ = subItem[1]) !== null && _subItem$ !== void 0 && (_subItem$$props = _subItem$.props) !== null && _subItem$$props !== void 0 && (_subItem$$props$conte = _subItem$$props.content) !== null && _subItem$$props$conte !== void 0 && _subItem$$props$conte.items) { return subItem[1].props.content.items.every(item => nodes.indexOf(item) > -1); } return isMainNodeSupported; } return nodes.indexOf(subItem) > -1; } return true; }) // Filter marks .map(subItem => Array.isArray(subItem) && marks ? /** * TODO: Probably try something like immer, but it's 3.3kb gzipped. * Not worth it just for this. */ [subItem[0], mapMarksItems(subItem[1])] : subItem)); } } newSpecs[k] = spec; return newSpecs; }, {}); } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any function getOptionsForType(type, list) { if (!list) { return {}; } for (let i = 0, len = list.length; i < len; i++) { const spec = list[i]; let name = spec; let options = {}; if (Array.isArray(spec)) { [name, options] = spec; } if (name === type) { return options; } } return false; } const isValidatorSpecAttrs = spec => { return !!spec.props; }; export function validateAttrs(spec, value) { if (!isDefined(value)) { return !!spec.optional; } if (isValidatorSpecAttrs(spec)) { // If spec has ".props" it is ValidatorSpecAttrs and need to pipe back in recursively const [_, invalidKeys] = partitionObject(spec.props, (key, subSpec) => // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any validateAttrs(subSpec, value[key])); return invalidKeys.length === 0; } // extension_node parameters has no type if (!isDefined(spec.type)) { return !!spec.optional; } switch (spec.type) { case 'boolean': return isBoolean(value); case 'number': return isNumber(value) && (isDefined(spec.minimum) ? spec.minimum <= value : true) && (isDefined(spec.maximum) ? spec.maximum >= value : true); case 'integer': return isInteger(value) && (isDefined(spec.minimum) ? spec.minimum <= value : true) && (isDefined(spec.maximum) ? spec.maximum >= value : true); case 'string': const validatorFnPassed = rule => typeof value === 'string' && isDefined(validatorFnMap[rule]) && validatorFnMap[rule](value); return isString(value) && ( // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion isDefined(spec.minLength) ? spec.minLength <= value.length : true) && (isDefined(spec.validatorFn) ? validatorFnPassed(spec.validatorFn) : true) && ( // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp spec.pattern ? new RegExp(spec.pattern).test(value) : true); case 'object': return isPlainObject(value); case 'array': if (Array.isArray(value)) { const isTuple = !!spec.isTupleLike; const { minItems, maxItems } = spec; if (minItems !== undefined && value.length < minItems || maxItems !== undefined && value.length > maxItems) { return false; } if (isTuple) { // If value has fewer items than tuple has specs - we are fine with that. const numberOfItemsToCheck = Math.min(spec.items.length, value.length); return Array(numberOfItemsToCheck).fill(null).every((_, i) => validateAttrs(spec.items[i], value[i])); } else { return value.every(valueItem => // We check that at least one of the specs in the list (spec.items) matches each value from spec.items.some(itemSpec => validateAttrs(itemSpec, valueItem))); } } return false; case 'enum': return isString(value) && spec.values.indexOf(value) > -1; } } const errorMessageFor = (type, message) => `${type}: ${message}.`; const getUnsupportedOptions = spec => { if (spec && spec.props && spec.props.content) { const { allowUnsupportedBlock, allowUnsupportedInline } = spec.props.content; return { allowUnsupportedBlock, allowUnsupportedInline }; } return {}; }; const invalidChildContent = (child, errorCallback, parentSpec) => { const message = errorMessageFor(child.type, 'invalid content'); if (!errorCallback) { throw new Error(message); } else { var _parentSpec$props, _parentSpec$props$typ; return errorCallback({ ...child }, { code: 'INVALID_CONTENT', message, meta: { parentType: parentSpec === null || parentSpec === void 0 ? void 0 : (_parentSpec$props = parentSpec.props) === null || _parentSpec$props === void 0 ? void 0 : (_parentSpec$props$typ = _parentSpec$props.type) === null || _parentSpec$props$typ === void 0 ? void 0 : _parentSpec$props$typ.values[0] } }, getUnsupportedOptions(parentSpec)); } }; const unsupportedMarkContent = (errorCode, mark, errorCallback, errorMessage) => { const message = errorMessage || errorMessageFor(mark.type, 'unsupported mark'); if (!errorCallback) { throw new Error(message); } else { return errorCallback({ ...mark }, { code: errorCode, message, meta: mark }, { allowUnsupportedBlock: false, allowUnsupportedInline: false, isMark: true }); } }; const unsupportedNodeAttributesContent = (entity, errorCode, invalidAttributes, message, errorCallback) => { if (!errorCallback) { throw new Error(message); } else { return errorCallback({ type: entity.type }, { code: errorCode, message, meta: invalidAttributes }, { allowUnsupportedBlock: false, allowUnsupportedInline: false, isMark: false, isNodeAttribute: true }); } }; /** * Map of base spec names to a preferred variant spec that should be used in their place during validation. * * WARNING: The variant spec must be a strict superset of the base spec, i.e. any content valid * under the base spec must also be valid under the variant */ const variantSpecOverrides = { listItem: 'listItem_with_flexible_first_child', taskList: 'taskList_with_flexible_first_child' }; /** * Replaces base validator specs with their designated variant overrides */ const applyVariantSpecOverrides = validatorSpecs => { Object.entries(variantSpecOverrides).forEach(([base, variant]) => { const baseSpec = validatorSpecs[base]; const variantOverride = validatorSpecs[variant]; if (baseSpec !== null && baseSpec !== void 0 && baseSpec.props && variantOverride !== null && variantOverride !== void 0 && variantOverride.props && typeof baseSpec.props === 'object' && typeof variantOverride.props === 'object') { // Merge variant overrides INTO the base spec baseSpec.props = { ...baseSpec.props, // keeps type, attrs, marks, etc. ...variantOverride.props // overrides content (and anything else the variant changes) }; } }); }; export function validator(nodes, marks, options) { const validatorSpecs = createSpec(nodes, marks); if (expValEqualsNoExposure('platform_editor_flexible_list_schema', 'isEnabled', true)) { applyVariantSpecOverrides(validatorSpecs); } const { mode = 'strict', allowPrivateAttributes = false } = options || {}; const validate = (entity, errorCallback, allowed, parentSpec) => { if (!allowed) { for (const allowed of extractAllowedContent(validatorSpecs, entity)) { const validationResult = validateNode(entity, errorCallback, allowed, parentSpec); if (validationResult.valid) { return { entity: validationResult.entity, valid: validationResult.valid }; } } } // If `allowed` was provided or we haven't passed yet, return the initial result const validationResult = validateNode(entity, errorCallback, allowed, parentSpec); return { entity: validationResult.entity, valid: validationResult.valid }; }; const validateNode = (entity, errorCallback, allowed, parentSpec, isMark = false) => { const { type } = entity; const newEntity = { ...entity }; const err = (code, msg, meta) => { const message = errorMessageFor(type, msg); if (errorCallback) { return { valid: false, entity: errorCallback(newEntity, { code, message, meta }, getUnsupportedOptions(parentSpec)) }; } else { throw new Error(message); } }; if (type) { const typeOptions = getOptionsForType(type, allowed); if (typeOptions === false) { return isMark ? { valid: false } : err('INVALID_TYPE', 'type not allowed here'); } const spec = validatorSpecs[type]; if (!spec) { return err('INVALID_TYPE', `${type}: No validation spec found for type!`); } const specBasedValidationResult = specBasedValidationFor(spec, typeOptions, entity, err, newEntity, type, errorCallback, isMark); if (specBasedValidationResult.hasValidated && specBasedValidationResult.result) { return specBasedValidationResult.result; } } else { return err('INVALID_TYPE', 'ProseMirror Node/Mark should contain a `type`'); } return { valid: true, entity: newEntity }; }; return validate; function marksValidationFor(validator, entity, errorCallback, newEntity, err) { let validationResult; if (validator.props && validator.props.marks) { const marksSet = allowedMarksFor(validator); const marksValidationResult = marksAfterValidation(entity, errorCallback, marksSet, validator); validationResult = { valid: true, entity: newEntity, marksValidationOutput: marksValidationResult }; } else { validationResult = marksForEntitySpecNotSupportingMarks(entity, newEntity, errorCallback, err); } return validationResult; } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any function validatorFor(spec, typeOptions) { return { ...spec, ...typeOptions, // options with props can override props of spec ...(spec.props ? { props: { ...spec.props, ...(typeOptions['props'] || {}) } } : {}) }; } function marksAfterValidation(entity, errorCallback, marksSet, validator) { return entity.marks ? entity.marks.map(mark => { const isAKnownMark = marks ? marks.indexOf(mark.type) > -1 : true; if (mode === 'strict' && isAKnownMark) { const finalResult = validateNode(mark, errorCallback, marksSet, validator, true); const finalMark = finalResult.entity; if (finalMark) { return { valid: true, originalMark: mark, newMark: finalMark }; } // this checks for mark level attribute errors // and propagates error code and message else if (finalResult.marksValidationOutput && finalResult.marksValidationOutput.length) { return { valid: false, originalMark: mark, errorCode: finalResult.marksValidationOutput[0].errorCode, message: finalResult.marksValidationOutput[0].message }; } else { return { valid: false, originalMark: mark, errorCode: 'INVALID_TYPE' }; } } else { return { valid: false, originalMark: mark, errorCode: 'INVALID_CONTENT' }; } }) : []; } function allowedMarksFor(validator) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { items } = validator.props.marks; const marksSet = items.length ? Array.isArray(items[0]) ? items[0] : items : []; return marksSet; } function marksForEntitySpecNotSupportingMarks(prevEntity, newEntity, errorCallback, err) { const errorCode = 'REDUNDANT_MARKS'; const currentMarks = prevEntity.marks || []; const newMarks = currentMarks.map(mark => { const isUnsupportedNodeAttributeMark = mark.type === 'unsupportedNodeAttribute'; if (isUnsupportedNodeAttributeMark) { return mark; } return unsupportedMarkContent(errorCode, mark, errorCallback); }); if (newMarks.length) { newEntity.marks = newMarks; return { valid: true, entity: newEntity }; } else { return err('REDUNDANT_MARKS', 'redundant marks', { marks: Object.keys(currentMarks) }); } } function requiredPropertyValidationFor(validatorSpec, prevEntity, err) { let result = { valid: true, entity: prevEntity }; if (validatorSpec.required) { if (!validatorSpec.required.every(prop => isDefined(prevEntity[prop]))) { result = err('MISSING_PROPERTIES', 'required prop missing'); } } return result; } function textPropertyValidationFor(validatorSpec, prevEntity, err) { let result = { valid: true, entity: prevEntity }; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (validatorSpec.props.text) { if (isDefined(prevEntity.text) && // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion !validateAttrs(validatorSpec.props.text, prevEntity.text)) { result = err('INVALID_TEXT', `'text' validation failed`); } } return result; } function contentLengthValidationFor(validatorSpec, prevEntity, err) { let result = { valid: true, entity: prevEntity }; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (validatorSpec.props.content && prevEntity.content) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { minItems, maxItems } = validatorSpec.props.content; const length = prevEntity.content.length; if (isDefined(minItems) && minItems > length) { result = err('INVALID_CONTENT_LENGTH', `'content' should have more than ${minItems} child`, { length, requiredLength: minItems, type: 'minimum' }); } else if (isDefined(maxItems) && maxItems < length) { result = err('INVALID_CONTENT_LENGTH', `'content' should have less than ${maxItems} child`, { length, requiredLength: maxItems, type: 'maximum' }); } } return result; } function invalidAttributesFor(validatorSpec, prevEntity) { let invalidAttrs = []; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any let validatorAttrs = {}; if (validatorSpec.props && validatorSpec.props.attrs) { const attrOptions = makeArray(validatorSpec.props.attrs); /** * Attrs can be union type so try each path * attrs: [{ props: { url: { type: 'string' } } }, { props: { data: {} } }], * Gotcha: It will always report the last failure. */ for (let i = 0, length = attrOptions.length; i < length; ++i) { const attrOption = attrOptions[i]; if (attrOption && attrOption.props) { [, invalidAttrs] = partitionObject(attrOption.props, (key, spec) => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const valueToValidate = prevEntity.attrs[key]; return validateAttrs(spec, valueToValidate); }); } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion validatorAttrs = attrOption; if (!invalidAttrs.length) { break; } } } return { invalidAttrs, validatorAttrs }; } function attributesValidationFor(validatorSpec, prevEntity, newEntity, isMark, errorCallback) { const validatorSpecAllowsAttributes = validatorSpec.props && validatorSpec.props.attrs; if (prevEntity.attrs) { if (!validatorSpecAllowsAttributes) { if (isMark) { return handleNoAttibutesAllowedInSpecForMark(prevEntity, prevEntity.attrs); } const attrs = Object.keys(prevEntity.attrs); return handleUnsupportedNodeAttributes(prevEntity, newEntity, [], attrs, errorCallback); } const { hasUnsupportedAttrs, redundantAttrs, invalidAttrs } = validateAttributes(validatorSpec, prevEntity, prevEntity.attrs); if (hasUnsupportedAttrs) { if (isMark) { return handleUnsupportedMarkAttributes(prevEntity, invalidAttrs, redundantAttrs); } return handleUnsupportedNodeAttributes(prevEntity, newEntity, invalidAttrs, redundantAttrs, errorCallback); } } return { valid: true, entity: prevEntity }; } function validateAttributes(validatorSpec, prevEntity, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any attributes) { const invalidAttributesResult = invalidAttributesFor(validatorSpec, prevEntity); const { invalidAttrs } = invalidAttributesResult; const validatorAttrs = invalidAttributesResult.validatorAttrs; const attrs = Object.keys(attributes).filter(k => !(allowPrivateAttributes && k.startsWith('__'))); // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const redundantAttrs = attrs.filter(a => !validatorAttrs.props[a]); const hasRedundantAttrs = redundantAttrs.length > 0; const hasUnsupportedAttrs = invalidAttrs.length || hasRedundantAttrs; return { hasUnsupportedAttrs, invalidAttrs, redundantAttrs }; } function handleUnsupportedNodeAttributes(prevEntity, newEntity, invalidAttrs, redundantAttrs, errorCallback) { const attr = invalidAttrs.concat(redundantAttrs); let result = { valid: true, entity: prevEntity }; const message = errorMessageFor(prevEntity.type, `'attrs' validation failed`); const errorCode = 'UNSUPPORTED_ATTRIBUTES'; newEntity.marks = wrapUnSupportedNodeAttributes(prevEntity, newEntity, attr, errorCode, message, errorCallback); result = { valid: true, entity: newEntity }; return result; } function handleUnsupportedMarkAttributes(prevEntity, invalidAttrs, redundantAttrs) { let errorCode = 'INVALID_ATTRIBUTES'; let message = errorMessageFor(prevEntity.type, `'attrs' validation failed`); const hasRedundantAttrs = redundantAttrs.length; const hasBothInvalidAndRedundantAttrs = hasRedundantAttrs && invalidAttrs.length; if (!hasBothInvalidAndRedundantAttrs && hasRedundantAttrs) { errorCode = 'REDUNDANT_ATTRIBUTES'; message = errorMessageFor('redundant attributes found', redundantAttrs.join(', ')); } const markValidationResult = { valid: true, originalMark: prevEntity, errorCode: errorCode, message: message }; return { valid: false, marksValidationOutput: [markValidationResult] }; } function handleNoAttibutesAllowedInSpecForMark(prevEntity, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any attributes) { const message = errorMessageFor('redundant attributes found', Object.keys(attributes).join(', ')); const errorCode = 'REDUNDANT_ATTRIBUTES'; const markValidationResult = { valid: true, originalMark: prevEntity, errorCode: errorCode, message: message }; return { valid: false, marksValidationOutput: [markValidationResult] }; } function wrapUnSupportedNodeAttributes(prevEntity, newEntity, invalidAttrs, errorCode, message, errorCallback) { const invalidValues = {}; // eslint-disable-next-line guard-for-in for (const invalidAttr in invalidAttrs) { invalidValues[invalidAttrs[invalidAttr]] = prevEntity.attrs && prevEntity.attrs[invalidAttrs[invalidAttr]]; if (newEntity.attrs) { delete newEntity.attrs[invalidAttrs[invalidAttr]]; } } const unsupportedNodeAttributeValues = unsupportedNodeAttributesContent(prevEntity, errorCode, invalidValues, message, errorCallback); const finalEntity = { ...newEntity }; if (finalEntity.marks) { if (!unsupportedNodeAttributeValues) { return finalEntity.marks; } // If there is an existing unsupported node attribute mark, overwrite it to avoid duplicate marks const existingMark = finalEntity.marks.find(mark => mark.type === unsupportedNodeAttributeValues.type); if (existingMark) { existingMark.attrs = unsupportedNodeAttributeValues.attrs; } else { finalEntity.marks.push(unsupportedNodeAttributeValues); } return finalEntity.marks; } else { return [unsupportedNodeAttributeValues]; } } function extraPropsValidationFor(validatorSpec, prevEntity, err, newEntity, type) { const result = { valid: true, entity: prevEntity }; const [requiredProps, redundantProps] = partitionObject(prevEntity, k => // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any isDefined(validatorSpec.props[k])); if (redundantProps.length) { if (mode === 'loose') { newEntity = { type }; requiredProps.reduce((acc, p) => copy(prevEntity, acc, p), newEntity); } else { if (!((redundantProps.indexOf('marks') > -1 || redundantProps.indexOf('attrs') > -1) && redundantProps.length === 1)) { return err('REDUNDANT_PROPERTIES', `redundant props found: ${redundantProps.join(', ')}`, { props: redundantProps }); } } } return result; } function specBasedValidationFor(spec, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any typeOptions, prevEntity, err, newEntity, type, errorCallback, isMark) { const specBasedValidationResult = { hasValidated: false }; const validatorSpec = validatorFor(spec, typeOptions); if (!validatorSpec) { return specBasedValidationResult; } // Required Props // For array format where `required` is an array const requiredPropertyValidatonResult = requiredPropertyValidationFor(validatorSpec, prevEntity, err); if (!requiredPropertyValidatonResult.valid) { return { hasValidated: true, result: requiredPropertyValidatonResult }; } if (!validatorSpec.props) { const props = Object.keys(prevEntity); // If there's no validator.props then there shouldn't be any key except `type` if (props.length > 1) { return { hasValidated: true, result: err('REDUNDANT_PROPERTIES', `redundant props found: ${Object.keys(prevEntity).join(', ')}`, { props }) }; } return specBasedValidationResult; } // Check text const textPropertyValidationResult = textPropertyValidationFor(validatorSpec, prevEntity, err); if (!textPropertyValidationResult.valid) { return { hasValidated: true, result: textPropertyValidationResult }; } // Content Length const contentLengthValidationResult = contentLengthValidationFor(validatorSpec, prevEntity, err); if (!contentLengthValidationResult.valid) { return { hasValidated: true, result: contentLengthValidationResult }; } // Required Props // For object format based on `optional` property const [, missingProps] = partitionObject(validatorSpec.props, (k, v) => { var _validatorSpec$requir; // if the validator is an array, then check // if the `required` field contains the key. const isOptional = Array.isArray(v) ? !((_validatorSpec$requir = validatorSpec.required) !== null && _validatorSpec$requir !== void 0 && _validatorSpec$requir.includes(k)) : typeof v === 'object' && v !== null && 'optional' in v ? v.optional : false; return isOptional || isDefined(prevEntity[k]); }); if (missingProps.length) { return { hasValidated: true, result: err('MISSING_PROPERTIES', 'required prop missing', { props: missingProps }) }; } const attributesValidationResult = attributesValidationFor(validatorSpec, prevEntity, newEntity, isMark, errorCallback); if (!attributesValidationResult.valid) { return { hasValidated: true, result: attributesValidationResult }; } if (isMark && attributesValidationResult.valid) { return { hasValidated: true, result: attributesValidationResult }; } const extraPropsValidationResult = extraPropsValidationFor(validatorSpec, prevEntity, err, newEntity, type); if (!extraPropsValidationResult.valid) { return { hasValidated: true, result: extraPropsValidationResult }; } // Children if (validatorSpec.props.content) { const contentValidatorSpec = validatorSpec.props.content; if (prevEntity.content) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const validateChildNode = (child, index) => { if (child === undefined) { return child; } const validateChildMarks = (childEntity, marksValidationOutput, errorCallback, isLastValidationSpec, isParentTupleLike = false) => { let marksAreValid = true; if (childEntity && childEntity.marks && marksValidationOutput) { const validMarks = marksValidationOutput.filter(mark => mark.valid); // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const finalMarks = marksValidationOutput.map(mr => { if (mr.valid) { return mr.newMark; } else { if (validMarks.length || isLastValidationSpec || isParentTupleLike || mr.errorCode === 'INVALID_TYPE' || mr.errorCode === 'INVALID_CONTENT' || mr.errorCode === 'REDUNDANT_ATTRIBUTES' || mr.errorCode === 'INVALID_ATTRIBUTES') { return unsupportedMarkContent( // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion mr.errorCode, mr.originalMark, errorCallback, mr.message); } return; } }).filter(Boolean); if (finalMarks.length) { childEntity.marks = finalMarks; } else { delete childEntity.marks; marksAreValid = false; } } return { valid: marksAreValid, entity: childEntity }; }; const hasMultipleCombinationOfContentAllowed = !!contentValidatorSpec.isTupleLike; if (hasMultipleCombinationOfContentAllowed) { const { entity: newChildEntity, marksValidationOutput } = validateNode(child, errorCallback, makeArray(contentValidatorSpec.items[index] || contentValidatorSpec.items[contentValidatorSpec.items.length - 1]), validatorSpec); const { entity } = validateChildMarks(newChildEntity, marksValidationOutput, errorCallback, false, true); return entity; } // Only go inside valid branch const allowedSpecsForEntity = contentValidatorSpec.items.filter(item => Array.isArray(item) ? item.some( // [p, hr, ...] or [p, [text, {}], ...] spec => (Array.isArray(spec) ? spec[0] : spec) === child.type) : true); if (allowedSpecsForEntity.length) { if (allowedSpecsForEntity.length > 1) { throw new Error('Consider using Tuple instead!'); } const maybeArray = makeArray(allowedSpecsForEntity[0]); const allowedSpecsForChild = maybeArray.filter(item => (Array.isArray(item) ? item[0] : item) === child.type); if (allowedSpecsForChild.length === 0) { return invalidChildContent(child, errorCallback, validatorSpec); } /** * When there's multiple possible branches try all of them. * If all of them fails, throw the first one. * e.g.- [['text', { marks: ['a'] }], ['text', { marks: ['b'] }]] */ let firstError; let firstChild; for (let i = 0, len = allowedSpecsForChild.length; i < len; i++) { try { const allowedValueForCurrentSpec = [allowedSpecsForChild[i]]; const { valid, entity: newChildEntity, marksValidationOutput } = validateNode(child, errorCallback, allowedValueForCurrentSpec, validatorSpec); if (valid) { const isLastValidationSpec = i === allowedSpecsForChild.length - 1; const { valid: marksAreValid, entity } = validateChildMarks(newChildEntity, marksValidationOutput, errorCallback, isLastValidationSpec); const unsupportedMarks = entity && entity.marks && entity.marks.filter(mark => mark.type === 'unsupportedMark') || []; if (marksAreValid && !unsupportedMarks.length) { return entity; } else { firstChild = firstChild || newChildEntity; } } else { firstChild = firstChild || newChildEntity; } } catch (error) { firstError = firstError || error; } } if (!errorCallback) { throw firstError; } else { return firstChild; } } else { return invalidChildContent(child, errorCallback, validatorSpec); } }; newEntity.content = prevEntity.content.map(validateChildNode).filter(Boolean); } else if (!contentValidatorSpec.optional) { return { hasValidated: true, result: err('MISSING_PROPERTIES', 'missing `content` prop') }; } } // Marks if (prevEntity.marks) { return { hasValidated: true, result: marksValidationFor(validatorSpec, prevEntity, errorCallback, newEntity, err) }; } return specBasedValidationResult; } }