mork-parser 0.1.1

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,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: []