safari_bookmarks_parser 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4b79cf6a3fb83906b78ab06394d50c9cf2d15abed628a4951cc32a0d209aaed2
4
+ data.tar.gz: 590db97d372270a5880a857885fa8ac6a836b63e61c55a83844cd9f3a0de35c0
5
+ SHA512:
6
+ metadata.gz: 75e07a0d4a48b2569b6164284f5f626bb6dba173a98678b6fe1a0875484eaa400a9673d76330ba9f34850507127474c3adaa8c01dd7b43fc4eb9e1bf518afc64
7
+ data.tar.gz: 44eece1dad295720cd00f30319fa9e37e837e7a66cd69bd7975d4900de542c84eaa384b31ddffea13d99034e1b4b6673feedeb3157e288395d323541ccc5a18c
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 healthypackrat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,62 @@
1
+ # safari\_bookmarks\_parser
2
+
3
+ This gem provides a command to dump `~/Library/Safari/Bookmarks.plist` as JSON/YAML.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ $ gem install safari_bookmarks_parser
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Dump
14
+
15
+ Dump `Bookmarks.plist`:
16
+
17
+ ```
18
+ $ safari_bookmarks_parser dump
19
+ ```
20
+
21
+ Dump `Bookmarks.plist` as list:
22
+
23
+ ```
24
+ $ safari_bookmarks_parser dump --list
25
+ ```
26
+
27
+ Dump `Bookmarks.plist` as YAML:
28
+
29
+ ```
30
+ $ safari_bookmarks_parser dump -f yaml
31
+ ```
32
+
33
+ Dump Reading List only:
34
+
35
+ ```
36
+ $ safari_bookmarks_parser dump -r
37
+ ```
38
+
39
+ Dump without Reading List:
40
+
41
+ ```
42
+ $ safari_bookmarks_parser dump -R
43
+ ```
44
+
45
+ Dump other `Bookmarks.plist`:
46
+
47
+ ```
48
+ $ safari_bookmarks_parser dump /path/to/Bookmarks.plist
49
+ ```
50
+
51
+ ## Development
52
+
53
+ - Run `bin/rubocop` to check syntax
54
+ - Run `bin/rspec` to test
55
+
56
+ ## Contributing
57
+
58
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/healthypackrat/safari_bookmarks_parser>.
59
+
60
+ ## License
61
+
62
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'safari_bookmarks_parser'
5
+
6
+ begin
7
+ runner = SafariBookmarksParser::Runner.new(ARGV)
8
+ runner.run
9
+ rescue SafariBookmarksParser::Error => e
10
+ warn e.message
11
+ exit 1
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'safari_bookmarks_parser/bookmark'
4
+ require 'safari_bookmarks_parser/bookmark_folder'
5
+
6
+ require 'safari_bookmarks_parser/parser'
7
+
8
+ require 'safari_bookmarks_parser/runner'
9
+
10
+ require 'safari_bookmarks_parser/commands/dump_command'
11
+
12
+ require 'safari_bookmarks_parser/version'
13
+
14
+ module SafariBookmarksParser
15
+ class Error < StandardError; end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafariBookmarksParser
4
+ class Bookmark
5
+ attr_reader :url, :title, :folder_names
6
+
7
+ def initialize(url:, title:, folder_names:)
8
+ @url = url
9
+ @title = title
10
+ @folder_names = folder_names
11
+ end
12
+
13
+ def to_h
14
+ { 'url' => url, 'title' => title, 'folder_names' => folder_names }
15
+ end
16
+
17
+ def to_json(options)
18
+ to_h.to_json(options)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafariBookmarksParser
4
+ class BookmarkFolder
5
+ attr_reader :title, :children
6
+
7
+ def initialize(title:, children: [])
8
+ @title = title
9
+ @children = children
10
+ end
11
+
12
+ def empty?
13
+ to_a.empty?
14
+ end
15
+
16
+ def to_a
17
+ results = []
18
+ traverse(self, results)
19
+ results
20
+ end
21
+
22
+ def to_h
23
+ { 'title' => title, 'children' => children.map(&:to_h) }
24
+ end
25
+
26
+ def to_json(options)
27
+ to_h.to_json(options)
28
+ end
29
+
30
+ private
31
+
32
+ def traverse(node, results)
33
+ case node
34
+ when BookmarkFolder
35
+ node.children.each do |child|
36
+ traverse(child, results)
37
+ end
38
+ when Bookmark
39
+ results << node
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'optparse'
5
+ require 'yaml'
6
+
7
+ module SafariBookmarksParser
8
+ module Commands
9
+ class DumpCommand
10
+ attr_reader :plist_path, :output_format, :output_style, :output_parts
11
+
12
+ def initialize(argv)
13
+ @plist_path = File.expand_path('~/Library/Safari/Bookmarks.plist')
14
+ @output_format = :json
15
+ @output_style = :tree
16
+ @output_parts = :all
17
+
18
+ @option_parser = nil
19
+
20
+ parse_options(argv)
21
+ handle_argv(argv)
22
+ end
23
+
24
+ def run
25
+ plist_parser = Parser.parse(@plist_path)
26
+
27
+ result = result_for_output_parts(plist_parser)
28
+ result = result_for_output_style(result)
29
+
30
+ output_result(result)
31
+ end
32
+
33
+ def result_for_output_parts(plist_parser)
34
+ case @output_parts
35
+ when :all
36
+ plist_parser.root_folder
37
+ when :bookmarks
38
+ plist_parser.root_folder_without_reading_list
39
+ when :reading_list
40
+ plist_parser.reading_list
41
+ end
42
+ end
43
+
44
+ def result_for_output_style(result)
45
+ case @output_style
46
+ when :tree
47
+ result.to_h
48
+ when :list
49
+ result.to_a.map(&:to_h)
50
+ end
51
+ end
52
+
53
+ def output_result(result)
54
+ case @output_format
55
+ when :json
56
+ puts JSON.pretty_generate(result)
57
+ when :yaml
58
+ puts YAML.dump(result)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def parse_options(argv)
65
+ parser = OptionParser.new
66
+
67
+ parser.banner = "Usage: #{parser.program_name} dump [options] [~/Library/Safari/Bookmarks.plist]"
68
+
69
+ on_output_format(parser)
70
+
71
+ on_tree(parser)
72
+ on_list(parser)
73
+
74
+ on_reading_list_only(parser)
75
+ on_omit_reading_list(parser)
76
+
77
+ do_parse(parser, argv)
78
+ end
79
+
80
+ def on_output_format(parser)
81
+ desc = "Output format (default: #{@output_format}; one of json or yaml)"
82
+ parser.on('-f', '--output-format=FORMAT', %w[json yaml], desc) do |value|
83
+ @output_format = value.to_sym
84
+ end
85
+ end
86
+
87
+ def on_tree(parser)
88
+ parser.on('--tree', 'Output as tree (default)') do
89
+ @output_style = :tree
90
+ end
91
+ end
92
+
93
+ def on_list(parser)
94
+ parser.on('--list', 'Output as list') do
95
+ @output_style = :list
96
+ end
97
+ end
98
+
99
+ def on_reading_list_only(parser)
100
+ parser.on('-r', 'Output reading list only') do
101
+ @output_parts = :reading_list
102
+ end
103
+ end
104
+
105
+ def on_omit_reading_list(parser)
106
+ parser.on('-R', 'Omit reading list') do
107
+ @output_parts = :bookmarks
108
+ end
109
+ end
110
+
111
+ def do_parse(parser, argv)
112
+ parser.parse!(argv)
113
+ rescue OptionParser::ParseError => e
114
+ raise Error, e.message
115
+ end
116
+
117
+ def handle_argv(argv)
118
+ @plist_path = argv.first if argv.any?
119
+ end
120
+
121
+ Runner.register_command(:dump, self)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'tempfile'
5
+
6
+ require 'plist'
7
+
8
+ module SafariBookmarksParser
9
+ def self.parse(binary_plist_path)
10
+ Parser.parse(binary_plist_path)
11
+ end
12
+
13
+ class Parser
14
+ READING_LIST_KEY = 'com.apple.ReadingList'
15
+
16
+ def self.parse(binary_plist_path)
17
+ new.parse(binary_plist_path)
18
+ end
19
+
20
+ attr_reader :root_folder, :root_folder_without_reading_list, :reading_list
21
+
22
+ def initialize
23
+ @root_folder = nil
24
+ @root_folder_without_reading_list = nil
25
+ @reading_list = nil
26
+ end
27
+
28
+ def parse(binary_plist_path)
29
+ root_node = parse_xml_plist(binary_plist_to_xml_plist(binary_plist_path))
30
+
31
+ parse_combined(root_node)
32
+ parse_splitted(root_node)
33
+
34
+ self
35
+ end
36
+
37
+ private
38
+
39
+ def parse_combined(root_node)
40
+ @root_folder = traverse(root_node)
41
+ end
42
+
43
+ def parse_splitted(root_node)
44
+ root_folder = traverse(root_node)
45
+
46
+ index = root_folder.children.find_index {|child| child.title == READING_LIST_KEY }
47
+ @reading_list = root_folder.children.delete_at(index) if index
48
+
49
+ @root_folder_without_reading_list = root_folder
50
+ end
51
+
52
+ def traverse(node, folder_names = [])
53
+ case node.fetch('WebBookmarkType')
54
+ when 'WebBookmarkTypeList'
55
+ accept_list(node, folder_names)
56
+ when 'WebBookmarkTypeLeaf'
57
+ accept_leaf(node, folder_names)
58
+ end
59
+ end
60
+
61
+ def accept_list(node, folder_names)
62
+ title = node.fetch('Title')
63
+
64
+ children = node.fetch('Children', []).map do |child|
65
+ traverse(child, folder_names + [title])
66
+ end.compact
67
+
68
+ BookmarkFolder.new(title: title, children: children)
69
+ end
70
+
71
+ def accept_leaf(node, folder_names)
72
+ url = node.fetch('URLString')
73
+ title = node.fetch('URIDictionary').fetch('title')
74
+
75
+ Bookmark.new(url: url, title: title, folder_names: folder_names)
76
+ end
77
+
78
+ def parse_xml_plist(xml_plist)
79
+ Plist.parse_xml(xml_plist)
80
+ end
81
+
82
+ def binary_plist_to_xml_plist(binary_plist_path)
83
+ Tempfile.open(['Bookmarks', '.xml']) do |tempfile|
84
+ command = ['plutil', '-convert', 'xml1', '-o', tempfile.path, binary_plist_path]
85
+
86
+ raise Error, "plutil returned #{$CHILD_STATUS.exitstatus}" unless system(*command)
87
+
88
+ tempfile.read
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module SafariBookmarksParser
6
+ class Runner
7
+ class << self
8
+ def known_commands
9
+ @known_commands ||= {}
10
+ end
11
+
12
+ def register_command(command_name, command_class)
13
+ known_commands[command_name.to_sym] = command_class
14
+ end
15
+ end
16
+
17
+ def initialize(argv)
18
+ @argv = argv.dup
19
+
20
+ @parser = nil
21
+
22
+ parse_options(@argv)
23
+ end
24
+
25
+ def run
26
+ command_name = @argv.shift
27
+
28
+ if command_name
29
+ command_class = self.class.known_commands[command_name.to_sym]
30
+
31
+ raise Error, "unknown command: #{command_name}" unless command_class
32
+
33
+ command_class.new(@argv).run
34
+ else
35
+ show_help(@parser)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def show_help(parser)
42
+ puts parser
43
+ puts
44
+ puts <<~MESSAGE
45
+ Available commands are:
46
+
47
+ - dump
48
+ MESSAGE
49
+ end
50
+
51
+ def parse_options(argv)
52
+ parser = OptionParser.new
53
+
54
+ parser.banner = "Usage: #{parser.program_name} [options] command"
55
+
56
+ parser.version = VERSION
57
+
58
+ on_show_help(parser)
59
+ on_show_version(parser)
60
+
61
+ do_parse(parser, argv)
62
+
63
+ @parser = parser
64
+ end
65
+
66
+ def on_show_help(parser)
67
+ parser.on('-h', '--help', 'Show this message') do
68
+ show_help(parser)
69
+ exit
70
+ end
71
+ end
72
+
73
+ def on_show_version(parser)
74
+ parser.on('-v', '--version', 'Show version number') do
75
+ puts "#{parser.program_name} #{VERSION}"
76
+ exit
77
+ end
78
+ end
79
+
80
+ def do_parse(parser, argv)
81
+ parser.order!(argv)
82
+ rescue OptionParser::ParseError => e
83
+ raise Error, e.message
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafariBookmarksParser
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: safari_bookmarks_parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - healthypackrat
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-01-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: plist
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.5'
27
+ description:
28
+ email:
29
+ - healthypackrat@gmail.com
30
+ executables:
31
+ - safari_bookmarks_parser
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE.txt
36
+ - README.md
37
+ - exe/safari_bookmarks_parser
38
+ - lib/safari_bookmarks_parser.rb
39
+ - lib/safari_bookmarks_parser/bookmark.rb
40
+ - lib/safari_bookmarks_parser/bookmark_folder.rb
41
+ - lib/safari_bookmarks_parser/commands/dump_command.rb
42
+ - lib/safari_bookmarks_parser/parser.rb
43
+ - lib/safari_bookmarks_parser/runner.rb
44
+ - lib/safari_bookmarks_parser/version.rb
45
+ homepage: https://github.com/healthypackrat/safari_bookmarks_parser
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ homepage_uri: https://github.com/healthypackrat/safari_bookmarks_parser
50
+ source_code_uri: https://github.com/healthypackrat/safari_bookmarks_parser
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.6.6
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements:
66
+ - plutil
67
+ rubygems_version: 3.2.3
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Dump ~/Library/Safari/Bookmarks.plist as JSON/YAML
71
+ test_files: []