safari_bookmarks_parser 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 707f428c27b200d30b70df6ec9a5f33b681b91144460ae9c973041e3d5fb1af2
4
- data.tar.gz: eb151045988fc6226db28cb3ffc562a3a18b4f1a28c52eb0526ecb0ffc978d87
3
+ metadata.gz: 64c367d9f7044efc558a5aeec069249c6152edfd35081e8cb1bb718bd0531c7c
4
+ data.tar.gz: 0c65ce14b8b0fb6705dec8eb61d0ac61ede0c039c0873af830fac68becb5cbdd
5
5
  SHA512:
6
- metadata.gz: bcaebcb7129e64301411f88e6a2275f50c64be6d998406531246c34bf81a436b4ef3b524c5c1edca13a43dfac8c4579a12e68feffe94cd5b01068ba20eaa7086
7
- data.tar.gz: 6db98a62641040b2ff3f0da81bd25f19eddf288650ebf9e5ccb1b729c36c211b9c4901b7cfc5dd32a709710bb8c2e28538a34202d3f55b1050fa4621f6330a69
6
+ metadata.gz: 39daa035d8e210d82ff706e229fee99f4ca705112275c5cc1b5517c1f30e07c67a39da0288cdc3a6bc48cec5ef9cd5f7395b96ee14a7c703dc603493fd1666d1
7
+ data.tar.gz: 0e81f0eb742de04011d38a42cc25d273d05775478a2a065ea0a190efad67d084724b148f684dab4c47e805feea10740849cacb1af2446270487138deb5616cb3
data/README.md CHANGED
@@ -22,6 +22,12 @@ Dump `Bookmarks.plist`:
22
22
  $ safari_bookmarks_parser dump
23
23
  ```
24
24
 
25
+ Dump `Bookmarks.plist` to `Bookmarks.json':
26
+
27
+ ```
28
+ $ safari_bookmarks_parser dump -o Bookmarks.json
29
+ ```
30
+
25
31
  Dump `Bookmarks.plist` as list:
26
32
 
27
33
  ```
@@ -52,6 +58,28 @@ Dump other `Bookmarks.plist`:
52
58
  $ safari_bookmarks_parser dump /path/to/Bookmarks.plist
53
59
  ```
54
60
 
61
+ ### Dups
62
+
63
+ Find duplicated bookmarks:
64
+
65
+ ```
66
+ $ safari_bookmarks_parser dups
67
+ ```
68
+
69
+ Find duplicated bookmarks excluding reading list:
70
+
71
+ ```
72
+ $ safari_bookmarks_parser dups -R
73
+ ```
74
+
75
+ ### Empty
76
+
77
+ Find empty folders:
78
+
79
+ ```
80
+ $ safari_bookmarks_parser empty
81
+ ```
82
+
55
83
  ## Development
56
84
 
57
85
  - Run `bin/rubocop` to check syntax
@@ -5,9 +5,15 @@ require 'safari_bookmarks_parser/bookmark_folder'
5
5
 
6
6
  require 'safari_bookmarks_parser/parser'
7
7
 
8
+ require 'safari_bookmarks_parser/services/find_duplicated_bookmarks'
9
+ require 'safari_bookmarks_parser/services/find_empty_folders'
10
+
8
11
  require 'safari_bookmarks_parser/runner'
9
12
 
13
+ require 'safari_bookmarks_parser/commands/base_command'
10
14
  require 'safari_bookmarks_parser/commands/dump_command'
15
+ require 'safari_bookmarks_parser/commands/dups_command'
16
+ require 'safari_bookmarks_parser/commands/empty_command'
11
17
 
12
18
  require 'safari_bookmarks_parser/version'
13
19
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  module SafariBookmarksParser
4
4
  class BookmarkFolder
5
- attr_reader :title, :children
5
+ attr_reader :title, :folder_names, :children
6
6
 
7
- def initialize(title:, children: [])
7
+ def initialize(title:, folder_names:, children: [])
8
8
  @title = title
9
+ @folder_names = folder_names
9
10
  @children = children
10
11
  end
11
12
 
@@ -20,7 +21,7 @@ module SafariBookmarksParser
20
21
  end
21
22
 
22
23
  def to_h
