/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical
import UIKit

open class CodeHighlightPlugin: Plugin {
  weak var editor: Editor?
  var codeTransform: (() -> Void)?
  var textTransform: (() -> Void)?
  var highlightTransform: (() -> Void)?

  public func setUp(editor: Editor) {
    self.editor = editor
    do {
      try editor.registerNode(nodeType: NodeType.codeHighlight, class: CodeHighlightNode.self)
      codeTransform = editor.addNodeTransform(nodeType: NodeType.code, transform: { [weak self] in
        guard
          let strongSelf = self,
          let node = $0 as? CodeNode
        else { return }
        try strongSelf.codeNodeTransform(node: node)
      })
      textTransform = editor.addNodeTransform(nodeType: NodeType.text, transform: { [weak self] in
        guard
          let strongSelf = self,
          let node = $0 as? TextNode
        else { return }
        try strongSelf.textNodeTransform(node: node)
      })
      highlightTransform = editor.addNodeTransform(nodeType: NodeType.codeHighlight, transform: { [weak self] in
        guard
          let strongSelf = self,
          let node = $0 as? TextNode
        else { return }
        try strongSelf.textNodeTransform(node: node)
      })
    } catch {
    }
  }

  public func tearDown() {
  }

  public init() {}

  // Using `skipTransforms` to prevent extra transforms since reformatting the code
  // will not affect code block content itself.
  //
  // Using extra flag (`isHighlighting`) since both CodeNode and CodeHighlightNode
  // transforms might be called at the same time (e.g. new CodeHighlight node inserted) and
  // in both cases we'll rerun whole reformatting over CodeNode, which is redundant.
  // Especially when pasting code into CodeBlock.
  var isHighlighting = false
  func codeNodeTransform(node: CodeNode) throws {
    guard let editor else {
      return
    }

    if isHighlighting {
      return
    }

    isHighlighting = true

    try editor.update {

      try updateAndRetainSelection(node: node, updateFn: {
        let code = node.getTextContent()
        let highlightNodes = getHighlightNodes(text: code)
        guard let (from, to, nodesForReplacement) = getDiffRange(prevNodes: node.getChildren(), nextNodes: highlightNodes) else {
          return false
        }
        try replaceRange(node: node, from: from, to: to, nodesToInsert: nodesForReplacement)
        return true
      })
    }
  }

  func textNodeTransform(node: TextNode) throws {
    // Since CodeNode has flat children structure we only need to check
    // if node's parent is a code node and run highlighting if so
    let parentNode = node.getParent()
    if let codeNode = parentNode as? CodeNode {
      try codeNodeTransform(node: codeNode)
    } else if let node = node as? CodeHighlightNode {
      // When code block converted into paragraph or other element
      // code highlight nodes converted back to normal text
      try node.replace(replaceWith: createTextNode(text: node.getTextPart()))
    }
  }

  func getHighlightNodes(text: String) -> [Node] {
    var nodes: [Node] = []
    let attributedString = NSAttributedString(string: text)
    let partials = attributedString.splitByNewlines()
    var i = 0

    for attrStr in partials {
      if attrStr.length > 0 {
        nodes.append(createCodeHighlightNode(text: attrStr.string, highlightType: ""))
      }
      if i < partials.count - 1 {
        nodes.append(createLineBreakNode())
      }
      i += 1
    }

    return nodes
  }

  // Wrapping update function into selection retainer, that tries to keep cursor at the same
  // position as before.
  func updateAndRetainSelection(node: CodeNode, updateFn: () throws -> Bool) throws {
    guard let selection = try getSelection() as? RangeSelection else {
      return
    }

    let anchor = selection.anchor
    let anchorOffset = anchor.offset
    let isNewLineAnchor = anchor.type == .element && isLineBreakNode(node.getChildAtIndex(index: anchor.offset - 1))
    var textOffset = 0

    // Calculating previous text offset (all text node prior to anchor + anchor own text offset)
    if !isNewLineAnchor {
      let anchorNode = try anchor.getNode()
      textOffset = anchorOffset + anchorNode.getPreviousSiblings().reduce(0, { offset, node in
        return offset + (isLineBreakNode(node) ? 0 : node.getTextContentSize(includeInert: false, includeDirectionless: false))
      })
    }

    let hasChanges = try updateFn()
    if !hasChanges {
      return
    }

    // Non-text anchors only happen for line breaks, otherwise
    // selection will be within text node (code highlight node)
    if isNewLineAnchor {
      try (anchor.getNode() as? ElementNode)?.select(anchorOffset: anchorOffset, focusOffset: anchorOffset)
      return
    }

    // If it was non-element anchor then we walk through child nodes
    // and looking for a position of original text offset
    _ = try node.getChildren().contains { node in
      if let node = node as? TextNode {
        let textContentSize = node.getTextContentSize()
        if textContentSize >= textOffset {
          try node.select(anchorOffset: textOffset, focusOffset: textOffset)
          return true
        }
        textOffset -= textContentSize
      }
      return false
    }
  }

  // Inserts notes into specific range of node's children. Works for replacement (from != to && nodesToInsert not empty),
  // insertion (from == to && nodesToInsert not empty) and deletion (from != to && nodesToInsert is empty)
  func replaceRange(node: ElementNode, from: Int, to: Int, nodesToInsert: [Node]) throws {
    var children = node.getChildren()
    for i in from..<to {
      try children[i].remove()
    }

    children = node.getChildren()
    if children.count == 0 {
      try node.append(nodesToInsert)
      return
    }

    if from == 0 {
      let firstChild = children[0]
      for node in nodesToInsert {
        try firstChild.insertBefore(nodeToInsert: node)
      }
    } else {
      var currentNode = children.count < from - 1 ? children[from - 1] : children[children.count - 1]
      for node in nodesToInsert {
        try currentNode.insertAfter(nodeToInsert: node)
        currentNode = node
      }
    }
  }

  // Finds minimal diff range between two nodes lists. It returns from/to range boundaries of prevNodes
  // that needs to be replaced with `nodes` (subset of nextNodes) to make prevNodes equal to nextNodes.
  func getDiffRange(prevNodes: [Node], nextNodes: [Node]) -> (from: Int, to: Int, nodesForReplacement: [Node])? {
    var leadingMatch = 0
    while leadingMatch < prevNodes.count {
      if !isEqual(nodeA: prevNodes[leadingMatch], nodeB: nextNodes[leadingMatch]) {
        break
      }
      leadingMatch += 1
    }

    let prevNodesLength = prevNodes.count
    let nextNodesLength = nextNodes.count
    let maxTrailingMatch = min(prevNodesLength, nextNodesLength) - leadingMatch

    var trailingMatch = 0
    while trailingMatch < maxTrailingMatch {
      trailingMatch += 1
      if !isEqual(nodeA: prevNodes[prevNodesLength - trailingMatch], nodeB: nextNodes[nextNodesLength - trailingMatch]) {
        trailingMatch -= 1
        break
      }
    }

    let from = leadingMatch
    let to = prevNodesLength - trailingMatch
    let nodesForReplacement = nextNodes[leadingMatch..<(nextNodesLength - trailingMatch)]

    let  hasChanges = from != to || nodesForReplacement.count > 0
    return hasChanges ? (from, to, Array(nodesForReplacement)) : nil
  }

  func isEqual(nodeA: Node, nodeB: Node) -> Bool {
    // Only checking for code higlight nodes and linebreaks. If it's regular text node
    // returning false so that it's transformed into code highlight node
    if
      let nodeACodeHighlight = nodeA as? CodeHighlightNode,
      let nodeBCodeHighlight = nodeB as? CodeHighlightNode {
      return nodeACodeHighlight.getTextPart() == nodeBCodeHighlight.getTextPart() &&
        nodeACodeHighlight.highlightType == nodeBCodeHighlight.highlightType
    }

    if isLineBreakNode(nodeA) && isLineBreakNode(nodeB) {
      return true
    }

    return false
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Lexical
import SwiftSoup

extension Lexical.ParagraphNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let dom = SwiftSoup.Element(Tag("p"), "")
    return (after: nil, element: dom)
  }
}

extension Lexical.TextNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let outerTag = getFormat().code ? "code" : "span"

    var element = SwiftSoup.Element(Tag(outerTag), "")
    try element.appendText(self.getTextPart())

    if getFormat().bold {
      element = try wrapDomElement(element, with: "b")
    }
    if getFormat().italic {
      element = try wrapDomElement(element, with: "i")
    }
    if getFormat().strikethrough {
      element = try wrapDomElement(element, with: "s")
    }
    if getFormat().underline {
      element = try wrapDomElement(element, with: "u")
    }

    return (after: nil, element: element)
  }

  private func wrapDomElement(_ element: SwiftSoup.Element, with tagString: String) throws -> SwiftSoup.Element {
    let newElement = SwiftSoup.Element(Tag(tagString), "")
    try newElement.appendChild(element)
    return newElement
  }
}

extension Lexical.CodeNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let dom = SwiftSoup.Element(Tag("code"), "")
    return (after: nil, element: dom)
  }
}

extension Lexical.LineBreakNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let dom = SwiftSoup.Element(Tag("br"), "")
    return (after: nil, element: dom)
  }
}

extension Lexical.QuoteNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let dom = SwiftSoup.Element(Tag("blockquote"), "")
    return (after: nil, element: dom)
  }
}

extension Lexical.HeadingNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let tag = self.getTag().rawValue
    let dom = SwiftSoup.Element(Tag(tag), "")
    return (after: nil, element: dom)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical
import UIKit

open class HTMLPlugin: Plugin {
  public init() {}

  weak var editor: Editor?

  public func setUp(editor: Editor) {
    self.editor = editor
  }

