mahjong-render 0.1.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 +7 -0
- data/CHANGELOG.md +12 -0
- data/CONTRIBUTING.md +7 -0
- data/LICENSE +202 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/README.ja.md +90 -0
- data/README.md +112 -0
- data/SECURITY.md +5 -0
- data/THIRD_PARTY_NOTICES.md +17 -0
- data/assets/manifest.json +45 -0
- data/assets/tiles/Chun.svg +232 -0
- data/assets/tiles/Front.svg +334 -0
- data/assets/tiles/Haku.svg +214 -0
- data/assets/tiles/Hatsu.svg +309 -0
- data/assets/tiles/Man1.svg +276 -0
- data/assets/tiles/Man2.svg +286 -0
- data/assets/tiles/Man3.svg +292 -0
- data/assets/tiles/Man4.svg +292 -0
- data/assets/tiles/Man5-Dora.svg +319 -0
- data/assets/tiles/Man5.svg +313 -0
- data/assets/tiles/Man6.svg +297 -0
- data/assets/tiles/Man7.svg +283 -0
- data/assets/tiles/Man8.svg +286 -0
- data/assets/tiles/Man9.svg +292 -0
- data/assets/tiles/Nan.svg +270 -0
- data/assets/tiles/Pei.svg +236 -0
- data/assets/tiles/Pin1.svg +544 -0
- data/assets/tiles/Pin2.svg +400 -0
- data/assets/tiles/Pin3.svg +362 -0
- data/assets/tiles/Pin4.svg +407 -0
- data/assets/tiles/Pin5-Dora.svg +460 -0
- data/assets/tiles/Pin5.svg +454 -0
- data/assets/tiles/Pin6.svg +477 -0
- data/assets/tiles/Pin7.svg +567 -0
- data/assets/tiles/Pin8.svg +570 -0
- data/assets/tiles/Pin9.svg +623 -0
- data/assets/tiles/Shaa.svg +248 -0
- data/assets/tiles/Sou1.svg +764 -0
- data/assets/tiles/Sou2.svg +372 -0
- data/assets/tiles/Sou3.svg +462 -0
- data/assets/tiles/Sou4.svg +573 -0
- data/assets/tiles/Sou5-Dora.svg +614 -0
- data/assets/tiles/Sou5.svg +608 -0
- data/assets/tiles/Sou6.svg +688 -0
- data/assets/tiles/Sou7.svg +628 -0
- data/assets/tiles/Sou8.svg +712 -0
- data/assets/tiles/Sou9.svg +748 -0
- data/assets/tiles/Ton.svg +242 -0
- data/docs/adr/0001-renderer-selection.md +7 -0
- data/docs/adr/0002-license.md +5 -0
- data/docs/adr/0003-notation.md +5 -0
- data/docs/adr/0004-assets.md +5 -0
- data/docs/adr/0005-package.md +5 -0
- data/docs/release.md +32 -0
- data/docs/research/existing-renderers.md +21 -0
- data/examples/basic.adoc +13 -0
- data/examples/hand.png +0 -0
- data/lib/mahjong_render/asciidoctor.rb +32 -0
- data/lib/mahjong_render/parser.rb +113 -0
- data/lib/mahjong_render/renderer.rb +105 -0
- data/lib/mahjong_render/version.rb +8 -0
- data/lib/mahjong_render.rb +14 -0
- metadata +106 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
3
|
+
|
|
4
|
+
<svg
|
|
5
|
+
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
|
6
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
7
|
+
xmlns:cc="http://creativecommons.org/ns#"
|
|
8
|
+
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
9
|
+
xmlns:svg="http://www.w3.org/2000/svg"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
12
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
13
|
+
width="300"
|
|
14
|
+
height="400"
|
|
15
|
+
viewBox="0 0 300 400"
|
|
16
|
+
id="svg2"
|
|
17
|
+
version="1.1"
|
|
18
|
+
inkscape:version="0.91 r13725"
|
|
19
|
+
sodipodi:docname="Ton.svg"
|
|
20
|
+
inkscape:export-filename="C:\Users\Fluffy\Documents\Projects\ExtraRiichi\Ton.png"
|
|
21
|
+
inkscape:export-xdpi="180"
|
|
22
|
+
inkscape:export-ydpi="180">
|
|
23
|
+
<defs
|
|
24
|
+
id="defs4">
|
|
25
|
+
<inkscape:path-effect
|
|
26
|
+
effect="skeletal"
|
|
27
|
+
id="path-effect7963"
|
|
28
|
+
is_visible="true"
|
|
29
|
+
pattern="m -90.825902,-314.06958 23.03016,41.38503 13.798268,-41.38503 z"
|
|
30
|
+
copytype="repeated_stretched"
|
|
31
|
+
prop_scale="1"
|
|
32
|
+
scale_y_rel="false"
|
|
33
|
+
spacing="0"
|
|
34
|
+
normal_offset="0"
|
|
35
|
+
tang_offset="0"
|
|
36
|
+
prop_units="false"
|
|
37
|
+
vertical_pattern="false"
|
|
38
|
+
fuse_tolerance="0"
|
|
39
|
+
pattern-nodetypes="cccc" />
|
|
40
|
+
<inkscape:path-effect
|
|
41
|
+
effect="skeletal"
|
|
42
|
+
id="path-effect7830"
|
|
43
|
+
is_visible="true"
|
|
44
|
+
pattern="M -12.828427,33.715729 -17,-11 l 9.0000001,0 z"
|
|
45
|
+
copytype="repeated_stretched"
|
|
46
|
+
prop_scale="-1"
|
|
47
|
+
scale_y_rel="false"
|
|
48
|
+
spacing="5.1"
|
|
49
|
+
normal_offset="0"
|
|
50
|
+
tang_offset="0"
|
|
51
|
+
prop_units="false"
|
|
52
|
+
vertical_pattern="false"
|
|
53
|
+
fuse_tolerance="0"
|
|
54
|
+
pattern-nodetypes="cccc" />
|
|
55
|
+
<linearGradient
|
|
56
|
+
id="linearGradient10055"
|
|
57
|
+
osb:paint="solid">
|
|
58
|
+
<stop
|
|
59
|
+
style="stop-color:#000000;stop-opacity:1;"
|
|
60
|
+
offset="0"
|
|
61
|
+
id="stop10057" />
|
|
62
|
+
</linearGradient>
|
|
63
|
+
<marker
|
|
64
|
+
inkscape:stockid="Arrow1Lstart"
|
|
65
|
+
orient="auto"
|
|
66
|
+
refY="0"
|
|
67
|
+
refX="0"
|
|
68
|
+
id="Arrow1Lstart"
|
|
69
|
+
style="overflow:visible"
|
|
70
|
+
inkscape:isstock="true">
|
|
71
|
+
<path
|
|
72
|
+
id="path4978"
|
|
73
|
+
d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
|
|
74
|
+
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ff5c00;stroke-width:1pt;stroke-opacity:1"
|
|
75
|
+
transform="matrix(0.8,0,0,0.8,10,0)"
|
|
76
|
+
inkscape:connector-curvature="0" />
|
|
77
|
+
</marker>
|
|
78
|
+
<clipPath
|
|
79
|
+
clipPathUnits="userSpaceOnUse"
|
|
80
|
+
id="clipPath4243">
|
|
81
|
+
<circle
|
|
82
|
+
style="opacity:1;fill:#000000;fill-opacity:0.29670332;fill-rule:nonzero;stroke:#000000;stroke-width:19.13299942;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
|
83
|
+
id="circle4245"
|
|
84
|
+
cx="-264.65997"
|
|
85
|
+
cy="-198.20665"
|
|
86
|
+
r="293.95438" />
|
|
87
|
+
</clipPath>
|
|
88
|
+
<clipPath
|
|
89
|
+
clipPathUnits="userSpaceOnUse"
|
|
90
|
+
id="clipPath7847">
|
|
91
|
+
<ellipse
|
|
92
|
+
style="opacity:1;fill:#822600;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
|
93
|
+
id="ellipse7849"
|
|
94
|
+
cx="394"
|
|
95
|
+
cy="552.36218"
|
|
96
|
+
rx="349.49533"
|
|
97
|
+
ry="216" />
|
|
98
|
+
</clipPath>
|
|
99
|
+
<clipPath
|
|
100
|
+
clipPathUnits="userSpaceOnUse"
|
|
101
|
+
id="clipPath4243-1">
|
|
102
|
+
<circle
|
|
103
|
+
style="opacity:1;fill:#000000;fill-opacity:0.29670332;fill-rule:nonzero;stroke:#000000;stroke-width:19.13299942;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
|
104
|
+
id="circle4245-4"
|
|
105
|
+
cx="-264.65997"
|
|
106
|
+
cy="-198.20665"
|
|
107
|
+
r="293.95438" />
|
|
108
|
+
</clipPath>
|
|
109
|
+
<clipPath
|
|
110
|
+
clipPathUnits="userSpaceOnUse"
|
|
111
|
+
id="clipPath7876">
|
|
112
|
+
<circle
|
|
113
|
+
style="opacity:1;fill:#000000;fill-opacity:0.29670332;fill-rule:nonzero;stroke:#000000;stroke-width:19.13299942;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
|
114
|
+
id="circle7878"
|
|
115
|
+
cx="-264.65997"
|
|
116
|
+
cy="-198.20665"
|
|
117
|
+
r="293.95438" />
|
|
118
|
+
</clipPath>
|
|
119
|
+
<clipPath
|
|
120
|
+
clipPathUnits="userSpaceOnUse"
|
|
121
|
+
id="clipPath14693">
|
|
122
|
+
<rect
|
|
123
|
+
style="opacity:1;fill:#a53c3c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
|
124
|
+
id="rect14695"
|
|
125
|
+
width="131.78395"
|
|
126
|
+
height="168.82127"
|
|
127
|
+
x="-332.59583"
|
|
128
|
+
y="383.49765"
|
|
129
|
+
rx="1.2551664"
|
|
130
|
+
ry="3.7514515"
|
|
131
|
+
transform="matrix(0.99939083,-0.03489951,0.03489951,0.99939083,0,0)" />
|
|
132
|
+
</clipPath>
|
|
133
|
+
<clipPath
|
|
134
|
+
clipPathUnits="userSpaceOnUse"
|
|
135
|
+
id="clipPath14952">
|
|
136
|
+
<ellipse
|
|
137
|
+
style="opacity:1;fill:#a53c3c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
|
138
|
+
id="ellipse14954"
|
|
139
|
+
cx="-271.34384"
|
|
140
|
+
cy="647.25604"
|
|
141
|
+
rx="69.057365"
|
|
142
|
+
ry="116.91089"
|
|
143
|
+
transform="matrix(0.99939083,-0.03489951,0.03489951,0.99939083,0,0)" />
|
|
144
|
+
</clipPath>
|
|
145
|
+
</defs>
|
|
146
|
+
<sodipodi:namedview
|
|
147
|
+
id="base"
|
|
148
|
+
pagecolor="#aeffff"
|
|
149
|
+
bordercolor="#666666"
|
|
150
|
+
borderopacity="1"
|
|
151
|
+
inkscape:pageopacity="0"
|
|
152
|
+
inkscape:pageshadow="2"
|
|
153
|
+
inkscape:zoom="0.50548197"
|
|
154
|
+
inkscape:cx="363.15718"
|
|
155
|
+
inkscape:cy="458.34028"
|
|
156
|
+
inkscape:document-units="px"
|
|
157
|
+
inkscape:current-layer="layer1"
|
|
158
|
+
showgrid="true"
|
|
159
|
+
inkscape:window-width="1920"
|
|
160
|
+
inkscape:window-height="1017"
|
|
161
|
+
inkscape:window-x="1912"
|
|
162
|
+
inkscape:window-y="-8"
|
|
163
|
+
inkscape:window-maximized="1"
|
|
164
|
+
showguides="true"
|
|
165
|
+
inkscape:guide-bbox="true"
|
|
166
|
+
units="px">
|
|
167
|
+
<inkscape:grid
|
|
168
|
+
type="xygrid"
|
|
169
|
+
id="grid4774"
|
|
170
|
+
visible="true"
|
|
171
|
+
dotted="false"
|
|
172
|
+
color="#3f3fff"
|
|
173
|
+
opacity="0.03921569"
|
|
174
|
+
empcolor="#3f3fff"
|
|
175
|
+
empopacity="0.07843137"
|
|
176
|
+
enabled="false" />
|
|
177
|
+
<sodipodi:guide
|
|
178
|
+
position="150,200"
|
|
179
|
+
orientation="0,1"
|
|
180
|
+
id="guide8231"
|
|
181
|
+
inkscape:label=""
|
|
182
|
+
inkscape:color="rgb(0,0,255)" />
|
|
183
|
+
<sodipodi:guide
|
|
184
|
+
position="150,200"
|
|
185
|
+
orientation="1,0"
|
|
186
|
+
id="guide8233"
|
|
187
|
+
inkscape:label=""
|
|
188
|
+
inkscape:color="rgb(0,0,255)" />
|
|
189
|
+
</sodipodi:namedview>
|
|
190
|
+
<metadata
|
|
191
|
+
id="metadata7">
|
|
192
|
+
<rdf:RDF>
|
|
193
|
+
<cc:Work
|
|
194
|
+
rdf:about="">
|
|
195
|
+
<dc:format>image/svg+xml</dc:format>
|
|
196
|
+
<dc:type
|
|
197
|
+
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
198
|
+
<dc:title />
|
|
199
|
+
</cc:Work>
|
|
200
|
+
</rdf:RDF>
|
|
201
|
+
</metadata>
|
|
202
|
+
<g
|
|
203
|
+
inkscape:label="Layer 1"
|
|
204
|
+
inkscape:groupmode="layer"
|
|
205
|
+
id="layer1"
|
|
206
|
+
transform="translate(0,-652.36216)">
|
|
207
|
+
<g
|
|
208
|
+
id="g4182"
|
|
209
|
+
transform="matrix(0.90070005,0,0,0.97528977,22.480539,21.079387)">
|
|
210
|
+
<path
|
|
211
|
+
sodipodi:nodetypes="ccccccccccccccccccccccc"
|
|
212
|
+
inkscape:connector-curvature="0"
|
|
213
|
+
id="path4169"
|
|
214
|
+
d="M 233.62512,755.29109 C 178.13441,766.77996 64.370638,790.79873 58.640429,785.71129 42.658218,774.8367 25.686157,781.4533 35.789288,796.11051 c 16.240064,27.58037 41.08272,52.95267 53.054176,82.84405 2.288277,4.33456 4.578233,5.00952 6.866511,4.97624 34.894055,0.59778 59.617015,-7.13696 92.959705,-10.00028 5.24648,-0.50585 12.18032,5.19713 16.07777,3.49424 3.45211,-1.88029 4.88869,-8.56661 5.7989,-12.75873 6.85855,-25.41545 28.05224,-47.67676 39.89617,-72.71328 1.50503,-2.99478 9.84505,-3.54971 9.78218,-6.44028 -1.21917,-10.81663 -17.3035,-30.7056 -26.59958,-30.22138 z m -23.13944,33.57895 c 12.46189,-0.30489 -0.39856,25.04533 -5.5016,26.11044 -5.68441,0.83505 -6.59045,-7.30247 -13.55667,-7.44593 -5.23392,0.54881 -4.04327,3.93106 -9.43905,5.54094 -32.92047,6.46479 -57.05235,8.76033 -91.40148,12.13597 -4.332557,0.10941 -15.065211,-17.22193 -9.013958,-18.63046 40.940618,-9.93723 109.784008,-17.44402 128.912758,-17.71096 z m -16.38918,50.02681 c 4.01395,6.86764 -2.84402,13.79985 -7.80917,14.56359 -30.19331,4.51502 -78.25363,12.14709 -82.00667,8.23091 -2.66879,-2.84434 -8.082537,-10.97917 -7.221842,-16.53793 11.136252,-3.71556 92.300252,-14.42426 97.037682,-6.25657 z"
|
|
215
|
+
style="fill:#142896;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
|
216
|
+
<path
|
|
217
|
+
sodipodi:nodetypes="ccccccccccscccc"
|
|
218
|
+
inkscape:connector-curvature="0"
|
|
219
|
+
id="path4165"
|
|
220
|
+
d="m 123.10112,656.13043 c -18.37065,-7.40086 -20.37189,5.83216 -17.90295,14.33847 6.2135,20.20143 14.42853,28.63207 24.53852,42.67785 1.80515,2.41778 3.80884,7.05879 3.50496,9.27068 l -2.80317,207.5969 c 0.0245,6.49844 -1.22391,27.05521 -5.33713,23.81363 -16.25707,-12.35739 -21.17086,-19.29273 -47.244611,-51.54601 -11.141011,-14.92882 -19.888636,-6.9441 -6.365572,12.59128 42.459393,54.62391 46.079123,59.83408 46.706613,115.10687 1.6238,29.1788 31.75855,24.6486 34.5721,6.1383 3.42954,-19.9718 -1.64734,-51.78318 -1.84988,-73.7618 -0.77433,-84.02764 -0.85925,-158.3381 12.84541,-239.53344 2.59385,-12.6668 22.99001,-23.06614 33.97476,-26.71095 4.86607,-1.61964 10.28698,-13.97417 0.90028,-14.45229 -25.17978,-2.64878 -52.28301,-15.55116 -75.53933,-25.52949 z"
|
|
221
|
+
style="fill:#142896;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
|
222
|
+
<path
|
|
223
|
+
sodipodi:nodetypes="cccccc"
|
|
224
|
+
inkscape:connector-curvature="0"
|
|
225
|
+
id="path4167"
|
|
226
|
+
d="m 90.058998,749.95518 c -28.498411,-1.30744 -7.40777,15.78722 -1.003352,15.35489 35.469434,-3.22236 93.934624,-11.66451 130.102704,-19.88981 2.50231,-0.0783 -0.19108,-17.18993 -17.33839,-18.91906 -5.21285,-0.25484 -4.31683,2.60115 -10.47943,4.83506 -27.63258,6.00227 -76.15552,20.22066 -101.281532,18.61892 z"
|
|
227
|
+
style="fill:#142896;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
|
228
|
+
<path
|
|
229
|
+
sodipodi:nodetypes="ccccc"
|
|
230
|
+
inkscape:connector-curvature="0"
|
|
231
|
+
id="path4178"
|
|
232
|
+
d="M 101.07932,860.47476 C 81.038575,899.28326 62.611644,913.1005 1.9825743,939.28575 -21.477214,948.67859 -19.805065,962.93934 8.6272375,953.18208 85.796234,923.19531 101.97857,904.47533 114.56321,868.12926 Z"
|
|
233
|
+
style="fill:#142896;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
|
234
|
+
<path
|
|
235
|
+
sodipodi:nodetypes="ccccccc"
|
|
236
|
+
inkscape:connector-curvature="0"
|
|
237
|
+
id="path4180"
|
|
238
|
+
d="m 148.90381,861.23655 c 24.81185,28.93873 58.74403,106.95253 77.55965,95.36697 14.85312,-9.54021 33.94987,-10.66782 42.06647,-8.48169 10.62302,2.79871 11.13375,7.75465 17.07981,10.96967 15.63489,6.5882 12.36449,-2.02074 6.9373,-9.59456 -14.04344,-21.5996 -80.57109,-34.66827 -122.31035,-92.57081 z"
|
|
239
|
+
style="fill:#142896;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
|
240
|
+
</g>
|
|
241
|
+
</g>
|
|
242
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# ADR 0001: Renderer selection
|
|
2
|
+
|
|
3
|
+
Status: accepted for v0.1.0.
|
|
4
|
+
|
|
5
|
+
The core must run with Ruby alone. The [ecosystem review](../research/existing-renderers.md) found capable JavaScript and TeX renderers, but using either at runtime would add another language or a TeX installation. The Ruby Jekyll plugin is coupled to Liquid and separate image files, and its regex parser does not enforce valid input.
|
|
6
|
+
|
|
7
|
+
Reuse [FluffyStuff's CC0 tile artwork](https://github.com/FluffyStuff/riichi-mahjong-tiles), pinned by commit and checksums. Implement only the missing strict compact-notation parser and a Ruby SVG compositor. Keep Asciidoctor in an optional adapter. Do not copy code from the rejected renderers.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ADR 0002: License
|
|
2
|
+
|
|
3
|
+
Status: accepted for v0.1.0.
|
|
4
|
+
|
|
5
|
+
Use Apache-2.0 (`SPDX-License-Identifier: Apache-2.0`) for project code and documentation, attributed to Maejima Kenya. The bundled tile SVGs and `examples/hand.png`, which is generated from those SVGs, remain independently CC0-1.0. Gem metadata lists both identifiers; `THIRD_PARTY_NOTICES.md` explains which files each covers because a license list alone cannot express component boundaries. Preserve the upstream declaration, full CC0 legal text, attribution, source revision, and file hashes. Keep optional and development dependency licenses separate from bundled content. CI fails when the Apache license copy changes or a locked dependency has no approved license metadata.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ADR 0003: Compact notation
|
|
2
|
+
|
|
3
|
+
Status: accepted for v0.1.0.
|
|
4
|
+
|
|
5
|
+
Use established MPSZ grouping: each digit before `m`, `p`, `s`, or `z` is one tile. `0` is a red five in suited groups; honors accept only `1` through `7`. `405m` means `4m 0m 5m`, and `10m` means `1m 0m`. The original request's `10m` invalid example conflicts with this rule and is corrected here. Reject malformed input without skipping characters, and report token, zero-based character position, and reason. Hand legality and tile-count checks are outside this rendering library.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ADR 0004: Asset strategy
|
|
2
|
+
|
|
3
|
+
Status: accepted for v0.1.0.
|
|
4
|
+
|
|
5
|
+
Bundle the 37 normal and red-five SVG faces plus the tile-front SVG from the pinned CC0 source. Layer the front below each face. Each rendered SVG embeds the needed asset bytes as `data:image/svg+xml;base64` image references. This keeps repeated SVGs deterministic and independent of external files, network requests, document-wide IDs, and installed fonts. The tradeoff is a larger output for hands containing repeated tiles; v0.1 prioritizes offline portability. CI verifies source hashes, XML structure, and absence of active or external SVG content.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ADR 0005: Package strategy
|
|
2
|
+
|
|
3
|
+
Status: accepted for v0.1.0.
|
|
4
|
+
|
|
5
|
+
Distribute one Ruby gem named `mahjong-render`. `require "mahjong_render"` exposes the renderer without loading Asciidoctor. `require "mahjong_render/asciidoctor"` registers the HTML5 block macro when the separate `asciidoctor` gem is installed. This meets Ruby-only operation without splitting the small project into several packages. GitHub and RubyGems publication are separate follow-up actions after the local package, documentation, examples, CI, and license checks pass.
|
data/docs/release.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Release process
|
|
2
|
+
|
|
3
|
+
1. Confirm `CHANGELOG.md` and `lib/mahjong_render/version.rb` use the intended SemVer version. Recheck that `mahjong-render` is available on RubyGems; a prior missing-name response does not reserve it.
|
|
4
|
+
2. In the devcontainer, run `bundle exec rake lint test assets:check licenses:check example:build`, `gem build mahjong-render.gemspec`, and `ruby script/check_package.rb mahjong-render-0.1.0.gem`. Confirm the build has no warnings.
|
|
5
|
+
3. Install the built gem locally and reproduce the Ruby and AsciiDoc commands in both READMEs without network access. Inspect the gem contents and confirm the Apache-2.0 license, CC0 legal text, artwork, notices, preview image, examples, and linked documentation are present.
|
|
6
|
+
4. For the first release, create the public `kjun1/mahjong-render` GitHub repository. Confirm the homepage and gem metadata URLs resolve, verify CI on the default branch, and enable GitHub private vulnerability reporting.
|
|
7
|
+
5. Review and date the 0.1.0 changelog entry. Configure Trusted Publishing as described below, then choose one of the release triggers.
|
|
8
|
+
6. After publication, confirm the RubyGems version and ownership, install the gem from RubyGems, and check that the pending publisher became a trusted publisher.
|
|
9
|
+
|
|
10
|
+
## Trusted Publishing setup
|
|
11
|
+
|
|
12
|
+
1. In the GitHub repository, create an environment named `release` under **Settings → Environments**. If deployment branch and tag restrictions are enabled, allow the `main` branch and `v*` tags.
|
|
13
|
+
2. Enable MFA for the RubyGems account. The gemspec sets `rubygems_mfa_required` to `true`.
|
|
14
|
+
3. Sign in to RubyGems and open [Pending trusted publishers](https://rubygems.org/profile/oidc/pending_trusted_publishers). Create a pending publisher with these exact values:
|
|
15
|
+
|
|
16
|
+
| Field | Value |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Gem name | `mahjong-render` |
|
|
19
|
+
| Repository owner | `kjun1` |
|
|
20
|
+
| Repository name | `mahjong-render` |
|
|
21
|
+
| Workflow filename | `release.yml` |
|
|
22
|
+
| Environment | `release` |
|
|
23
|
+
| Workflow repository owner/name | Leave blank |
|
|
24
|
+
|
|
25
|
+
No RubyGems API key or GitHub secret is needed. RubyGems converts the pending publisher after the first successful push.
|
|
26
|
+
|
|
27
|
+
## Choose a release trigger
|
|
28
|
+
|
|
29
|
+
- **Manual event:** Open GitHub **Actions → Publish gem → Run workflow**, select the `main` branch, and enter version `0.1.0`. The workflow publishes the version in `main` without creating a Git tag.
|
|
30
|
+
- **Tag push:** Push a tag matching the gem version, for example `git tag v0.1.0` followed by `git push origin v0.1.0`. The tag push publishes that version automatically.
|
|
31
|
+
|
|
32
|
+
Both routes validate the version, run the release checks, build the gem, and use the `release` environment. If a version from this repository is already on RubyGems, a later run checks its source metadata and skips the duplicate push. The RubyGems version cannot be overwritten; bump the gem version for any subsequent release.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Existing ecosystem review
|
|
2
|
+
|
|
3
|
+
Reviewed for v0.1.0 on 2026-09-24. `yes/no/unclear` describes documented capability, not a promise of API compatibility. Maintenance is based on upstream activity visible at review time. Browser and Node columns concern direct use without a translation layer.
|
|
4
|
+
|
|
5
|
+
| Project / repository | Language | Code / asset license | Notation / parser | SVG / red five / meld | Browser / Node | Maintenance | Reusable / integration cost / decision |
|
|
6
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
7
|
+
| [mahjong-font](https://github.com/rutopio/mahjong-font) | TypeScript | MIT / SIL OFL 1.1 font | tile strings; parser tied to application | yes / yes / unclear | yes / unclear | active in 2026 | Font is reusable in principle; hosted image API violates offline runtime, application is not a Ruby gem. Do not depend on it. |
|
|
8
|
+
| [mjimage](https://github.com/konoui/mjimage) | TypeScript | MIT / tile-image provenance not established in repository docs | compact notation; parser yes | yes / yes / yes | yes / yes | active in 2026 | npm package exists, but requires a JS runtime and separately hosted tile images by default. High integration cost for Ruby-only operation; do not depend on it. |
|
|
9
|
+
| [mahjong-tiles](https://github.com/DrCMWither/mahjong-tiles) | LaTeX3 | MIT / CC0 art stated upstream | extended MPSZF; parser internal | PDF asset composition, not direct SVG / yes / yes | no / no | active in 2026 | Requires TeX and PDF assets. Good notation reference; not a suitable runtime renderer. |
|
|
10
|
+
| [mahjong-tex](https://github.com/Schmytzi/mahjong-tex) | LaTeX3 | MIT / README credits CC BY art | MPSZ; parser internal | PDF asset composition, not direct SVG / yes / yes | no / no | maintenance unclear | Requires TeX and cannot provide a small Ruby-only SVG path. Do not depend on it. |
|
|
11
|
+
| [tilekit](https://github.com/csimi/tilekit) | JavaScript | ISC / Arphic Public License glyph data | compact notation; parser yes | yes / yes / no | yes / yes | 2026 release, short history | Small direct SVG library, but Ruby-only runtime excludes it; transitive glyph-data terms would need separate attribution. Do not depend on it. |
|
|
12
|
+
| [jekyll-mahjong](https://github.com/peter1357908/jekyll-mahjong) | Ruby | MIT / Uzaku font-derived art credited; asset terms need separate review | compact notation; regex in Liquid tag | individual SVG files in HTML / unclear / sideways tile | no / no | last repository activity in 2023 | Ruby-native, but coupled to Jekyll/Liquid, external CSS and separate image files. Regex skips invalid text. Do not adapt it for the public renderer. |
|
|
13
|
+
| [riichi-mahjong-tiles](https://github.com/FluffyStuff/riichi-mahjong-tiles) | SVG assets | CC0-1.0 | not applicable | yes / yes / not applicable | yes / yes | stable art; last pinned revision 2024 | Reuse 37 tile faces and the tile front. Low integration cost and clear redistribution terms. |
|
|
14
|
+
|
|
15
|
+
## AsciiDoc integration
|
|
16
|
+
|
|
17
|
+
[Ruby Asciidoctor's block macro extension](https://docs.asciidoctor.org/asciidoctor/latest/extensions/block-macro-processor/) already handles macro syntax and registration. The adapter uses that API and delegates only tile rendering to `MahjongRender.render`. No custom AsciiDoc parser is needed.
|
|
18
|
+
|
|
19
|
+
## Decision summary
|
|
20
|
+
|
|
21
|
+
The public runtime must work with Ruby alone. Reusing the CC0 tile vectors avoids recreating artwork. A small parser is needed because the available Ruby/Jekyll candidate silently skips invalid notation, while the JS and TeX parsers cannot be used without extra runtimes. The SVG composer only places those tile vectors into one offline image. See the [ADR](../adr/0001-renderer-selection.md).
|
data/examples/basic.adoc
ADDED
data/examples/hand.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Maejima Kenya
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require "asciidoctor"
|
|
7
|
+
require "asciidoctor/extensions"
|
|
8
|
+
require_relative "../mahjong_render"
|
|
9
|
+
|
|
10
|
+
module MahjongRender
|
|
11
|
+
class UnsupportedBackendError < StandardError; end
|
|
12
|
+
class UnsupportedAttributeError < StandardError; end
|
|
13
|
+
|
|
14
|
+
module Asciidoctor
|
|
15
|
+
class BlockMacro < ::Asciidoctor::Extensions::BlockMacroProcessor
|
|
16
|
+
use_dsl
|
|
17
|
+
named :mahjong
|
|
18
|
+
|
|
19
|
+
def process(parent, target, attrs)
|
|
20
|
+
raise UnsupportedBackendError, "mahjong block macro supports HTML output only" unless parent.document.basebackend?("html")
|
|
21
|
+
raise UnsupportedAttributeError, "mahjong block macro does not support attributes: #{attrs.keys.join(', ')}" unless attrs.empty?
|
|
22
|
+
|
|
23
|
+
svg = MahjongRender.render(target)
|
|
24
|
+
create_pass_block(parent, %(<div class="mahjong-render">#{svg}</div>), {}, subs: nil)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Asciidoctor::Extensions.register do
|
|
31
|
+
block_macro MahjongRender::Asciidoctor::BlockMacro
|
|
32
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Maejima Kenya
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
module MahjongRender
|
|
7
|
+
class NotationError < StandardError
|
|
8
|
+
attr_reader :token, :position, :reason
|
|
9
|
+
|
|
10
|
+
def initialize(token:, position:, reason:)
|
|
11
|
+
@token = token
|
|
12
|
+
@position = position
|
|
13
|
+
@reason = reason
|
|
14
|
+
super("#{reason} at character #{position}: #{token.inspect}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Parser
|
|
19
|
+
SUITS = %w[m p s z].freeze
|
|
20
|
+
MULTIPLIER = /\A[0-9]+(?:\.[0-9]+)?\z/
|
|
21
|
+
DEFAULT_GAP_MULTIPLIER = Rational(1, 4)
|
|
22
|
+
Layout = Struct.new(:tiles, :gaps, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def parse(notation)
|
|
27
|
+
parse_layout(notation).tiles
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_layout(notation)
|
|
31
|
+
raise TypeError, "notation must be a String" unless notation.is_a?(String)
|
|
32
|
+
|
|
33
|
+
layout = Layout.new(tiles: [], gaps: [])
|
|
34
|
+
digits = +""
|
|
35
|
+
start = nil
|
|
36
|
+
pending_gap = nil
|
|
37
|
+
separator_position = nil
|
|
38
|
+
chars = notation.chars
|
|
39
|
+
position = 0
|
|
40
|
+
|
|
41
|
+
while position < chars.length
|
|
42
|
+
char = chars[position]
|
|
43
|
+
if char.match?(/[0-9]/)
|
|
44
|
+
start ||= position
|
|
45
|
+
digits << char
|
|
46
|
+
elsif SUITS.include?(char)
|
|
47
|
+
error(char, position, "suit needs at least one digit") if digits.empty?
|
|
48
|
+
append_tiles(layout, digits, char, start, pending_gap)
|
|
49
|
+
pending_gap = nil
|
|
50
|
+
digits.clear
|
|
51
|
+
start = nil
|
|
52
|
+
elsif char.match?(/\s/)
|
|
53
|
+
error(digits, start, "missing suit before whitespace") unless digits.empty?
|
|
54
|
+
elsif char == "|"
|
|
55
|
+
validate_separator(layout.tiles, digits, start, pending_gap, position)
|
|
56
|
+
separator_position = position
|
|
57
|
+
pending_gap, position = parse_gap(chars, position)
|
|
58
|
+
else
|
|
59
|
+
error(char, position, "unknown notation character")
|
|
60
|
+
end
|
|
61
|
+
position += 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
error(digits, start, "missing suit") unless digits.empty?
|
|
65
|
+
error("|", separator_position, "separator needs a following group") unless pending_gap.nil?
|
|
66
|
+
error("", 0, "notation is empty") if layout.tiles.empty?
|
|
67
|
+
freeze_layout(layout)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def append_tiles(layout, digits, suit, start, gap)
|
|
71
|
+
digits.each_char.with_index do |digit, offset|
|
|
72
|
+
error("#{digit}z", start + offset, "honor rank must be 1–7") if suit == "z" && !digit.match?(/[1-7]/)
|
|
73
|
+
layout.gaps << (gap || 0) unless layout.tiles.empty?
|
|
74
|
+
layout.tiles << "#{digit}#{suit}"
|
|
75
|
+
gap = nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
private_class_method :append_tiles
|
|
79
|
+
|
|
80
|
+
def freeze_layout(layout)
|
|
81
|
+
layout.tiles.freeze
|
|
82
|
+
layout.gaps.freeze
|
|
83
|
+
layout.freeze
|
|
84
|
+
end
|
|
85
|
+
private_class_method :freeze_layout
|
|
86
|
+
|
|
87
|
+
def validate_separator(tiles, digits, start, pending_gap, position)
|
|
88
|
+
error(digits, start, "missing suit before separator") unless digits.empty?
|
|
89
|
+
error("|", position, "separator needs a preceding group") if tiles.empty?
|
|
90
|
+
error("|", position, "consecutive separators") unless pending_gap.nil?
|
|
91
|
+
end
|
|
92
|
+
private_class_method :validate_separator
|
|
93
|
+
|
|
94
|
+
def parse_gap(chars, position)
|
|
95
|
+
return [DEFAULT_GAP_MULTIPLIER, position] unless chars[position + 1] == "("
|
|
96
|
+
|
|
97
|
+
closing = position + 2
|
|
98
|
+
closing += 1 while closing < chars.length && chars[closing] != ")"
|
|
99
|
+
error("|(", position, "missing closing parenthesis for gap multiplier") if closing == chars.length
|
|
100
|
+
|
|
101
|
+
token = chars[(position + 2)...closing].join
|
|
102
|
+
error("|(#{token})", position, "invalid gap multiplier") unless MULTIPLIER.match?(token)
|
|
103
|
+
|
|
104
|
+
[Rational(token), closing]
|
|
105
|
+
end
|
|
106
|
+
private_class_method :parse_gap
|
|
107
|
+
|
|
108
|
+
def error(token, position, reason)
|
|
109
|
+
raise NotationError.new(token: token.dup, position: position, reason: reason)
|
|
110
|
+
end
|
|
111
|
+
private_class_method :error
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Maejima Kenya
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
module MahjongRender
|
|
7
|
+
module Renderer
|
|
8
|
+
TILE_WIDTH = 300
|
|
9
|
+
TILE_HEIGHT = 400
|
|
10
|
+
GAP = 12
|
|
11
|
+
ASSET_DIRECTORY = File.expand_path("../../assets/tiles", __dir__)
|
|
12
|
+
XML_ESCAPE = { "&" => "&", '"' => """, "<" => "<", ">" => ">" }.freeze
|
|
13
|
+
SUIT_NAMES = { "m" => "characters", "p" => "circles", "s" => "bamboo" }.freeze
|
|
14
|
+
ASSET_SUITS = { "m" => "Man", "p" => "Pin", "s" => "Sou" }.freeze
|
|
15
|
+
HONORS = {
|
|
16
|
+
"1" => ["Ton.svg", "east wind"],
|
|
17
|
+
"2" => ["Nan.svg", "south wind"],
|
|
18
|
+
"3" => ["Shaa.svg", "west wind"],
|
|
19
|
+
"4" => ["Pei.svg", "north wind"],
|
|
20
|
+
"5" => ["Haku.svg", "white dragon"],
|
|
21
|
+
"6" => ["Hatsu.svg", "green dragon"],
|
|
22
|
+
"7" => ["Chun.svg", "red dragon"]
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def render(layout)
|
|
28
|
+
tiles = layout.tiles
|
|
29
|
+
width = (tiles.length * TILE_WIDTH) + ((tiles.length - 1) * GAP) + layout.gaps.sum { |gap| gap * TILE_WIDTH }
|
|
30
|
+
%(<svg #{svg_attributes(tiles, width)}>#{image_elements(layout).join}</svg>)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def svg_attributes(tiles, width)
|
|
34
|
+
label = "Mahjong tiles: #{tiles.map { |tile| tile_label(tile) }.join(', ')}".gsub(/[&"<>]/, XML_ESCAPE)
|
|
35
|
+
%(xmlns="http://www.w3.org/2000/svg" width="#{svg_number(width)}" height="#{TILE_HEIGHT}" ) +
|
|
36
|
+
%(viewBox="0 0 #{svg_number(width)} #{TILE_HEIGHT}" role="img" aria-label="#{label}" style="max-width:100%;height:auto")
|
|
37
|
+
end
|
|
38
|
+
private_class_method :svg_attributes
|
|
39
|
+
|
|
40
|
+
def image_elements(layout)
|
|
41
|
+
uris = {}
|
|
42
|
+
front = encode_asset("Front.svg")
|
|
43
|
+
layout.tiles.zip(tile_positions(layout.gaps)).map do |tile, x|
|
|
44
|
+
uri = uris[tile] ||= data_uri(tile)
|
|
45
|
+
image_element(x, front) + image_element(x, uri)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
private_class_method :image_elements
|
|
49
|
+
|
|
50
|
+
def tile_positions(gaps)
|
|
51
|
+
positions = [0]
|
|
52
|
+
gaps.each { |gap| positions << (positions.last + TILE_WIDTH + GAP + (gap * TILE_WIDTH)) }
|
|
53
|
+
positions
|
|
54
|
+
end
|
|
55
|
+
private_class_method :tile_positions
|
|
56
|
+
|
|
57
|
+
def image_element(left, uri)
|
|
58
|
+
%(<image x="#{svg_number(left)}" y="0" width="#{TILE_WIDTH}" height="#{TILE_HEIGHT}" href="#{uri}" />)
|
|
59
|
+
end
|
|
60
|
+
private_class_method :image_element
|
|
61
|
+
|
|
62
|
+
def svg_number(value)
|
|
63
|
+
return value.numerator.to_s if value.denominator == 1
|
|
64
|
+
|
|
65
|
+
whole, remainder = value.numerator.divmod(value.denominator)
|
|
66
|
+
fraction = +""
|
|
67
|
+
until remainder.zero?
|
|
68
|
+
digit, remainder = (remainder * 10).divmod(value.denominator)
|
|
69
|
+
fraction << digit.to_s
|
|
70
|
+
end
|
|
71
|
+
"#{whole}.#{fraction}"
|
|
72
|
+
end
|
|
73
|
+
private_class_method :svg_number
|
|
74
|
+
|
|
75
|
+
def tile_label(tile)
|
|
76
|
+
rank = tile[0]
|
|
77
|
+
suit = tile[1]
|
|
78
|
+
return HONORS.fetch(rank).last if suit == "z"
|
|
79
|
+
return "red 5 of #{SUIT_NAMES.fetch(suit)}" if rank == "0"
|
|
80
|
+
|
|
81
|
+
"#{rank} of #{SUIT_NAMES.fetch(suit)}"
|
|
82
|
+
end
|
|
83
|
+
private_class_method :tile_label
|
|
84
|
+
|
|
85
|
+
def data_uri(tile)
|
|
86
|
+
rank = tile[0]
|
|
87
|
+
suit = tile[1]
|
|
88
|
+
filename = if suit == "z"
|
|
89
|
+
HONORS.fetch(rank).first
|
|
90
|
+
elsif rank == "0"
|
|
91
|
+
"#{ASSET_SUITS.fetch(suit)}5-Dora.svg"
|
|
92
|
+
else
|
|
93
|
+
"#{ASSET_SUITS.fetch(suit)}#{rank}.svg"
|
|
94
|
+
end
|
|
95
|
+
encode_asset(filename)
|
|
96
|
+
end
|
|
97
|
+
private_class_method :data_uri
|
|
98
|
+
|
|
99
|
+
def encode_asset(filename)
|
|
100
|
+
bytes = File.binread(File.join(ASSET_DIRECTORY, filename))
|
|
101
|
+
"data:image/svg+xml;base64,#{[bytes].pack('m0')}"
|
|
102
|
+
end
|
|
103
|
+
private_class_method :encode_asset
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Maejima Kenya
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require_relative "mahjong_render/version"
|
|
7
|
+
require_relative "mahjong_render/parser"
|
|
8
|
+
require_relative "mahjong_render/renderer"
|
|
9
|
+
|
|
10
|
+
module MahjongRender
|
|
11
|
+
def self.render(notation)
|
|
12
|
+
Renderer.render(Parser.parse_layout(notation))
|
|
13
|
+
end
|
|
14
|
+
end
|