safari_bookmarks_parser 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +28 -0
- data/lib/safari_bookmarks_parser.rb +6 -0
- data/lib/safari_bookmarks_parser/bookmark_folder.rb +4 -3
- data/lib/safari_bookmarks_parser/commands/base_command.rb +64 -0
- data/lib/safari_bookmarks_parser/commands/dump_command.rb +11 -61
- data/lib/safari_bookmarks_parser/commands/dups_command.rb +64 -0
- data/lib/safari_bookmarks_parser/commands/empty_command.rb +34 -0
- data/lib/safari_bookmarks_parser/parser.rb +14 -1
- data/lib/safari_bookmarks_parser/runner.rb +2 -0
- data/lib/safari_bookmarks_parser/services/find_duplicated_bookmarks.rb +18 -0
- data/lib/safari_bookmarks_parser/services/find_empty_folders.rb +30 -0
- data/lib/safari_bookmarks_parser/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64c367d9f7044efc558a5aeec069249c6152edfd35081e8cb1bb718bd0531c7c
|
4
|
+
data.tar.gz: 0c65ce14b8b0fb6705dec8eb61d0ac61ede0c039c0873af830fac68becb5cbdd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 :
|
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
|
-
|
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 =
|
29
|
-
result =
|
18
|
+
result = select_output_parts(plist_parser)
|
19
|
+
result = select_output_style(result)
|
30
20
|
|
31
|
-
text =
|
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
|
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
|
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
|
-
|
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
|
124
|
-
parser.on('-R', '
|
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
|
@@ -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
|
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.
|
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-
|
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:
|