pangrid 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ca64b939037c4c658be062373bc66156605c3833
4
- data.tar.gz: 8792223200b3f4a5bfdcd212127c6724b4a087ae
2
+ SHA256:
3
+ metadata.gz: 33b68bb3ffccf5e357c16637245e4488b3541070737f171e2773e035ccdfbcfd
4
+ data.tar.gz: 05b0853605cccef29232acb6f96e1a53e1356e830e85a7c158595c426c179fbf
5
5
  SHA512:
6
- metadata.gz: a3361137ff447f19ae7f2db13ca66c259b10a1f13387382881c25d5f63e5b84a7516999bf19c27a4b984ee08553f8461e7db66389e8391fe2f867b8523bd3aa5
7
- data.tar.gz: becb38b39f34b7e25876e5d1a7d148bb92a205fd59230fbb11ea465680099bf65bc46952391df8dafb8a6cf8f9aba7d073388d20504a92c2224058e0ccc689e7
6
+ metadata.gz: '00778b056954ae735461bc4782e5c9aae027ac6d0b2928a6253507afa8113abbc26ee908ba4e68b49c42d128a15b38406604eb6303e6214af88969824c2854b9'
7
+ data.tar.gz: de0cd413482c54b815ecf2eb97b41511b1a309858d61a2e3571cfff98fb1d6478fda2d5f4c332142253ae25969fa682dca97ddcb8d68d49f39b162f248614630
@@ -26,11 +26,30 @@ class Servlet < WEBrick::HTTPServlet::AbstractServlet
26
26
  rescue Exception => e
27
27
  out = e.inspect
28
28
  end
29
-
30
- template = IO.read(TEMPLATE)
31
- response.status = 200
32
- response.content_type = "text/html"
33
- response.body = template % out
29
+
30
+ response.header['Access-Control-Allow-Origin'] = '*'
31
+
32
+ case request.path
33
+ when "/"
34
+ template = IO.read(TEMPLATE)
35
+ response.status = 200
36
+ response.content_type = "text/html"
37
+ response.body = template % out
38
+ when "/blob"
39
+ response.status = 200
40
+ response.content_type = "application/octet-stream"
41
+ response.body = out
42
+ when "/json"
43
+ if request.query["to"] == "json"
44
+ response.status = 200
45
+ response.content_type = "text/json"
46
+ response.body = out
47
+ else
48
+ response.status = 200
49
+ response.content_type = "text/json"
50
+ response.body = '{ "error" : "non-json format requested" }'
51
+ end
52
+ end
34
53
  end
35
54
  end
36
55
 
@@ -35,6 +35,8 @@ class Plugin
35
35
  FAILED = []
36
36
  MISSING_DEPS = {}
37
37
 
38
+ DESCRIPTION = ""
39
+
38
40
  def self.inherited(subclass)
39
41
  name = class_to_name(subclass.name)
40
42
  #puts "Registered #{subclass} as #{name}"
@@ -62,13 +64,13 @@ class Plugin
62
64
 
63
65
  def self.list_all
64
66
  puts "-------------------------------------------------------"
65
- puts "Available plugins:"
67
+ puts "Available plugins (F = from, T = to):"
66
68
  puts "-------------------------------------------------------"
67
69
  REGISTRY.keys.sort.each do |name|
68
70
  plugin = REGISTRY[name]
69
71
  provides = [:read, :write].select {|m| plugin.method_defined? m}
70
- provides = provides.map {|m| {read: 'from', write: 'to'}[m]}
71
- puts " " + name + " [" + provides.join(", ") + "]"
72
+ provides = provides.map {|m| {read: 'F', write: 'T'}[m]}.join
73
+ puts " #{name} [#{provides}]".ljust(30) + plugin.const_get(:DESCRIPTION)
72
74
  end
73
75
  if !MISSING_DEPS.empty?
74
76
  puts
@@ -75,6 +75,8 @@ class AcrossLiteBinary < Plugin
75
75
  # crossword, checksums
76
76
  attr_accessor :xw, :cs
77
77
 
78
+ DESCRIPTION = "AcrossLite binary format (.puz)"
79
+
78
80
  HEADER_FORMAT = "v A12 v V2 A4 v2 A12 c2 v3"
79
81
  HEADER_CHECKSUM_FORMAT = "c2 v3"
80
82
  EXT_HEADER_FORMAT = "A4 v2"