23
- { 'title' => title, 'children' => children.map(&:to_h) }
24
+ { 'title' => title, 'folder_names' => folder_names, 'children' => children.map(&:to_h) }
24
25
  end
25
26
 
26
27
  def to_json(options)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'optparse'
5
+ require 'yaml'
6
+
7
+ module SafariBookmarksParser
8
+ module Commands
9
+ class BaseCommand
10
+ attr_reader :plist_path, :output_path, :output_format
11
+
12
+ def initialize(argv)
13
+ @plist_path = File.expand_path('~/Library/Safari/Bookmarks.plist')
14
+ @output_path = nil
15
+ @output_format = :json
16
+
17
+ parse_options(argv)
18
+ handle_argv(argv)
19
+ end
20
+
21
+ private
22
+
23
+ def format_to_text(obj)
24
+ case @output_format
25
+ when :json
26
+ JSON.pretty_generate(obj)
27
+ when :yaml
28
+ YAML.dump(obj)
29
+ end
30
+ end
31
+
32
+ def output_text(text)
33
+ if @output_path
34
+ File.write(@output_path, text)
35
+ else
36
+ puts text
37
+ end
38
+ end
39
+
40
+ def on_output_path(parser)
41
+ parser.on('-o', '--output-path=PATH', 'Output path (default: output to $stdout)') do |value|
42
+ @output_path = value
43
+ end
44
+ end
45
+
46
+ def on_output_format(parser)
47
+ desc = %(Output format (default: "#{@output_format}"; one of "json" or "yaml"))
48
+ parser.on('-f', '--output-format=FORMAT', %w[json yaml], desc) do |value|
49
+ @output_format = value.to_sym
50
+ end
51
+ end
52
+
53
+ def do_parse(parser, argv)
54
+ parser.parse!(argv)
55
+ rescue OptionParser::ParseError => e
56
+ raise Error, e.message
57
+ end
58
+
59
+ def handle_argv(argv)
60
+ @plist_path = argv.first if argv.any?
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,41 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'optparse'
5
- require 'yaml'
6
-
7
3
  module SafariBookmarksParser
8
4
  module Commands
9
- class DumpCommand
10
- attr_reader :plist_path, :output_path, :output_format, :output_style, :output_parts
5
+ class DumpCommand < BaseCommand
6
+ attr_reader :output_style, :output_parts
11
7
 
12
8
  def initialize(argv)
13
- @plist_path = File.expand_path('~/Library/Safari/Bookmarks.plist')
14
- @output_path = nil
15
- @output_format = :json
16
9
  @output_style = :tree
17
10
  @output_parts = :all
18
11
 
19
- @option_parser = nil
20
-
21
- parse_options(argv)
22
- handle_argv(argv)
12
+ super
23
13
  end
24
14
 
25
15
  def run
26
16
  plist_parser = Parser.parse(@plist_path)
27
17
 
28
- result = result_for_output_parts(plist_parser)
29
- result = result_for_output_style(result)
18
+ result = select_output_parts(plist_parser)
19
+ result = select_output_style(result)
30
20
 
31
- text = format_result(result)
21
+ text = format_to_text(result)
32
22
 
33
23
  output_text(text)
34
24
  end
35
25
 
36
26
  private
37
27
 
38
- def result_for_output_parts(plist_parser)
28
+ def select_output_parts(plist_parser)
39
29
  case @output_parts
40
30
  when :all
41
31
  plist_parser.root_folder
@@ -46,7 +36,7 @@ module SafariBookmarksParser
46
36
  end
47
37
  end
48
38
 
49
- def result_for_output_style(result)
39
+ def select_output_style(result)
50
40
  case @output_style
51
41
  when :tree
52
42
  result.to_h
@@ -55,23 +45,6 @@ module SafariBookmarksParser
55
45
  end
56
46
  end
57
47
 
58
- def format_result(result)
59
- case @output_format
60
- when :json
61
- JSON.pretty_generate(result)
62
- when :yaml
63
- YAML.dump(result)
64
- end
65
- end
66
-
67
- def output_text(text)
68
- if @output_path
69
- File.write(@output_path, text)
70
- else
71
- puts text
72
- end
73
- end
74
-
75
48
  def parse_options(argv)
76
49
  parser = OptionParser.new
77
50
 
