mork-parser 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mork/resolved/id"
4
+
5
+ module Mork
6
+ class Raw; end # rubocop:disable Lint/EmptyClass
7
+
8
+ # A raw Mork ID
9
+ class Raw::Id
10
+ attr_reader :raw
11
+
12
+ ACTIONS = {
13
+ "-" => :delete,
14
+ nil => :add
15
+ }.freeze
16
+
17
+ def initialize(raw:)
18
+ @raw = raw
19
+ end
20
+
21
+ def resolve(dictionaries:)
22
+ namespace = resolve_namespace(dictionaries)
23
+ Resolved::Id.new(action: action, namespace: namespace, id: id)
24
+ end
25
+
26
+ private
27
+
28
+ def action
29
+ ACTIONS[parts[:action]]
30
+ end
31
+
32
+ def id
33
+ parts[:id]
34
+ end
35
+
36
+ def raw_namespace
37
+ parts[:raw_namespace]
38
+ end
39
+
40
+ def resolve_namespace(dictionaries)
41
+ case
42
+ when raw_namespace.nil?
43
+ nil
44
+ when raw_namespace.start_with?("^")
45
+ value = raw_namespace[1..]
46
+ dictionary = dictionaries.fetch("c")
47
+ dictionary.fetch(value)
48
+ else
49
+ raw_namespace
50
+ end
51
+ end
52
+
53
+ # rubocop:disable Lint/MixedRegexpCaptureTypes
54
+ # Rubocop gives a false positive here
55
+ RAW_ID_MATCH = /
56
+ \A
57
+ \{? # The lexer captures the table delimiter
58
+ (?<action>-)? # The optional action can indicate deletion
59
+ (?<id>[0-9]+) # Tables are numbered
60
+ (
61
+ :
62
+ (?<raw_namespace>
63
+ \^? # The raw namespace may be a reference
64
+ \S+ # The name is everything but trailing whitespace
65
+ )
66
+ )? # The namespace is optional
67
+ /x.freeze
68
+ # rubocop:enable Lint/MixedRegexpCaptureTypes
69
+
70
+ def parts
71
+ @parts ||= RAW_ID_MATCH.match(raw)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mork
4
+ class Raw; end # rubocop:disable Lint/EmptyClass
5
+
6
+ # An alias indicating a Dictionary's scope
7
+ class Raw::MetaAlias
8
+ attr_reader :raw
9
+
10
+ def initialize(raw:)
11
+ @raw = raw
12
+ end
13
+
14
+ def scope
15
+ _from, to = raw.split("=")
16
+ to
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mork
4
+ class Raw; end # rubocop:disable Lint/EmptyClass
5
+
6
+ # A meta table
7
+ class Raw::MetaTable
8
+ attr_reader :raw
9
+
10
+ def initialize(raw:)
11
+ @raw = raw
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mork/raw/id"
4
+ require "mork/resolved/row"
5
+
6
+ module Mork
7
+ class Raw; end # rubocop:disable Lint/EmptyClass
8
+
9
+ # A row of cells
10
+ class Raw::Row
11
+ attr_reader :raw_id
12
+ attr_reader :cells
13
+
14
+ def initialize(raw_id:, cells:)
15
+ @raw_id = raw_id
16
+ @cells = cells
17
+ end
18
+
19
+ def resolve(dictionaries:)
20
+ resolved_id = raw_id_resolver.resolve(dictionaries: dictionaries)
21
+ Resolved::Row.new(
22
+ action: resolved_id.action,
23
+ namespace: resolved_id.namespace,
24
+ id: resolved_id.id,
25
+ cells: resolved_cells(dictionaries)
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ def raw_id_resolver
32
+ @raw_id_resolver ||= Raw::Id.new(raw: raw_id)
33
+ end
34
+
35
+ def resolved_cells(dictionaries)
36
+ cells.map { |c| c.resolve(dictionaries: dictionaries) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mork/raw/id"
4
+ require "mork/raw/row"
5
+ require "mork/resolved/table"
6
+
7
+ module Mork
8
+ class Raw; end # rubocop:disable Lint/EmptyClass
9
+
10
+ # A table of rows
11
+ class Raw::Table
12
+ attr_reader :raw_id
13
+ attr_reader :values
14
+
15
+ def initialize(raw_id:, values:)
16
+ @raw_id = raw_id
17
+ @values = values
18
+ end
19
+
20
+ def rows
21
+ values.filter { |c| c.is_a?(Raw::Row) }
22
+ end
23
+
24
+ def resolve(dictionaries:)
25
+ resolved_id = resolve_id(dictionaries)
26
+ Resolved::Table.new(
27
+ action: resolved_id.action, namespace: resolved_id.namespace, id: resolved_id.id,
28
+ rows: resolved_rows(dictionaries)
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ def raw_id_resolver
35
+ @raw_id_resolver ||= Raw::Id.new(raw: raw_id)
36
+ end
37
+
38
+ def resolve_id(dictionaries)
39
+ raw_id_resolver.resolve(dictionaries: dictionaries)
40
+ end
41
+
42
+ def resolved_rows(dictionaries)
43
+ rows.map { |r| r.resolve(dictionaries: dictionaries) }
44
+ end
45
+ end
46
+ end
data/lib/mork/raw.rb ADDED
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mork/data"
4
+ require "mork/raw/dictionary"
5
+ require "mork/raw/group"
6
+
7
+ module Mork
8
+ # Raw data returned by the Parser
9
+ class Raw
10
+ attr_reader :values
11
+
12
+ def initialize(values:)
13
+ @values = values
14
+ end
15
+
16
+ # build a Hash of scopes (usually "a" and "c")
17
+ # each value being a Hash of key-value entries
18
+ def dictionaries
19
+ @dictionaries ||=
20
+ raw_dictionaries.each.with_object({}) do |d, scopes|
21
+ scope = d.scope
22
+ scopes[scope] ||= {}
23
+ scopes[scope].merge!(d.to_h)
24
+ end
25
+ end
26
+
27
+ def data
28
+ Data.new(
29
+ rows: resolved_rows(dictionaries),
30
+ tables: resolved_tables(dictionaries)
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ def raw_dictionaries
37
+ @raw_dictionaries ||= top_level_dictionaries + group_dictionaries
38
+ end
39
+
40
+ def top_level_dictionaries
41
+ @top_level_dictionaries ||= values.filter { |v| v.is_a?(Raw::Dictionary) }
42
+ end
43
+
44
+ def group_dictionaries
45
+ @group_dictionaries ||= raw_groups.flat_map(&:dictionaries)
46
+ end
47
+
48
+ def raw_groups
49
+ @raw_groups ||= values.filter { |v| v.is_a?(Raw::Group) }
50
+ end
51
+
52
+ def resolved_groups(dictionaries)
53
+ raw_groups.
54
+ map { |g| g.resolve(dictionaries: dictionaries) }
55
+ end
56
+
57
+ ####################
58
+ # rows
59
+
60
+ def raw_rows
61
+ @raw_rows ||= values.filter { |v| v.is_a?(Raw::Row) }
62
+ end
63
+
64
+ def raw_tables
65
+ @raw_tables ||= values.filter { |v| v.is_a?(Raw::Table) }
66
+ end
67
+
68
+ def unmerged_rows(dictionaries)
69
+ top_level_rows = raw_rows.map { |r| r.resolve(dictionaries: dictionaries) }
70
+ group_rows =
71
+ resolved_groups(dictionaries).
72
+ map { |g| g[:rows] }.
73
+ flatten
74
+ (top_level_rows + group_rows).group_by(&:namespace)
75
+ end
76
+
77
+ def resolved_rows(dictionaries)
78
+ unmerged_rows(dictionaries).
79
+ each.with_object({}) do |(namespace, rows), acc|
80
+ merged = reduce_rows(rows)
81
+ acc[namespace] = merged if merged.any?
82
+ end
83
+ end
84
+
85
+ def reduce_rows(rows)
86
+ rows.each.with_object({}) do |row, acc|
87
+ case row.action
88
+ when :add
89
+ acc[row.id] = row.to_h
90
+ when :delete
91
+ acc.delete(row.id)
92
+ end
93
+ end
94
+ end
95
+
96
+ ####################
97
+ # tables
98
+
99
+ def resolved_tables(dictionaries)
100
+ unmerged = unmerged_tables(dictionaries)
101
+ merged = merge_tables(unmerged)
102
+ reduce_tables(merged)
103
+ end
104
+
105
+ def reduce_tables(tables)
106
+ tables.
107
+ each.with_object({}) do |(namespace, namespace_tables), acc1|
108
+ acc1[namespace] =
109
+ namespace_tables.each.with_object({}) do |(id, rows), acc2|
110
+ acc2[id] = reduce_rows(rows)
111
+ end
112
+ end
113
+ end
114
+
115
+ def merge_tables(tables)
116
+ tables.
117
+ each.with_object({}) do |(namespace, namespace_tables), acc|
118
+ merged = merge_namespace_tables(namespace_tables)
119
+ acc[namespace] = merged if merged.any?
120
+ end
121
+ end
122
+
123
+ def unmerged_tables(dictionaries)
124
+ top_level_tables = unmerged_resolved_tables(dictionaries)
125
+ group_tables =
126
+ resolved_groups(dictionaries).
127
+ map { |g| g[:tables] }.
128
+ flatten
129
+ (top_level_tables + group_tables).group_by(&:namespace)
130
+ end
131
+
132
+ def merge_namespace_tables(tables)
133
+ tables.each.with_object({}) do |table, acc|
134
+ case table.action
135
+ when :add
136
+ acc[table.id] ||= []
137
+ acc[table.id] += table.rows
138
+ when :delete
139
+ acc.delete(table.id)
140
+ end
141
+ end
142
+ end
143
+
144
+ def unmerged_resolved_tables(dictionaries)
145
+ raw_tables.map { |t| t.resolve(dictionaries: dictionaries) }
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mork
4
+ class Resolved; end # rubocop:disable Lint/EmptyClass
5
+
6
+ # A resolved Cell
7
+ class Resolved::Cell
8
+ attr_reader :key
9
+ attr_reader :value
10
+
11
+ def initialize(key:, value:)
12
+ @key = key
13
+ @value = value
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mork
4
+ class Resolved; end # rubocop:disable Lint/EmptyClass
5
+
6
+ # A resolved Mork Id
7
+ class Resolved::Id
8
+ attr_reader :action
9
+ attr_reader :namespace
10
+ attr_reader :id
11
+
12
+ def initialize(action:, namespace:, id:)
13
+ @action = action
14
+ @namespace = namespace
15
+ @id = id
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mork
4
+ class Resolved; end # rubocop:disable Lint/EmptyClass
5
+
6
+ # A resolved Row
7
+ class Resolved::Row
8
+ attr_reader :action
9
+ attr_reader :namespace
10
+ attr_reader :id
11
+ attr_reader :cells
12
+
13
+ def initialize(action:, namespace:, id:, cells:)
14
+ @action = action
15
+ @namespace = namespace
16
+ @id = id
17
+ @cells = cells
18
+ end
19
+
20
+ def to_h
21
+ cells.
22
+ each.with_object({}) do |cell, acc|
23
+ acc[cell.key] = cell.value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mork
4
+ class Resolved; end # rubocop:disable Lint/EmptyClass
5
+
6
+ # A resolved Table
7
+ class Resolved::Table
8
+ attr_reader :action
9
+ attr_reader :namespace
10
+ attr_reader :id
11
+ attr_reader :rows
12
+
13
+ def initialize(action:, namespace:, id:, rows:)
14
+ @action = action
15
+ @namespace = namespace
16
+ @id = id
17
+ @rows = rows
18
+ end
19
+ end
20
+ end
data/lib/mork.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mork
4
+ VERSION = "0.1.1"
5
+ end
data/mork.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/mork"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "mork-parser"
7
+ spec.version = Mork::VERSION
8
+ spec.authors = ["Joe Yates"]
9
+ spec.email = ["joe.g.yates@gmail.com"]
10
+
11
+ spec.summary = "Parse Mork databases (as used by Mozilla Thunderbird)"
12
+ spec.homepage = "https://github.com/joeyates/mork"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.7"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = File.join(spec.homepage, "blob/main/CHANGELOG.md")
19
+ spec.metadata["rubygems_mfa_required"] = "true"
20
+
21
+ spec.files = Dir.glob("lib/**/*.rb")
22
+ spec.files += ["mork.gemspec"]
23
+ spec.files += %w[LICENSE.txt README.md]
24
+ spec.bindir = "exe"
25
+ spec.executables = []
26
+ spec.require_paths = ["lib"]
27
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mork-parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Yates
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-01-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - joe.g.yates@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - lib/mork.rb
23
+ - lib/mork/data.rb
24
+ - lib/mork/lexer.rb
25
+ - lib/mork/parser.rb
26
+ - lib/mork/raw.rb
27
+ - lib/mork/raw/alias.rb
28
+ - lib/mork/raw/cell.rb
29
+ - lib/mork/raw/dictionary.rb
30
+ - lib/mork/raw/group.rb
31
+ - lib/mork/raw/id.rb
32
+ - lib/mork/raw/meta_alias.rb
33
+ - lib/mork/raw/meta_table.rb
34
+ - lib/mork/raw/row.rb
35
+ - lib/mork/raw/table.rb
36
+ - lib/mork/resolved/cell.rb
37
+ - lib/mork/resolved/id.rb
38
+ - lib/mork/resolved/row.rb
39
+ - lib/mork/resolved/table.rb
40
+ - mork.gemspec
41
+ homepage: https://github.com/joeyates/mork
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://github.com/joeyates/mork
46
+ source_code_uri: https://github.com/joeyates/mork
47
+ changelog_uri: https://github.com/joeyates/mork/blob/main/CHANGELOG.md
48
+ rubygems_mfa_required: 'true'
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '2.7'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.4.10
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: Parse Mork databases (as used by Mozilla Thunderbird)
68
+ test_files: []