chromium/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.mm

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

#import "chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h"

#include "base/strings/sys_string_conversions.h"
#import "chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h"
#import "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
#include "chrome/browser/ui/cocoa/applescript/error_applescript.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/common/bookmark_metrics.h"
#include "url/gurl.h"

using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;

// /!\ Warning
//
// The design of the AppleScript dictionary with regards to bookmarks is
// deficient. The ideal design would mirror the design of the actual bookmark
// system, where a bookmark element could be either a folder or an item, and
// bookmark folders could hold any number of them, in any order.
//
// However, that's not the design that is implemented. The current design is
// that bookmark folders and bookmark items are separate and unrelated things,
// and that bookmark folders have a list of bookmark folders they contain, as
// well as a list of bookmark items they contain.
//
// That is, there are _two separate lists_.
//
// Translating from the real bookmarks system, where the children of a folder
// are a list of intermingled folders and items, is easy: walk the list of
// children, and filter out the wrong kind. (See `-bookmarkFolders` and
// `-bookmarkItems`.)
//
// Translating _to_ the real bookmarks system cannot be done in the general
// case. There is no control over the mingling of folders and items, and there
// is only vague control over the ordering of items that share a type.
//
// All this is to explain the difference in terminology between "index" and
// "bookmark manager position". An "index" value is relative to the two separate
// child lists that exist from the AppleScript perspective. The "bookmark
// manager position" is relative to the true list of (both types of) children of
// a folder in the bookmarks system.
//
// Because AppleScript maintains no state, no translation ever needs to be done
// from positions to indexes. AppleScript keeps object identifiers, and if it
// ever needs to update the index, it will re-request the list and find the item
// again by matching its ID.
//
// However, when a script requests a folder or bookmark to be moved around or
// inserted, AppleScript will request a change in index. That index will need to
// be converted into a position in the real list of children of a folder, and
// that's what the `-bookmarkManagerPositionOf[Folder|Item]At:` methods are for.

@interface BookmarkFolderAppleScript ()

// Does the actual insertion of a bookmark folder at an absolute position.
- (void)insertFolder:(BookmarkFolderAppleScript*)bookmarkFolder
    atBookmarkManagerPosition:(size_t)position;

// Does the actual insertion of a bookmark item at an absolute position.
- (void)insertItem:(BookmarkItemAppleScript*)bookmarkItem
    atBookmarkManagerPosition:(size_t)position;

// Returns the position of a bookmark folder within the current bookmark folder
// which consists of bookmark folders as well as bookmark items.
- (size_t)bookmarkManagerPositionOfFolderAt:(size_t)index;

// Returns the position of a bookmark item within the current bookmark folder
// which consists of bookmark folders as well as bookmark items.
- (size_t)bookmarkManagerPositionOfItemAt:(size_t)index;

@end

@implementation BookmarkFolderAppleScript

- (NSArray<BookmarkFolderAppleScript*>*)bookmarkFolders {
  NSMutableArray<BookmarkFolderAppleScript*>* bookmarkFolders =
      [NSMutableArray arrayWithCapacity:self.bookmarkNode->children().size()];

  for (const auto& node : self.bookmarkNode->children()) {
    if (!node->is_folder()) {
      continue;
    }

    BookmarkFolderAppleScript* bookmarkFolder =
        [[BookmarkFolderAppleScript alloc] initWithBookmarkNode:node.get()];
    [bookmarkFolder setContainer:self
                        property:AppleScript::kBookmarkFoldersProperty];
    [bookmarkFolders addObject:bookmarkFolder];
  }

  return bookmarkFolders;
}

- (NSArray<BookmarkItemAppleScript*>*)bookmarkItems {
  NSMutableArray<BookmarkItemAppleScript*>* bookmarkItems =
      [NSMutableArray arrayWithCapacity:self.bookmarkNode->children().size()];

  for (const auto& node : self.bookmarkNode->children()) {
    if (!node->is_url()) {
      continue;
    }

    BookmarkItemAppleScript* bookmarkItem =
        [[BookmarkItemAppleScript alloc] initWithBookmarkNode:node.get()];
    [bookmarkItem setContainer:self
                      property:AppleScript::kBookmarkItemsProperty];
    [bookmarkItems addObject:bookmarkItem];
  }

  return bookmarkItems;
}