@@ -359,6 +361,8 @@ class AcrossLiteText < Plugin
359
361
 
360
362
  attr_accessor :xw, :rebus
361
363
 
364
+ DESCRIPTION = "AcrossLite text format"
365
+
362
366
  def initialize
363
367
  @xw = XWord.new
364
368
  end
@@ -26,6 +26,9 @@ require 'csv'
26
26
  module Pangrid
27
27
 
28
28
  class CSV < Plugin
29
+
30
+ DESCRIPTION = "CSV reader (see source comments for format)"
31
+
29
32
  def read(data)
30
33
  s = ::CSV.parse(data)
31
34
  s.reject! {|row| row.compact.empty?}
@@ -11,6 +11,9 @@ module Pangrid
11
11
  require_for_plugin 'excel', ['axlsx']
12
12
 
13
13
  class ExcelXSLX < Plugin
14
+
15
+ DESCRIPTION = "Excel writer. Useful to upload to google sheets."
16
+
14
17
  # styles
15
18
  STYLES = {
16
19
  :black => {:bg_color => "00"},
@@ -0,0 +1,128 @@
1
+ # Exolve (https://github.com/viresh-ratnakar/exolve) is a browser-based
2
+ # crossword solver that allows you to embed the crossword and solving software
3
+ # in a single HTML file.
4
+
5
+ module Pangrid
6
+
7
+ module ExolveWriter
8
+ def write(xw)
9
+ headers = format_headers(xw)
10
+ across, down = xw.number
11
+ grid = format_grid(xw)
12
+ ac = format_clues(across, xw.across_clues)
13
+ dn = format_clues(down, xw.down_clues)
14
+ across = ["exolve-across:"] + indent(ac)
15
+ down = ["exolve-down:"] + indent(dn)
16
+ grid = ["exolve-grid:"] + indent(grid)
17
+ body = headers + grid + across + down
18
+ out = ["exolve-begin"] + indent(body) + ["exolve-end"]
19
+ out.join("\n")
20
+ end
21
+
22
+ def format_headers(xw)
23
+ headers = [
24
+ 'id', 'replace-with-unique-id',
25
+ 'title', xw.title,
26
+ 'setter', xw.author,
27
+ 'width', xw.width,
28
+ 'height', xw.height,
29
+ 'copyright', xw.copyright,
30
+ 'prelude', xw.preamble
31
+ ]
32
+ headers.each_slice(2).select {|k, v| v}.map {|k, v| "exolve-#{k}: #{v}"}
33
+ end
34
+
35
+ def format_clues(numbers, clues)
36
+ numbers.zip(clues).map {|n, c| "#{n.to_s.rjust(2)} #{c}"}
37
+ end
38
+
39
+ def indent(lines)
40
+ lines.map {|x| " " + x}
41
+ end
42
+ end
43
+
44
+ module ExolveReader
45
+ def read(data)
46
+ s = data.each_line.map(&:rstrip)
47
+ first = s.index('exolve-begin')
48
+ check("exolve-begin missing") { first }
49
+ last = s.index('exolve-end')
50
+ check("exolve-end missing") { last }
51
+ lines = s[(first + 1)...last]
52
+ xw = XWord.new
53
+ s = sections(lines)
54
+ s.each do |_, field, data|
55
+ if %w(title setter copyright prelude).include? field
56
+ xw[field] = data
57
+ elsif %w(height width).include? field
58
+ xw[field] = data.to_i
59
+ elsif %(across down).include? field
60
+ xw["#{field}_clues"] = data
61
+ elsif field == "grid"
62
+ xw.solution = parse_grid(data)
63
+ end
64
+ end
65
+ xw
66
+ end
67
+
68
+ def sections(lines)
69
+ headers = lines.each.with_index.map do |l, i|
70
+ m = l.match(/^(\s+)exolve-(\w+):(.*)$/)
71
+ if m
72
+ _, indent, field, data = m.to_a
73
+ [i, field, data.strip]
74
+ else
75
+ nil
76
+ end
77
+ end
78
+ headers.compact!
79
+ headers.push([lines.length, "", ""])
80
+ headers.each_cons(2) do |i, j|
81
+ if i[2].empty?
82
+ i[2] = lines[(i[0] + 1)...j[0]].map(&:strip)
83
+ end
84
+ end
85
+ headers.pop
86
+ headers
87
+ end
88
+
89
+ def parse_grid_char(char)
90
+ case char
91
+ when '0'; :null
92
+ when '.'; :black
93
+ else; char
94
+ end
95
+ end
96
+
97
+ def parse_grid(lines)
98
+ grid = lines.map(&:strip).map {|x| x.split(//)}
99
+ grid.map do |col|
100
+ col.map do |c|
101
+ Cell.new(:solution => parse_grid_char(c))
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ class ExolveFilled < Plugin
108
+ include ExolveReader
109
+ include ExolveWriter
110
+
111
+ DESCRIPTION = "Exolve writer with solutions"
112
+
113
+ def format_grid(xw)
114
+ xw.to_array(:black => '.', :null => '0').map(&:join)
115
+ end
116
+ end
117
+
118
+ class ExolveBlank < Plugin
119
+ include ExolveWriter
120
+
121
+ DESCRIPTION = "Exolve writer without solutions"
122
+
123
+ def format_grid(xw)
124
+ xw.to_array(:black => '.', :null => '0') {|c| 0}.map(&:join)
125
+ end
126
+ end
127
+
128
+ end # module Pangrid
@@ -0,0 +1,78 @@
1
+ # Json
2
+ #
3
+ # cell = { x : int, y : int, contents : string }
4
+ #
5
+ # xword = { rows : int,
6
+ # cols : int,
7
+ # cells : [cell],
8
+ # across : [string]
9
+ # down : [string]
10
+ # }
11
+
12
+ require 'json'
13
+
14
+ module Pangrid
15
+
16
+ class Json < Plugin
17
+
18
+ DESCRIPTION = "Simple JSON format"
19
+
20
+ def write(xw)
21
+ cells = []
22
+ (0 ... xw.height).each do |y|
23
+ (0 ... xw.width).each do |x|
24
+ cell = xw.solution[y][x]
25
+ s = case cell.solution
26
+ when :black; '#'
27
+ when :null; ''
28
+ when Rebus; cell.solution.inspect
29
+ else; cell.solution
30
+ end
31
+
32
+ cells.push({ x: x, y: y, contents: s })
33
+ end
34
+ end
35
+
36
+ h = {
37
+ rows: xw.height,
38
+ cols: xw.width,
39
+ cells: cells,
40
+ across: xw.across_clues,
41
+ down: xw.down_clues
42
+ }
43
+
44
+ ::JSON.generate(h)
45
+ end
46
+
47
+ def read(data)
48
+ json = ::JSON.parse(data)
49
+ xw = XWord.new
50
+
51
+ xw.height = json['rows']
52
+ xw.width = json['cols']
53
+ xw.solution = Array.new(xw.height) { Array.new(xw.width) }
54
+ json['cells'].each do |c|
55
+ cell = Cell.new
56
+ s = c['contents']
57
+ cell.solution =
58
+ case s
59
+ when ""; :null
60
+ when "#"; :black
61
+ else
62
+ if s.length == 1
63
+ s
64
+ else
65
+ Rebus.new s
66
+ end
67
+ end
68
+ x, y = c['x'], c['y']
69
+ xw.solution[y][x] = cell
70
+ end
71
+ xw.across_clues = json['across']
72
+ xw.down_clues = json['down']
73
+ xw
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,25 @@
1
+ require 'chunky_png'
2
+
3
+ module Pangrid
4
+
5
+ class PNGThumbnail < Plugin
6
+
7
+ def write(xw)
8
+ black = ChunkyPNG::Color::BLACK
9
+ white = ChunkyPNG::Color::WHITE
10
+ red = ChunkyPNG::Color::rgb(255, 0, 0)
11
+ grey = ChunkyPNG::Color::rgb(128, 128, 128)
12
+ scale = 4
13
+ png = ChunkyPNG::Image.new(64, 64, ChunkyPNG::Color::TRANSPARENT)
14
+ xw.each_cell_with_coords do |x, y, cell|
15
+ c = cell.black? ? black : white
16
+ png.rect(
17
+ x * scale, y * scale, (x + 1) * scale, (y + 1) * scale,
18
+ stroke_color = grey, fill_color = c)
19
+ end
20
+ png.rect(0, 0, xw.width * scale, xw.height * scale, stroke_color = red)
21
+ png.to_blob
22
+ end
23
+ end
24
+
25
+ end # module Pangrid
@@ -0,0 +1,49 @@
1
+ # Qxw savefile for rectangular grids [http://www.quinapalus.com/qxw.html]
2
+
3
+ module Pangrid
4
+
5
+ QXW_GRID_ERROR = "Could not read grid from .qxw file"
6
+
7
+ class Qxw < Plugin
8
+
9
+ DESCRIPTION = "QXW grid reader (rectangular grids only)"
10
+
11
+ def read(data)
12
+ xw = XWord.new
13
+ lines = data.lines.map(&:chomp)
14
+ gp = lines.find {|x| x =~ /^GP( \d+){6}$/}
15
+ check("Could not read grid size from .qxw file") { gp }
16
+ type, w, h, _ = gp.scan(/\d+/).map(&:to_i)
17
+ check("Only rectangular grids are supported") { type == 0 }
18
+ xw.width = w
19
+ xw.height = h
20
+ xw.solution = []
21
+ grid = lines.select {|x| x =~ /^SQ /}
22
+ grid.each do |line|
23
+ parts = line.scan(/\w+/)
24
+ check(QXW_GRID_ERROR) { parts.length == 6 || parts.length == 7 }
25
+ col, row, b, c = parts[1].to_i, parts[2].to_i, parts[5].to_i, parts[6]
26
+ cell = Cell.new
27
+ if b == 1
28
+ cell.solution = :black
29
+ elsif c == nil
30
+ cell.solution = :null
31
+ else
32
+ cell.solution = c
33
+ end
34
+ xw.solution[row] ||= []
35
+ xw.solution[row][col] = cell
36
+ end
37
+ check(QXW_GRID_ERROR) { xw.solution.length == xw.height }
38
+ check(QXW_GRID_ERROR) { xw.solution.all? {|i| i.compact.length == xw.width} }
39
+
40
+ # Placeholder clues
41
+ _, _, words_a, words_d = xw.number(true)
42
+ xw.across_clues = words_a.map {|i| "[#{i}]" }
43
+ xw.down_clues = words_d.map {|i| "[#{i}]" }
44
+
45
+ xw
46
+ end
47
+ end
48
+
49
+ end # module Pangrid
@@ -34,6 +34,8 @@ end
34
34
  class RedditFilled < Plugin
35
35
  include RedditWriter
36
36
 
37
+ DESCRIPTION = "Reddit format (with filled squares)"
38
+
37
39
  def write(xw)
38
40
  write_xw(xw)
39
41
  end
@@ -48,6 +50,8 @@ end
48
50
  class RedditBlank < Plugin
49
51
  include RedditWriter
50
52
 
53
+ DESCRIPTION = "Reddit format (with blank squares)"
54
+
51
55
  def write(xw)
52
56
  write_xw(xw)
53
57
  end
@@ -88,6 +92,7 @@ class RedditBlank < Plugin
88
92
  #
89
93
  # we have to be careful about leading/trailing |s
90
94
  ix = lines.find_index {|row| row =~ /^[|\s-]+$/}
95
+ check("Could not find grid") { not ix.nil? }
91
96
  width = lines[ix].gsub(/\s/, '').split('|').reject(&:empty?).length
92
97
  lines = [lines[(ix - 1)]] + lines[(ix + 1) .. -1]
93
98
  grid = lines.take_while {|i| is_grid_row(i)}
@@ -9,6 +9,9 @@
9
9
  module Pangrid
10
10
 
11
11
  class Text < Plugin
12
+
13
+ DESCRIPTION = "Basic text format (mostly for debugging)"
14
+
12
15
  def write(xw)
13
16
  across, down = xw.number
14
17
  rows = xw.to_array(:black => '#', :null => '.')
@@ -27,7 +30,7 @@ class Text < Plugin
27
30
  end
28
31
 
29
32
  def format_clues(numbers, clues, indent)
30
- numbers.zip(clues).map {|n, c| " "*indent + "#{n}. #{c}"}.join("\n")
33
+ numbers.zip(clues).map {|n, c| " "*indent + "#{n.to_s.rjust(2)}. #{c}"}.join("\n")
31
34
  end
32
35
  end
33
36
 
@@ -1,3 +1,3 @@
1
1
  module Pangrid
2
- VERSION='0.3.0'
2
+ VERSION='0.5.0'
3
3
  end
data/lib/pangrid/xw.rb CHANGED
@@ -53,6 +53,15 @@ class Cell
53
53
  rebus? ? solution.to_char : solution
54
54
  end
55
55
 
56
+ def to_w
57
+ case solution
58
+ when :black; '#'
59
+ when :null; '.'
60
+ when Rebus; solution.inspect
61
+ else; solution
62
+ end
63
+ end
64
+
56
65
  def inspect
57
66
  case solution
58
67
  when :black; '#'
@@ -86,19 +95,40 @@ class XWord < OpenStruct
86
95
  boundary?(x, y - 1) && !boundary?(x, y) && !boundary?(x, y + 1)
87
96
  end
88
97
 
89
- def number
98
+ # Word starting from a square
99
+ def word_from(x, y, dir)
100
+ s = ""
101
+ while !boundary?(x, y) do
102
+ s += solution[y][x].to_w
103
+ if dir == :across
104
+ x += 1
105
+ else
106
+ y += 1
107
+ end
108
+ end
109
+ s
110
+ end
111
+
112
+ def number(words = false)
90
113
  n, across, down = 1, [], []
114
+ words_a, words_d = [], []
91
115
  (0 ... height).each do |y|
92
116
  (0 ... width).each do |x|
93
- across << n if across? x, y
94
- down << n if down? x, y
117
+ if across? x, y
118
+ across << n
119
+ words_a << word_from(x, y, :across)
120
+ end
121
+ if down? x, y
122
+ down << n
123
+ words_d << word_from(x, y, :down)
124
+ end
95
125
  if across.last == n || down.last == n
96
126
  solution[y][x].number = n
97
127
  n += 1
98
128
  end
99
129
  end
100
130
  end
101
- [across, down]
131
+ words ? [across, down, words_a, words_d] : [across, down]
102
132
  end
103
133
 
104
134
  def each_cell
@@ -109,6 +139,14 @@ class XWord < OpenStruct
109
139
  end
110
140
  end
111
141
 
142
+ def each_cell_with_coords
143
+ (0 ... height).each do |y|
144
+ (0 ... width).each do |x|
145
+ yield [x, y, solution[y][x]]
146
+ end
147
+ end
148
+ end
149
+
112
150
  # {:black => char, :null => char} -> Any[][]
113
151
  def to_array(opts = {})
114
152
  opts = {:black => '#', :null => ' '}.merge(opts)
@@ -137,7 +175,7 @@ class XWord < OpenStruct
137
175
  if c.rebus?
138
176
  r = c.solution
139
177
  if self.rebus[s]
140
- sym, char = self.rebus[s]
178
+ sym, _ = self.rebus[s]
141
179
  r.symbol = sym.to_s
142
180
  else
143
181
  k += 1
metadata CHANGED
@@ -1,16 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pangrid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin DeMello
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-11 00:00:00.000000000 Z
12
- dependencies: []
13
- description:
11
+ date: 2021-10-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: webrick
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.7.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.7.0
27
+ description:
14
28
  email: martindemello@gmail.com
15
29
  executables:
16
30
  - pangrid
@@ -29,6 +43,10 @@ files:
29
43
  - lib/pangrid/plugins/acrosslite.rb
30
44
  - lib/pangrid/plugins/csv.rb
31
45
  - lib/pangrid/plugins/excel.rb
46
+ - lib/pangrid/plugins/exolve.rb
47
+ - lib/pangrid/plugins/json.rb
48
+ - lib/pangrid/plugins/png.rb
49
+ - lib/pangrid/plugins/qxw.rb
32
50
  - lib/pangrid/plugins/reddit.rb
33
51
  - lib/pangrid/plugins/text.rb
34
52
  - lib/pangrid/utils.rb
@@ -38,7 +56,7 @@ homepage: https://github.com/martindemello/pangrid
38
56
  licenses:
39
57
  - MIT
40
58
  metadata: {}
41
- post_install_message:
59
+ post_install_message:
42
60
  rdoc_options:
43
61
  - "--main"
44
62
  - README
@@ -55,9 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
73
  - !ruby/object:Gem::Version
56
74
  version: '0'
57
75
  requirements: []
58
- rubyforge_project:
59
- rubygems_version: 2.5.1
60
- signing_key:
76
+ rubygems_version: 3.2.22
77
+ signing_key:
61
78
  specification_version: 4
62
79
  summary: A crossword file format converter
63
80
  test_files: []