Repository: ianstormtaylor/slate
File: packages/slate/src/core/normalize-node.ts (TypeScript)

Code before:

  if (!('children' in node)) {
    // If the node is not a text node, and doesn't have a `children` field,
    // then we have an invalid node that will upset slate.
    //
    // eg: `{ type: 'some_node' }`.
    //
    // To prevent slate from breaking, we can add the `children` field,
    // and now that it is valid, we can to many more operations easily,
    // such as extend normalizers to fix erronous structure.
    ;(node as Element).children = []
  }
  let element = node as Ancestor // we will have to refetch the element any time we modify its children since it clones to a new immutable reference when we do

  // Ensure that elements have at least one child.
  if (element !== editor && element.children.length === 0) {
    const child = { text: '' }
    Transforms.insertNodes(editor, child, { at: path.concat(0), voids: true })
    element = Node.get(editor, path) as Element
  }

  // Determine whether the node should have only block or only inline children.
  // - The editor should have only block children.
  // - Inline elements should have only inline children.
  // - Elements that begin with a text child or an inline element child should have only inline children.
  // - All other elements should have only block children.
  const shouldHaveInlines =
    !(element === editor) &&
    (editor.isInline(element) ||
      Text.isText(element.children[0]) ||
      editor.isInline(element.children[0]))

  if (shouldHaveInlines) {
    // Since we'll be applying operations while iterating, we also modify `n` when adding/removing nodes.
    for (let n = 0; n < element.children.length; n++) {
      const child = element.children[n]
      const prev = element.children[n - 1] as Descendant | undefined

      if (Text.isText(child)) {
        if (prev != null && Text.isText(prev)) {
          // Merge adjacent text nodes that are empty or match.
          if (child.text === '') {
            Transforms.removeNodes(editor, {
              at: path.concat(n),
              voids: true,
            })
            element = Node.get(editor, path) as Element
            n--
          } else if (prev.text === '') {
            Transforms.removeNodes(editor, {
              at: path.concat(n - 1),

Review comment (performance):
old implementation used `i` as an index into the snapshot of the node children passed into `normalizeNode` and `n` as an index into the children of the live node as its changed--this would be useful if we could use `i` to avoid `Node.get` calls by reading from the cached `node` whenever possible, but needing to know the prev child, which might be a newly added node, creates complexity.

Code after:

  if (!('children' in node)) {
    // If the node is not a text node, and doesn't have a `children` field,
    // then we have an invalid node that will upset slate.
    //
    // eg: `{ type: 'some_node' }`.
    //
    // To prevent slate from breaking, we can add the `children` field,
    // and now that it is valid, we can to many more operations easily,
    // such as extend normalizers to fix erronous structure.
    ;(node as Element).children = []
  }
  let element = node as Ancestor // we will have to refetch the element any time we modify its children since it clones to a new immutable reference when we do

  // Ensure that elements have at least one child.
  if (element !== editor && element.children.length === 0) {
    const child = { text: '' }
    Transforms.insertNodes(editor, child, { at: path.concat(0), voids: true })
    element = Node.get(editor, path) as Element
  }

  // Determine whether the node should have only block or only inline children.
  // - The editor should have only block children.
  // - Inline elements should have only inline children.
  // - Elements that begin with a text child or an inline element child should have only inline children.
  // - All other elements should have only block children.
  const shouldHaveInlines =
    !(element === editor) &&
    (editor.isInline(element) ||
      Node.isText(element.children[0]) ||
      editor.isInline(element.children[0]))

  if (shouldHaveInlines) {
    // Since we'll be applying operations while iterating, we also modify `n` when adding/removing nodes.
    for (let n = 0; n < element.children.length; n++) {
      const child = element.children[n]
      const prev = element.children[n - 1] as Descendant | undefined

      if (Node.isText(child)) {
        if (prev != null && Node.isText(prev)) {
          // Merge adjacent text nodes that are empty or match.
          if (child.text === '') {
            Transforms.removeNodes(editor, {
              at: path.concat(n),
              voids: true,
            })
            element = Node.get(editor, path) as Element
            n--
          } else if (prev.text === '') {
            Transforms.removeNodes(editor, {
              at: path.concat(n - 1),