pangrid 0.5.3 → 0.6.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.
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
- def inspect
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 < OpenStruct
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[s]
178
- sym, _ = self.rebus[s]
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
- solution.map {|row|
192
- row.map {|c|
193
- s = c.solution
194
- o = case s
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
- require_relative 'deps/trollop'
1
+ require 'optparse'
2
2
 
3
3
  require_relative 'pangrid/version'
4
4
  require_relative 'pangrid/plugin'
5
- require_relative 'pangrid/frontend/webrick'
5
+ require_relative 'pangrid/clues'
6
6
 
7
7
  module Pangrid
8
- def self.run_command_line
9
- # command line options
10
- p = Trollop::Parser.new do
11
- version "pangrid #{VERSION}"
12
- opt :from, "Format to convert from", :type => :string
13
- opt :to, "Format to convert to", :type => :string
14
- opt :in, "Input file", :type => :string
15
- opt :out, "Output file", :type => :string
16
- opt :list, "List available format plugins"
17
- opt :web, "Launch webserver"
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
- Trollop::with_standard_exception_handling p do
21
- opts = p.parse ARGV
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
- if opts[:web]
24
- run_webserver 1234
25
- elsif opts[:list] || [:from, :to, :in, :out].all? {|k| opts[k]}
26
- run opts
27
- else
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
- if !File.exist? opts[:in]
57
- $stderr.puts "Cannot find file #{opts[:in]}"
58
- return
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
- input = IO.read(opts[:in])
64
- output = writer.write(reader.read(input))
65
- File.open(opts[:out], "w") do |f|
66
- f.print output
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.5.3
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin DeMello
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-27 00:00:00.000000000 Z
11
+ date: 2026-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: webrick
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.7.0
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.7.0
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/data/webform.html
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