@@ -84,24 +57,11 @@ module SafariBookmarksParser
84
57
  on_list(parser)
85
58
 
86
59
  on_reading_list_only(parser)
87
- on_omit_reading_list(parser)
60
+ on_exclude_reading_list(parser)
88
61
 
89
62
  do_parse(parser, argv)
90
63
  end
91
64
 
92
- def on_output_path(parser)
93
- parser.on('-o', '--output-path=PATH', 'Output path (default: output to $stdout)') do |value|
94
- @output_path = value
95
- end
96
- end
97
-
98
- def on_output_format(parser)
99
- desc = "Output format (default: #{@output_format}; one of json or yaml)"
100
- parser.on('-f', '--output-format=FORMAT', %w[json yaml], desc) do |value|
101
- @output_format = value.to_sym
102
- end
103
- end
104
-
105
65
  def on_tree(parser)
106
66
  parser.on('--tree', 'Output as tree (default)') do
107
67
  @output_style = :tree
@@ -120,22 +80,12 @@ module SafariBookmarksParser
120
80
  end
121
81
  end
122
82
 
123
- def on_omit_reading_list(parser)
124
- parser.on('-R', 'Omit reading list') do
83
+ def on_exclude_reading_list(parser)
84
+ parser.on('-R', 'Exclude reading list') do
125
85
  @output_parts = :bookmarks
126
86
  end
127
87
  end
128
88
 
129
- def do_parse(parser, argv)
130
- parser.parse!(argv)
131
- rescue OptionParser::ParseError => e
132
- raise Error, e.message
133
- end
134
-
135
- def handle_argv(argv)
136
- @plist_path = argv.first if argv.any?
137
- end
138
-
139
89
  Runner.register_command(:dump, self)
140
90
  end
141
91
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafariBookmarksParser
4
+ module Commands
5
+ class DupsCommand < BaseCommand
6
+ attr_reader :exclude_reading_list
7
+
8
+ def initialize(argv)
9
+ @exclude_reading_list = false
10
+
11
+ super
12
+ end
13
+
14
+ def run
15
+ plist_parser = Parser.parse(@plist_path)
16
+
17
+ bookmarks = bookmarks_to_process(plist_parser).to_a
18
+
19
+ duplicated_bookmark_groups = Services::FindDuplicatedBookmarks.call(bookmarks: bookmarks)
20
+
21
+ return if duplicated_bookmark_groups.empty?
22
+
23
+ duplicated_bookmark_groups = duplicated_bookmark_groups.map do |group|
24
+ group.map(&:to_h)
25
+ end
26
+
27
+ text = format_to_text(duplicated_bookmark_groups)
28
+
29
+ output_text(text)
30
+ end
31
+
32
+ private
33
+
34
+ def bookmarks_to_process(plist_parser)
35
+ if @exclude_reading_list
36
+ plist_parser.root_folder_without_reading_list
37
+ else
38
+ plist_parser.root_folder
39
+ end
40
+ end
41
+
42
+ def parse_options(argv)
43
+ parser = OptionParser.new
44
+
45
+ parser.banner = "Usage: #{parser.program_name} dups [options] [~/Library/Safari/Bookmarks.plist]"
46
+
47
+ on_output_path(parser)
48
+ on_output_format(parser)
49
+
50
+ on_exclude_reading_list(parser)
51
+
52
+ do_parse(parser, argv)
53
+ end
54
+
55
+ def on_exclude_reading_list(parser)
56
+ parser.on('-R', "Exclude reading list (default: #{@exclude_reading_list})") do
57
+ @exclude_reading_list = true
58
+ end
59
+ end
60
+
61
+ Runner.register_command(:dups, self)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafariBookmarksParser
4
+ module Commands
5
+ class EmptyCommand < BaseCommand
6
+ def run
7
+ plist_parser = Parser.parse(@plist_path)
8
+
9
+ empty_folders = Services::FindEmptyFolders.call(root_folder: plist_parser.root_folder)
10
+
11
+ return if empty_folders.empty?
12
+
13
+ text = format_to_text(empty_folders.map(&:to_h))
14
+
15
+ output_text(text)
16
+ end
17
+
18
+ private
19
+
20
+ def parse_options(argv)
21
+ parser = OptionParser.new
22
+
23
+ parser.banner = "Usage: #{parser.program_name} empty [options] [~/Library/Safari/Bookmarks.plist]"
24
+
25
+ on_output_path(parser)
26
+ on_output_format(parser)
27
+
28
+ do_parse(parser, argv)
29
+ end
30
+
31
+ Runner.register_command(:empty, self)
32
+ end
33
+ end
34
+ end
@@ -65,7 +65,7 @@ module SafariBookmarksParser
65
65
  traverse(child, folder_names + [title])