- (void)insertInBookmarkFolders:(BookmarkFolderAppleScript*)bookmarkFolder {
  [self insertFolder:bookmarkFolder
      atBookmarkManagerPosition:self.bookmarkNode->children().size()];
}

- (void)insertInBookmarkFolders:(BookmarkFolderAppleScript*)bookmarkFolder
                        atIndex:(size_t)index {
  [self insertFolder:bookmarkFolder
      atBookmarkManagerPosition:[self bookmarkManagerPositionOfFolderAt:index]];
}

- (void)insertFolder:(BookmarkFolderAppleScript*)bookmarkFolder
    atBookmarkManagerPosition:(size_t)position {
  [bookmarkFolder setContainer:self
                      property:AppleScript::kBookmarkFoldersProperty];

  BookmarkModel* model = self.bookmarkModel;
  if (!model) {
    return;
  }

  const BookmarkNode* node = model->AddFolder(
      self.bookmarkNode, position,
      /*title=*/std::u16string(), /*meta_info=*/nullptr,
      /*creation_time=*/std::nullopt, bookmarkFolder.bookmarkGUID);
  if (!node) {
    AppleScript::SetError(AppleScript::Error::kCreateBookmarkFolder);
    return;
  }

  [bookmarkFolder didCreateBookmarkNode:node];
}

- (void)removeFromBookmarkFoldersAtIndex:(size_t)index {
  size_t position = [self bookmarkManagerPositionOfFolderAt:index];

  BookmarkModel* model = self.bookmarkModel;
  if (!model) {
    return;
  }

  model->Remove(self.bookmarkNode->children()[position].get(),
                bookmarks::metrics::BookmarkEditSource::kUser, FROM_HERE);
}

- (void)insertInBookmarkItems:(BookmarkItemAppleScript*)bookmarkItem {
  [self insertItem:bookmarkItem
      atBookmarkManagerPosition:self.bookmarkNode->children().size()];
}

- (void)insertInBookmarkItems:(BookmarkItemAppleScript*)bookmarkItem
                      atIndex:(size_t)index {
  [self insertItem:bookmarkItem
      atBookmarkManagerPosition:[self bookmarkManagerPositionOfItemAt:index]];
}

- (void)insertItem:(BookmarkItemAppleScript*)bookmarkItem
    atBookmarkManagerPosition:(size_t)position {
  [bookmarkItem setContainer:self property:AppleScript::kBookmarkItemsProperty];

  BookmarkModel* model = self.bookmarkModel;
  if (!model) {
    return;
  }

  GURL url(base::SysNSStringToUTF8(bookmarkItem.URL));
  if (!url.is_valid()) {
    AppleScript::SetError(AppleScript::Error::kInvalidURL);
    return;
  }

  const BookmarkNode* node = model->AddURL(
      self.bookmarkNode, position, /*title=*/std::u16string(), url,
      /*meta_info=*/nullptr, /*creation_time=*/std::nullopt,
      bookmarkItem.bookmarkGUID, /*added_by_user=*/true);
  if (!node) {
    AppleScript::SetError(AppleScript::Error::kCreateBookmarkItem);
    return;
  }

  [bookmarkItem didCreateBookmarkNode:node];
}

- (void)removeFromBookmarkItemsAtIndex:(size_t)index {
  size_t position = [self bookmarkManagerPositionOfItemAt:index];

  BookmarkModel* model = self.bookmarkModel;
  if (!model) {
    return;
  }

  model->Remove(self.bookmarkNode->children()[position].get(),
                bookmarks::metrics::BookmarkEditSource::kUser, FROM_HERE);
}

- (size_t)bookmarkManagerPositionOfFolderAt:(size_t)index {
  // Traverse through all the child nodes until the required node is found and
  // return its position.
  // AppleScript is 1-based therefore index is incremented by 1.
  ++index;
  size_t count = 0;
  while (index) {
    if (self.bookmarkNode->children()[count++]->is_folder()) {
      --index;
    }
  }
  return count - 1;
}

- (size_t)bookmarkManagerPositionOfItemAt:(size_t)index {
  // Traverse through all the child nodes until the required node is found and
  // return its position.
  // AppleScript is 1-based therefore index is incremented by 1.
  ++index;
  size_t count = 0;
  while (index) {
    if (self.bookmarkNode->children()[count++]->is_url()) {
      --index;
    }
  }
  return count - 1;
}

@end