pangrid 0.5.3 → 0.6.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 +28 -9
- data/lib/pangrid/clues.rb +80 -0
- data/lib/pangrid/plugin.rb +7 -2
- data/lib/pangrid/plugins/acrosslite.rb +53 -19
- data/lib/pangrid/plugins/csv.rb +18 -13
- data/lib/pangrid/plugins/excel.rb +1 -1
- data/lib/pangrid/plugins/exolve.rb +15 -3
- data/lib/pangrid/plugins/json.rb +19 -3
- data/lib/pangrid/plugins/png.rb +5 -5
- data/lib/pangrid/plugins/reddit.rb +11 -19
- data/lib/pangrid/plugins/text.rb +0 -8
- data/lib/pangrid/utils.rb +17 -0
- data/lib/pangrid/version.rb +1 -1
- data/lib/pangrid/xw.rb +62 -36
- data/lib/pangrid.rb +78 -31
- metadata +20 -8
- data/lib/deps/trollop.rb +0 -838
- data/lib/pangrid/data/webform.html +0 -24
- data/lib/pangrid/frontend/webrick.rb +0 -74
data/lib/pangrid/xw.rb
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require 'ostruct'
|
|
2
|
-
|
|
3
1
|
module Pangrid
|
|
4
2
|
|
|
5
3
|
class PuzzleFormatError < StandardError
|
|
@@ -62,14 +60,7 @@ class Cell
|
|
|
62
60
|
end
|
|
63
61
|
end
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
case solution
|
|
67
|
-
when :black; '#'
|
|
68
|
-
when :null; '.'
|
|
69
|
-
when Rebus; solution.inspect
|
|
70
|
-
else; solution
|
|
71
|
-
end
|
|
72
|
-
end
|
|
63
|
+
alias_method :inspect, :to_w
|
|
73
64
|
end
|
|
74
65
|
|
|
75
66
|
# solution = Cell[][]
|
|
@@ -77,7 +68,53 @@ end
|
|
|
77
68
|
# across_clues = string[]
|
|
78
69
|
# down_clues = string[]
|
|
79
70
|
# rebus = { solution => [int, rebus_char] }
|
|
80
|
-
class XWord
|
|
71
|
+
class XWord
|
|
72
|
+
# Metadata shared by all formats
|
|
73
|
+
COMMON = %i(width height solution across_clues down_clues
|
|
74
|
+
title author copyright notes preamble rebus)
|
|
75
|
+
|
|
76
|
+
# AcrossLite binary format fields
|
|
77
|
+
# clues: across and down clues interleaved in numerical order
|
|
78
|
+
# fill: the solver's progress
|
|
79
|
+
# mark: AcrossLite text format rebus flag
|
|
80
|
+
ACROSSLITE = %i(clues n_clues fill version puzzle_type scrambled_state
|
|
81
|
+
extensions mark time_elapsed)
|
|
82
|
+
|
|
83
|
+
ATTRIBUTES = COMMON + ACROSSLITE
|
|
84
|
+
|
|
85
|
+
attr_accessor(*ATTRIBUTES)
|
|
86
|
+
|
|
87
|
+
# Unknown attributes raise NoMethodError rather than being silently
|
|
88
|
+
# accepted, so that typos show up immediately.
|
|
89
|
+
def initialize(**args)
|
|
90
|
+
@across_clues = []
|
|
91
|
+
@down_clues = []
|
|
92
|
+
args.each {|k, v| self[k] = v}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Writers can rely on the clue lists being arrays
|
|
96
|
+
def across_clues=(clues)
|
|
97
|
+
@across_clues = clues || []
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def down_clues=(clues)
|
|
101
|
+
@down_clues = clues || []
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def [](k)
|
|
105
|
+
check_attribute(k)
|
|
106
|
+
public_send(k)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def []=(k, v)
|
|
110
|
+
check_attribute(k)
|
|
111
|
+
public_send(:"#{k}=", v)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def to_h
|
|
115
|
+
ATTRIBUTES.to_h {|a| [a, public_send(a)]}.compact
|
|
116
|
+
end
|
|
117
|
+
|
|
81
118
|
# Clue numbering
|
|
82
119
|
def black?(x, y)
|
|
83
120
|
solution[y][x].black?
|
|
@@ -112,6 +149,7 @@ class XWord < OpenStruct
|
|
|
112
149
|
def number(words = false)
|
|
113
150
|
n, across, down = 1, [], []
|
|
114
151
|
words_a, words_d = [], []
|
|
152
|
+
each_cell {|c| c.number = nil}
|
|
115
153
|
(0 ... height).each do |y|
|
|
116
154
|
(0 ... width).each do |x|
|
|
117
155
|
if across? x, y
|
|
@@ -156,9 +194,7 @@ class XWord < OpenStruct
|
|
|
156
194
|
case s
|
|
157
195
|
when :black, :null
|
|
158
196
|
opts[s]
|
|
159
|
-
when String
|
|
160
|
-
block_given? ? (yield c) : c.to_char
|
|
161
|
-
when Rebus
|
|
197
|
+
when String, Rebus
|
|
162
198
|
block_given? ? (yield c) : c.to_char
|
|
163
199
|
else
|
|
164
200
|
raise PuzzleFormatError, "Unrecognised cell #{c}"
|
|
@@ -174,8 +210,8 @@ class XWord < OpenStruct
|
|
|
174
210
|
each_cell do |c|
|
|
175
211
|
if c.rebus?
|
|
176
212
|
r = c.solution
|
|
177
|
-
if self.rebus[
|
|
178
|
-
sym, _ = self.rebus[
|
|
213
|
+
if self.rebus[r.solution]
|
|
214
|
+
sym, _ = self.rebus[r.solution]
|
|
179
215
|
r.symbol = sym.to_s
|
|
180
216
|
else
|
|
181
217
|
k += 1
|
|
@@ -186,28 +222,18 @@ class XWord < OpenStruct
|
|
|
186
222
|
end
|
|
187
223
|
end
|
|
188
224
|
|
|
225
|
+
def check_attribute(k)
|
|
226
|
+
unless ATTRIBUTES.include?(k.to_sym)
|
|
227
|
+
raise NoMethodError, "unknown XWord attribute #{k}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
189
231
|
def inspect_grid
|
|
190
232
|
number
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
when :black
|
|
196
|
-
"#"
|
|
197
|
-
when :null
|
|
198
|
-
"."
|
|
199
|
-
when String
|
|
200
|
-
c.to_char
|
|
201
|
-
when Rebus
|
|
202
|
-
c.to_char
|
|
203
|
-
else
|
|
204
|
-
raise PuzzleFormatError, "Unrecognised cell #{c}"
|
|
205
|
-
end
|
|
206
|
-
if c.number && c.number > 0
|
|
207
|
-
o = "#{c.number} #{o}"
|
|
208
|
-
end
|
|
209
|
-
o = o.rjust(4)
|
|
210
|
-
}.join("|")
|
|
233
|
+
to_array(:black => '#', :null => '.') {|c|
|
|
234
|
+
c.number ? "#{c.number} #{c.to_char}" : c.to_char
|
|
235
|
+
}.map {|row|
|
|
236
|
+
row.map {|o| o.rjust(4)}.join("|")
|
|
211
237
|
}.join("\n")
|
|
212
238
|
end
|
|
213
239
|
end
|
data/lib/pangrid.rb
CHANGED
|
@@ -1,41 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
require 'optparse'
|
|
2
2
|
|
|
3
3
|
require_relative 'pangrid/version'
|
|
4
4
|
require_relative 'pangrid/plugin'
|
|
5
|
-
require_relative 'pangrid/
|
|
5
|
+
require_relative 'pangrid/clues'
|
|
6
6
|
|
|
7
7
|
module Pangrid
|
|
8
|
-
def self.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
def self.option_parser(opts)
|
|
9
|
+
OptionParser.new do |p|
|
|
10
|
+
p.banner = "Usage: pangrid -f FORMAT -t FORMAT -i INPUT -o OUTPUT [-c CLUES]\n" +
|
|
11
|
+
" pangrid -l"
|
|
12
|
+
p.version = VERSION
|
|
13
|
+
p.separator ""
|
|
14
|
+
p.separator "Options:"
|
|
15
|
+
p.on("-f", "--from=FORMAT", "Format to convert from") {|v| opts[:from] = v}
|
|
16
|
+
p.on("-t", "--to=FORMAT", "Format to convert to") {|v| opts[:to] = v}
|
|
17
|
+
p.on("-i", "--in=FILE", "Input file") {|v| opts[:in] = v}
|
|
18
|
+
p.on("-o", "--out=FILE", "Output file") {|v| opts[:out] = v}
|
|
19
|
+
p.on("-c", "--clues=FILE", "Add clues from a text file (see README)") {|v|
|
|
20
|
+
opts[:clues] = v
|
|
21
|
+
}
|
|
22
|
+
p.on("-l", "--list", "List available format plugins") { opts[:list] = true }
|
|
23
|
+
p.on("-v", "--version", "Print version and exit") {
|
|
24
|
+
puts "pangrid #{VERSION}"
|
|
25
|
+
exit
|
|
26
|
+
}
|
|
27
|
+
p.on("-h", "--help", "Show this message") {
|
|
28
|
+
puts p
|
|
29
|
+
exit
|
|
30
|
+
}
|
|
18
31
|
end
|
|
32
|
+
end
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
def self.run_command_line(argv = ARGV)
|
|
35
|
+
opts = {}
|
|
36
|
+
parser = option_parser(opts)
|
|
37
|
+
begin
|
|
38
|
+
parser.parse!(argv.dup)
|
|
39
|
+
rescue OptionParser::ParseError => e
|
|
40
|
+
$stderr.puts "Error: #{e.message}"
|
|
41
|
+
$stderr.puts "Try --help for help."
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
p.educate
|
|
29
|
-
end
|
|
45
|
+
if opts[:list] || [:from, :to, :in, :out].all? {|k| opts[k]}
|
|
46
|
+
exit 1 unless run opts
|
|
47
|
+
else
|
|
48
|
+
$stderr.puts parser
|
|
49
|
+
exit 1
|
|
30
50
|
end
|
|
31
51
|
end
|
|
32
52
|
|
|
53
|
+
# Returns true on success. Errors are reported on stderr.
|
|
54
|
+
|
|
33
55
|
def self.run(opts)
|
|
34
56
|
Plugin.load_all
|
|
35
57
|
|
|
36
58
|
if opts[:list]
|
|
37
59
|
Plugin.list_all
|
|
38
|
-
return
|
|
60
|
+
return true
|
|
39
61
|
end
|
|
40
62
|
|
|
41
63
|
# run the converter
|
|
@@ -45,25 +67,50 @@ module Pangrid
|
|
|
45
67
|
|
|
46
68
|
if !from or !from.method_defined? :read
|
|
47
69
|
$stderr.puts "No reader for #{opts[:from]}"
|
|
48
|
-
return
|
|
70
|
+
return false
|
|
49
71
|
end
|
|
50
72
|
|
|
51
73
|
if !to or !to.method_defined? :write
|
|
52
74
|
$stderr.puts "No writer for #{opts[:to]}"
|
|
53
|
-
return
|
|
75
|
+
return false
|
|
54
76
|
end
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
[opts[:in], opts[:clues]].compact.each do |f|
|
|
79
|
+
if !File.exist? f
|
|
80
|
+
$stderr.puts "Cannot find file #{f}"
|
|
81
|
+
return false
|
|
82
|
+
end
|
|
59
83
|
end
|
|
60
84
|
|
|
61
85
|
reader = from.new
|
|
62
86
|
writer = to.new
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
# Read and write in binary mode so that binary formats (.puz, .png,
|
|
88
|
+
# .xlsx) aren't mangled by newline translation on Windows. Text readers
|
|
89
|
+
# expect UTF-8; the binary reader reinterprets the bytes itself.
|
|
90
|
+
input = File.binread(opts[:in]).force_encoding(Encoding::UTF_8)
|
|
91
|
+
begin
|
|
92
|
+
xw = reader.read(input)
|
|
93
|
+
rescue PuzzleFormatError => e
|
|
94
|
+
$stderr.puts "Could not convert #{opts[:in]}: #{e.message}"
|
|
95
|
+
return false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if opts[:clues]
|
|
99
|
+
begin
|
|
100
|
+
ClueList.read(File.read(opts[:clues], encoding: "UTF-8")).apply(xw)
|
|
101
|
+
rescue PuzzleFormatError => e
|
|
102
|
+
$stderr.puts "Could not add clues from #{opts[:clues]}: #{e.message}"
|
|
103
|
+
return false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
output = writer.write(xw)
|
|
109
|
+
rescue PuzzleFormatError => e
|
|
110
|
+
$stderr.puts "Could not convert #{opts[:in]}: #{e.message}"
|
|
111
|
+
return false
|
|
67
112
|
end
|
|
113
|
+
File.binwrite(opts[:out], output)
|
|
114
|
+
true
|
|
68
115
|
end
|
|
69
116
|
end
|
metadata
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pangrid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin DeMello
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-11-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: chunky_png
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 1.
|
|
19
|
+
version: '1.4'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 1.
|
|
26
|
+
version: '1.4'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: csv
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
27
41
|
description:
|
|
28
42
|
email: martindemello@gmail.com
|
|
29
43
|
executables:
|
|
@@ -35,10 +49,8 @@ files:
|
|
|
35
49
|
- LICENSE
|
|
36
50
|
- README
|
|
37
51
|
- bin/pangrid
|
|
38
|
-
- lib/deps/trollop.rb
|
|
39
52
|
- lib/pangrid.rb
|
|
40
|
-
- lib/pangrid/
|
|
41
|
-
- lib/pangrid/frontend/webrick.rb
|
|
53
|
+
- lib/pangrid/clues.rb
|
|
42
54
|
- lib/pangrid/plugin.rb
|
|
43
55
|
- lib/pangrid/plugins/acrosslite.rb
|
|
44
56
|
- lib/pangrid/plugins/csv.rb
|