66
66
  end.compact
67
67
 
68
- BookmarkFolder.new(title: title, children: children)
68
+ BookmarkFolder.new(title: title, folder_names: folder_names, children: children)
69
69
  end
70
70
 
71
71
  def accept_leaf(node, folder_names)
@@ -80,6 +80,8 @@ module SafariBookmarksParser
80
80
  end
81
81
 
82
82
  def binary_plist_to_xml_plist(binary_plist_path)
83
+ binary_plist_readable?(binary_plist_path)
84
+
83
85
  Tempfile.open(['Bookmarks', '.xml']) do |tempfile|
84
86
  command = ['plutil', '-convert', 'xml1', '-o', tempfile.path, binary_plist_path]
85
87
 
@@ -88,5 +90,16 @@ module SafariBookmarksParser
88
90
  tempfile.read
89
91
  end
90
92
  end
93
+
94
+ def binary_plist_readable?(binary_plist_path)
95
+ return if File.readable?(binary_plist_path)
96
+
97
+ message = <<~MESSAGE
98
+ Couldn't read #{binary_plist_path}
99
+ In "System Preferences" -> "Security & Privacy" -> "Privacy" -> "Full Disk Access", check "Terminal".
100
+ MESSAGE
101
+
102
+ raise Error, message
103
+ end
91
104
  end
92
105
  end
@@ -45,6 +45,8 @@ module SafariBookmarksParser
45
45
  Available commands are:
46
46
 
47
47
  - dump
48
+ - dups
49
+ - empty
48
50
  MESSAGE
49
51
  end
50
52
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafariBookmarksParser
4
+ module Services
5
+ class FindDuplicatedBookmarks
6
+ def self.call(bookmarks:)
7
+ cache = Hash.new {|hash, key| hash[key] = [] }
8
+
9
+ bookmarks.to_a.each do |bookmark|
10
+ key = bookmark.url.sub(%r{\Ahttps?://}, '')
11
+ cache[key] << bookmark
12
+ end
13
+
14
+ cache.select {|_, value| value.size > 1 }.values
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafariBookmarksParser
4
+ module Services
5
+ class FindEmptyFolders
6
+ class << self
7
+ def call(root_folder:)
8
+ results = []
9
+
10
+ traverse(root_folder, results)
11
+
12
+ results
13
+ end
14
+
15
+ def traverse(node, results)
16
+ case node
17
+ when BookmarkFolder
18
+ if node.children.empty?
19
+ results << node
20
+ else
21
+ node.children.each do |child|
22
+ traverse(child, results)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafariBookmarksParser
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safari_bookmarks_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - healthypackrat
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-22 00:00:00.000000000 Z
11
+ date: 2021-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: plist
@@ -38,9 +38,14 @@ files:
38
38
  - lib/safari_bookmarks_parser.rb
39
39
  - lib/safari_bookmarks_parser/bookmark.rb
40
40
  - lib/safari_bookmarks_parser/bookmark_folder.rb
41
+ - lib/safari_bookmarks_parser/commands/base_command.rb
41
42
  - lib/safari_bookmarks_parser/commands/dump_command.rb
43
+ - lib/safari_bookmarks_parser/commands/dups_command.rb
44
+ - lib/safari_bookmarks_parser/commands/empty_command.rb
42
45
  - lib/safari_bookmarks_parser/parser.rb
43
46
  - lib/safari_bookmarks_parser/runner.rb
47
+ - lib/safari_bookmarks_parser/services/find_duplicated_bookmarks.rb
48
+ - lib/safari_bookmarks_parser/services/find_empty_folders.rb
44
49
  - lib/safari_bookmarks_parser/version.rb
45
50
  homepage: https://github.com/healthypackrat/safari_bookmarks_parser
46
51
  licenses: