chromium/ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.mm

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.h"

#import <MaterialComponents/MaterialSnackbar.h>
#import <stdint.h>

#import <algorithm>
#import <iterator>
#import <memory>
#import <vector>

#import "base/check.h"
#import "base/containers/contains.h"
#import "base/containers/flat_map.h"
#import "base/hash/hash.h"
#import "base/i18n/message_formatter.h"
#import "base/i18n/string_compare.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/notreached.h"
#import "base/ranges/algorithm.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/bookmarks/browser/bookmark_utils.h"
#import "components/bookmarks/common/bookmark_features.h"
#import "components/bookmarks/common/bookmark_metrics.h"
#import "components/query_parser/query_parser.h"
#import "components/signin/public/identity_manager/account_info.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "components/url_formatter/url_fixer.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_storage_type.h"
#import "ios/chrome/browser/bookmarks/model/bookmarks_utils.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/undo_manager_wrapper.h"
#import "ios/chrome/browser/ntp/shared/metrics/home_metrics.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/util/snackbar_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/grit/ios_strings.h"
#import "third_party/skia/include/core/SkColor.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/base/models/tree_node_iterator.h"

using bookmarks::BookmarkNode;

namespace bookmark_utils_ios {

NSString* const kBookmarksSnackbarCategory = @"BookmarksSnackbarCategory";

namespace {

void RemoveBookmarksRecursive(const std::set<const BookmarkNode*>& bookmarks,
                              bookmarks::BookmarkModel* model,
                              bookmarks::metrics::BookmarkEditSource source,
                              const BookmarkNode* node,
                              const base::Location& location) {
  // Remove children in reverse order, so that the index remains valid.
  for (size_t i = node->children().size(); i > 0; --i) {
    RemoveBookmarksRecursive(bookmarks, model, source,
                             node->children()[i - 1].get(), location);
  }

  if (base::Contains(bookmarks, node)) {
    model->Remove(node, source, location);
  }
}

}  // namespace

const BookmarkNode* FindFolderById(const bookmarks::BookmarkModel* model,
                                   int64_t id) {
  CHECK(model);
  const bookmarks::BookmarkNode* node =
      bookmarks::GetBookmarkNodeByID(model, id);
  return (node && node->is_folder()) ? node : nullptr;
}

NSString* TitleForBookmarkNode(const BookmarkNode* node) {
  NSString* title;

  if (node->type() == BookmarkNode::BOOKMARK_BAR) {
    title = l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_BOOKMARKS_BAR_TITLE);
  } else if (node->type() == BookmarkNode::MOBILE) {
    title = l10n_util::GetNSString(IDS_BOOKMARK_BAR_MOBILE_FOLDER_NAME);
  } else if (node->type() == BookmarkNode::OTHER_NODE) {
    title = l10n_util::GetNSString(IDS_BOOKMARK_BAR_OTHER_FOLDER_NAME);
  } else {
    title = base::SysUTF16ToNSString(node->GetTitle());
  }

  // Assign a default bookmark name if it is at top level.
  if (node->is_root() && ![title length]) {
    title = l10n_util::GetNSString(IDS_SYNC_DATATYPE_BOOKMARKS);
  }

  return title;
}

#pragma mark - Profile and account

BookmarkStorageType GetBookmarkStorageType(
    const BookmarkNode* bookmark_node,
    const bookmarks::BookmarkModel* bookmark_model) {
  DCHECK(bookmark_node);
  DCHECK(bookmark_model);
  return bookmark_model->IsLocalOnlyNode(*bookmark_node)
             ? BookmarkStorageType::kLocalOrSyncable
             : BookmarkStorageType::kAccount;
}

bool IsAccountBookmarkStorageOptedIn(syncer::SyncService* sync_service) {
  if (sync_service->GetAccountInfo().IsEmpty()) {
    return false;
  }
  // TODO(crbug.com/40066949): Remove this after UNO phase 3. See
  // ConsentLevel::kSync documentation for more details.
  if (sync_service->HasSyncConsent()) {
    return false;
  }
  syncer::UserSelectableTypeSet selected_types =
      sync_service->GetUserSettings()->GetSelectedTypes();
  return selected_types.Has(syncer::UserSelectableType::kBookmarks);
}

bool IsAccountBookmarkStorageAvailable(const bookmarks::BookmarkModel* model) {
  CHECK(model);
  return model->account_mobile_node() != nullptr;
}

#pragma mark - Updating Bookmarks

// Creates a toast which will undo the changes made to the bookmark model if
// the user presses the undo button, and the UndoManagerWrapper allows the undo
// to go through.
MDCSnackbarMessage* CreateUndoToastWithWrapper(UndoManagerWrapper* wrapper,
                                               NSString* text,
                                               std::string user_action) {
  DCHECK(!user_action.empty());
  // Create the block that will be executed if the user taps the undo button.
  MDCSnackbarMessageAction* action = [[MDCSnackbarMessageAction alloc] init];
  action.handler = ^{
    if (![wrapper hasUndoManagerChanged]) {
      base::RecordAction(base::UserMetricsAction(user_action.c_str()));
      [wrapper undo];
    }
  };

  action.title = l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_UNDO_BUTTON_TITLE);
  action.accessibilityIdentifier = @"Undo";
  action.accessibilityLabel =
      l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_UNDO_BUTTON_TITLE);
  TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeSuccess);
  MDCSnackbarMessage* message = CreateSnackbarMessage(text);
  message.action = action;
  message.category = kBookmarksSnackbarCategory;
  return message;
}

bool UpdateBookmark(const BookmarkNode* node,
                    NSString* title,
                    const GURL& url,
                    const BookmarkNode* folder,
                    bookmarks::BookmarkModel* model) {
  DCHECK(node);
  DCHECK(folder);
  std::u16string titleString = base::SysNSStringToUTF16(title);
  if (node->GetTitle() == titleString && node->url() == url &&
      node->parent() == folder) {
    // Nothing to do.
    return false;
  }

  model->SetTitle(node, titleString,
                  bookmarks::metrics::BookmarkEditSource::kUser);
  model->SetURL(node, url, bookmarks::metrics::BookmarkEditSource::kUser);

  DCHECK(!folder->HasAncestor(node));
  if (node->parent() != folder) {
    model->Move(node, folder, folder->children().size());
  }
  return true;
}

bool bookmarkSavedIntoAccount(
    BookmarkStorageType bookmarkStorageType,
    base::WeakPtr<AuthenticationService> authenticationService,
    raw_ptr<syncer::SyncService> syncService) {
  // TODO(crbug.com/40066949): Simplify once kSync becomes unreachable or is
  // deleted from the codebase. See ConsentLevel::kSync documentation for
  // details.
  BOOL hasSyncConsent =
      authenticationService->HasPrimaryIdentity(signin::ConsentLevel::kSync);
  BOOL savedIntoAccount =
      (bookmarkStorageType == BookmarkStorageType::kAccount) ||
      (hasSyncConsent && syncService->GetUserSettings()->GetSelectedTypes().Has(
                             syncer::UserSelectableType::kBookmarks));
  return savedIntoAccount;
}

NSString* messageForAddingBookmarksInFolder(
    const BookmarkNode* folder,
    const bookmarks::BookmarkModel* model,
    bool chosenByUser,
    bool showCount,
    int count,
    base::WeakPtr<AuthenticationService> authenticationService,
    raw_ptr<syncer::SyncService> syncService) {
  CHECK(folder);
  CHECK(model);

  NSString* folderTitle = TitleForBookmarkNode(folder);
  id<SystemIdentity> identity =
      authenticationService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);

  if (!identity || !syncService->GetUserSettings()->GetSelectedTypes().Has(
                       syncer::UserSelectableType::kBookmarks)) {
    // The user is signed-out or bookmark sync is disabled.
    if (chosenByUser) {
      std::u16string title = base::SysNSStringToUTF16(folderTitle);
      std::u16string pattern = l10n_util::GetStringUTF16(
          (showCount) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_FOLDER
                      : IDS_IOS_BOOKMARK_PAGE_SAVED_FOLDER);
      return base::SysUTF16ToNSString(
          base::i18n::MessageFormatter::FormatWithNamedArgs(
              pattern, "count", count, "title", title));
    } else {
      return base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
          (showCount) ? IDS_IOS_BOOKMARKS_BULK_SAVED : IDS_IOS_BOOKMARKS_SAVED,
          count));
    }
  }

  // The user is signed in and bookmark sync is on (either account bookmarks or
  // the legacy sync-the-feature). It is still possible that the folder saving
  // into is a local-only folder.
  if (model->IsLocalOnlyNode(*folder)) {
    std::u16string title = base::SysNSStringToUTF16(folderTitle);
    std::u16string pattern = l10n_util::GetStringUTF16(
        (showCount) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_FOLDER_TO_DEVICE
                    : IDS_IOS_BOOKMARK_PAGE_SAVED_FOLDER_TO_DEVICE);
    std::u16string message = base::i18n::MessageFormatter::FormatWithNamedArgs(
        pattern, "count", count, "title", title);
    return base::SysUTF16ToNSString(message);
  }

  // Tell the user the bookmark is synced in their account.
  std::u16string email = base::SysNSStringToUTF16(identity.userEmail);
  if (chosenByUser) {
    // Also mentions the folder in which the bookmark is saved.
    std::u16string title = base::SysNSStringToUTF16(folderTitle);
    std::u16string pattern = l10n_util::GetStringUTF16(
        (showCount) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_INTO_ACCOUNT_FOLDER
                    : IDS_IOS_BOOKMARK_PAGE_SAVED_INTO_ACCOUNT_FOLDER);
    return base::SysUTF16ToNSString(
        base::i18n::MessageFormatter::FormatWithNamedArgs(
            pattern, "count", count, "title", title, "email", email));
  } else {
    std::u16string pattern = l10n_util::GetStringUTF16(
        (showCount) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_INTO_ACCOUNT
                    : IDS_IOS_BOOKMARK_PAGE_SAVED_INTO_ACCOUNT);
    return base::SysUTF16ToNSString(
        base::i18n::MessageFormatter::FormatWithNamedArgs(
            pattern, "count", count, "email", email));
  }
}

MDCSnackbarMessage* UpdateBookmarkWithUndoToast(
    const BookmarkNode* node,
    NSString* title,
    const GURL& url,
    const BookmarkNode* original_folder,
    const BookmarkNode* folder,
    bookmarks::BookmarkModel* model,
    ChromeBrowserState* browser_state,
    base::WeakPtr<AuthenticationService> authenticationService,
    raw_ptr<syncer::SyncService> syncService) {
  CHECK(node);

  // Secondly, create an Undo group for all undoable actions.
  UndoManagerWrapper* wrapper =
      [[UndoManagerWrapper alloc] initWithBrowserState:browser_state];

  // Create or update the bookmark.
  [wrapper startGroupingActions];

  bool did_change_anything = UpdateBookmark(node, title, url, folder, model);

  [wrapper stopGroupingActions];
  [wrapper resetUndoManagerChanged];

  if (!did_change_anything) {
    return nil;
  }
  NSString* text = nil;
  if (original_folder == folder) {
    text = l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_BOOKMARK_UPDATED);
  } else {
    text = messageForAddingBookmarksInFolder(
        folder, model,
        /*chosenByUser =*/true,
        /*showCount=*/false, /*count=*/1, authenticationService, syncService);
  }
  return CreateUndoToastWithWrapper(
      wrapper, text, "MobileBookmarkManagerUpdatedBookmarkUndone");
}

MDCSnackbarMessage* CreateBookmarkAtPositionWithUndoToast(
    NSString* title,
    const GURL& url,
    const BookmarkNode* folder,
    int position,
    bookmarks::BookmarkModel* model,
    ChromeBrowserState* browser_state) {
  std::u16string titleString = base::SysNSStringToUTF16(title);

  UndoManagerWrapper* wrapper =
      [[UndoManagerWrapper alloc] initWithBrowserState:browser_state];
  [wrapper startGroupingActions];

  RecordModuleFreshnessSignal(ContentSuggestionsModuleType::kShortcuts);
  base::RecordAction(base::UserMetricsAction("BookmarkAdded"));
  const BookmarkNode* node =
      model->AddNewURL(folder, folder->children().size(), titleString, url);
  model->Move(node, folder, position);

  [wrapper stopGroupingActions];
  [wrapper resetUndoManagerChanged];

  NSString* text =
      l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_BOOKMARK_CREATED);
  return CreateUndoToastWithWrapper(wrapper, text,
                                    "MobileBookmarkManagerAddedBookmarkUndone");
}

MDCSnackbarMessage* UpdateBookmarkPositionWithUndoToast(
    const BookmarkNode* node,
    const BookmarkNode* folder,
    size_t position,
    bookmarks::BookmarkModel* model,
    ChromeBrowserState* browser_state) {
  DCHECK(node);
  DCHECK(folder);
  DCHECK(!folder->HasAncestor(node));

  size_t old_index = node->parent()->GetIndexOf(node).value();
  // Early return if no change in position.
  if (node->parent() == folder && old_index == position) {
    return nil;
  }

  // Secondly, create an Undo group for all undoable actions.
  UndoManagerWrapper* wrapper =
      [[UndoManagerWrapper alloc] initWithBrowserState:browser_state];

  // Update the bookmark.
  [wrapper startGroupingActions];

  model->Move(node, folder, position);

  [wrapper stopGroupingActions];
  [wrapper resetUndoManagerChanged];

  NSString* text =
      l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_BOOKMARK_UPDATED);
  return CreateUndoToastWithWrapper(wrapper, text,
                                    "MobileBookmarkManagerMoveToFolderUndone");
}

void DeleteBookmarks(const std::set<const BookmarkNode*>& bookmarks,
                     bookmarks::BookmarkModel* bookmark_model,
                     const base::Location& location) {
  CHECK(bookmark_model);
  CHECK(bookmark_model->loaded());
  RemoveBookmarksRecursive(bookmarks, bookmark_model,
                           bookmarks::metrics::BookmarkEditSource::kUser,
                           bookmark_model->root_node(), location);
}

MDCSnackbarMessage* DeleteBookmarksWithUndoToast(
    const std::set<const BookmarkNode*>& nodes,
    bookmarks::BookmarkModel* bookmark_model,
    ChromeBrowserState* browser_state,
    const base::Location& location) {
  size_t node_count = nodes.size();
  DCHECK_GT(node_count, 0u);

  UndoManagerWrapper* wrapper =
      [[UndoManagerWrapper alloc] initWithBrowserState:browser_state];

  // Delete the selected bookmarks.
  [wrapper startGroupingActions];
  bookmark_utils_ios::DeleteBookmarks(nodes, bookmark_model, location);
  [wrapper stopGroupingActions];
  [wrapper resetUndoManagerChanged];

  return CreateUndoToastWithWrapper(
      wrapper,
      l10n_util::GetPluralNSStringF(IDS_IOS_BOOKMARK_DELETED_BOOKMARKS,
                                    node_count),
      "MobileBookmarkManagerDeletedEntryUndone");
}

using BookmarkNodeVectorIterator = std::vector<const BookmarkNode*>::iterator;

bool MoveBookmarks(const std::vector<const BookmarkNode*>& bookmarks_to_move,
                   bookmarks::BookmarkModel* model,
                   const BookmarkNode* destination_folder) {
  bool did_perform_move = false;

  for (const BookmarkNode* node : bookmarks_to_move) {
    if (destination_folder->HasAncestor(node)) {
      continue;  // Cannot move a folder into one of its descendants.
    }

    if (node->parent() == destination_folder) {
      continue;  // Already in `destination_folder`, nothing to do.
    }

    model->Move(node, destination_folder,
                destination_folder->children().size());
    did_perform_move = true;
  }

  return did_perform_move;
}

