@atlaskit/adf-schema-generator
Advanced tools
Comparing version 1.17.4 to 1.17.5
# @atlaskit/adf-schema-generator | ||
## 1.17.4 | ||
## 1.17.5 | ||
### Patch Changes | ||
- 9789fe6: Alphabetically sort imports in Generated PM Spec files | ||
- 2a8c66a: Add allOf field to JSON transformer from ADF DSL |
@@ -73,2 +73,12 @@ "use strict"; | ||
/** | ||
* If true, the node doesn't allow marks to be set. | ||
* This is stricter than simply having an empty marks list. | ||
*/ | ||
}, { | ||
key: "hasNoMarks", | ||
value: function hasNoMarks() { | ||
return _classPrivateFieldGet(_spec, this).noMarks; | ||
} | ||
/** | ||
* Define a node. | ||
@@ -81,3 +91,3 @@ * | ||
value: function define(spec) { | ||
var _spec$marks$map, _spec$marks; | ||
var _spec$marks, _spec$marks$map, _spec$marks2; | ||
if (_classPrivateFieldGet(_spec, this)) { | ||
@@ -87,3 +97,6 @@ throw new Error('Cannot re-define a node'); | ||
_classPrivateFieldSet(_spec, this, spec); | ||
_classPrivateFieldSet(_marks, this, (_spec$marks$map = (_spec$marks = spec.marks) === null || _spec$marks === void 0 ? void 0 : _spec$marks.map(function (mark) { | ||
if (spec.noMarks && ((_spec$marks = spec.marks) === null || _spec$marks === void 0 ? void 0 : _spec$marks.length) > 0) { | ||
throw new Error('Node with noMarks true has marks'); | ||
} | ||
_classPrivateFieldSet(_marks, this, (_spec$marks$map = (_spec$marks2 = spec.marks) === null || _spec$marks2 === void 0 ? void 0 : _spec$marks2.map(function (mark) { | ||
return mark.getType(); | ||
@@ -90,0 +103,0 @@ })) !== null && _spec$marks$map !== void 0 ? _spec$marks$map : []); |
@@ -8,59 +8,95 @@ "use strict"; | ||
exports.buildContent = buildContent; | ||
exports.isADFNode = isADFNode; | ||
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); | ||
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); | ||
var _uniqBy = _interopRequireDefault(require("lodash/uniqBy")); | ||
var _adfNode = require("../../adfNode"); | ||
var _transformerNames = require("../transformerNames"); | ||
var _inconsistentNameResolver = require("./inconsistentNameResolver"); | ||
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } | ||
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } | ||
function buildContent(content, name) { | ||
function buildContent(content, name, adfNodeContent) { | ||
if (content.length === 0) return {}; | ||
var minItems = determineMinItems(content, name); | ||
var items = determineItems(content, name); | ||
var items = determineItems(content, name, adfNodeContent); | ||
var maxItems = determineMaxItems(content, adfNodeContent); | ||
return { | ||
content: _objectSpread({ | ||
content: _objectSpread(_objectSpread({ | ||
type: 'array', | ||
items: items | ||
}, minItems) | ||
}, minItems !== null ? { | ||
minItems: minItems | ||
} : {}), maxItems !== null ? { | ||
maxItems: maxItems | ||
} : {}) | ||
}; | ||
} | ||
function determineItems(content, name) { | ||
// These are arrays instead of objects in the JSON Schema | ||
if (name === 'listItem' || name === 'taskList') { | ||
return handleListContent(content); | ||
} | ||
// TODO: codeblock is insane | ||
function isADFGroup(value) { | ||
return value && 'group' in value; | ||
} | ||
function isADFNode(value) { | ||
return value && value instanceof _adfNode.ADFNode; | ||
} | ||
function determineItems(content, name, adfNodeContent) { | ||
// TODO: codeblock has a variant pattern instead of a normal node pattern | ||
// This is probably an expanded reference to the inline code node, a variant of text node | ||
// To remove this exception, we must update the JSON Schema to use a reference here instead | ||
if (name === 'codeBlock') { | ||
return {}; | ||
return { | ||
allOf: [{ | ||
$ref: '#/definitions/text_node' | ||
}, { | ||
properties: { | ||
marks: { | ||
maxItems: 0, | ||
type: 'array' | ||
} | ||
}, | ||
type: 'object' | ||
}] | ||
}; | ||
} | ||
// If there is one piece of content, defined as $zeroPlus($or([content])) or $onePlus($or([content])), then it's a single $ref in an object | ||
if (content.length === 1 && content[0].contentTypes.length === 1) { | ||
if (name === 'doc') { | ||
var jsonSchema = adfNodeContent.reduce(function (acc, value) { | ||
// We're expecting a single one+ with a nested $or | ||
if (value.type === '$one+' && value.content.type === '$or') { | ||
return [].concat((0, _toConsumableArray2.default)(acc), (0, _toConsumableArray2.default)(flattenContent(value.content.content).map(function (node) { | ||
return { | ||
$ref: "#/definitions/".concat((0, _inconsistentNameResolver.resolveName)(node.getName())) | ||
}; | ||
}))); | ||
} | ||
return acc; | ||
}, []); | ||
return { | ||
$ref: "#/definitions/".concat((0, _inconsistentNameResolver.resolveName)(content[0].contentTypes[0])) | ||
anyOf: (0, _uniqBy.default)(jsonSchema, function (v) { | ||
return v.$ref; | ||
}) | ||
}; | ||
} | ||
if (content.length === 1 && content[0].contentTypes.length > 1) { | ||
return handleObjectContent(content[0]); | ||
} | ||
return {}; | ||
var processedContentGroups = processContentGroups(content); | ||
// The JSON schema omits the array if there is only 1 item | ||
return flattenArray(processedContentGroups); | ||
} | ||
function handleObjectContent(content) { | ||
var itemsArray = content.contentTypes.map(function (piece) { | ||
function processContentTypes(contentTypes) { | ||
var itemsArray = []; | ||
contentTypes.forEach(function (piece) { | ||
itemsArray.push({ | ||
$ref: "#/definitions/".concat((0, _inconsistentNameResolver.resolveName)(piece)) | ||
}); | ||
}); | ||
// We flatten an array here as well, but using anyOf to fit the JSON schema | ||
if (itemsArray.length === 1) { | ||
return itemsArray[0]; | ||
} else { | ||
return { | ||
$ref: "#/definitions/".concat((0, _inconsistentNameResolver.resolveName)(piece)) | ||
anyOf: itemsArray | ||
}; | ||
}); | ||
return { | ||
anyOf: itemsArray | ||
}; | ||
} | ||
} | ||
function handleListContent(content) { | ||
function processContentGroups(content) { | ||
var contentArray = []; | ||
content.forEach(function (item) { | ||
if (item.contentTypes.length === 1) { | ||
contentArray.push({ | ||
$ref: "#/definitions/".concat((0, _inconsistentNameResolver.resolveName)(item.contentTypes[0])) | ||
}); | ||
} else if (item.contentTypes.length > 1) { | ||
contentArray.push(handleObjectContent(item)); | ||
} | ||
contentArray.push(processContentTypes(item.contentTypes)); | ||
}); | ||
@@ -70,18 +106,79 @@ return contentArray; | ||
function determineMinItems(content, name) { | ||
var minItems; | ||
content.forEach(function (value) { | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if (value.minItems === 1 && name !== 'tableRow' && name !== 'doc') { | ||
minItems = { | ||
minItems: 1 | ||
}; | ||
var _content$find, _content$find2; | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one minItem value for all content on a node. | ||
if (!content) return null; | ||
var _ref = (_content$find = content.find(function (v) { | ||
return !isNaN(v.minItems); | ||
})) !== null && _content$find !== void 0 ? _content$find : {}, | ||
minItems = _ref.minItems; | ||
var _ref2 = (_content$find2 = content.find(function (v) { | ||
return v.range; | ||
})) !== null && _content$find2 !== void 0 ? _content$find2 : {}, | ||
range = _ref2.range; | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if (minItems === 1 && name !== 'tableRow' && name !== 'doc' && name !== 'layoutSection') { | ||
return minItems; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
if (minItems === 0 && name === 'caption') { | ||
return minItems; | ||
} | ||
// If it's a range, take minItems from that | ||
if (range) { | ||
return range.min; | ||
} | ||
return null; | ||
} | ||
// This function is not comprehensive, it is only defined for certain inputs | ||
function determineMaxItems(content, adfNodeContent) { | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one maxItem value for all content on a node. | ||
// If there's only one item, we can simply calculate it and return | ||
if (content.length === 1) { | ||
// If it's a range, grab maxItems from that | ||
if (content[0].range) { | ||
return content[0].range.max; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
else if (name === 'caption' && value.minItems === 0) { | ||
minItems = { | ||
minItems: 0 | ||
}; | ||
} else if (adfNodeContent.length > 1) { | ||
var types = adfNodeContent.map(function (v) { | ||
return v.type; | ||
}); | ||
if (types.includes('$one+') || types.includes('$zero+')) { | ||
return null; | ||
} | ||
}); | ||
return minItems; | ||
var maxItems = types.filter(function (v) { | ||
return v === '$or'; | ||
}).length; | ||
return maxItems || null; | ||
} | ||
return null; | ||
} | ||
function flattenArray(array) { | ||
if (array.length === 1) { | ||
return array[0]; | ||
} else { | ||
return array; | ||
} | ||
} | ||
/** | ||
* Flattens ADF groups and nodes into an array of nodes. | ||
* @param content | ||
* @returns ADFNode[] | ||
*/ | ||
function flattenContent(content) { | ||
return content.reduce(function (acc, item) { | ||
if (isADFGroup(item)) { | ||
// Expand the group into its member nodes | ||
return [].concat((0, _toConsumableArray2.default)(acc), (0, _toConsumableArray2.default)(flattenContent(item.members))); | ||
} else if (isADFNode(item) && !item.isIgnored(_transformerNames.JSONSchemaTransformerName)) { | ||
return [].concat((0, _toConsumableArray2.default)(acc), [item]); | ||
} | ||
return acc; | ||
}, []); | ||
} |
@@ -13,2 +13,3 @@ "use strict"; | ||
var _contentBuilder = require("./contentBuilder"); | ||
var _inconsistentNameResolver = require("./inconsistentNameResolver"); | ||
var _requiredBuilder = require("./requiredBuilder"); | ||
@@ -26,3 +27,3 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } | ||
}).filter(Boolean)) || []; | ||
var marks = buildNodeMarks(nodeMarks); | ||
var marks = buildNodeMarks(nodeMarks, node.hasNoMarks()); | ||
var attrs = (0, _attrBuilder.buildAttrs)(node.getSpec().attrs); | ||
@@ -36,3 +37,3 @@ // Strangely, text has this extra property that looks like an attribute | ||
} : {}; | ||
var jsonContent = (0, _contentBuilder.buildContent)(content, node.getName()); | ||
var jsonContent = (0, _contentBuilder.buildContent)(content, node.getName(), node.getSpec().content); | ||
var version = node.getSpec().version; | ||
@@ -55,6 +56,31 @@ var jsonVersion = version ? { | ||
json.required = required; | ||
} else { | ||
var _required = (0, _requiredBuilder.buildVariantRequired)(content); | ||
var allOfItem = _objectSpread({ | ||
type: 'object', | ||
properties: _objectSpread(_objectSpread({}, marks), jsonContent) | ||
}, _required); | ||
if (node.getName() === 'layoutSection_full') { | ||
// Only on the layoutSection_full variant we specify type | ||
allOfItem.properties.type = { | ||
enum: ['layoutSection'] | ||
}; | ||
allOfItem.required = ['type', 'content']; | ||
allOfItem.additionalProperties = false; | ||
} | ||
json.allOf = [{ | ||
$ref: "#/definitions/".concat((0, _inconsistentNameResolver.resolveName)(node.getBase().getName())) | ||
}, allOfItem]; | ||
} | ||
return json; | ||
}; | ||
function buildNodeMarks(nodeMarks) { | ||
function buildNodeMarks(nodeMarks, hasNoMarks) { | ||
if (hasNoMarks) { | ||
return { | ||
marks: { | ||
type: 'array', | ||
maxItems: 0 | ||
} | ||
}; | ||
} | ||
if (nodeMarks.length === 0) return {}; | ||
@@ -61,0 +87,0 @@ if (nodeMarks.length === 1) return { |
@@ -7,2 +7,3 @@ "use strict"; | ||
exports.buildRequired = buildRequired; | ||
exports.buildVariantRequired = buildVariantRequired; | ||
var _isAnyOf = require("../../utils/isAnyOf"); | ||
@@ -57,2 +58,11 @@ function buildRequired(attrs, hasContent, name) { | ||
return required; | ||
} | ||
function buildVariantRequired(content) { | ||
var _content$, _content$2, _content$2$range; | ||
if (((_content$ = content[0]) === null || _content$ === void 0 ? void 0 : _content$.minItems) >= 1 || Boolean((_content$2 = content[0]) === null || _content$2 === void 0 ? void 0 : (_content$2$range = _content$2.range) === null || _content$2$range === void 0 ? void 0 : _content$2$range.type)) { | ||
return { | ||
required: ['content'] | ||
}; | ||
} | ||
return {}; | ||
} |
@@ -33,3 +33,5 @@ "use strict"; | ||
if (_node.isIgnored(_transformerNames.PMSpecTransformerName)) { | ||
return; | ||
return { | ||
node: _node | ||
}; | ||
} | ||
@@ -56,8 +58,12 @@ var marks = (_node$getSpec$marks = _node.getSpec().marks) !== null && _node$getSpec$marks !== void 0 ? _node$getSpec$marks : []; | ||
group: function group(_group, members) { | ||
nodeGroupMap[_group.group] = _group.members.map(function (m) { | ||
return m.getName(); | ||
nodeGroupMap[_group.group] = _group.members.filter(function (node) { | ||
return !node.isIgnored(_transformerNames.PMSpecTransformerName); | ||
}).map(function (node) { | ||
return node.getName(); | ||
}); | ||
return { | ||
group: _group.group, | ||
members: members | ||
members: members.filter(function (m) { | ||
return !m.node.isIgnored(_transformerNames.PMSpecTransformerName); | ||
}) | ||
}; | ||
@@ -64,0 +70,0 @@ }, |
@@ -53,2 +53,10 @@ function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); } | ||
/** | ||
* If true, the node doesn't allow marks to be set. | ||
* This is stricter than simply having an empty marks list. | ||
*/ | ||
hasNoMarks() { | ||
return _classPrivateFieldGet(_spec, this).noMarks; | ||
} | ||
/** | ||
* Define a node. | ||
@@ -59,3 +67,3 @@ * | ||
define(spec) { | ||
var _spec$marks$map, _spec$marks; | ||
var _spec$marks, _spec$marks$map, _spec$marks2; | ||
if (_classPrivateFieldGet(_spec, this)) { | ||
@@ -65,3 +73,6 @@ throw new Error('Cannot re-define a node'); | ||
_classPrivateFieldSet(_spec, this, spec); | ||
_classPrivateFieldSet(_marks, this, (_spec$marks$map = (_spec$marks = spec.marks) === null || _spec$marks === void 0 ? void 0 : _spec$marks.map(mark => mark.getType())) !== null && _spec$marks$map !== void 0 ? _spec$marks$map : []); | ||
if (spec.noMarks && ((_spec$marks = spec.marks) === null || _spec$marks === void 0 ? void 0 : _spec$marks.length) > 0) { | ||
throw new Error('Node with noMarks true has marks'); | ||
} | ||
_classPrivateFieldSet(_marks, this, (_spec$marks$map = (_spec$marks2 = spec.marks) === null || _spec$marks2 === void 0 ? void 0 : _spec$marks2.map(mark => mark.getType())) !== null && _spec$marks$map !== void 0 ? _spec$marks$map : []); | ||
return this; | ||
@@ -68,0 +79,0 @@ } |
@@ -0,6 +1,10 @@ | ||
import uniqBy from 'lodash/uniqBy'; | ||
import { ADFNode } from '../../adfNode'; | ||
import { JSONSchemaTransformerName } from '../transformerNames'; | ||
import { resolveName } from './inconsistentNameResolver'; | ||
export function buildContent(content, name) { | ||
export function buildContent(content, name, adfNodeContent) { | ||
if (content.length === 0) return {}; | ||
const minItems = determineMinItems(content, name); | ||
const items = determineItems(content, name); | ||
const items = determineItems(content, name, adfNodeContent); | ||
const maxItems = determineMaxItems(content, adfNodeContent); | ||
return { | ||
@@ -10,48 +14,75 @@ content: { | ||
items, | ||
...minItems | ||
...(minItems !== null ? { | ||
minItems | ||
} : {}), | ||
...(maxItems !== null ? { | ||
maxItems | ||
} : {}) | ||
} | ||
}; | ||
} | ||
function determineItems(content, name) { | ||
// These are arrays instead of objects in the JSON Schema | ||
if (name === 'listItem' || name === 'taskList') { | ||
return handleListContent(content); | ||
} | ||
// TODO: codeblock is insane | ||
function isADFGroup(value) { | ||
return value && 'group' in value; | ||
} | ||
export function isADFNode(value) { | ||
return value && value instanceof ADFNode; | ||
} | ||
function determineItems(content, name, adfNodeContent) { | ||
// TODO: codeblock has a variant pattern instead of a normal node pattern | ||
// This is probably an expanded reference to the inline code node, a variant of text node | ||
// To remove this exception, we must update the JSON Schema to use a reference here instead | ||
if (name === 'codeBlock') { | ||
return {}; | ||
return { | ||
allOf: [{ | ||
$ref: '#/definitions/text_node' | ||
}, { | ||
properties: { | ||
marks: { | ||
maxItems: 0, | ||
type: 'array' | ||
} | ||
}, | ||
type: 'object' | ||
}] | ||
}; | ||
} | ||
// If there is one piece of content, defined as $zeroPlus($or([content])) or $onePlus($or([content])), then it's a single $ref in an object | ||
if (content.length === 1 && content[0].contentTypes.length === 1) { | ||
if (name === 'doc') { | ||
const jsonSchema = adfNodeContent.reduce((acc, value) => { | ||
// We're expecting a single one+ with a nested $or | ||
if (value.type === '$one+' && value.content.type === '$or') { | ||
return [...acc, ...flattenContent(value.content.content).map(node => ({ | ||
$ref: `#/definitions/${resolveName(node.getName())}` | ||
}))]; | ||
} | ||
return acc; | ||
}, []); | ||
return { | ||
$ref: `#/definitions/${resolveName(content[0].contentTypes[0])}` | ||
anyOf: uniqBy(jsonSchema, v => v.$ref) | ||
}; | ||
} | ||
if (content.length === 1 && content[0].contentTypes.length > 1) { | ||
return handleObjectContent(content[0]); | ||
} | ||
return {}; | ||
const processedContentGroups = processContentGroups(content); | ||
// The JSON schema omits the array if there is only 1 item | ||
return flattenArray(processedContentGroups); | ||
} | ||
function handleObjectContent(content) { | ||
const itemsArray = content.contentTypes.map(piece => { | ||
function processContentTypes(contentTypes) { | ||
const itemsArray = []; | ||
contentTypes.forEach(piece => { | ||
itemsArray.push({ | ||
$ref: `#/definitions/${resolveName(piece)}` | ||
}); | ||
}); | ||
// We flatten an array here as well, but using anyOf to fit the JSON schema | ||
if (itemsArray.length === 1) { | ||
return itemsArray[0]; | ||
} else { | ||
return { | ||
$ref: `#/definitions/${resolveName(piece)}` | ||
anyOf: itemsArray | ||
}; | ||
}); | ||
return { | ||
anyOf: itemsArray | ||
}; | ||
} | ||
} | ||
function handleListContent(content) { | ||
function processContentGroups(content) { | ||
const contentArray = []; | ||
content.forEach(item => { | ||
if (item.contentTypes.length === 1) { | ||
contentArray.push({ | ||
$ref: `#/definitions/${resolveName(item.contentTypes[0])}` | ||
}); | ||
} else if (item.contentTypes.length > 1) { | ||
contentArray.push(handleObjectContent(item)); | ||
} | ||
contentArray.push(processContentTypes(item.contentTypes)); | ||
}); | ||
@@ -61,18 +92,73 @@ return contentArray; | ||
function determineMinItems(content, name) { | ||
let minItems; | ||
content.forEach(value => { | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if (value.minItems === 1 && name !== 'tableRow' && name !== 'doc') { | ||
minItems = { | ||
minItems: 1 | ||
}; | ||
var _content$find, _content$find2; | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one minItem value for all content on a node. | ||
if (!content) return null; | ||
const { | ||
minItems | ||
} = (_content$find = content.find(v => !isNaN(v.minItems))) !== null && _content$find !== void 0 ? _content$find : {}; | ||
const { | ||
range | ||
} = (_content$find2 = content.find(v => v.range)) !== null && _content$find2 !== void 0 ? _content$find2 : {}; | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if (minItems === 1 && name !== 'tableRow' && name !== 'doc' && name !== 'layoutSection') { | ||
return minItems; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
if (minItems === 0 && name === 'caption') { | ||
return minItems; | ||
} | ||
// If it's a range, take minItems from that | ||
if (range) { | ||
return range.min; | ||
} | ||
return null; | ||
} | ||
// This function is not comprehensive, it is only defined for certain inputs | ||
function determineMaxItems(content, adfNodeContent) { | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one maxItem value for all content on a node. | ||
// If there's only one item, we can simply calculate it and return | ||
if (content.length === 1) { | ||
// If it's a range, grab maxItems from that | ||
if (content[0].range) { | ||
return content[0].range.max; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
else if (name === 'caption' && value.minItems === 0) { | ||
minItems = { | ||
minItems: 0 | ||
}; | ||
} else if (adfNodeContent.length > 1) { | ||
const types = adfNodeContent.map(v => v.type); | ||
if (types.includes('$one+') || types.includes('$zero+')) { | ||
return null; | ||
} | ||
}); | ||
return minItems; | ||
const maxItems = types.filter(v => v === '$or').length; | ||
return maxItems || null; | ||
} | ||
return null; | ||
} | ||
function flattenArray(array) { | ||
if (array.length === 1) { | ||
return array[0]; | ||
} else { | ||
return array; | ||
} | ||
} | ||
/** | ||
* Flattens ADF groups and nodes into an array of nodes. | ||
* @param content | ||
* @returns ADFNode[] | ||
*/ | ||
function flattenContent(content) { | ||
return content.reduce((acc, item) => { | ||
if (isADFGroup(item)) { | ||
// Expand the group into its member nodes | ||
return [...acc, ...flattenContent(item.members)]; | ||
} else if (isADFNode(item) && !item.isIgnored(JSONSchemaTransformerName)) { | ||
return [...acc, item]; | ||
} | ||
return acc; | ||
}, []); | ||
} |
@@ -5,3 +5,4 @@ import { JSONSchemaTransformerName } from '../transformerNames'; | ||
import { buildContent } from './contentBuilder'; | ||
import { buildRequired } from './requiredBuilder'; | ||
import { resolveName } from './inconsistentNameResolver'; | ||
import { buildRequired, buildVariantRequired } from './requiredBuilder'; | ||
export const buildNode = (node, content) => { | ||
@@ -16,3 +17,3 @@ var _node$getSpec$marks, _content$, _content$2, _content$2$range; | ||
}).filter(Boolean)) || []; | ||
const marks = buildNodeMarks(nodeMarks); | ||
const marks = buildNodeMarks(nodeMarks, node.hasNoMarks()); | ||
const attrs = buildAttrs(node.getSpec().attrs); | ||
@@ -26,3 +27,3 @@ // Strangely, text has this extra property that looks like an attribute | ||
} : {}; | ||
const jsonContent = buildContent(content, node.getName()); | ||
const jsonContent = buildContent(content, node.getName(), node.getSpec().content); | ||
const version = node.getSpec().version; | ||
@@ -50,6 +51,35 @@ const jsonVersion = version ? { | ||
json.required = required; | ||
} else { | ||
const required = buildVariantRequired(content); | ||
const allOfItem = { | ||
type: 'object', | ||
properties: { | ||
...marks, | ||
...jsonContent | ||
}, | ||
...required | ||
}; | ||
if (node.getName() === 'layoutSection_full') { | ||
// Only on the layoutSection_full variant we specify type | ||
allOfItem.properties.type = { | ||
enum: ['layoutSection'] | ||
}; | ||
allOfItem.required = ['type', 'content']; | ||
allOfItem.additionalProperties = false; | ||
} | ||
json.allOf = [{ | ||
$ref: `#/definitions/${resolveName(node.getBase().getName())}` | ||
}, allOfItem]; | ||
} | ||
return json; | ||
}; | ||
function buildNodeMarks(nodeMarks) { | ||
function buildNodeMarks(nodeMarks, hasNoMarks) { | ||
if (hasNoMarks) { | ||
return { | ||
marks: { | ||
type: 'array', | ||
maxItems: 0 | ||
} | ||
}; | ||
} | ||
if (nodeMarks.length === 0) return {}; | ||
@@ -56,0 +86,0 @@ if (nodeMarks.length === 1) return { |
@@ -50,2 +50,11 @@ import { isAnyOf } from '../../utils/isAnyOf'; | ||
return required; | ||
} | ||
export function buildVariantRequired(content) { | ||
var _content$, _content$2, _content$2$range; | ||
if (((_content$ = content[0]) === null || _content$ === void 0 ? void 0 : _content$.minItems) >= 1 || Boolean((_content$2 = content[0]) === null || _content$2 === void 0 ? void 0 : (_content$2$range = _content$2.range) === null || _content$2$range === void 0 ? void 0 : _content$2$range.type)) { | ||
return { | ||
required: ['content'] | ||
}; | ||
} | ||
return {}; | ||
} |
@@ -23,3 +23,5 @@ import { traverse } from '../../traverse'; | ||
if (node.isIgnored(PMSpecTransformerName)) { | ||
return; | ||
return { | ||
node | ||
}; | ||
} | ||
@@ -46,6 +48,6 @@ const marks = (_node$getSpec$marks = node.getSpec().marks) !== null && _node$getSpec$marks !== void 0 ? _node$getSpec$marks : []; | ||
group: (group, members) => { | ||
nodeGroupMap[group.group] = group.members.map(m => m.getName()); | ||
nodeGroupMap[group.group] = group.members.filter(node => !node.isIgnored(PMSpecTransformerName)).map(node => node.getName()); | ||
return { | ||
group: group.group, | ||
members | ||
members: members.filter(m => !m.node.isIgnored(PMSpecTransformerName)) | ||
}; | ||
@@ -52,0 +54,0 @@ }, |
@@ -65,2 +65,12 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty"; | ||
/** | ||
* If true, the node doesn't allow marks to be set. | ||
* This is stricter than simply having an empty marks list. | ||
*/ | ||
}, { | ||
key: "hasNoMarks", | ||
value: function hasNoMarks() { | ||
return _classPrivateFieldGet(_spec, this).noMarks; | ||
} | ||
/** | ||
* Define a node. | ||
@@ -73,3 +83,3 @@ * | ||
value: function define(spec) { | ||
var _spec$marks$map, _spec$marks; | ||
var _spec$marks, _spec$marks$map, _spec$marks2; | ||
if (_classPrivateFieldGet(_spec, this)) { | ||
@@ -79,3 +89,6 @@ throw new Error('Cannot re-define a node'); | ||
_classPrivateFieldSet(_spec, this, spec); | ||
_classPrivateFieldSet(_marks, this, (_spec$marks$map = (_spec$marks = spec.marks) === null || _spec$marks === void 0 ? void 0 : _spec$marks.map(function (mark) { | ||
if (spec.noMarks && ((_spec$marks = spec.marks) === null || _spec$marks === void 0 ? void 0 : _spec$marks.length) > 0) { | ||
throw new Error('Node with noMarks true has marks'); | ||
} | ||
_classPrivateFieldSet(_marks, this, (_spec$marks$map = (_spec$marks2 = spec.marks) === null || _spec$marks2 === void 0 ? void 0 : _spec$marks2.map(function (mark) { | ||
return mark.getType(); | ||
@@ -82,0 +95,0 @@ })) !== null && _spec$marks$map !== void 0 ? _spec$marks$map : []); |
@@ -0,58 +1,93 @@ | ||
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; | ||
import _defineProperty from "@babel/runtime/helpers/defineProperty"; | ||
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } | ||
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } | ||
import uniqBy from 'lodash/uniqBy'; | ||
import { ADFNode } from '../../adfNode'; | ||
import { JSONSchemaTransformerName } from '../transformerNames'; | ||
import { resolveName } from './inconsistentNameResolver'; | ||
export function buildContent(content, name) { | ||
export function buildContent(content, name, adfNodeContent) { | ||
if (content.length === 0) return {}; | ||
var minItems = determineMinItems(content, name); | ||
var items = determineItems(content, name); | ||
var items = determineItems(content, name, adfNodeContent); | ||
var maxItems = determineMaxItems(content, adfNodeContent); | ||
return { | ||
content: _objectSpread({ | ||
content: _objectSpread(_objectSpread({ | ||
type: 'array', | ||
items: items | ||
}, minItems) | ||
}, minItems !== null ? { | ||
minItems: minItems | ||
} : {}), maxItems !== null ? { | ||
maxItems: maxItems | ||
} : {}) | ||
}; | ||
} | ||
function determineItems(content, name) { | ||
// These are arrays instead of objects in the JSON Schema | ||
if (name === 'listItem' || name === 'taskList') { | ||
return handleListContent(content); | ||
} | ||
// TODO: codeblock is insane | ||
function isADFGroup(value) { | ||
return value && 'group' in value; | ||
} | ||
export function isADFNode(value) { | ||
return value && value instanceof ADFNode; | ||
} | ||
function determineItems(content, name, adfNodeContent) { | ||
// TODO: codeblock has a variant pattern instead of a normal node pattern | ||
// This is probably an expanded reference to the inline code node, a variant of text node | ||
// To remove this exception, we must update the JSON Schema to use a reference here instead | ||
if (name === 'codeBlock') { | ||
return {}; | ||
return { | ||
allOf: [{ | ||
$ref: '#/definitions/text_node' | ||
}, { | ||
properties: { | ||
marks: { | ||
maxItems: 0, | ||
type: 'array' | ||
} | ||
}, | ||
type: 'object' | ||
}] | ||
}; | ||
} | ||
// If there is one piece of content, defined as $zeroPlus($or([content])) or $onePlus($or([content])), then it's a single $ref in an object | ||
if (content.length === 1 && content[0].contentTypes.length === 1) { | ||
if (name === 'doc') { | ||
var jsonSchema = adfNodeContent.reduce(function (acc, value) { | ||
// We're expecting a single one+ with a nested $or | ||
if (value.type === '$one+' && value.content.type === '$or') { | ||
return [].concat(_toConsumableArray(acc), _toConsumableArray(flattenContent(value.content.content).map(function (node) { | ||
return { | ||
$ref: "#/definitions/".concat(resolveName(node.getName())) | ||
}; | ||
}))); | ||
} | ||
return acc; | ||
}, []); | ||
return { | ||
$ref: "#/definitions/".concat(resolveName(content[0].contentTypes[0])) | ||
anyOf: uniqBy(jsonSchema, function (v) { | ||
return v.$ref; | ||
}) | ||
}; | ||
} | ||
if (content.length === 1 && content[0].contentTypes.length > 1) { | ||
return handleObjectContent(content[0]); | ||
} | ||
return {}; | ||
var processedContentGroups = processContentGroups(content); | ||
// The JSON schema omits the array if there is only 1 item | ||
return flattenArray(processedContentGroups); | ||
} | ||
function handleObjectContent(content) { | ||
var itemsArray = content.contentTypes.map(function (piece) { | ||
function processContentTypes(contentTypes) { | ||
var itemsArray = []; | ||
contentTypes.forEach(function (piece) { | ||
itemsArray.push({ | ||
$ref: "#/definitions/".concat(resolveName(piece)) | ||
}); | ||
}); | ||
// We flatten an array here as well, but using anyOf to fit the JSON schema | ||
if (itemsArray.length === 1) { | ||
return itemsArray[0]; | ||
} else { | ||
return { | ||
$ref: "#/definitions/".concat(resolveName(piece)) | ||
anyOf: itemsArray | ||
}; | ||
}); | ||
return { | ||
anyOf: itemsArray | ||
}; | ||
} | ||
} | ||
function handleListContent(content) { | ||
function processContentGroups(content) { | ||
var contentArray = []; | ||
content.forEach(function (item) { | ||
if (item.contentTypes.length === 1) { | ||
contentArray.push({ | ||
$ref: "#/definitions/".concat(resolveName(item.contentTypes[0])) | ||
}); | ||
} else if (item.contentTypes.length > 1) { | ||
contentArray.push(handleObjectContent(item)); | ||
} | ||
contentArray.push(processContentTypes(item.contentTypes)); | ||
}); | ||
@@ -62,18 +97,79 @@ return contentArray; | ||
function determineMinItems(content, name) { | ||
var minItems; | ||
content.forEach(function (value) { | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if (value.minItems === 1 && name !== 'tableRow' && name !== 'doc') { | ||
minItems = { | ||
minItems: 1 | ||
}; | ||
var _content$find, _content$find2; | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one minItem value for all content on a node. | ||
if (!content) return null; | ||
var _ref = (_content$find = content.find(function (v) { | ||
return !isNaN(v.minItems); | ||
})) !== null && _content$find !== void 0 ? _content$find : {}, | ||
minItems = _ref.minItems; | ||
var _ref2 = (_content$find2 = content.find(function (v) { | ||
return v.range; | ||
})) !== null && _content$find2 !== void 0 ? _content$find2 : {}, | ||
range = _ref2.range; | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if (minItems === 1 && name !== 'tableRow' && name !== 'doc' && name !== 'layoutSection') { | ||
return minItems; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
if (minItems === 0 && name === 'caption') { | ||
return minItems; | ||
} | ||
// If it's a range, take minItems from that | ||
if (range) { | ||
return range.min; | ||
} | ||
return null; | ||
} | ||
// This function is not comprehensive, it is only defined for certain inputs | ||
function determineMaxItems(content, adfNodeContent) { | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one maxItem value for all content on a node. | ||
// If there's only one item, we can simply calculate it and return | ||
if (content.length === 1) { | ||
// If it's a range, grab maxItems from that | ||
if (content[0].range) { | ||
return content[0].range.max; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
else if (name === 'caption' && value.minItems === 0) { | ||
minItems = { | ||
minItems: 0 | ||
}; | ||
} else if (adfNodeContent.length > 1) { | ||
var types = adfNodeContent.map(function (v) { | ||
return v.type; | ||
}); | ||
if (types.includes('$one+') || types.includes('$zero+')) { | ||
return null; | ||
} | ||
}); | ||
return minItems; | ||
var maxItems = types.filter(function (v) { | ||
return v === '$or'; | ||
}).length; | ||
return maxItems || null; | ||
} | ||
return null; | ||
} | ||
function flattenArray(array) { | ||
if (array.length === 1) { | ||
return array[0]; | ||
} else { | ||
return array; | ||
} | ||
} | ||
/** | ||
* Flattens ADF groups and nodes into an array of nodes. | ||
* @param content | ||
* @returns ADFNode[] | ||
*/ | ||
function flattenContent(content) { | ||
return content.reduce(function (acc, item) { | ||
if (isADFGroup(item)) { | ||
// Expand the group into its member nodes | ||
return [].concat(_toConsumableArray(acc), _toConsumableArray(flattenContent(item.members))); | ||
} else if (isADFNode(item) && !item.isIgnored(JSONSchemaTransformerName)) { | ||
return [].concat(_toConsumableArray(acc), [item]); | ||
} | ||
return acc; | ||
}, []); | ||
} |
@@ -8,3 +8,4 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty"; | ||
import { buildContent } from './contentBuilder'; | ||
import { buildRequired } from './requiredBuilder'; | ||
import { resolveName } from './inconsistentNameResolver'; | ||
import { buildRequired, buildVariantRequired } from './requiredBuilder'; | ||
export var buildNode = function buildNode(node, content) { | ||
@@ -19,3 +20,3 @@ var _node$getSpec$marks, _content$, _content$2, _content$2$range; | ||
}).filter(Boolean)) || []; | ||
var marks = buildNodeMarks(nodeMarks); | ||
var marks = buildNodeMarks(nodeMarks, node.hasNoMarks()); | ||
var attrs = buildAttrs(node.getSpec().attrs); | ||
@@ -29,3 +30,3 @@ // Strangely, text has this extra property that looks like an attribute | ||
} : {}; | ||
var jsonContent = buildContent(content, node.getName()); | ||
var jsonContent = buildContent(content, node.getName(), node.getSpec().content); | ||
var version = node.getSpec().version; | ||
@@ -48,6 +49,31 @@ var jsonVersion = version ? { | ||
json.required = required; | ||
} else { | ||
var _required = buildVariantRequired(content); | ||
var allOfItem = _objectSpread({ | ||
type: 'object', | ||
properties: _objectSpread(_objectSpread({}, marks), jsonContent) | ||
}, _required); | ||
if (node.getName() === 'layoutSection_full') { | ||
// Only on the layoutSection_full variant we specify type | ||
allOfItem.properties.type = { | ||
enum: ['layoutSection'] | ||
}; | ||
allOfItem.required = ['type', 'content']; | ||
allOfItem.additionalProperties = false; | ||
} | ||
json.allOf = [{ | ||
$ref: "#/definitions/".concat(resolveName(node.getBase().getName())) | ||
}, allOfItem]; | ||
} | ||
return json; | ||
}; | ||
function buildNodeMarks(nodeMarks) { | ||
function buildNodeMarks(nodeMarks, hasNoMarks) { | ||
if (hasNoMarks) { | ||
return { | ||
marks: { | ||
type: 'array', | ||
maxItems: 0 | ||
} | ||
}; | ||
} | ||
if (nodeMarks.length === 0) return {}; | ||
@@ -54,0 +80,0 @@ if (nodeMarks.length === 1) return { |
@@ -50,2 +50,11 @@ import { isAnyOf } from '../../utils/isAnyOf'; | ||
return required; | ||
} | ||
export function buildVariantRequired(content) { | ||
var _content$, _content$2, _content$2$range; | ||
if (((_content$ = content[0]) === null || _content$ === void 0 ? void 0 : _content$.minItems) >= 1 || Boolean((_content$2 = content[0]) === null || _content$2 === void 0 ? void 0 : (_content$2$range = _content$2.range) === null || _content$2$range === void 0 ? void 0 : _content$2$range.type)) { | ||
return { | ||
required: ['content'] | ||
}; | ||
} | ||
return {}; | ||
} |
@@ -26,3 +26,5 @@ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } | ||
if (_node.isIgnored(PMSpecTransformerName)) { | ||
return; | ||
return { | ||
node: _node | ||
}; | ||
} | ||
@@ -49,8 +51,12 @@ var marks = (_node$getSpec$marks = _node.getSpec().marks) !== null && _node$getSpec$marks !== void 0 ? _node$getSpec$marks : []; | ||
group: function group(_group, members) { | ||
nodeGroupMap[_group.group] = _group.members.map(function (m) { | ||
return m.getName(); | ||
nodeGroupMap[_group.group] = _group.members.filter(function (node) { | ||
return !node.isIgnored(PMSpecTransformerName); | ||
}).map(function (node) { | ||
return node.getName(); | ||
}); | ||
return { | ||
group: _group.group, | ||
members: members | ||
members: members.filter(function (m) { | ||
return !m.node.isIgnored(PMSpecTransformerName); | ||
}) | ||
}; | ||
@@ -57,0 +63,0 @@ }, |
@@ -20,2 +20,7 @@ import type { TransformerNames } from './transforms/transformerNames'; | ||
/** | ||
* If true, the node doesn't allow marks to be set. | ||
* This is stricter than simply having an empty marks list. | ||
*/ | ||
hasNoMarks(): boolean; | ||
/** | ||
* Define a node. | ||
@@ -22,0 +27,0 @@ * |
import type { ADFNode } from '../../adfNode'; | ||
export declare function adfToJSON(adf: ADFNode<[string], any>): Record<string, any>; | ||
import type { JSONSchema4 } from 'json-schema'; | ||
export declare function adfToJSON(adf: ADFNode<[string], any>): JSONSchema4; |
@@ -0,9 +1,14 @@ | ||
import { JSONSchema4 } from 'json-schema-types'; | ||
import { ADFNode } from '../../adfNode'; | ||
import { ADFNodeGroup } from '../../types/ADFNodeGroup'; | ||
import { ADFNodeContentSpec } from '../../types/ADFNodeSpec'; | ||
import { ContentVisitorReturnType } from './adfToJsonVisitor'; | ||
export declare function buildContent(content: Array<ContentVisitorReturnType>, name: string): { | ||
export declare function buildContent(content: Array<ContentVisitorReturnType>, name: string, adfNodeContent: Array<ADFNodeContentSpec>): { | ||
content?: undefined; | ||
} | { | ||
content: { | ||
minItems: 0 | 1; | ||
maxItems?: number; | ||
minItems?: number; | ||
type: string; | ||
items: ({ | ||
items: { | ||
anyOf: { | ||
@@ -14,3 +19,3 @@ $ref: string; | ||
$ref: string; | ||
})[] | { | ||
} | ({ | ||
anyOf: { | ||
@@ -20,7 +25,25 @@ $ref: string; | ||
} | { | ||
$ref?: undefined; | ||
$ref: string; | ||
})[] | { | ||
allOf: ({ | ||
$ref: string; | ||
properties?: undefined; | ||
type?: undefined; | ||
} | { | ||
properties: { | ||
marks: { | ||
maxItems: number; | ||
type: string; | ||
}; | ||
}; | ||
type: string; | ||
$ref?: undefined; | ||
})[]; | ||
anyOf?: undefined; | ||
} | { | ||
$ref: string; | ||
anyOf: JSONSchema4[]; | ||
allOf?: undefined; | ||
}; | ||
}; | ||
}; | ||
export declare function isADFNode(value: ADFNode<any> | ADFNodeGroup): value is ADFNode; |
import { ADFAttributes } from '../../types/ADFAttribute'; | ||
import { ContentVisitorReturnType } from './adfToJsonVisitor'; | ||
export declare function buildRequired(attrs: ADFAttributes, hasContent: boolean, name: string): string[]; | ||
export declare function buildVariantRequired(content: ContentVisitorReturnType[]): { | ||
required: string[]; | ||
} | { | ||
required?: undefined; | ||
}; |
@@ -109,2 +109,9 @@ import { ADFNode } from '../adfNode'; | ||
/** | ||
* This is specifically for the JSON Schema | ||
* | ||
* If true, it means marks are not allowed on this node. | ||
* This is different to simply having an empty mark list. | ||
*/ | ||
noMarks?: boolean; | ||
/** | ||
* https://prosemirror.net/docs/ref/#model.NodeSpec.selectable | ||
@@ -111,0 +118,0 @@ * |
{ | ||
"name": "@atlaskit/adf-schema-generator", | ||
"version": "1.17.4", | ||
"version": "1.17.5", | ||
"description": "Generates ADF and PM schemas", | ||
@@ -28,4 +28,5 @@ "repository": "https://bitbucket.org/atlassian/adf-schema", | ||
"devDependencies": { | ||
"@types/lodash": "^4.17.4" | ||
"@types/lodash": "^4.17.4", | ||
"@types/json-schema": "^7.0.15" | ||
} | ||
} |
@@ -49,2 +49,10 @@ import type { TransformerNames } from './transforms/transformerNames'; | ||
/** | ||
* If true, the node doesn't allow marks to be set. | ||
* This is stricter than simply having an empty marks list. | ||
*/ | ||
hasNoMarks() { | ||
return this.#spec.noMarks; | ||
} | ||
/** | ||
* Define a node. | ||
@@ -62,2 +70,7 @@ * | ||
this.#spec = spec; | ||
if (spec.noMarks && spec.marks?.length > 0) { | ||
throw new Error('Node with noMarks true has marks'); | ||
} | ||
this.#marks = spec.marks?.map((mark) => mark.getType()) ?? []; | ||
@@ -64,0 +77,0 @@ return this; |
@@ -11,2 +11,3 @@ import type { ADFNode } from '../../adfNode'; | ||
} from './adfToJsonVisitor'; | ||
import type { JSONSchema4 } from 'json-schema'; | ||
@@ -27,3 +28,3 @@ function transform(adf: ADFNode<[string], ADFNodeSpec>) { | ||
result: Record<string, NodeVisitorReturnType>, | ||
): Record<string, any> { | ||
): JSONSchema4 { | ||
const formattedResult = {}; | ||
@@ -35,3 +36,3 @@ | ||
const finalResult: Record<string, any> = { | ||
const finalResult: JSONSchema4 = { | ||
$ref: '#/definitions/doc_node', | ||
@@ -47,4 +48,4 @@ description: 'Schema for Atlassian Document Format.', | ||
// Placeholder | ||
export function adfToJSON(adf: ADFNode<[string], any>): Record<string, any> { | ||
export function adfToJSON(adf: ADFNode<[string], any>): JSONSchema4 { | ||
return transform(adf); | ||
} |
@@ -0,1 +1,7 @@ | ||
import uniqBy from 'lodash/uniqBy'; | ||
import { JSONSchema4 } from 'json-schema-types'; | ||
import { ADFNode } from '../../adfNode'; | ||
import { ADFNodeGroup } from '../../types/ADFNodeGroup'; | ||
import { ADFNodeContentSpec } from '../../types/ADFNodeSpec'; | ||
import { JSONSchemaTransformerName } from '../transformerNames'; | ||
import { ContentVisitorReturnType } from './adfToJsonVisitor'; | ||
@@ -7,2 +13,3 @@ import { resolveName } from './inconsistentNameResolver'; | ||
name: string, | ||
adfNodeContent: Array<ADFNodeContentSpec>, | ||
) { | ||
@@ -12,53 +19,94 @@ if (content.length === 0) return {}; | ||
const minItems = determineMinItems(content, name); | ||
const items = determineItems(content, name); | ||
const items = determineItems(content, name, adfNodeContent); | ||
const maxItems = determineMaxItems(content, adfNodeContent); | ||
return { content: { type: 'array', items, ...minItems } }; | ||
return { | ||
content: { | ||
type: 'array', | ||
items, | ||
...(minItems !== null ? { minItems } : {}), | ||
...(maxItems !== null ? { maxItems } : {}), | ||
}, | ||
}; | ||
} | ||
function isADFGroup(value: ADFNode<any> | ADFNodeGroup): value is ADFNodeGroup { | ||
return value && 'group' in value; | ||
} | ||
export function isADFNode( | ||
value: ADFNode<any> | ADFNodeGroup, | ||
): value is ADFNode { | ||
return value && value instanceof ADFNode; | ||
} | ||
function determineItems( | ||
content: Array<ContentVisitorReturnType>, | ||
name: string, | ||
adfNodeContent: Array<ADFNodeContentSpec>, | ||
) { | ||
// These are arrays instead of objects in the JSON Schema | ||
if (name === 'listItem' || name === 'taskList') { | ||
return handleListContent(content); | ||
} | ||
// TODO: codeblock is insane | ||
// TODO: codeblock has a variant pattern instead of a normal node pattern | ||
// This is probably an expanded reference to the inline code node, a variant of text node | ||
// To remove this exception, we must update the JSON Schema to use a reference here instead | ||
if (name === 'codeBlock') { | ||
return {}; | ||
return { | ||
allOf: [ | ||
{ | ||
$ref: '#/definitions/text_node', | ||
}, | ||
{ | ||
properties: { | ||
marks: { | ||
maxItems: 0, | ||
type: 'array', | ||
}, | ||
}, | ||
type: 'object', | ||
}, | ||
], | ||
}; | ||
} | ||
// If there is one piece of content, defined as $zeroPlus($or([content])) or $onePlus($or([content])), then it's a single $ref in an object | ||
if (content.length === 1 && content[0].contentTypes.length === 1) { | ||
if (name === 'doc') { | ||
const jsonSchema: JSONSchema4[] = adfNodeContent.reduce((acc, value) => { | ||
// We're expecting a single one+ with a nested $or | ||
if (value.type === '$one+' && value.content.type === '$or') { | ||
return [ | ||
...acc, | ||
...flattenContent(value.content.content).map((node) => ({ | ||
$ref: `#/definitions/${resolveName(node.getName())}`, | ||
})), | ||
]; | ||
} | ||
return acc; | ||
}, [] as JSONSchema4[]); | ||
return { | ||
$ref: `#/definitions/${resolveName(content[0].contentTypes[0])}`, | ||
anyOf: uniqBy(jsonSchema, (v) => v.$ref), | ||
}; | ||
} | ||
if (content.length === 1 && content[0].contentTypes.length > 1) { | ||
return handleObjectContent(content[0]); | ||
} | ||
return {}; | ||
const processedContentGroups = processContentGroups(content); | ||
// The JSON schema omits the array if there is only 1 item | ||
return flattenArray(processedContentGroups); | ||
} | ||
function handleObjectContent(content: ContentVisitorReturnType) { | ||
const itemsArray = content.contentTypes.map((piece) => { | ||
return { $ref: `#/definitions/${resolveName(piece)}` }; | ||
function processContentTypes(contentTypes: string[]) { | ||
const itemsArray = []; | ||
contentTypes.forEach((piece) => { | ||
itemsArray.push({ $ref: `#/definitions/${resolveName(piece)}` }); | ||
}); | ||
return { anyOf: itemsArray }; | ||
// We flatten an array here as well, but using anyOf to fit the JSON schema | ||
if (itemsArray.length === 1) { | ||
return itemsArray[0]; | ||
} else { | ||
return { anyOf: itemsArray }; | ||
} | ||
} | ||
function handleListContent(content: Array<ContentVisitorReturnType>) { | ||
function processContentGroups(content: Array<ContentVisitorReturnType>) { | ||
const contentArray: Array<{ anyOf: { $ref: string }[] } | { $ref: string }> = | ||
[]; | ||
content.forEach((item) => { | ||
if (item.contentTypes.length === 1) { | ||
contentArray.push({ | ||
$ref: `#/definitions/${resolveName(item.contentTypes[0])}`, | ||
}); | ||
} else if (item.contentTypes.length > 1) { | ||
contentArray.push(handleObjectContent(item)); | ||
} | ||
contentArray.push(processContentTypes(item.contentTypes)); | ||
}); | ||
@@ -72,16 +120,80 @@ return contentArray; | ||
) { | ||
let minItems: { minItems: 1 | 0 }; | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one minItem value for all content on a node. | ||
if (!content) return null; | ||
const { minItems } = content.find((v) => !isNaN(v.minItems)) ?? {}; | ||
const { range } = content.find((v) => v.range) ?? {}; | ||
content.forEach((value) => { | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if (value.minItems === 1 && name !== 'tableRow' && name !== 'doc') { | ||
minItems = { minItems: 1 }; | ||
// Despite being oneplus, tableRow and doc have no minItems field | ||
if ( | ||
minItems === 1 && | ||
name !== 'tableRow' && | ||
name !== 'doc' && | ||
name !== 'layoutSection' | ||
) { | ||
return minItems; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
if (minItems === 0 && name === 'caption') { | ||
return minItems; | ||
} | ||
// If it's a range, take minItems from that | ||
if (range) { | ||
return range.min; | ||
} | ||
return null; | ||
} | ||
// This function is not comprehensive, it is only defined for certain inputs | ||
function determineMaxItems( | ||
content: Array<ContentVisitorReturnType>, | ||
adfNodeContent: Array<ADFNodeContentSpec>, | ||
) { | ||
// Despite it being possible for there to be multiple content groups on one node in DSL, | ||
// the JSON schema has only one maxItem value for all content on a node. | ||
// If there's only one item, we can simply calculate it and return | ||
if (content.length === 1) { | ||
// If it's a range, grab maxItems from that | ||
if (content[0].range) { | ||
return content[0].range.max; | ||
} | ||
// Only caption has minItems 0. If we can confirm it's redundant, we can remove this | ||
else if (name === 'caption' && value.minItems === 0) { | ||
minItems = { minItems: 0 }; | ||
} else if (adfNodeContent.length > 1) { | ||
const types = adfNodeContent.map((v) => v.type); | ||
if (types.includes('$one+') || types.includes('$zero+')) { | ||
return null; | ||
} | ||
}); | ||
return minItems; | ||
const maxItems = types.filter((v) => v === '$or').length; | ||
return maxItems || null; | ||
} | ||
return null; | ||
} | ||
function flattenArray<T>(array: Array<T>) { | ||
if (array.length === 1) { | ||
return array[0]; | ||
} else { | ||
return array; | ||
} | ||
} | ||
/** | ||
* Flattens ADF groups and nodes into an array of nodes. | ||
* @param content | ||
* @returns ADFNode[] | ||
*/ | ||
function flattenContent(content: (ADFNodeGroup | ADFNode)[]): ADFNode[] { | ||
return content.reduce((acc, item) => { | ||
if (isADFGroup(item)) { | ||
// Expand the group into its member nodes | ||
return [...acc, ...flattenContent(item.members)]; | ||
} else if (isADFNode(item) && !item.isIgnored(JSONSchemaTransformerName)) { | ||
return [...acc, item]; | ||
} | ||
return acc; | ||
}, [] as ADFNode[]); | ||
} |
@@ -7,3 +7,4 @@ import { ADFNode } from '../../adfNode'; | ||
import { buildContent } from './contentBuilder'; | ||
import { buildRequired } from './requiredBuilder'; | ||
import { resolveName } from './inconsistentNameResolver'; | ||
import { buildRequired, buildVariantRequired } from './requiredBuilder'; | ||
@@ -26,3 +27,3 @@ export const buildNode = ( | ||
.filter(Boolean) || []; | ||
const marks = buildNodeMarks(nodeMarks); | ||
const marks = buildNodeMarks(nodeMarks, node.hasNoMarks()); | ||
const attrs = buildAttrs(node.getSpec().attrs); | ||
@@ -32,3 +33,7 @@ // Strangely, text has this extra property that looks like an attribute | ||
node.getName() === 'text' ? { text: { minLength: 1, type: 'string' } } : {}; | ||
const jsonContent = buildContent(content, node.getName()); | ||
const jsonContent = buildContent( | ||
content, | ||
node.getName(), | ||
node.getSpec().content, | ||
); | ||
const version = node.getSpec().version; | ||
@@ -57,2 +62,26 @@ const jsonVersion = version ? { version: { enum: [version] } } : {}; | ||
json.required = required; | ||
} else { | ||
const required = buildVariantRequired(content); | ||
const allOfItem: Record<string, any> = { | ||
type: 'object', | ||
properties: { | ||
...marks, | ||
...jsonContent, | ||
}, | ||
...required, | ||
}; | ||
if (node.getName() === 'layoutSection_full') { | ||
// Only on the layoutSection_full variant we specify type | ||
allOfItem.properties.type = { enum: ['layoutSection'] }; | ||
allOfItem.required = ['type', 'content']; | ||
allOfItem.additionalProperties = false; | ||
} | ||
json.allOf = [ | ||
{ | ||
$ref: `#/definitions/${resolveName(node.getBase().getName())}`, | ||
}, | ||
allOfItem, | ||
]; | ||
} | ||
@@ -63,3 +92,11 @@ | ||
function buildNodeMarks(nodeMarks: string[]) { | ||
function buildNodeMarks(nodeMarks: string[], hasNoMarks: boolean) { | ||
if (hasNoMarks) { | ||
return { | ||
marks: { | ||
type: 'array', | ||
maxItems: 0, | ||
}, | ||
}; | ||
} | ||
if (nodeMarks.length === 0) return {}; | ||
@@ -66,0 +103,0 @@ if (nodeMarks.length === 1) |
import { ADFAttributes } from '../../types/ADFAttribute'; | ||
import { isAnyOf } from '../../utils/isAnyOf'; | ||
import { ContentVisitorReturnType } from './adfToJsonVisitor'; | ||
@@ -64,1 +65,8 @@ export function buildRequired( | ||
} | ||
export function buildVariantRequired(content: ContentVisitorReturnType[]) { | ||
if (content[0]?.minItems >= 1 || Boolean(content[0]?.range?.type)) { | ||
return { required: ['content'] }; | ||
} | ||
return {}; | ||
} |
@@ -42,3 +42,3 @@ import type { ADFNode } from '../../adfNode'; | ||
if (node.isIgnored(PMSpecTransformerName)) { | ||
return; | ||
return { node }; | ||
} | ||
@@ -64,4 +64,11 @@ const marks = node.getSpec().marks ?? []; | ||
group: (group, members) => { | ||
nodeGroupMap[group.group] = group.members.map((m) => m.getName()); | ||
return { group: group.group, members }; | ||
nodeGroupMap[group.group] = group.members | ||
.filter((node) => !node.isIgnored(PMSpecTransformerName)) | ||
.map((node) => node.getName()); | ||
return { | ||
group: group.group, | ||
members: members.filter( | ||
(m) => !m.node.isIgnored(PMSpecTransformerName), | ||
), | ||
}; | ||
}, | ||
@@ -68,0 +75,0 @@ $or(children) { |
@@ -128,2 +128,10 @@ import { ADFNode } from '../adfNode'; | ||
/** | ||
* This is specifically for the JSON Schema | ||
* | ||
* If true, it means marks are not allowed on this node. | ||
* This is different to simply having an empty mark list. | ||
*/ | ||
noMarks?: boolean; | ||
/** | ||
* https://prosemirror.net/docs/ref/#model.NodeSpec.selectable | ||
@@ -130,0 +138,0 @@ * |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
411417
10564
2