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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/lib/mork/data.rb +14 -0
- data/lib/mork/lexer.rb +315 -0
- data/lib/mork/parser.rb +503 -0
- data/lib/mork/raw/alias.rb +16 -0
- data/lib/mork/raw/cell.rb +55 -0
- data/lib/mork/raw/dictionary.rb +37 -0
- data/lib/mork/raw/group.rb +46 -0
- data/lib/mork/raw/id.rb +74 -0
- data/lib/mork/raw/meta_alias.rb +19 -0
- data/lib/mork/raw/meta_table.rb +14 -0
- data/lib/mork/raw/row.rb +39 -0
- data/lib/mork/raw/table.rb +46 -0
- data/lib/mork/raw.rb +148 -0
- data/lib/mork/resolved/cell.rb +16 -0
- data/lib/mork/resolved/id.rb +18 -0
- data/lib/mork/resolved/row.rb +27 -0
- data/lib/mork/resolved/table.rb +20 -0
- data/lib/mork.rb +5 -0
- data/mork.gemspec +27 -0
- metadata +68 -0
data/lib/mork/raw/id.rb
ADDED
@@ -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
|
data/lib/mork/raw/row.rb
ADDED
@@ -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
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: []
|