  public func tearDown() {
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Lexical
import SwiftSoup

public func generateHTMLFromNodes(editor: Editor, selection: BaseSelection?) throws -> String {
  let container = SwiftSoup.Element(Tag("div"), "")
  guard let root = getRoot() else {
    return ""
  }
  let topLevelChildren = root.getChildren()

  for topLevelNode in topLevelChildren {
    _ = try appendNodesToHTML(editor: editor, currentNode: topLevelNode, parentElement: container, selection: selection)
  }

  return try container.html()
}

private func appendNodesToHTML(editor: Editor, currentNode: Lexical.Node, parentElement: SwiftSoup.Element, selection: BaseSelection?) throws -> Bool {
  var shouldInclude = selection != nil ? try currentNode.isSelected() : true
  let shouldExclude: Bool
  if let currentNode = currentNode as? Lexical.ElementNode, currentNode.excludeFromCopy(destination: .html) {
    shouldExclude = true
  } else {
    shouldExclude = false
  }
  let target = currentNode

  if let selection {
    var clone = try cloneWithProperties(node: currentNode)
    guard let selection = selection as? RangeSelection else {
      throw LexicalError.internal("TODO: support selections that are not range selection")
    }
    if let cloneAsText = clone as? Lexical.TextNode {
      clone = try sliceSelectedTextNodeContent(selection: selection, textNode: cloneAsText)
    }
  }

  let children: [Lexical.Node]
  if let target = target as? Lexical.ElementNode {
    children = target.getChildren()
  } else {
    children = []
  }

  guard let target = target as? NodeHTMLSupport else {
    return false
  }

  let (after, element) = try target.exportDOM(editor: editor)

  guard let element else { return false }

  let fragmentElement = SwiftSoup.Element(Tag("div"), "")
  for childNode in children {
    let shouldIncludeChild = try appendNodesToHTML(editor: editor, currentNode: childNode, parentElement: fragmentElement, selection: selection)
    if !shouldInclude, let currentNode = currentNode as? Lexical.ElementNode, shouldIncludeChild, currentNode.extractWithChild(child: childNode, selection: selection, destination: .html) {
      shouldInclude = true
    }
  }

  if shouldInclude && !shouldExclude {
    for fragmentChild in fragmentElement.children() {
      try element.appendChild(fragmentChild)
    }
    try parentElement.appendChild(element)

    if let after {
      if let newElement = try after(target, element) {
        try element.replaceWith(newElement)
      }
    }
  } else {
    for fragmentChild in fragmentElement.children() {
      try parentElement.appendChild(fragmentChild)
    }
  }

  return shouldInclude
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Lexical
import SwiftSoup

public typealias DOMConversionOutputAfter = ([Lexical.Node]) throws -> [Lexical.Node]
public typealias DOMChildConversion = (Lexical.Node, Lexical.Node?) throws -> Lexical.Node? // arguments: node, parent
public typealias DOMConversionOutput = (after: DOMConversionOutputAfter?, forChild: DOMChildConversion?, node: [Lexical.Node])

public typealias DOMExportOutputAfter = (Lexical.Node, SwiftSoup.Element?) throws -> SwiftSoup.Element?
public typealias DOMExportOutput = (after: DOMExportOutputAfter?, element: SwiftSoup.Element?)

public protocol NodeHTMLSupport: Lexical.Node {
  static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput
  func exportDOM(editor: Editor) throws -> DOMExportOutput
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
@testable import Lexical
@testable import LexicalHTML
import XCTest

class LexicalHTMLTests: XCTestCase {

  var lexicalView: LexicalView?
  var editor: Editor? {
    get {
      return lexicalView?.editor
    }
  }

  override func setUp() {
    lexicalView = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  }

  override func tearDown() {
    lexicalView = nil
  }

  func testNodesToHTML() throws {
    guard let editor else { XCTFail(); return }

    let comparison = """

  hello world

"""

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello world")

      guard let root = getRoot(), let paragraphNode = root.getFirstChild() as? ParagraphNode else {
        XCTFail()
        return
      }
      try paragraphNode.append([textNode])

      let html = try generateHTMLFromNodes(editor: editor, selection: nil)
      XCTAssertEqual(html, comparison, "Incorrect HTML")
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical

open class InlineImagePlugin: Plugin {
  public init() {}

  weak var editor: Editor?

  public func setUp(editor: Editor) {
    self.editor = editor

    do {
      try editor.registerNode(nodeType: NodeType.image, class: ImageNode.self)
    } catch {
      editor.log(.other, .error, "\(error)")
    }
  }

  public func tearDown() {}

  public func createImageNode(url: String, size: CGSize, sourceID: String?) -> ImageNode {
    ImageNode(url: url, size: size, sourceID: sourceID ?? "")
  }

  public func isImageNode(_ node: Node?) -> Bool {
    node is ImageNode
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import AVFoundation
import Foundation
import Lexical
import UIKit

extension NodeType {
  static let image = NodeType(rawValue: "image")
}

extension CommandType {
  public static let imageTap = CommandType(rawValue: "imageTap")
}

public class ImageNode: DecoratorNode {
  var url: URL?
  var size = CGSize.zero
  var sourceID: String = ""

  public required init(url: String, size: CGSize, sourceID: String, key: NodeKey? = nil) {
    super.init(key)

    self.url = URL(string: url)
    self.size = size
    self.type = NodeType.image
    self.sourceID = sourceID
  }

  required init(_ key: NodeKey? = nil) {
    super.init(key)

    self.type = NodeType.image
  }

  public required init(from decoder: Decoder) throws {
    try super.init(from: decoder)

    self.type = NodeType.image
  }

  override public func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
  }

  override public func clone() -> Self {
    Self(url: url?.absoluteString ?? "", size: size, sourceID: sourceID, key: key)
  }

  override public func createView() -> UIImageView {
    editorForTapHandling = getActiveEditor()
    let imageView = createImageView()
    loadImage(imageView: imageView)
    return imageView
  }

  override open func decorate(view: UIView) {
    if let view = view as? UIImageView {
      for gr in view.gestureRecognizers ?? [] {
        view.removeGestureRecognizer(gr)
      }
      let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(gestureRecognizer:)))
      view.addGestureRecognizer(gestureRecognizer)
      loadImage(imageView: view)
    }
  }

  public func getURL() -> String? {
    let latest: ImageNode = getLatest()

    return latest.url?.absoluteString
  }

  public func setURL(_ url: String) throws {
    try errorOnReadOnly()

    try getWritable().url = URL(string: url)
  }

  public func getSourceID() -> String? {
    let latest: ImageNode = getLatest()

    return latest.sourceID
  }

  public func setSourceID(_ sourceID: String) throws {
    try errorOnReadOnly()

    try getWritable().sourceID = sourceID
  }

  private func createImageView() -> UIImageView {
    let view = UIImageView(frame: CGRect(origin: CGPoint.zero, size: size))
    view.isUserInteractionEnabled = true

    view.backgroundColor = .lightGray

    return view
  }

  private weak var editorForTapHandling: Editor?
  @objc internal func handleTap(gestureRecognizer: UITapGestureRecognizer) {
    guard let editorForTapHandling else { return }
    do {
      try editorForTapHandling.update {
        editorForTapHandling.dispatchCommand(type: .imageTap, payload: getSourceID())
      }
    } catch {
      editorForTapHandling.log(.node, .error, "Error thrown in tap handler, \(error)")
    }
  }

  private func loadImage(imageView: UIImageView) {
    guard let url else { return }

    URLSession.shared.dataTask(with: url) { (data, response, error) in
      if error != nil {
        return
      }

      guard let data else {
        return
      }

      DispatchQueue.main.async {
        imageView.image = UIImage(data: data)
      }
    }.resume()
  }

  let maxImageHeight: CGFloat = 600.0

  override open func sizeForDecoratorView(textViewWidth: CGFloat, attributes: [NSAttributedString.Key: Any]) -> CGSize {

    if size.width <= textViewWidth {
      return size
    }
    return AVMakeRect(aspectRatio: size, insideRect: CGRect(x: 0, y: 0, width: textViewWidth, height: maxImageHeight)).size
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical
import UIKit

extension CommandType {
  public static let link = CommandType(rawValue: "link")
}

public struct LinkPayload {
  let urlString: String?
  let originalSelection: RangeSelection?

  public init(urlString: String?, originalSelection: RangeSelection?) {
    self.urlString = urlString
    self.originalSelection = originalSelection
  }
}

open class LinkPlugin: Plugin {
  public init() {}

  weak var editor: Editor?
  public var lexicalView: LexicalView?

  public func setUp(editor: Editor) {
    self.editor = editor
    do {
      try editor.registerNode(nodeType: NodeType.link, class: LinkNode.self)
    } catch {
      print("\(error)")
    }

    _ = editor.registerCommand(type: .link, listener: { [weak self] payload in
      guard let strongSelf = self,
            let linkPayload = payload as? LinkPayload,
            let editor = strongSelf.editor
      else { return false }

      strongSelf.insertLink(linkPayload: linkPayload, editor: editor)
      return true
    })
  }

  public func tearDown() {
  }

  public func createLinkNode(url: String) -> LinkNode {
    LinkNode(url: url, key: nil)
  }

  public func isLinkNode(_ node: Node?) -> Bool {
    node is LinkNode
  }

  func insertLink(linkPayload: LinkPayload?, editor: Editor) {
    do {
      try editor.update {
        getActiveEditorState()?.selection = linkPayload?.originalSelection
        try toggleLink(url: linkPayload?.urlString)
      }
    } catch {
      print("\(error)")
    }

    lexicalView?.textViewBecomeFirstResponder()
  }

  func toggleLink(url: String?) throws {
    guard let selection = try getSelection() else { return }
    let nodes = try selection.extract()

    guard let url else {
      // Remove linkNode
      try nodes.forEach { node in
        if let parent = node.getParent() as? LinkNode {
          for child in parent.getChildren() {
            try parent.insertBefore(nodeToInsert: child)
          }

          try parent.remove()
        }
      }

      return
    }

    // Add or merge LinkNodes
    if nodes.count == 1 {
      // if the firstNode is LinkNode or if its parent is a LinkNode, we update the url
      if let firstNode = nodes.first as? LinkNode {
        try firstNode.setURL(url)
        return
      } else if let parent = nodes.first?.getParent() as? LinkNode {
        // set parent to be current linkNode so that other nodes in the same parent aren't handled separately below.
        try parent.setURL(url)
        return
      }
    }

    var prevParent: ElementNode?
    var linkNode: LinkNode?

    try nodes.forEach { node in
      let parent = node.getParent()

      if let elementNode = node as? ElementNode, !elementNode.isInline() || parent == linkNode || parent == nil {
        return
      }

      if let parentNode = parent as? LinkNode {
        linkNode = parentNode
        try parentNode.setURL(url)
        return
      }

      if parent != prevParent {
        prevParent = parent
        linkNode = createLinkNode(url: url)

        guard let linkNode else {
          return
        }

        if isLinkNode(parent) {
          if node.getPreviousSibling() == nil {
            try parent?.insertBefore(nodeToInsert: linkNode)
          } else {
            try parent?.insertAfter(nodeToInsert: linkNode)
          }
        } else {
          try node.insertBefore(nodeToInsert: linkNode)
        }
      }

      if isLinkNode(node) {
        if node == linkNode {
          return
        }

        if linkNode != nil {
          guard let elementNode = node as? ElementNode else { return }

          let children = elementNode.getChildren()
          for child in children {
            try linkNode?.append([child])
          }
        }

        try node.remove()
        return
      }

      if linkNode != nil {
        try linkNode?.append([node])
      }
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Lexical
import UIKit

extension NodeType {
  static let link = NodeType(rawValue: "link")
}

open class LinkNode: ElementNode {
  enum CodingKeys: String, CodingKey {
    case url
  }

  public var url: String = ""

  override public init() {
    super.init()
    self.type = NodeType.link
  }

  public required init(url: String, key: NodeKey?) {
    super.init(key)
    self.url = url
    self.type = NodeType.link
  }

  public required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    try super.init(from: decoder)

    self.url = try container.decode(String.self, forKey: .url)
    self.type = NodeType.link
  }

  override open func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.url, forKey: .url)
  }

  public func getURL() -> String {
    let latest: LinkNode = getLatest()
    return latest.url
  }

  public func setURL(_ url: String) throws {
    try errorOnReadOnly()
    try getWritable().url = url
  }

  override public func canInsertTextBefore() -> Bool {
    return false
  }

  override public func canInsertTextAfter() -> Bool {
    return false
  }

  override public func canBeEmpty() -> Bool {
    return false
  }

  override public func isInline() -> Bool {
    return true
  }

  override open func clone() -> Self {
    Self(url: url, key: key)
  }

  override public func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] {
    if url.isEmpty {
      return [:]
    }

    var attribs: [NSAttributedString.Key: Any] = theme.link ?? [:]
    attribs[.link] = url
    return attribs
  }

  override open func insertNewAfter(selection: RangeSelection?) throws -> Node? {
    if let element = try getParentOrThrow().insertNewAfter(selection: selection) as? ElementNode {
      let linkNode = LinkNode(url: url, key: nil)
      try element.append([linkNode])
      return linkNode
    }

    return nil
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
@testable import LexicalLinkPlugin
import XCTest

class LinkNodeTests: XCTestCase {

  func testLinkAttributeLengthForAccessibility() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      guard let editorState = getActiveEditorState(),
            let rootNode = editorState.getRootNode(),
            let paragraphNode = rootNode.getChildren().first as? ElementNode
      else {
        XCTFail("should have editor state")
        return
      }

      let linkNode = LinkNode()
      try linkNode.setURL("http://www.example.com")
      try paragraphNode.append([linkNode])

      let textNode = TextNode()
      try textNode.setText("Hello World")
      try linkNode.append([textNode])
    }

    // I have confirmed via the simulator that UITextView uses longestEffectiveRange to find the bounds of a link.
    var range = NSRange(location: NSNotFound, length: 0)
    let url: String? = editor.textStorage?.attribute(.link, at: 0, longestEffectiveRange: &range, in: NSRange(location: 0, length: editor.textStorage?.length ?? 0)) as? String

    XCTAssertEqual(url, "http://www.example.com", "Link address should match")
    XCTAssertEqual(range.length, 11, "link should span eleven characters")
  }

  func testLinkNodeTypeAndProperties() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())

    try view.editor.update {
      let linkNode = LinkNode()
      XCTAssertTrue(linkNode.type == NodeType.link)
      XCTAssertFalse(linkNode.canInsertTextBefore())
      XCTAssertFalse(linkNode.canInsertTextAfter())
      XCTAssertFalse(linkNode.canBeEmpty())
      XCTAssertTrue(linkNode.isInline())
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical
import LexicalHTML
import LexicalListPlugin
import SwiftSoup

extension LexicalListPlugin.ListNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let tag = self.getListType() == .number ? "ol" : "ul"
    let dom = SwiftSoup.Element(Tag(tag), "")

    if getStart() != 1 {
      try dom.attr("start", "\(getStart())")
    }

    return (after: nil, element: dom)
  }
}

extension LexicalListPlugin.ListItemNode: NodeHTMLSupport {
  public static func importDOM(domNode: SwiftSoup.Node) throws -> DOMConversionOutput {
    return (after: nil, forChild: nil, node: [])
  }

  public func exportDOM(editor: Lexical.Editor) throws -> DOMExportOutput {
    let dom = SwiftSoup.Element(Tag("li"), "")
    if getValue() > 0 {
      try dom.attr("value", "\(getValue())")
    }
    return (after: nil, element: dom)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import CoreGraphics
import Foundation
import Lexical

extension NSAttributedString.Key {
  public static let listItem: NSAttributedString.Key = .init(rawValue: "list_item")
}

internal struct ListItemAttribute: Hashable, Equatable {
  // Node key is to make sure that consecutive list items have list item attributes that are not equal
  var itemNodeKey: NodeKey

  var listItemCharacter: String
  var characterIndentationPixels: CGFloat
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical
import UIKit

extension NodeType {
  static let listItem = NodeType(rawValue: "listItem")
}

public class ListItemNode: ElementNode {

  private var value: Int = 0

  override public init() {
    super.init()
    self.type = NodeType.listItem
  }

  override public required init(_ key: NodeKey?) {
    super.init(key)
    self.type = NodeType.listItem
  }

  public required init(from decoder: Decoder) throws {
    try super.init(from: decoder)
    self.type = NodeType.listItem
  }

  override open func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
  }

  override public func clone() -> Self {
    Self(key)
  }

  public func getValue() -> Int {
    let node = self.getLatest()
    return node.value
  }

  public func setValue(value: Int) {
    let node = try? self.getWritable()
    node?.value = value
  }

  override public func append(_ nodesToAppend: [Node]) throws {
    for node in nodesToAppend {
      if let node = node as? ElementNode, self.canMergeWith(node: node) {
        let children = node.getChildren()
        try self.append(children)
        try node.remove()
      } else {
        try super.append([node])
      }
    }
  }

  override public func replace<T>(replaceWith replaceWithNode: T, includeChildren: Bool = false) throws -> T where T: Node {
    if replaceWithNode is ListItemNode {
      return try super.replace(replaceWith: replaceWithNode)
    }
    try self.setIndent(0)
    let list = try getParentOrThrow()
    guard let list = list as? ListNode else {
      return replaceWithNode
    }
    if let firstChild = list.getFirstChild(), firstChild.key == getKey() {
      try list.insertBefore(nodeToInsert: replaceWithNode)
    } else if let lastChild = list.getLastChild(), lastChild.key == getKey() {
      try list.insertAfter(nodeToInsert: replaceWithNode)
    } else {
      // Split the list
      let newList = createListNode(listType: list.getListType())
      var nextSibling = self.getNextSibling()
      while nextSibling != nil {
        guard let nodeToAppend = nextSibling else { continue }
        nextSibling = nextSibling?.getNextSibling()
        try newList.append([nodeToAppend])
      }
      try list.insertAfter(nodeToInsert: replaceWithNode)
      try replaceWithNode.insertAfter(nodeToInsert: newList)
    }
    if includeChildren, let replaceWithNode = replaceWithNode as? ElementNode {
      for child in self.getChildren() {
        try replaceWithNode.append([child])
      }
    }
    try self.remove()
    if list.getChildrenSize() == 0 {
      try list.remove()
    }
    return replaceWithNode
  }

  override public func insertAfter(nodeToInsert node: Node) throws -> Node {
    guard let listNode = try self.getParentOrThrow() as? ListNode else {
      throw LexicalError.invariantViolation("list node is not parent of list item node")
    }

    let siblings = self.getNextSiblings()

    if let node = node as? ListItemNode {
      let after = try super.insertAfter(nodeToInsert: node)
      let afterListNode = try node.getParentOrThrow()

      if let afterListNode = afterListNode as? ListNode {
        try updateChildrenListItemValue(list: afterListNode, children: nil)
      }

      return after
    }

    // Attempt to merge if the list is of the same type.

    if let node = node as? ListNode, node.getListType() == listNode.getListType() {
      let child = node
      // TODO: @amyworrall not sure about this porting section
      if let children = node.getChildren() as? [ListNode] {
        for child in children {
          _ = try self.insertAfter(nodeToInsert: child)
        }
      }
      return child
    }

    // Otherwise, split the list
    // Split the lists and insert the node in between them
    _ = try listNode.insertAfter(nodeToInsert: node)

    if !siblings.isEmpty {
      let newListNode = createListNode(listType: listNode.getListType())
      try newListNode.append(siblings)
      _ = try node.insertAfter(nodeToInsert: newListNode)
    }

    return node
  }

  override public func remove() throws {
    let prevSibling = self.getPreviousSibling()
    let nextSibling = self.getNextSibling()
    try super.remove()

    if
      let prevSibling = prevSibling as? ListItemNode,
      let nextSibling = nextSibling as? ListItemNode,
      isNestedListNode(node: prevSibling),
      isNestedListNode(node: nextSibling),
      let list1 = prevSibling.getFirstChild() as? ListNode,
      let list2 = nextSibling.getFirstChild() as? ListNode
    {
      try mergeLists(list1: list1, list2: list2)
      try nextSibling.remove()
    } else if let nextSibling {
      let parent = nextSibling.getParent()

      if let parent = parent as? ListNode {
        try updateChildrenListItemValue(list: parent, children: nil)
      }
    }
  }

  override public func insertNewAfter(selection: RangeSelection?) throws -> Node? {
    let newElement = ListItemNode()
    _ = try self.insertAfter(nodeToInsert: newElement)

    return newElement
  }

  override public func collapseAtStart(selection: RangeSelection) throws -> Bool {
    let paragraph = createParagraphNode()
    let children = self.getChildren()
    try paragraph.append(children)
    let listNode = try self.getParentOrThrow()
    let listNodeParent = try listNode.getParentOrThrow()
    let isIndented = listNodeParent is ListItemNode

    if listNode.getChildrenSize() == 1 {
      if isIndented {
        // if the list node is nested, we just want to remove it,
        // effectively unindenting it.
        try listNode.remove()
        try listNodeParent.select(anchorOffset: nil, focusOffset: nil)
      } else {
        try listNode.insertBefore(nodeToInsert: paragraph)
        try listNode.remove()
        // If we have selection on the list item, we'll need to move it
        // to the paragraph
        let anchor = selection.anchor
        let focus = selection.focus
        let key = paragraph.getKey()

        if anchor.type == .element && anchor.key == self.getKey() {
          anchor.updatePoint(key: key, offset: anchor.offset, type: .element)
        }

        if focus.type == .element && focus.key == self.getKey() {
          focus.updatePoint(key: key, offset: focus.offset, type: .element)
        }
      }
    } else {
      try listNode.insertBefore(nodeToInsert: paragraph)
      try self.remove()
    }

    return true
  }

  override public func getIndent() -> Int {
    guard let parent = getParent() as? ListNode else {
      // If we don't have a parent, we are likely serializing
      return super.getIndent()
    }
    // ListItemNode should always have a ListNode for a parent.
    var listNodeParent = parent.getParent()
    var indentLevel = 0
    while listNodeParent is ListItemNode {
      listNodeParent = listNodeParent?.getParent()?.getParent()
      indentLevel += 1
    }
    return indentLevel
  }

  @discardableResult
  override public func setIndent(_ indent: Int) throws -> ElementNode {
    try errorOnReadOnly()
    let node = try getWritable() as ListItemNode
    var currentIndent = getIndent()
    while currentIndent != indent {
      if currentIndent < indent {
        try handleIndent(self)
        currentIndent += 1
      } else {
        try handleOutdent(self)
        currentIndent -= 1
      }
    }
    return node
  }

  // TODO: support other types of list. Correctly derive the item number in this method.
  override public func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] {
    let node: ListItemNode = getLatest()
    let listNode = node.getParent() as? ListNode

    var attributes: [NSAttributedString.Key: Any] = theme.listItem ?? [:]

    attributes[.paddingHead] = attributes[.paddingHead] ?? theme.indentSize

    if node.getChildren().first is ListNode {
      // Don't apply styles for this list item, because there's another list inside it (don't want to draw two bullets!)
      return attributes
    }

    var character = ""

    if listNode?.getListType() == .bullet {
      character = "\u{2022}"
    } else {
      // list is numbered; count previous siblings
      if let listNode {
        let start = listNode.getStart()
        let prevItems = getPreviousSiblings().filter { $0 is ListItemNode }.count
        character = String("\(start + prevItems).")
      }
    }

    // the magic number is to horizontally position the bullet further left than the indent size, but not so far as to hit the previous indent stop.
    attributes[.listItem] = ListItemAttribute(itemNodeKey: node.key, listItemCharacter: character, characterIndentationPixels: (CGFloat(getIndent() + 1) - 0.8) * theme.indentSize)

    return attributes
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical

public enum ListType {
  case bullet
  case number
  case check
}

extension NodeType {
  public static let list = NodeType(rawValue: "list")
}

public class ListNode: ElementNode {
  private var listType: ListType = .bullet
  private var start: Int = 1

  public required convenience init(listType: ListType, start: Int, key: NodeKey? = nil) {
    self.init(key)
    self.listType = listType
    self.start = start
  }

  override public init() {
    super.init()
    self.type = NodeType.list
  }

  override public init(_ key: NodeKey?) {
    super.init(key)
    self.type = NodeType.list
  }

  public required init(from decoder: Decoder) throws {
    try super.init(from: decoder)
    self.type = NodeType.list
  }

  // MARK: getters/setters

  public func getListType() -> ListType {
    return getLatest().listType
  }

  @discardableResult
  public func setListType(_ type: ListType) throws -> ListNode {
    let node: ListNode = try getWritable()
    node.listType = type
    return node
  }

  public func getStart() -> Int {
    return getLatest().start
  }

  @discardableResult
  public func setStart(_ start: Int) throws -> ListNode {
    let node: ListNode = try getWritable()
    node.start = start
    return node
  }

  override public func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
  }

  override public func clone() -> Self {
    Self(listType: listType, start: start, key: key)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical
import UIKit

extension CommandType {
  public static let insertUnorderedList = CommandType(rawValue: "insertUnorderedList")
  public static let insertOrderedList = CommandType(rawValue: "insertOrderedList")
  public static let removeList = CommandType(rawValue: "removeList")
}

open class ListPlugin: Plugin {
  public init() {}

  weak var editor: Editor?

  public func setUp(editor: Editor) {
    self.editor = editor
    do {
      try editor.registerNode(nodeType: NodeType.list, class: ListNode.self)
      try editor.registerNode(nodeType: NodeType.listItem, class: ListItemNode.self)

      _ = editor.registerCommand(type: .insertUnorderedList, listener: { [weak editor] payload in
        guard let editor else { return false }
        try? insertList(editor: editor, listType: .bullet)
        return true
      })

      _ = editor.registerCommand(type: .insertOrderedList, listener: { [weak editor] payload in
        guard let editor else { return false }
        try? insertList(editor: editor, listType: .number)
        return true
      })

      try editor.registerCustomDrawing(customAttribute: .listItem, layer: .text, granularity: .contiguousParagraphs) {
        attributeKey, attributeValue, layoutManager, characterRange, expandedCharRange, glyphRange, rect, firstLineFragment in

        guard let attributeValue = attributeValue as? ListItemAttribute, let textStorage = layoutManager.textStorage as? TextStorage else {
          return
        }

        // we only want to do the drawing if we're the first character in a paragraph.
        // We could optimise this in the future by either (1) hooking in to TextKit string normalisation, or (2) subclassing
        // NSParagraphStyle
        if characterRange.location != 0 && (textStorage.string as NSString).substring(with: NSRange(location: characterRange.location - 1, length: 1)) != "\n" {
          return
        }

        var attributes = textStorage.attributes(at: characterRange.location, effectiveRange: nil)

        var spacingBefore = 0.0
        if let paragraphStyle = attributes[.paragraphStyle] as? NSParagraphStyle, let mutableParagraphStyle = paragraphStyle.mutableCopy() as? NSMutableParagraphStyle {
          mutableParagraphStyle.headIndent = 0
          mutableParagraphStyle.firstLineHeadIndent = 0
          mutableParagraphStyle.tailIndent = 0
          spacingBefore = paragraphStyle.paragraphSpacingBefore
          mutableParagraphStyle.paragraphSpacingBefore = 0
          attributes[.paragraphStyle] = mutableParagraphStyle
        }
        attributes.removeValue(forKey: .underlineStyle)
        attributes.removeValue(forKey: .strikethroughStyle)
        let bulletDrawRect = firstLineFragment.inset(by: UIEdgeInsets(top: spacingBefore, left: attributeValue.characterIndentationPixels, bottom: 0, right: 0))

        attributeValue.listItemCharacter.draw(in: bulletDrawRect, withAttributes: attributes)
      }
    } catch {
      print("\(error)")
    }
  }

  public func tearDown() {
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical

public func formatBulletedList(editor: Editor) throws {
  return
}

private func isSelectingEmptyListItem(
  anchorNode: Node,
  nodes: [Node]
) -> Bool {
  guard let listItemNode = anchorNode as? ListItemNode else {
    return false
  }
  if nodes.count == 0 {
    return true
  }
  if let firstNode = nodes.first {
    return (listItemNode.key == firstNode.key && listItemNode.getChildrenSize() == 0)
  }
  return false
}

/**
 * Creates a ListNode of listType.
 * @param listType - The type of list to be created. Can be 'number', 'bullet', or 'check'.
 * @param start - Where an ordered list starts its count, start = 1 if left undefined.
 * @returns The new ListNode
 */
public func createListNode(listType: ListType, start: Int = 1) -> ListNode {
  return ListNode(listType: listType, start: start)
}

private func createListOrMerge(node: ElementNode, listType: ListType) throws -> ListNode {
  if let node = node as? ListNode {
    return node
  }

  let previousSibling = node.getPreviousSibling()
  let nextSibling = node.getNextSibling()
  let listItem = ListItemNode()
  //  listItem.setFormat(node.getFormatType());
  try listItem.setIndent(node.getIndent())
  try listItem.append(node.getChildren())

  if let previousSibling = previousSibling as? ListNode, listType == previousSibling.getListType() {
    try previousSibling.append([listItem])
    try node.remove()
    // if the same type of list is on both sides, merge them.

    if let nextSibling = nextSibling as? ListNode, listType == nextSibling.getListType() {
      try previousSibling.append(nextSibling.getChildren())
      try nextSibling.remove()
    }
    return previousSibling
  } else if let nextSibling = nextSibling as? ListNode, listType == nextSibling.getListType() {
    guard let nextSiblingFirstChild = nextSibling.getFirstChild() else {
      throw LexicalError.invariantViolation("no next sibling first child")
    }
    try nextSiblingFirstChild.insertBefore(nodeToInsert: listItem)
    try node.remove()
    return nextSibling
  } else {
    let list = createListNode(listType: listType)
    try list.append([listItem])
    try node.replace(replaceWith: list)
    try updateChildrenListItemValue(list: list, children: nil)
    return list
  }
}

private func getListItemValue(listItem: ListItemNode) throws -> Int {
  let list = listItem.getParent()

  var value = 1

  if let list {
    guard let list = list as? ListNode else {
      throw LexicalError.invariantViolation("list node is not parent of list item node")
    }
    value = list.getStart()
  }

  let siblings = listItem.getPreviousSiblings()
  for sibling in siblings {
    if let sibling = sibling as? ListItemNode, let firstChild = sibling.getFirstChild(), !(firstChild is ListNode) {
      value += 1
    }
  }
  return value
}

/**
 * Takes the value of a child ListItemNode and makes it the value the ListItemNode
 * should be if it isn't already. If only certain children should be updated, they
 * can be passed optionally in an array.
 * @param list - The list whose children are updated.
 * @param children - An array of the children to be updated.
 */
public func updateChildrenListItemValue(
  list: ListNode,
  children: [ListNode]? = nil
) throws {
  let childrenOrExisting = children ?? list.getChildren()
  for child in childrenOrExisting {
    if let child = child as? ListItemNode {
      let prevValue = child.getValue()
      let nextValue = try getListItemValue(listItem: child)

      if prevValue != nextValue {
        child.setValue(value: nextValue)
      }
    }
  }
}

/**
 * Inserts a new ListNode. If the selection's anchor node is an empty ListItemNode and is a child of
 * the root/shadow root, it will replace the ListItemNode with a ListNode and the old ListItemNode.
 * Otherwise it will replace its parent with a new ListNode and re-insert the ListItemNode and any previous children.
 * If the selection's anchor node is not an empty ListItemNode, it will add a new ListNode or merge an existing ListNode,
 * unless the the node is a leaf node, in which case it will attempt to find a ListNode up the branch and replace it with
 * a new ListNode, or create a new ListNode at the nearest root/shadow root.
 * @param editor - The lexical editor.
 * @param listType - The type of list, "number" | "bullet" | "check".
 */
public func insertList(editor: Editor, listType: ListType) throws {
  try editor.update {
    guard let selection = try getSelection() else {
      throw LexicalError.invariantViolation("no selection")
    }

    guard let selection = selection as? RangeSelection else {
      return
    }
    let nodes = try selection.getNodes()
    let anchor = selection.anchor
    let anchorNode = try anchor.getNode()
    let anchorNodeParent = anchorNode.getParent()

    if isSelectingEmptyListItem(anchorNode: anchorNode, nodes: nodes) {
      let list = createListNode(listType: listType)

      if isRootNode(node: anchorNodeParent) {
        try anchorNode.replace(replaceWith: list)
        let listItem = ListItemNode()
        if let anchorNode = anchorNode as? ElementNode {
          //          listItem.setFormat(anchorNode.getFormatType())
          try listItem.setIndent(anchorNode.getIndent())
        }
        try list.append([listItem])
      } else if let anchorNode = anchorNode as? ListItemNode {
        let parent = try anchorNode.getParentOrThrow()
        try list.append(parent.getChildren())
        try parent.replace(replaceWith: list)
      }

      return
    } else {
      var handled: Set<NodeKey> = Set()
      for node in nodes {
        if let node = node as? ElementNode,
           node.isEmpty(),
           !handled.contains(node.getKey()) {
          _ = try createListOrMerge(node: node, listType: listType)
          continue
        }

        if isLeafNode(node) {
          var parent = node.getParent()
          while let parentIterator = parent {
            let parentKey = parentIterator.getKey()

            if let parent = parentIterator as? ListNode {
              if !handled.contains(parentKey) {
                let newListNode = createListNode(listType: listType)
                try newListNode.append(parent.getChildren())
                try parent.replace(replaceWith: newListNode)
                try updateChildrenListItemValue(list: newListNode, children: nil)
                handled.insert(parentKey)
              }

              break
            } else {
              let nextParent = parentIterator.getParent()

              if
                isRootNode(node: nextParent) &&
                  !handled.contains(parentKey) {
                handled.insert(parentKey)
                _ = try createListOrMerge(node: parentIterator, listType: listType)
                break
              }

              parent = nextParent
            }
          }
        }
      }
    }
  }
}

/**
 * Checks to see if the passed node is a ListItemNode and has a ListNode as a child.
 * @param node - The node to be checked.
 * @returns true if the node is a ListItemNode and has a ListNode child, false otherwise.
 */
public func isNestedListNode(node: Node?) -> Bool {
  if let node = node as? ListItemNode {
    return node.getFirstChild() is ListNode
  }
  return false
}

/**
 * A recursive function that goes through each list and their children, including nested lists,
 * appending list2 children after list1 children and updating ListItemNode values.
 * @param list1 - The first list to be merged.
 * @param list2 - The second list to be merged.
 */
public func mergeLists(list1: ListNode, list2: ListNode) throws {
  let listItem1 = list1.getLastChild()
  let listItem2 = list2.getFirstChild()

  if
    let listItem1 = listItem1 as? ListItemNode,
    let listItem2 = listItem2 as? ListItemNode,
    isNestedListNode(node: listItem1),
    isNestedListNode(node: listItem2),
    let child1 = listItem1.getFirstChild() as? ListNode,
    let child2 = listItem2.getFirstChild() as? ListNode
  {
    try mergeLists(list1: child1, list2: child2)
    try listItem2.remove()
  }

  let toMerge = list2.getChildren()
  if !toMerge.isEmpty {
    try list1.append(toMerge)
    try updateChildrenListItemValue(list: list1, children: nil)
  }

  try list2.remove()
}

/**
 * Adds an empty ListNode/ListItemNode chain at listItemNode, so as to
 * create an indent effect. Won't indent ListItemNodes that have a ListNode as
 * a child, but does merge sibling ListItemNodes if one has a nested ListNode.
 * @param listItemNode - The ListItemNode to be indented.
 */
internal func handleIndent(_ listItemNode: ListItemNode) throws {
  // go through each node and decide where to move it.
  var removed: Set<NodeKey> = Set()

  if isNestedListNode(node: listItemNode) {
    return
  }

  let parent = listItemNode.getParent()

  let nextSibling = listItemNode.getNextSibling() as? ListItemNode
  let previousSibling = listItemNode.getPreviousSibling() as? ListItemNode
  // if there are nested lists on either side, merge them all together.

  if isNestedListNode(node: nextSibling) && isNestedListNode(node: previousSibling) {
    let innerList = previousSibling?.getFirstChild()

    if let innerList = innerList as? ListNode {
      try innerList.append([listItemNode])
      let nextInnerList = nextSibling?.getFirstChild()

      if let nextSibling, let nextInnerList = nextInnerList as? ListNode {
        let children = nextInnerList.getChildren()
        try innerList.append(children)
        try nextSibling.remove()
        removed.insert(nextSibling.getKey())
      }
      try updateChildrenListItemValue(list: innerList)
    }
  } else if let nextSibling, isNestedListNode(node: nextSibling) {
    // if the ListItemNode is next to a nested ListNode, merge them
    let innerList = nextSibling.getFirstChild()

    if let innerList = innerList as? ListNode {
      let firstChild = innerList.getFirstChild()

      if let firstChild {
        try firstChild.insertBefore(nodeToInsert: listItemNode)
      }
      try updateChildrenListItemValue(list: innerList)
    }
  } else if isNestedListNode(node: previousSibling) {
    let innerList = previousSibling?.getFirstChild()

    if let innerList = innerList as? ListNode {
      try innerList.append([listItemNode])
      try updateChildrenListItemValue(list: innerList)
    }
  } else {
    // otherwise, we need to create a new nested ListNode

    if let parent = parent as? ListNode {
      let newListItem = ListItemNode()
      let newList = createListNode(listType: parent.getListType())
      try newListItem.append([newList])
      try newList.append([listItemNode])

      if let previousSibling {
        _ = try previousSibling.insertAfter(nodeToInsert: newListItem)
      } else if let nextSibling {
        try nextSibling.insertBefore(nodeToInsert: newListItem)
      } else {
        try parent.append([newListItem])
      }
      try updateChildrenListItemValue(list: newList)
    }
  }

  if let parent = parent as? ListNode {
    try updateChildrenListItemValue(list: parent)
  }
}

/**
 * Removes an indent by removing an empty ListNode/ListItemNode chain. An indented ListItemNode
 * has a great grandparent node of type ListNode, which is where the ListItemNode will reside
 * within as a child.
 * @param listItemNode - The ListItemNode to remove the indent (outdent).
 */
internal func handleOutdent(_ listItemNode: ListItemNode) throws {
  // go through each node and decide where to move it.

  if isNestedListNode(node: listItemNode) {
    return
  }
  let parentList = listItemNode.getParent()
  let grandparentListItem = parentList?.getParent()
  let greatGrandparentList = grandparentListItem?.getParent()
  // If it doesn't have these ancestors, it's not indented.

  if let greatGrandparentList = greatGrandparentList as? ListNode,
     let grandparentListItem = grandparentListItem as? ListItemNode,
     let parentList = parentList as? ListNode {
    // if it's the first child in it's parent list, insert it into the
    // great grandparent list before the grandparent
    let firstChild = parentList.getFirstChild()
    let lastChild = parentList.getLastChild()

    if let firstChild, listItemNode.isSameNode(firstChild) {
      try grandparentListItem.insertBefore(nodeToInsert: listItemNode)

      if parentList.isEmpty() {
        try grandparentListItem.remove()
      }
      // if it's the last child in it's parent list, insert it into the
      // great grandparent list after the grandparent.
    } else if let lastChild, listItemNode.isSameNode(lastChild) {
      _ = try grandparentListItem.insertAfter(nodeToInsert: listItemNode)

      if parentList.isEmpty() {
        try grandparentListItem.remove()
      }
    } else {
      // otherwise, we need to split the siblings into two new nested lists
      let listType = parentList.getListType()
      let previousSiblingsListItem = ListItemNode()
      let previousSiblingsList = createListNode(listType: listType)
      try previousSiblingsListItem.append([previousSiblingsList])
      try previousSiblingsList.append(listItemNode.getPreviousSiblings())
      let nextSiblingsListItem = ListItemNode()
      let nextSiblingsList = createListNode(listType: listType)
      try nextSiblingsListItem.append([nextSiblingsList])
      try nextSiblingsList.append(listItemNode.getNextSiblings())
      // put the sibling nested lists on either side of the grandparent list item in the great grandparent.
      try grandparentListItem.insertBefore(nodeToInsert: previousSiblingsListItem)
      _ = try grandparentListItem.insertAfter(nodeToInsert: nextSiblingsListItem)
      // replace the grandparent list item (now between the siblings) with the outdented list item.
      _ = try grandparentListItem.replace(replaceWith: listItemNode)
    }
    try updateChildrenListItemValue(list: parentList)
    try updateChildrenListItemValue(list: greatGrandparentList)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical

let mentionCaptureGroupName = "mention"

public struct MentionMatch {
  public var range: NSRange
  public var text: String
}

public enum MentionsMode {
  case users
  case bots
}

public typealias OnSearchForMentions = (_ match: MentionMatch) -> Void
public typealias OnClearMentionSearch = () -> Void

open class MentionsPlugin: Plugin {
  public var onSearchForMentions: OnSearchForMentions?
  public var onClearMentionSearch: OnClearMentionSearch?

  var unregister: (() -> Void)?

  // Update listener state
  var previousText: String?
  var matchRange: NSRange?

  let modes: Set<MentionsMode>
  var regexByMode: [MentionsMode: String] = [:]

  public init(modes: Set<MentionsMode>) {
    self.modes = modes

    for mode in modes {
      regexByMode[mode] = regex(for: mode)
    }
  }

  public func setUp(editor: Editor) {
    unregister = editor.registerUpdateListener(listener: updateListener)

    do {
      try editor.registerNode(nodeType: NodeType.mention, class: MentionNode.self)
    } catch {}
  }

  public func tearDown() {
    unregister?()
  }

  func updateListener(activeEditorState: EditorState, _ previousEditorState: EditorState, _ dirtyNodes: DirtyNodeMap) {
    do {
      let text = try getMentionsTextToSearch(activeEditorState: activeEditorState)

      if text == previousText {
        return
      }

      previousText = text

      guard let text, let match = getPossibleMentionMatch(text: text) else {
        onClearMentionSearch?()

        return
      }

      onSearchForMentions?(match)
    } catch {}
  }

  func getMentionsTextToSearch(activeEditorState: EditorState) throws -> String? {
    var text: String?

    try activeEditorState.read {
      guard let selection = try getSelection() as? RangeSelection else {
        return
      }

      text = getTextUpToAnchor(selection: selection)
    }

    return text
  }

  func getTextUpToAnchor(selection: RangeSelection) -> String? {
    let anchor = selection.anchor

    if anchor.getType() != .text {
      return nil
    }

    do {
      let anchorNode = try anchor.getNode()

      // We should not be attempting to extract mentions out of nodes
      // that are already being used for other core things. This is
      // especially true for immutable nodes, which can't be mutated at all.
      if let textNode = anchorNode as? TextNode, !textNode.isSimpleText() {
        return nil
      }

      let anchorOffset = anchor.getOffset()

      return (anchorNode.getTextContent() as NSString).substring(to: anchorOffset)
    } catch {
      return nil
    }
  }

  func regex(for mode: MentionsMode) -> String {
    let prefixSymbol: String
    switch mode {
    case .users:
      prefixSymbol = "@"
    case .bots:
      prefixSymbol = "#"
    }
    return "(^|\\s|\\()(?<\(mentionCaptureGroupName)>"
      + prefixSymbol
      + "[a-zA-Z0-9_.\\s]{0,75})"
  }

  func getPossibleMentionMatch(text: String) -> MentionMatch? {
    do {
      for mode in modes {
        // Can't be nil as regexes for all modes are populated during init
        let regex = regexByMode[mode]! // swiftlint:disable:this force_unwrapping
        if let result = try checkForMentions(text: text, regex: regex, minMatchLength: 1) {
          return result
        }
      }
    } catch {
      return nil
    }

    return nil
  }

  func checkForMentions(
    text: String,
    regex: String,
    minMatchLength: Int
  ) throws -> MentionMatch? {
    let nameRegex = try NSRegularExpression(
      pattern: regex,
      options: .caseInsensitive
    )
    let searchRange = NSRange(location: 0, length: text.lengthAsNSString())

    let matches = nameRegex.matches(
      in: text,
      options: [],
      range: searchRange
    )

    guard let match = matches.first else {
      return nil
    }

    matchRange = match.range(withName: mentionCaptureGroupName)

    guard let matchRange else {
      return nil
    }

    if let substringRange = Range(matchRange, in: text) {
      let capture = String(text[substringRange])

      return MentionMatch(
        range: matchRange,
        text: capture
      )
    }

    return nil
  }

  public func onSelectMention(editor: Editor?, mentionName: String, mentionID: String) throws {
    guard let editor else {
      return
    }

    try editor.update {
      guard let selection = try getSelection() as? RangeSelection else { return }

      let anchor = selection.anchor

      guard let anchorNode = try anchor.getNode() as? TextNode, anchorNode.isSimpleText(), let matchRange else {
        return
      }

      let textNodesSplitByMention = try anchorNode.splitText(splitOffsets: [matchRange.lowerBound, matchRange.upperBound])
      var nodeToReplace: TextNode

      if matchRange.lowerBound == 0 {
        nodeToReplace = textNodesSplitByMention[0]
      } else {
        nodeToReplace = textNodesSplitByMention[1]
      }

      let mentionNode = createMentionNode(mention: mentionID, text: mentionName)
      try nodeToReplace.replace(replaceWith: mentionNode)
      try mentionNode.select(anchorOffset: nil, focusOffset: nil)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical
import UIKit

extension NodeType {
  static let mention = NodeType(rawValue: "mention")
}

public class MentionNode: TextNode {
  enum CodingKeys: String, CodingKey {
    case mention
  }

  private var mention: String = ""

  public required init(mention: String, text: String?, key: NodeKey?) {
    super.init(text: text ?? mention, key: key)
    self.mention = mention
    self.type = NodeType.mention
  }

  public required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    try super.init(from: decoder)
    self.mention = try container.decode(String.self, forKey: .mention)
    self.type = NodeType.mention
  }

  required init(text: String, key: NodeKey?) {
    super.init(text: text, key: key)
  }

  public func getMention() -> String {
    let node: MentionNode = getLatest()
    return node.mention
  }

  override public func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.mention, forKey: .mention)
  }

  override public func clone() -> Self {
    Self(mention: mention, text: getText_dangerousPropertyAccess(), key: key)
  }

  override public func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] {
    var attributeDictionary = super.getAttributedStringAttributes(theme: theme)
    attributeDictionary[.backgroundColor] = UIColor.lightGray
    return attributeDictionary
  }
}

public func createMentionNode(mention: String, text: String) -> TextNode {
  let result = MentionNode(mention: mention, text: text, key: nil)
  do {
    try result.setMode(mode: .segmented).toggleDirectionless()
  } catch {
    // Fail silently. It doesn't matter if a mention node is not segmented
  }
  return result
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical

open class StatePersistencePlugin: Plugin {
  weak var editor: Editor?
  private var unregister: (() -> Void)?

  public func setUp(editor: Editor) {
    self.editor = editor
  }

  public func tearDown() {
    unregister?()
  }

  public init() {}

  public func generateStateJsonString() throws -> String {
    guard let editor = getActiveEditor() else {
      throw LexicalError.invariantViolation("Could not get editor")
    }

    guard let rootNode = editor.getEditorState().getRootNode() else {
      throw LexicalError.invariantViolation("Could not get RootNode")
    }

    let persistedEditorState = SerializedEditorState(rootNode: rootNode)
    let encodedData = try JSONEncoder().encode(persistedEditorState)
    guard let jsonString = String(data: encodedData, encoding: .utf8) else { return "" }
    return jsonString
  }

  public func replaceTextWithJsonState(state: String) throws {
    guard let stateData = state.data(using: .utf8) else {
      throw LexicalError.internal("Could not generate data from string JSON state")
    }
    do {
      let decodedEditorState = try JSONDecoder().decode(SerializedEditorState.self, from: stateData)

      guard let rootNode = decodedEditorState.rootNode else {
        throw LexicalError.internal("Failed to decode RootNode")
      }

      guard let editor = getActiveEditor() else {
        throw LexicalError.internal("Could not get editor")
      }

      if !editor.isTextViewEmpty() {
        try editor.clearEditor()
      }

      guard let selection = try getSelection() as? RangeSelection else {
        throw LexicalError.internal("Could not get selection; TODO: support non-range-selections here")
      }

      _ = try insertGeneratedNodes(editor: editor, nodes: rootNode.getChildren(), selection: selection)
    } catch {
      throw LexicalError.internal("Error restoring text. Protecting selection")
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import AVFoundation
import Foundation
import Lexical
import UIKit

internal let minimumCellWidth: CGFloat = 150.0
let lineWidth = CGFloat(1.0)

public extension NodeType {
  static let table = NodeType(rawValue: "table")
}

extension NSAttributedString.Key {
  // table node also uses .backgroundColor built in attribute
  public static let borderColor: NSAttributedString.Key = .init(rawValue: "borderColor")
}

public class TableCell {
  internal init(textKitContext: LexicalReadOnlyTextKitContext, cachedHeight: CGFloat? = nil) {
    self.textKitContext = textKitContext
    self.cachedHeight = cachedHeight
  }

  var textKitContext: LexicalReadOnlyTextKitContext
  var cachedHeight: CGFloat?
  var cachedWidth: CGFloat?
  var cachedOrigin: CGPoint? // This is set by the TableNodeView when drawing
}

public class TableRow { // only public to use in initialiser
  var cells: [TableCell?] = []
}

public class TableNode: DecoratorNode {

  public enum ThemeSubtype {
    public static let tableDrawing = "tableDrawing"
  }

  var numColumns: Int = 0
  var numRows: Int = 0

  fileprivate var rows: [TableRow] = []

  public required init(numColumns: Int, numRows: Int, rows newRows: [TableRow]? = nil, key: NodeKey? = nil) {
    super.init(key)

    self.numColumns = numColumns
    self.numRows = numRows
    self.type = NodeType.table

    if let newRows {
      self.rows = newRows
      return
    }

    // No rows were passed in, so create new rows/cells

    // stuff to capture in listener block
    guard let parentEditor = getActiveEditor() else {
      fatalError()
    }
    let tableKey = self.key

    guard let editorConfigFactory = try? Self.editorConfigFactory() else { return }

    for r in 0..<numRows {
      let row = TableRow()
      rows.append(row)

      for c in 0..<numColumns {
        let cell = Self.createCell(editorConfig: editorConfigFactory(), parentEditor: parentEditor, tableKey: tableKey, row: r, col: c)
        row.cells.append(cell)

        do {
          try cell.textKitContext.editor.update {
            let node = TextNode()
            try node.setText("Row \(r) cell \(c) and a bunch more text")
            guard let para = getRoot()?.getFirstChild() as? ParagraphNode else { return }
            try para.append([node])
          }
        } catch {
          print("error \(error)")
        }
      }
    }
  }

  private static func editorConfigFactory() throws -> EditorConfigFactory {
    guard let tablePlugin = try? TablePlugin.installedInstance() else {
      throw LexicalError.invariantViolation("No table plugin installed instance")
    }
    return tablePlugin.editorConfigFactory
  }

  private static func createCell(editorConfig: EditorConfig, parentEditor: Editor, tableKey: NodeKey, row: Int, col: Int) -> TableCell {
    let textKitContext = LexicalReadOnlyTextKitContext(editorConfig: editorConfig, featureFlags: FeatureFlags())
    textKitContext.editor.parentEditor = parentEditor
    let cell = TableCell(textKitContext: textKitContext)

    _ = textKitContext.editor.registerUpdateListener { [weak parentEditor] activeEditorState, previousEditorState, dirtyNodes in
      try? parentEditor?.update {
        guard let tableNode = getNodeByKey(key: tableKey) as? TableNode else {
          return
        }
        tableNode.cellChangedSize(row: row, col: col)
      }
    }

    return cell
  }

  override public func didMoveTo(newEditor editor: Editor) {
    for row in rows {
      for cell in row.cells {
        if let cell {
          cell.textKitContext.editor.parentEditor = editor
        }
      }
    }
  }

  required init(_ key: NodeKey? = nil) {
    super.init(key)

    self.type = NodeType.table
  }

  public required init(from decoder: Decoder) throws {
    try super.init(from: decoder)

    self.type = NodeType.table
  }

  override public func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
  }

  override public func clone() -> Self {
    Self(numColumns: numColumns, numRows: numRows, rows: rows, key: key)
  }

  public func editorForCell(col: Int, row: Int) throws -> Editor {
    if col < 0 || col >= numColumns || row < 0 || row >= numRows {
      throw LexicalError.invariantViolation("Cell index out of bounds")
    }
    let rowObj = rows[row]
    let cell = rowObj.cells[col]

    if let cell {
      return cell.textKitContext.editor
    }

    let editorConfigFactory = try Self.editorConfigFactory()
    let tableKey = self.key
    guard let parentEditor = getActiveEditor() else {
      throw LexicalError.invariantViolation("Requires update block")
    }
    let newCell = Self.createCell(editorConfig: editorConfigFactory(), parentEditor: parentEditor, tableKey: tableKey, row: row, col: col)
    rowObj.cells[col] = newCell

    return newCell.textKitContext.editor
  }

  override public func createView() -> UIView {
    let tableNodeView = TableNodeView(frame: .zero)
    if let editor = getActiveEditor() {
      let theme = editor.getTheme()
      if let tableTheme = theme.getValue(.table, withSubtype: ThemeSubtype.tableDrawing) {
        if let borderColor = tableTheme[.borderColor] as? UIColor {
          tableNodeView.borderColor = borderColor
        }
        if let backgroundColor = tableTheme[NSAttributedString.Key.backgroundColor] as? UIColor {
          tableNodeView.backgroundColor = backgroundColor
        }
      }
    }

    let scrollableWrapperView = TableNodeScrollableWrapperView(frame: .zero)
    scrollableWrapperView.tableNodeView = tableNodeView

    return scrollableWrapperView
  }

  override open func decorate(view: UIView) {
    if let wrapperView = view as? TableNodeScrollableWrapperView, let tableNodeView = wrapperView.tableNodeView {
      tableNodeView.numColumns = getLatest().numColumns
      tableNodeView.numRows = getLatest().numRows
      tableNodeView.rows = rows
    }
  }

  override open func sizeForDecoratorView(textViewWidth: CGFloat, attributes: [NSAttributedString.Key: Any]) -> CGSize {

    // the +1 is because there is one more line than columns
    var cellWidth = floor((Double(textViewWidth) - (Double(numColumns + 1) * lineWidth)) / Double(numColumns))

    if cellWidth < minimumCellWidth {
      cellWidth = minimumCellWidth
    }

    var cumulativeHeight = CGFloat(0)

    for row in getLatest().rows {
      var maxRowSize = CGFloat(0)
      for cell in row.cells {
        if let cell, cellWidth != cell.cachedWidth {
          cell.cachedWidth = cellWidth
          cell.textKitContext.setTextContainerSizeWithUnlimitedHeight(forWidth: cellWidth)
          cell.cachedHeight = cell.textKitContext.requiredSize().height
        }
        maxRowSize = max(cell?.cachedHeight ?? 0, maxRowSize)
      }
      cumulativeHeight += maxRowSize
      cumulativeHeight += lineWidth
    }
    cumulativeHeight += lineWidth
    return CGSize(width: min(((cellWidth + lineWidth) * Double(numColumns)) + lineWidth, textViewWidth), height: cumulativeHeight)
  }

  fileprivate func cellChangedSize(row rowNum: Int, col colNum: Int) {
    guard let node = try? getWritable() else { return }
    let row = node.rows[rowNum]
    if let cell = row.cells[colNum] {
      cell.cachedHeight = cell.textKitContext.requiredSize().height
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Lexical
import UIKit

class TableNodeScrollableWrapperView: UIView {

  let scrollView: UIScrollView

  override init(frame: CGRect) {
    scrollView = UIScrollView()
    super.init(frame: frame)
    self.addSubview(scrollView)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  var tableNodeView: TableNodeView? {
    didSet {
      oldValue?.removeFromSuperview()
      if let tableNodeView {
        scrollView.addSubview(tableNodeView)
      }
    }
  }

  override func layoutSubviews() {
    super.layoutSubviews()

    scrollView.frame = self.bounds

    guard let tableNodeView else { return }

    let contentWidth = max((minimumCellWidth + lineWidth) * Double(tableNodeView.numColumns) + lineWidth, self.bounds.width)
    scrollView.contentSize = CGSize(width: contentWidth, height: self.bounds.height)
    tableNodeView.frame = CGRect(x: 0, y: 0, width: contentWidth, height: self.bounds.height)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Lexical
import UIKit

class TableNodeView: UIView {

  internal var borderColor: UIColor = .lightGray

  override init(frame: CGRect) {
    super.init(frame: frame)
    let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(gestureRecognizer:)))
    addGestureRecognizer(gestureRecognizer)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  internal var numColumns: Int = 1 {
    didSet {
      setNeedsDisplay()
    }
  }

  internal var numRows: Int = 1 {
    didSet {
      setNeedsDisplay()
    }
  }

  override var frame: CGRect {
    didSet {
      setNeedsDisplay()
    }
  }

  internal var rows: [TableRow] = [] {
    didSet {
      setNeedsDisplay()
    }
  }

  override func draw(_ rect: CGRect) {
    // Currently: assuming all cells in first row are populated, and column widths are defined by cells in first row
    guard let context = UIGraphicsGetCurrentContext() else { return }
    let lineWidth: CGFloat = 1.0
    context.setLineWidth(lineWidth)
    context.setStrokeColor(borderColor.cgColor) // TODO: fetch this from attributes
    context.setFillColor((backgroundColor ?? UIColor.white).cgColor)

    // All TextKit drawing is done after all custom table drawing, in order to avoid missing lines.
    var textKitDrawQueue: [TableCell] = []

    // 1. Cache widths
    guard let firstRow = rows.first else { return }
    var columnWidths: [CGFloat] = []
    var totalWidth: CGFloat = 0.0
    for cell in firstRow.cells {
      guard let cell, let width = cell.cachedWidth else { return }
      columnWidths.append(width)
      totalWidth += lineWidth
      totalWidth += width
    }
    totalWidth += lineWidth // for the line drawn after the cells

    // 2. Draw rows, including lines, and cache cumulative height
    var cumulativeHeight: CGFloat = 0.0
    for row in rows {
      // draw boundary before
      context.move(to: CGPoint(x: 0.5, y: cumulativeHeight + 0.5))
      context.addLine(to: CGPoint(x: totalWidth + 0.5, y: cumulativeHeight + 0.5))

      // draw cells, track cumulative width
      var cumulativeWidth = lineWidth
      var thisHeight: CGFloat = 0.0
      for (i, cell) in row.cells.enumerated() {
        guard let cell else { continue }
        let drawingPoint = CGPoint(x: cumulativeWidth, y: cumulativeHeight)
        cell.cachedOrigin = drawingPoint // cache the point that the cell needs drawing at
        textKitDrawQueue.append(cell) // queue up this cell for later drawing
        cumulativeWidth += columnWidths[i]
        cumulativeWidth += lineWidth
        thisHeight = max(thisHeight, cell.cachedHeight ?? 0)
      }
      cumulativeHeight += thisHeight
    }

    // divider after last row
    context.move(to: CGPoint(x: 0.5, y: cumulativeHeight + 0.5))
    context.addLine(to: CGPoint(x: totalWidth + 0.5, y: cumulativeHeight + 0.5))

    // 3. Draw vertical lines
    var cumulativeWidth = 0.0
    for colWidth in columnWidths {
      context.move(to: CGPoint(x: cumulativeWidth + 0.5, y: 0.5))
      context.addLine(to: CGPoint(x: cumulativeWidth + 0.5, y: cumulativeHeight + 0.5))
      cumulativeWidth += lineWidth
      cumulativeWidth += colWidth
    }

    // divider after last col
    context.move(to: CGPoint(x: cumulativeWidth + 0.5, y: 0.5))
    context.addLine(to: CGPoint(x: cumulativeWidth + 0.5, y: cumulativeHeight + 0.5))

    context.strokePath()

    // Now it's safe to do TextKit drawing
    for cell in textKitDrawQueue {
      guard let point = cell.cachedOrigin else { continue }
      cell.textKitContext.draw(inContext: context, point: point)
    }
  }

  @objc internal func handleTap(gestureRecognizer: UITapGestureRecognizer) {
    let pointInView = gestureRecognizer.location(in: self)

    for row in rows {
      for cell in row.cells {
        if let cell {
          let textKitContext = cell.textKitContext
          guard let textContainerOrigin = cell.cachedOrigin else { continue }
          let pointInTextContainer = CGPoint(x: pointInView.x - textKitContext.textContainerInsets.left - textContainerOrigin.x,
                                             y: pointInView.y - textKitContext.textContainerInsets.top - textContainerOrigin.y)
          let textContainerRelativeRect = CGRect(x: 0, y: 0, width: cell.cachedWidth ?? 0, height: cell.cachedHeight ?? 0)
          if !textContainerRelativeRect.contains(pointInTextContainer) { continue }
          let indexOfCharacter = textKitContext.layoutManager.characterIndex(for: pointInTextContainer,
                                                                             in: textKitContext.textContainer,
                                                                             fractionOfDistanceBetweenInsertionPoints: nil)
          if indexOfCharacter >= textKitContext.textStorage.length { continue }
          let attributes = textKitContext.textStorage.attributes(at: indexOfCharacter, effectiveRange: nil)

          if let link = attributes[.link] {
            if let link = link as? URL {
              textKitContext.editor.dispatchCommand(type: .linkTapped, payload: link)
            } else if let link = link as? String {
              if let linkURL = URL(string: link) {
                textKitContext.editor.dispatchCommand(type: .linkTapped, payload: linkURL)
              }
            }
          }
        }
      }
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical

extension CommandType {
  public static let tableNodeEditorConfig = CommandType(rawValue: "tableNodeEditorConfig")
}

public typealias EditorConfigFactory = () -> EditorConfig

public class TablePlugin: Plugin {
  public init(editorConfigFactory: @escaping EditorConfigFactory) {
    self.editorConfigFactory = editorConfigFactory
  }

  weak var editor: Editor?

  internal var editorConfigFactory: EditorConfigFactory

  public func setUp(editor: Editor) {
    self.editor = editor

    do {
      try editor.registerNode(nodeType: NodeType.table, class: TableNode.self)
    } catch {
      editor.log(.other, .error, "\(error)")
    }
  }

  public func tearDown() {}
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import yoga;
postfix operator %

extension Int {
    public static postfix func %(value: Int) -> YGValue {
        return YGValue(value: Float(value), unit: .percent)
    }
}

extension Float {
    public static postfix func %(value: Float) -> YGValue {
        return YGValue(value: value, unit: .percent)
    }
}

extension CGFloat {
    public static postfix func %(value: CGFloat) -> YGValue {
        return YGValue(value: Float(value), unit: .percent)
    }
}

extension YGValue : ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral {
    public init(integerLiteral value: Int) {
        self = YGValue(value: Float(value), unit: .point)
    }

    public init(floatLiteral value: Float) {
        self = YGValue(value: value, unit: .point)
    }

    public init(_ value: Float) {
        self = YGValue(value: value, unit: .point)
    }

    public init(_ value: CGFloat) {
        self = YGValue(value: Float(value), unit: .point)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func applicationDidFinishLaunching(_ application: UIApplication) {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        if let window = self.window {
            let navigationController = UINavigationController(rootViewController: ExamplesViewController())
            navigationController.navigationBar.isTranslucent = false

            window.rootViewController = navigationController
            window.backgroundColor = .white
            window.makeKeyAndVisible()
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit
import IGListKit

private final class ExampleModel {
    let title: String
    let controllerClass: UIViewController.Type

    init(title: String, controllerClass: UIViewController.Type) {
        self.title = title
        self.controllerClass = controllerClass
    }
}

extension ExampleModel: ListDiffable {
    fileprivate func diffIdentifier() -> NSObjectProtocol {
        return title as NSString
    }

    fileprivate func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        guard let otherObj = object as? ExampleModel else { return false }

        return (title == otherObj.title) &&
               (controllerClass == otherObj.controllerClass)
    }
}

final class ExamplesViewController: UIViewController, ListAdapterDataSource, ListSingleSectionControllerDelegate {

    private lazy var adapter: ListAdapter = {
        return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
    }()
    private lazy var collectionView: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        collectionView.backgroundColor = UIColor.systemBackground
        return collectionView
    }()

    // Update this to array to create more examples.
    private let models: [ExampleModel] = [ExampleModel(title: "Basic Layout", controllerClass: BasicViewController.self),
                                          ExampleModel(title: "Exclude Views in Layout", controllerClass: LayoutInclusionViewController.self)]

    //MARK: UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Examples"
        view.addSubview(collectionView)
        adapter.collectionView = collectionView
        adapter.dataSource = self
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        collectionView.frame = view.bounds
    }

    //MARK: IGListAdapterDataSource

    func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
        return models as [ListDiffable]
    }

    func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
        let sizeBlock: ListSingleSectionCellSizeBlock = { (model, context) in
            return CGSize(width: (context?.containerSize.width)!, height: 75.0)
        }

        let configureBlock: ListSingleSectionCellConfigureBlock = { (model, cell) in
            guard let m = model as? ExampleModel, let c = cell as? SingleLabelCollectionCell else {
                return
            }

            c.label.text = m.title
        }

        let sectionController = ListSingleSectionController(cellClass: SingleLabelCollectionCell.self,
                                                              configureBlock: configureBlock,
                                                              sizeBlock: sizeBlock)
        sectionController.selectionDelegate = self
        return sectionController
    }

    func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil }

    //MARK: IGListSingleSectionControllerDelegate
    func didSelect(_ sectionController: ListSingleSectionController, with object: Any) {
        let section = adapter.section(for: sectionController)
        let model = models[section]

        let controller = model.controllerClass.init()
        controller.title = model.title

        self.navigationController?.pushViewController(controller, animated: true)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit
import IGListKit
import YogaKit

struct DemoItem {
    let name: String
    root.backgroundColor = .red
    root.yoga.isEnabled = true
    root.yoga.width = YGValue(self.view.bounds.size.width)
    root.yoga.height = YGValue(self.view.bounds.size.height)
    root.yoga.alignItems = .center
}

final class SwiftViewController: UIViewController, IGListAdapterDataSource {

    lazy var adapter: IGListAdapter = {
        return IGListAdapter(updater: IGListAdapterUpdater(), viewController: self, workingRangeSize: 0)
    }()
    let collectionView = IGListCollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())

    //MARK: UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "YogaKit Examples"
        view.addSubview(collectionView)
        adapter.collectionView = collectionView
        adapter.dataSource = self
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        collectionView.frame = view.bounds
    }

    //MARK: IGListAdapterDataSource

    func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] {
        return ["Dustin" as IGListDiffable, "Ryan" as IGListDiffable]
    }

    func listAdapter(_ listAdapter: IGListAdapter, sectionControllerFor object: Any) -> IGListSectionController {
        let sizeBlock: IGListSingleSectionCellSizeBlock = { (model, context) in
            return CGSize(width: (context?.containerSize.width)!, height: 100.0)
        }
        let configureBlock: IGListSingleSectionCellConfigureBlock = { (model, cell) in
            guard let m = model as? String else {
                return
            }

            cell.backgroundColor = (m == "Dustin") ? .blue : .red
        }

        return IGListSingleSectionController(cellClass: UICollectionViewCell.self,
                                             configureBlock: configureBlock,
                                             sizeBlock: sizeBlock)
    }

    func emptyView(for listAdapter: IGListAdapter) -> UIView? {
        return nil
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit
import YogaKit

final class BasicViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let containerSize = self.view.bounds.size

        view.backgroundColor = UIColor.systemBackground
        view.configureLayout { (layout) in
            layout.isEnabled = true
            layout.width = YGValue(containerSize.width)
            layout.height = YGValue(containerSize.height)
            layout.alignItems = .center
            layout.justifyContent = .center
        }

        let child1 = UIView()
        child1.backgroundColor = .blue
        child1.configureLayout { (layout) in
            layout.isEnabled = true
            layout.width = 100
            layout.height = 10
            layout.marginBottom = 25
        }
        view.addSubview(child1)

        let child2 = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        child2.backgroundColor = .green
        child2.configureLayout { (layout) in
            layout.isEnabled = true
            layout.alignSelf = .flexEnd
            layout.width = 200
            layout.height = 200
        }
        view.addSubview(child2)

        let child3 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        child3.backgroundColor = .yellow
        child3.configureLayout { (layout) in
            layout.isEnabled = true
            layout.alignSelf = .flexStart
            layout.width = 100
            layout.height = 100

        }
        view.addSubview(child3)

        view.yoga.applyLayout(preservingOrigin: true)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit
import YogaKit

final class LayoutInclusionViewController: UIViewController {
    private let button: UIButton = UIButton(type: .system)
    private let disappearingView: UIView = UIView(frame: .zero)
    private let contentView: UIView = UIView(frame: .zero)

    override func viewDidLoad() {
        super.viewDidLoad()
        let root = self.view!
        root.backgroundColor = .white
        root.configureLayout { (layout) in
            layout.isEnabled = true
            layout.flexDirection = .column
            layout.justifyContent = .spaceAround
        }

        contentView.backgroundColor = .clear
        contentView.layer.borderColor = UIColor.lightGray.cgColor
        contentView.layer.borderWidth = 1.0
        contentView.configureLayout { (layout) in
            layout.isEnabled = true
            layout.height = 300
            layout.width = YGValue(self.view.bounds.size.width)
            layout.flexDirection = .row
            layout.justifyContent = .center
            layout.paddingHorizontal = 25
        }
        self.view.addSubview(contentView)

        let redView = UIView(frame: .zero)
        redView.backgroundColor = .red
        redView.configureLayout { (layout) in
            layout.isEnabled = true
            layout.flexGrow = 1
            layout.flexShrink = 1
        }
        contentView.addSubview(redView)

        disappearingView.backgroundColor = .blue
        disappearingView.configureLayout { (layout) in
            layout.isEnabled = true
            layout.flexGrow = 1
        }
        contentView.addSubview(disappearingView)

        button.setTitle("Add Blue View", for: .selected)
        button.setTitle("Remove Blue View", for: .normal)
        button.addTarget(self, action: #selector(buttonWasTapped), for: .touchUpInside)
        button.configureLayout { (layout) in
            layout.isEnabled = true
            layout.height = 300
            layout.width = 300
            layout.alignSelf = .center
        }
        root.addSubview(button)

        root.yoga.applyLayout(preservingOrigin: false)
    }

    // MARK - UIButton Action
    @objc func buttonWasTapped() {
        button.isSelected = !button.isSelected

        button.isUserInteractionEnabled = false
        disappearingView.yoga.isIncludedInLayout = !disappearingView.yoga.isIncludedInLayout
        disappearingView.isHidden = !disappearingView.isHidden

        contentView.yoga.applyLayout(preservingOrigin: true)
        button.isUserInteractionEnabled = true
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit
import YogaKit

final class SingleLabelCollectionCell: UICollectionViewCell {
    let label: UILabel = UILabel(frame: .zero)

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.configureLayout { (layout) in
            layout.isEnabled = true
            layout.flexDirection = .column
            layout.justifyContent = .flexEnd
        }

        label.textAlignment = .center
        label.numberOfLines = 1
        label.yoga.isIncludedInLayout = false
        contentView.addSubview(label)

        let border = UIView(frame: .zero)
        border.backgroundColor = .lightGray
        border.configureLayout { (layout) in
            layout.isEnabled = true
            layout.height = 0.5
            layout.marginHorizontal = 25
        }
        contentView.addSubview(border)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        contentView.yoga.applyLayout(preservingOrigin: false)
        label.frame = contentView.bounds
    }
}


// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "zstd",
    platforms: [
        .macOS(.v10_10), .iOS(.v9), .tvOS(.v9)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "libzstd",
            targets: [ "libzstd" ])
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "libzstd",
            path: "lib",
            sources: [ "common", "compress", "decompress", "dictBuilder" ],
            publicHeadersPath: ".",
            cSettings: [
                .headerSearchPath(".")
            ])
    ],
    swiftLanguageVersions: [.v5],
    cLanguageStandard: .gnu11,
    cxxLanguageStandard: .gnucxx14
)


import UIKit
import Flutter
import integration_test

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    if #available(iOS 10.0, *) {
      UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
    }

    GeneratedPluginRegistrant.register(with: self)
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    IntegrationTestPlugin.instance().setupChannels(controller.binaryMessenger)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}