MDCSnackbarMessage* MoveBookmarksWithUndoToast(
    const std::vector<const BookmarkNode*>& bookmarks_to_move,
    bookmarks::BookmarkModel* model,
    const BookmarkNode* destination_folder,
    ChromeBrowserState* browser_state,
    base::WeakPtr<AuthenticationService> authenticationService,
    raw_ptr<syncer::SyncService> syncService) {
  size_t node_count = bookmarks_to_move.size();
  DCHECK_GT(node_count, 0u);
  bool contains_a_folder =
      std::find_if(bookmarks_to_move.begin(), bookmarks_to_move.end(),
                   [](const BookmarkNode* node) {
                     return node->is_folder();
                   }) != bookmarks_to_move.end();
  // We assume a folder contains multiple bookmark, and thus plural can be used.
  bool multiple_bookmarks_to_move = node_count > 1 || contains_a_folder;

  UndoManagerWrapper* wrapper =
      [[UndoManagerWrapper alloc] initWithBrowserState:browser_state];

  // Move the selected bookmarks.
  [wrapper startGroupingActions];
  const bool did_perform_move =
      MoveBookmarks(bookmarks_to_move, model, destination_folder);
  [wrapper stopGroupingActions];
  [wrapper resetUndoManagerChanged];

  if (!did_perform_move) {
    return nil;  // Don't return a snackbar when no real move as happened.
  }

  // As the exact number of bookmarks is not displayed, assuming there are two
  // is fine for the purpose of the bookmark. What matters is that it leads to
  // "bookmarks" in plural form.
  int count = (multiple_bookmarks_to_move) ? 2 : 1;

  NSString* text = messageForAddingBookmarksInFolder(
      destination_folder, model,
      /*chosenByUser=*/true, /*showCount=*/false, count, authenticationService,
      syncService);
  return CreateUndoToastWithWrapper(wrapper, text,
                                    "MobileBookmarkManagerMoveToFolderUndone");
}

#pragma mark - Useful bookmark manipulation.

namespace {

// Adds all children of `folder` that are not obstructed to `results`. They are
// placed immediately after `folder`, using a depth-first, then alphabetically
// ordering. `results` must contain `folder`.
void UpdateFoldersFromNode(const BookmarkNode* folder,
                           NodeVector* results,
                           const NodeSet& obstructions);

// Returns whether `folder` has an ancestor in any of the nodes in
// `bookmarkNodes`.
bool FolderHasAncestorInBookmarkNodes(const BookmarkNode* folder,
                                      const NodeSet& bookmarkNodes);

// Returns true if the node is not a folder, is not visible, or is an ancestor
// of any of the nodes in `obstructions`.
bool IsObstructed(const BookmarkNode* node, const NodeSet& obstructions);

// Comparator used to sort bookmarks. No folders are allowed.
class FolderNodeComparator {
 public:
  explicit FolderNodeComparator(icu::Collator* collator)
      : collator_(collator) {}

  // Returns true if `n1` precedes `n2`.
  bool operator()(const BookmarkNode* n1, const BookmarkNode* n2) {
    if (!collator_) {
      return n1->GetTitle() < n2->GetTitle();
    }
    return base::i18n::CompareString16WithCollator(*collator_, n1->GetTitle(),
                                                   n2->GetTitle()) == UCOL_LESS;
  }

 private:
  raw_ptr<icu::Collator> collator_;
};

bool FolderHasAncestorInBookmarkNodes(const BookmarkNode* folder,
                                      const NodeSet& bookmarkNodes) {
  DCHECK(folder);
  DCHECK(folder->is_folder());
  for (const BookmarkNode* node : bookmarkNodes) {
    if (folder->HasAncestor(node)) {
      return true;
    }
  }
  return false;
}

bool IsObstructed(const BookmarkNode* node, const NodeSet& obstructions) {
  if (!node->is_folder()) {
    return true;
  }
  if (!node->IsVisible()) {
    return true;
  }
  if (FolderHasAncestorInBookmarkNodes(node, obstructions)) {
    return true;
  }
  return false;
}

void UpdateFoldersFromNode(const BookmarkNode* folder,
                           NodeVector* results,
                           const NodeSet& obstructions) {
  std::vector<const BookmarkNode*> directDescendants;
  for (const auto& subfolder : folder->children()) {
    if (!IsObstructed(subfolder.get(), obstructions)) {
      directDescendants.push_back(subfolder.get());
    }
  }

  bookmark_utils_ios::SortFolders(&directDescendants);

  auto it = base::ranges::find(*results, folder);
  DCHECK(it != results->end());
  ++it;
  results->insert(it, directDescendants.begin(), directDescendants.end());

  // Recursively perform the operation on each direct descendant.
  for (auto* node : directDescendants) {
    UpdateFoldersFromNode(node, results, obstructions);
  }
}

}  // namespace

