UNPKG

fastcomments-react-native-sdk

Version:

React Native FastComments Components. Add live commenting to any React Native application.

306 lines (305 loc) 13.4 kB
import { none } from "@hookstate/core"; import { getNextNodeId } from "./node-id"; import { EditorNodeType } from "./node-types"; import { graphToListStateWithoutNewlines, graphToListWithNewlines } from "./node-navigate"; import { deleteNodeRetainFocus } from "./node-delete"; import { createNewlineNode } from "./node-create"; import { focusNode } from "./node-focus"; export function stringToNodes(formatConfig, input) { return formatConfig.tokenize(input); } export function getNodeLength(type, content, config) { if (type === EditorNodeType.EMOTICON) { return config.emoticonLength || 0; } if (type === EditorNodeType.IMAGE) { return config.imageLength || 0; } if (!content) { return 0; } return content.length; } export function deleteNodeState(nodes, id) { const index = nodes.findIndex((searchingNode) => searchingNode.id.get() === id); console.log('Removing (index, id)', index, id); nodes[index].set(none); console.log('Done removing (index, id)', index, id); } /** * Note: length is based on the content the user sees, not the resulting representation. You should handle this validation server-side. */ export function graphToString(graph, formatConfig, maxLength) { let content = ''; if (!graph) { return content; } let length = 0; const nodes = graphToListWithNewlines(graph); for (const node of nodes) { if (!node || node === none) { continue; } // It really sucks that we have to do so many hashmap lookups due to the performance overhead, but not sure how else to make the library // be flexible. If you aren't getting the performance needed, maybe we could maintain a fork that uses a switch() to try to get the JIT to // create a jump table. We can't maintain a reference to a function on EditorNodeDefinition because serializing functions breaks usehookstate. const formattedValue = formatConfig.formatters[node.type](node); // OPTIMIZATION checking maxLength before doing type + EditorNodeType property lookup if (maxLength) { length += getNodeLength(node.type, 'content' in node ? node.content : undefined, formatConfig); } content += formattedValue; } // OPTIMIZATION - we only do this *after* creating the content as it's a less common scenario than just typing within limits if (maxLength && length > maxLength) { // recalculate content and trim content = ''; length = 0; for (const node of nodes) { if (!node || node === none) { continue; } const nodeLength = getNodeLength(node.type, 'content' in node ? node.content : undefined, formatConfig); // if adding this node would exceed the max length - add what we can and break. if (length + nodeLength > maxLength) { const remainingLength = maxLength - length; if (remainingLength > 0) { // console.log('???', node.type, typeof formatConfig.formatters[node.type]); const trimmedFormattedValue = formatConfig.formatters[node.type](node, remainingLength); if (trimmedFormattedValue) { // if we wanted to, we could trim the node in the graph here, too, but this might be a weird experience for the user. content += trimmedFormattedValue; // OPTIMIZATION: we don't need to do anything here with length because we're going to stop iteration. } else { // OPTIMIZATION: we don't need to do anything here with length because we're going to stop iteration. // empty image node does not make sense. We could remove the image here if we wanted to. } break; } else { // empty image node does not make sense. We could remove the image here if we wanted to. // OPTIMIZATION: we don't need to do anything here with length because we're going to stop iteration. break; } } else { // console.log('???', node.type, typeof formatConfig.formatters[node.type]); const fullFormattedValue = formatConfig.formatters[node.type](node); content += fullFormattedValue; } } } console.log('END graph to string'); return content; } /** * This function is not purely functional for performance reasons. That's why it takes a State<T>. * Note: length is based on the content the user sees, not the resulting representation. You should handle this validation server-side. */ export function enforceMaxLength(graph, formatConfig, maxLength) { let length = 0; let isEmpty = true; const nodes = graphToListStateWithoutNewlines(graph); for (const node of nodes) { const rawNode = node.get(); if (!rawNode || rawNode === none) { continue; } let nodeContent = 'content' in rawNode ? rawNode.content : undefined; if (nodeContent) { isEmpty = false; } // It really sucks that we have to do so many hashmap lookups due to the performance overhead, but not sure how else to make the library // be flexible. If you aren't getting the performance needed, maybe we could maintain a fork that uses a switch() to try to get the JIT to // create a jump table. We can't maintain a reference to a function on EditorNodeDefinition because serializing functions breaks usehookstate. // OPTIMIZATION checking maxLength before doing type + EditorNodeType property lookup if (maxLength) { length += getNodeLength(rawNode.type, nodeContent, formatConfig); } } // OPTIMIZATION - we only do this *after* creating the content as it's a less common scenario than just typing within limits if (maxLength && length > maxLength) { // recalculate content and trim length = 0; for (const node of nodes) { const rawNode = node.get(); if (!rawNode || rawNode === none) { continue; } const nodeLength = getNodeLength(rawNode.type, 'content' in rawNode ? rawNode.content : undefined, formatConfig); // if adding this node would exceed the max length - add what we can and break. if (length + nodeLength > maxLength) { const remainingLength = maxLength - length; if (remainingLength > 0) { const trimmedFormattedValue = formatConfig.formatters[rawNode.type](rawNode, remainingLength); if (trimmedFormattedValue) { node.content.set(trimmedFormattedValue); // OPTIMIZATION: we don't need to do anything here with length because we're going to stop iteration. } else { graph.set((graph) => { deleteNodeRetainFocus(graph, rawNode, rawNode); // example: empty image node does not make sense return graph; }); // OPTIMIZATION: we don't need to do anything here with length because we're going to stop iteration. } break; } else { graph.set((graph) => { deleteNodeRetainFocus(graph, rawNode, rawNode); // example: empty image node does not make sense return graph; }); // OPTIMIZATION: we don't need to do anything here with length because we're going to stop iteration. break; } } } } return isEmpty; } export function hasContent(graph) { for (const newline of graph) { if (newline.children) { for (const child of newline.children) { if (child.content) { return true; } } } } return false; } // HACK function isMention(type, text) { return type === EditorNodeType.TEXT_BOLD && text.startsWith('@') && text.length < 300; } export function defaultTokenizer(input, SupportedNodes) { console.log('calling defaultTokenizer', input); const result = []; let buffer = ''; let inNode = null; let currentNewLine = createNewlineNode([]); const inputLength = input.length; // don't re-read input length on every iteration for (let i = 0; i < inputLength; i++) { buffer += input[i]; if (inNode) { if (inNode.end && buffer.endsWith(inNode.end)) { const content = buffer.substring(0, buffer.length - inNode.end.length); currentNewLine.children.push({ id: getNextNodeId(), type: inNode.type, content, isFocused: false, deleteOnBackspace: isMention(inNode.type, content) }); inNode = null; buffer = ''; } } else { for (const startToken in SupportedNodes) { if (buffer.endsWith(startToken)) { // @ts-ignore const node = SupportedNodes[startToken]; if (node.lookaheadIgnore && input[i + 1] && node.lookaheadIgnore.some((ignore) => ignore === input[i + 1])) { continue; } if (node.type === EditorNodeType.NEWLINE) { const content = buffer.substring(0, buffer.length - startToken.length); if (content.length > 0) { currentNewLine.children.push({ id: getNextNodeId(), type: EditorNodeType.TEXT, content, isFocused: false, deleteOnBackspace: isMention(node.type, content) }); } inNode = null; buffer = ''; result.push(currentNewLine); currentNewLine = createNewlineNode([]); continue; } inNode = node; if (buffer.length - startToken.length > 0) { const content = buffer.substring(0, buffer.length - startToken.length); currentNewLine.children.push({ id: getNextNodeId(), type: EditorNodeType.TEXT, content, isFocused: false }); } if (!node.end) { // some node types like newlines do not have ends const content = buffer.substring(0, buffer.length - startToken.length); currentNewLine.children.push({ id: getNextNodeId(), type: node.type, content, isFocused: false, deleteOnBackspace: isMention(node.type, content) }); inNode = null; } buffer = ''; } } } } if (buffer.length > 0) { currentNewLine.children.push({ id: getNextNodeId(), type: EditorNodeType.TEXT, content: buffer, isFocused: false }); result.push(currentNewLine); } if (result.length === 0) { if (currentNewLine.children.length === 0) { const emptyTextNode = { id: getNextNodeId(), type: EditorNodeType.TEXT, content: '', isFocused: true }; focusNode(emptyTextNode); currentNewLine.children.push(emptyTextNode); } result.push(currentNewLine); } console.log('Tokenizer', input, '->', JSON.stringify(result)); return result; } export function toTextTrimmed(node, startToken, endToken, trimToLength) { if (!('content' in node)) { return ''; } const result = trimToLength ? node.content.substring(0, trimToLength) : node.content; if (!startToken && !endToken) { return result; } // count spaces before and after, then add them before and after the tokens. let spacesBefore = ''; for (let i = 0; i < result.length; i++) { if (result[i] === ' ') { spacesBefore += ' '; } else { break; } } let spacesAfter = ''; for (let i = result.length; i > 0; i--) { if (result[i] === ' ') { spacesAfter += ' '; } else { break; } } return spacesBefore + startToken + result.trim() + endToken + spacesAfter; }