/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ // TODO: Copied from ConversationLearner-UI. Need to improve Action design to find out how to consolidate knowledge to single layer // Originally ConversationLearner-models was intended to know have to understand the tree structure which is why we added the // pre-serialized 'text' field; however, now can't use the text field becuase it includes entity id's instead of human-readable names // Instead of having to decide wether to perform substitutions from the serialized text or from the tree it's better to // just have single method used everywhere // Can't import because it's from ConversationLearner-UI //import { NodeTypes } from "./models"; enum NodeTypes { Mention = 'mention-inline-node', Optional = 'optional-inline-node' } // Based on: https://github.com/ianstormtaylor/slate/blob/master/packages/slate-plain-serializer/src/index.js export interface IOptions { fallbackToOriginal: boolean preserveOptionalNodeWrappingCharacters: boolean } const defaultOptions: IOptions = { fallbackToOriginal: false, preserveOptionalNodeWrappingCharacters: false } function serialize(value: any, entityValuesMap: Map, userOptions: Partial = {}): string { const options = { ...defaultOptions, ...userOptions } const valueAsJson = typeof value.toJSON === 'function' ? value.toJSON() : value const processedDocument = removeOptionalNodesWithoutEntityValues(valueAsJson.document, Array.from(entityValuesMap.keys())) return serializeNode(processedDocument, entityValuesMap, options) } /** * Given node return filter out optional nodes without matching values provided * * E.g. You are welcome[, $name] -> You are welcome * @param node Slate Node * @param entityValues Key Value pair of entity id to entity display value */ function removeOptionalNodesWithoutEntityValues(node: any, entityIds: string[]): any | undefined { if (node.kind === 'inline' && node.type === NodeTypes.Optional) { const entityIdsWithinOptionalNode = getEntityIds(node) const hasValues = entityIdsWithinOptionalNode.every(x => entityIds.includes(x)) return hasValues ? node : undefined } if (Array.isArray(node.nodes)) { node.nodes = node.nodes.map((n: any) => removeOptionalNodesWithoutEntityValues(n, entityIds)).filter((n: any) => n) } return node } function getEntityIds(node: any): string[] { const entityIds: string[] = [] // If current node is inline node which we know to have entityId then save it in the list if (node.kind === 'inline' && node.type === NodeTypes.Mention) { // This check is required because when input is Slate Value node is Immutable.Map object // but it could also be a node from value.toJSON() const data = typeof node.data.toJS === 'function' ? node.data.toJS() : node.data const option = data.option if (!option) { throw new Error(`Attempting to serialize inline node but it did not have option`) } const entityId = option.id entityIds.push(entityId) } // Technically this would never get called because inline nodes shouldn't have other children which are inline nodes // however, it's good to have working depth-first-traversal anyways if (Array.isArray(node.nodes)) { const childrenEntityIds: string[] = node.nodes .map((n: any) => getEntityIds(n)) .reduce((totalIds: string[], nodeIds: string[]) => [...totalIds, ...nodeIds], []) entityIds.push(...childrenEntityIds) } return entityIds } function serializeNode(node: any, entityValues: Map, options: IOptions): string { if (node.kind === 'text') { return node.leaves.map((n: any) => n.text).join('') } const serializedChildNodes = node.nodes.map((n: any) => serializeNode(n, entityValues, options)) if (node.kind === 'inline' && node.type === NodeTypes.Mention) { // This check is required because when input is Slate Value node is Immutable.Map object // but it could also be a node from value.toJSON() const data = typeof node.data.toJS === 'function' ? node.data.toJS() : node.data if (!data.completed) { return serializedChildNodes.join('') } const option = data.option if (!option) { throw new Error(`Attempting to serialize inline node but it did not have option`) } const entityId = option.id const mapContainsEntity = entityValues.has(entityId) if (!mapContainsEntity) { if (options.fallbackToOriginal) { return serializedChildNodes.join('') } const entityValuesString = Array.from(entityValues.entries()) .map(([id, value]) => `${id}: ${value}`) .join(', ') throw new Error( `Inline node representing entity ${entityId} was NOT provided a value in the given entityValue map: [${entityValuesString}]` ) } return entityValues.get(entityId)! } if (node.kind === 'document') { return serializedChildNodes.join('\n') } const serializedChildren = serializedChildNodes.join('') return node.kind === 'inline' && node.type === NodeTypes.Optional ? options.preserveOptionalNodeWrappingCharacters ? serializedChildren : serializedChildren.slice(1, -1) : serializedChildren } export default { serialize }