void SortFolders(NodeVector* vector) {
  UErrorCode error = U_ZERO_ERROR;
  std::unique_ptr<icu::Collator> collator(icu::Collator::createInstance(error));
  if (U_FAILURE(error)) {
    collator.reset(NULL);
  }
  std::sort(vector->begin(), vector->end(),
            FolderNodeComparator(collator.get()));
}

NodeVector VisibleNonDescendantNodes(const NodeSet& obstructions,
                                     const bookmarks::BookmarkModel* model,
                                     BookmarkStorageType type) {
  NodeVector primary_nodes = PrimaryPermanentNodes(model, type);
  NodeVector filtered_primary_nodes;
  for (auto* node : primary_nodes) {
    if (IsObstructed(node, obstructions)) {
      continue;
    }

    filtered_primary_nodes.push_back(node);
  }

  // Copy the results over.
  NodeVector results = filtered_primary_nodes;

  // Iterate over a static copy of the filtered, root folders.
  for (auto* node : filtered_primary_nodes) {
    UpdateFoldersFromNode(node, &results, obstructions);
  }

  return results;
}

// Whether `vector1` contains only elements of `vector2` in the same order.
BOOL IsSubvectorOfNodes(const NodeVector& vector1, const NodeVector& vector2) {
  NodeVector::const_iterator it = vector2.begin();
  // Scan the first vector.
  for (const auto* node : vector1) {
    // Look for a match in the rest of the second vector. When found, advance
    // the iterator on vector2 to only focus on the remaining part of vector2,
    // so that ordering is verified.
    it = std::find(it, vector2.end(), node);
    if (it == vector2.end()) {
      return NO;
    }
    // If found in vector2, advance the iterator so that the match is only
    // matched once.
    it++;
  }
  return YES;
}

// Returns the indices in `vector2` of the items in `vector2` that are not
// present in `vector1`.
// `vector1` MUST be a subvector of `vector2` in the sense of `IsSubvector`.
std::vector<NodeVector::size_type> MissingNodesIndices(
    const NodeVector& vector1,
    const NodeVector& vector2) {
  DCHECK(IsSubvectorOfNodes(vector1, vector2))
      << "Can't compute missing nodes between nodes among which the first is "
         "not a subvector of the second.";

  std::vector<NodeVector::size_type> missing_nodes_indices;
  // Keep an iterator on vector1.
  NodeVector::const_iterator it1 = vector1.begin();
  // Scan vector2, looking for vector1 elements.
  for (NodeVector::size_type i2 = 0; i2 != vector2.size(); i2++) {
    // When vector1 has been fully traversed, all remaining elements of vector2
    // are to be added to the missing nodes.
    // Otherwise, while the element of vector2 is not equal to the element the
    // iterator on vector1 is pointing to, add vector2 elements to the missing
    // nodes.
    if (it1 == vector1.end() || vector2[i2] != *it1) {
      missing_nodes_indices.push_back(i2);
    } else {
      // When there is a match between vector2 and vector1, advance the iterator
      // of vector1.
      it1++;
    }
  }
  return missing_nodes_indices;
}

#pragma mark - Cache position in table view.

NSArray<NSNumber*>* CreateBookmarkPath(const bookmarks::BookmarkModel* model,
                                       int64_t folder_id) {
  const BookmarkNode* bookmark = FindFolderById(model, folder_id);
  if (!bookmark || bookmark->is_root()) {
    return nil;
  }

  NSMutableArray<NSNumber*>* bookmarkPath = [NSMutableArray array];
  while (!bookmark->is_root()) {
    [bookmarkPath addObject:[NSNumber numberWithLongLong:bookmark->id()]];
    bookmark = bookmark->parent();
    DCHECK(bookmark);
  }
  return [[bookmarkPath reverseObjectEnumerator] allObjects];
}

GURL ConvertUserDataToGURL(NSString* urlString) {
  if (urlString) {
    return url_formatter::FixupURL(base::SysNSStringToUTF8(urlString),
                                   std::string());
  } else {
    return GURL();
  }
}

}  // namespace bookmark_utils_ios