@atlaskit/adf-utils
Version:
Set of utilities to traverse, modify and create ADF documents.
983 lines (966 loc) • 36.9 kB
JavaScript
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;
}
}