pymdownx-mahjong 1.0.0__py3-none-any.whl
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.
- pymdownx_mahjong/__init__.py +23 -0
- pymdownx_mahjong/assets/dark/0m.svg +333 -0
- pymdownx_mahjong/assets/dark/0p.svg +491 -0
- pymdownx_mahjong/assets/dark/0s.svg +678 -0
- pymdownx_mahjong/assets/dark/1m.svg +273 -0
- pymdownx_mahjong/assets/dark/1p.svg +551 -0
- pymdownx_mahjong/assets/dark/1s.svg +764 -0
- pymdownx_mahjong/assets/dark/1z.svg +242 -0
- pymdownx_mahjong/assets/dark/2m.svg +283 -0
- pymdownx_mahjong/assets/dark/2p.svg +396 -0
- pymdownx_mahjong/assets/dark/2s.svg +369 -0
- pymdownx_mahjong/assets/dark/2z.svg +270 -0
- pymdownx_mahjong/assets/dark/3m.svg +289 -0
- pymdownx_mahjong/assets/dark/3p.svg +356 -0
- pymdownx_mahjong/assets/dark/3s.svg +462 -0
- pymdownx_mahjong/assets/dark/3z.svg +248 -0
- pymdownx_mahjong/assets/dark/4m.svg +289 -0
- pymdownx_mahjong/assets/dark/4p.svg +403 -0
- pymdownx_mahjong/assets/dark/4s.svg +570 -0
- pymdownx_mahjong/assets/dark/4z.svg +236 -0
- pymdownx_mahjong/assets/dark/5m.svg +313 -0
- pymdownx_mahjong/assets/dark/5p.svg +450 -0
- pymdownx_mahjong/assets/dark/5s.svg +608 -0
- pymdownx_mahjong/assets/dark/5z.svg +214 -0
- pymdownx_mahjong/assets/dark/6m.svg +294 -0
- pymdownx_mahjong/assets/dark/6p.svg +473 -0
- pymdownx_mahjong/assets/dark/6s.svg +695 -0
- pymdownx_mahjong/assets/dark/6z.svg +324 -0
- pymdownx_mahjong/assets/dark/7m.svg +283 -0
- pymdownx_mahjong/assets/dark/7p.svg +516 -0
- pymdownx_mahjong/assets/dark/7s.svg +628 -0
- pymdownx_mahjong/assets/dark/7z.svg +232 -0
- pymdownx_mahjong/assets/dark/8m.svg +283 -0
- pymdownx_mahjong/assets/dark/8p.svg +566 -0
- pymdownx_mahjong/assets/dark/8s.svg +712 -0
- pymdownx_mahjong/assets/dark/9m.svg +289 -0
- pymdownx_mahjong/assets/dark/9p.svg +609 -0
- pymdownx_mahjong/assets/dark/9s.svg +762 -0
- pymdownx_mahjong/assets/dark/back.svg +285 -0
- pymdownx_mahjong/assets/dark/blank.svg +225 -0
- pymdownx_mahjong/assets/dark/front.svg +334 -0
- pymdownx_mahjong/assets/light/0m.svg +319 -0
- pymdownx_mahjong/assets/light/0p.svg +460 -0
- pymdownx_mahjong/assets/light/0s.svg +614 -0
- pymdownx_mahjong/assets/light/1m.svg +276 -0
- pymdownx_mahjong/assets/light/1p.svg +544 -0
- pymdownx_mahjong/assets/light/1s.svg +764 -0
- pymdownx_mahjong/assets/light/1z.svg +242 -0
- pymdownx_mahjong/assets/light/2m.svg +286 -0
- pymdownx_mahjong/assets/light/2p.svg +400 -0
- pymdownx_mahjong/assets/light/2s.svg +372 -0
- pymdownx_mahjong/assets/light/2z.svg +270 -0
- pymdownx_mahjong/assets/light/3m.svg +292 -0
- pymdownx_mahjong/assets/light/3p.svg +362 -0
- pymdownx_mahjong/assets/light/3s.svg +462 -0
- pymdownx_mahjong/assets/light/3z.svg +248 -0
- pymdownx_mahjong/assets/light/4m.svg +292 -0
- pymdownx_mahjong/assets/light/4p.svg +407 -0
- pymdownx_mahjong/assets/light/4s.svg +573 -0
- pymdownx_mahjong/assets/light/4z.svg +236 -0
- pymdownx_mahjong/assets/light/5m.svg +313 -0
- pymdownx_mahjong/assets/light/5p.svg +454 -0
- pymdownx_mahjong/assets/light/5s.svg +608 -0
- pymdownx_mahjong/assets/light/5z.svg +214 -0
- pymdownx_mahjong/assets/light/6m.svg +297 -0
- pymdownx_mahjong/assets/light/6p.svg +477 -0
- pymdownx_mahjong/assets/light/6s.svg +688 -0
- pymdownx_mahjong/assets/light/6z.svg +309 -0
- pymdownx_mahjong/assets/light/7m.svg +283 -0
- pymdownx_mahjong/assets/light/7p.svg +567 -0
- pymdownx_mahjong/assets/light/7s.svg +628 -0
- pymdownx_mahjong/assets/light/7z.svg +232 -0
- pymdownx_mahjong/assets/light/8m.svg +286 -0
- pymdownx_mahjong/assets/light/8p.svg +570 -0
- pymdownx_mahjong/assets/light/8s.svg +712 -0
- pymdownx_mahjong/assets/light/9m.svg +292 -0
- pymdownx_mahjong/assets/light/9p.svg +623 -0
- pymdownx_mahjong/assets/light/9s.svg +748 -0
- pymdownx_mahjong/assets/light/back.svg +285 -0
- pymdownx_mahjong/assets/light/blank.svg +224 -0
- pymdownx_mahjong/assets/light/front.svg +334 -0
- pymdownx_mahjong/css/mahjong.css +416 -0
- pymdownx_mahjong/extension.py +206 -0
- pymdownx_mahjong/inline.py +78 -0
- pymdownx_mahjong/parser.py +363 -0
- pymdownx_mahjong/renderer.py +468 -0
- pymdownx_mahjong/superfences.py +126 -0
- pymdownx_mahjong/tiles.py +85 -0
- pymdownx_mahjong/utils.py +90 -0
- pymdownx_mahjong-1.0.0.dist-info/METADATA +44 -0
- pymdownx_mahjong-1.0.0.dist-info/RECORD +94 -0
- pymdownx_mahjong-1.0.0.dist-info/WHEEL +4 -0
- pymdownx_mahjong-1.0.0.dist-info/entry_points.txt +2 -0
- pymdownx_mahjong-1.0.0.dist-info/licenses/LICENSE +352 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--mahjong-tile-width: 1.8rem;
|
|
3
|
+
--mahjong-tile-height: 2.4rem;
|
|
4
|
+
--mahjong-tile-gap: 0.05rem;
|
|
5
|
+
--mahjong-meld-gap: 1.5rem;
|
|
6
|
+
--mahjong-bg: transparent;
|
|
7
|
+
--mahjong-border: transparent;
|
|
8
|
+
--mahjong-caption-color: #000;
|
|
9
|
+
--mahjong-error-bg: #fee2e2;
|
|
10
|
+
--mahjong-error-border: #ef4444;
|
|
11
|
+
--mahjong-error-color: #991b1b;
|
|
12
|
+
--mahjong-tile-bg: #f5f0eb;
|
|
13
|
+
--mahjong-tile-border: #8b8b7a;
|
|
14
|
+
--mahjong-tile-shadow: rgba(0, 0, 0, 0.15);
|
|
15
|
+
--mahjong-tile-radius: 4px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[data-md-color-scheme="slate"],
|
|
19
|
+
[data-theme="dark"],
|
|
20
|
+
.dark {
|
|
21
|
+
--mahjong-caption-color: #fff;
|
|
22
|
+
--mahjong-error-bg: #450a0a;
|
|
23
|
+
--mahjong-error-border: #dc2626;
|
|
24
|
+
--mahjong-error-color: #fca5a5;
|
|
25
|
+
--mahjong-tile-bg: #1e1e1e;
|
|
26
|
+
--mahjong-tile-border: #9a9a8a;
|
|
27
|
+
--mahjong-tile-shadow: rgba(0, 0, 0, 0.3);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Theme-specific tile visibility for auto-switching */
|
|
31
|
+
.mahjong-tile-light {
|
|
32
|
+
display: contents;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.mahjong-tile-dark {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[data-md-color-scheme="slate"] .mahjong-tile-light,
|
|
40
|
+
[data-theme="dark"] .mahjong-tile-light,
|
|
41
|
+
.dark .mahjong-tile-light {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[data-md-color-scheme="slate"] .mahjong-tile-dark,
|
|
46
|
+
[data-theme="dark"] .mahjong-tile-dark,
|
|
47
|
+
.dark .mahjong-tile-dark {
|
|
48
|
+
display: contents;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Main container */
|
|
52
|
+
.mahjong-hand {
|
|
53
|
+
display: block;
|
|
54
|
+
margin: 1em 0;
|
|
55
|
+
padding: 0.5em 0.5em 0.5em 0;
|
|
56
|
+
background: var(--mahjong-bg);
|
|
57
|
+
border: 1px solid var(--mahjong-border);
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Override MkDocs Material theme figure margins */
|
|
62
|
+
.md-typeset .mahjong-hand {
|
|
63
|
+
margin-left: 1rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Hand row - contains left (tiles/melds), draw, and right (dora) sections */
|
|
67
|
+
.mahjong-hand-row {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-wrap: nowrap;
|
|
70
|
+
align-items: flex-end;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Left section - closed tiles and melds */
|
|
74
|
+
.mahjong-hand-left {
|
|
75
|
+
flex: 0 0 auto;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Draw tile section - separated from main hand */
|
|
79
|
+
.mahjong-hand-draw {
|
|
80
|
+
flex: 0 0 auto;
|
|
81
|
+
margin-left: calc(var(--mahjong-meld-gap) * 0.75);
|
|
82
|
+
position: relative;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Draw label floats above the drawn tile */
|
|
86
|
+
.mahjong-hand-draw::before {
|
|
87
|
+
content: "Draw";
|
|
88
|
+
position: absolute;
|
|
89
|
+
bottom: calc(90% + 0.25em);
|
|
90
|
+
left: 50%;
|
|
91
|
+
transform: translateX(-50%);
|
|
92
|
+
font-size: 0.75em;
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
color: var(--mahjong-caption-color);
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
pointer-events: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Melds section - after draw tile */
|
|
100
|
+
.mahjong-hand-melds {
|
|
101
|
+
flex: 0 0 auto;
|
|
102
|
+
margin-left: calc(var(--mahjong-meld-gap) * 0.75);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Tiles container - flexbox layout */
|
|
106
|
+
.mahjong-tiles {
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-wrap: wrap;
|
|
109
|
+
align-items: flex-end;
|
|
110
|
+
gap: var(--mahjong-tile-gap);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Individual tile - styled like physical riichi tiles */
|
|
114
|
+
.mahjong-tile {
|
|
115
|
+
display: inline-flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
width: var(--mahjong-tile-width);
|
|
119
|
+
height: var(--mahjong-tile-height);
|
|
120
|
+
vertical-align: bottom;
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
background: var(--mahjong-tile-bg);
|
|
123
|
+
border: 2px solid var(--mahjong-tile-border);
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
box-shadow:
|
|
126
|
+
1px 2px 3px var(--mahjong-tile-shadow),
|
|
127
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
|
128
|
+
padding: 2px;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
position: relative;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.mahjong-tile svg,
|
|
134
|
+
.mahjong-tile img {
|
|
135
|
+
width: 100%;
|
|
136
|
+
height: 100%;
|
|
137
|
+
display: block;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.mahjong-tile img {
|
|
141
|
+
object-fit: contain;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Rotated tile (called from another player) */
|
|
145
|
+
.mahjong-tile-rotated {
|
|
146
|
+
transform: rotate(90deg);
|
|
147
|
+
margin-left: calc(var(--mahjong-tile-gap) * 6);
|
|
148
|
+
margin-right: calc(var(--mahjong-tile-gap) * 6);
|
|
149
|
+
position: relative;
|
|
150
|
+
top: calc((var(--mahjong-tile-height) - var(--mahjong-tile-width)) / 2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Stacked tiles container for added kan (shouminkan) */
|
|
154
|
+
.mahjong-tile-stack {
|
|
155
|
+
display: inline-flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: flex-end;
|
|
159
|
+
width: var(--mahjong-tile-height);
|
|
160
|
+
vertical-align: bottom;
|
|
161
|
+
margin-left: calc(var(--mahjong-tile-gap) * 1.5);
|
|
162
|
+
margin-right: calc(var(--mahjong-tile-gap) * 1.5);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Rotated tiles inside the stack */
|
|
166
|
+
.mahjong-tile-stack .mahjong-tile-rotated {
|
|
167
|
+
margin: 0;
|
|
168
|
+
margin-top: calc(
|
|
169
|
+
-1 * (var(--mahjong-tile-height) - var(--mahjong-tile-width)) / 2
|
|
170
|
+
);
|
|
171
|
+
margin-bottom: calc(
|
|
172
|
+
-1 * (var(--mahjong-tile-height) - var(--mahjong-tile-width)) / 2
|
|
173
|
+
);
|
|
174
|
+
top: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Gap between the two stacked tiles */
|
|
178
|
+
.mahjong-tile-stack .mahjong-tile-rotated:first-child {
|
|
179
|
+
margin-bottom: calc(
|
|
180
|
+
var(--mahjong-tile-gap) -
|
|
181
|
+
(var(--mahjong-tile-height) - var(--mahjong-tile-width)) / 2
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Unknown/placeholder tile */
|
|
186
|
+
.mahjong-tile-unknown {
|
|
187
|
+
font-size: 1.5em;
|
|
188
|
+
color: #999;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Meld container */
|
|
192
|
+
.mahjong-meld {
|
|
193
|
+
display: inline-flex;
|
|
194
|
+
align-items: flex-end;
|
|
195
|
+
gap: var(--mahjong-tile-gap);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Back tile - SVG contains full tile design */
|
|
199
|
+
.mahjong-tile-back {
|
|
200
|
+
background: none;
|
|
201
|
+
border: 0.5px solid transparent;
|
|
202
|
+
box-shadow: none;
|
|
203
|
+
padding: 0;
|
|
204
|
+
margin: 0 0.05rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Remove left margin when back tiles are adjacent */
|
|
208
|
+
.mahjong-tile-back + .mahjong-tile-back {
|
|
209
|
+
margin-left: 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Dora row - contains dora and uradora on same line, above hand */
|
|
213
|
+
.mahjong-dora-row {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 1.5em;
|
|
217
|
+
margin-bottom: 0.5em;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Dora tiles at 80% size */
|
|
221
|
+
.mahjong-dora-row .mahjong-tile {
|
|
222
|
+
width: calc(var(--mahjong-tile-width) * 0.8);
|
|
223
|
+
height: calc(var(--mahjong-tile-height) * 0.8);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Dora indicators */
|
|
227
|
+
.mahjong-dora {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.mahjong-dora-label {
|
|
233
|
+
font-size: 0.9em;
|
|
234
|
+
font-weight: 600;
|
|
235
|
+
margin-right: 0.5em;
|
|
236
|
+
color: var(--mahjong-caption-color);
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.mahjong-dora-tiles {
|
|
241
|
+
display: inline-flex;
|
|
242
|
+
align-items: flex-end;
|
|
243
|
+
gap: var(--mahjong-tile-gap);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Caption/title below the hand */
|
|
247
|
+
.mahjong-caption {
|
|
248
|
+
display: block;
|
|
249
|
+
margin-top: 0.75em;
|
|
250
|
+
font-size: 0.9em;
|
|
251
|
+
color: var(--mahjong-caption-color);
|
|
252
|
+
text-align: center;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Higher specificity override for MkDocs */
|
|
256
|
+
.md-typeset .mahjong-hand .mahjong-dora-label,
|
|
257
|
+
.md-typeset .mahjong-hand .mahjong-caption,
|
|
258
|
+
.md-typeset figure.mahjong-hand figcaption {
|
|
259
|
+
color: var(--mahjong-caption-color);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Error display */
|
|
263
|
+
.mahjong-error {
|
|
264
|
+
display: inline-block;
|
|
265
|
+
padding: 0.75em 1em;
|
|
266
|
+
background: var(--mahjong-error-bg);
|
|
267
|
+
border: 1px solid var(--mahjong-error-border);
|
|
268
|
+
border-radius: 4px;
|
|
269
|
+
color: var(--mahjong-error-color);
|
|
270
|
+
font-family: monospace;
|
|
271
|
+
font-size: 0.9em;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.mahjong-error strong {
|
|
275
|
+
color: var(--mahjong-error-border);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Size variants */
|
|
279
|
+
.mahjong-hand-small {
|
|
280
|
+
--mahjong-tile-width: 30px;
|
|
281
|
+
--mahjong-tile-height: 40px;
|
|
282
|
+
--mahjong-tile-gap: 1px;
|
|
283
|
+
--mahjong-meld-gap: 8px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.mahjong-hand-large {
|
|
287
|
+
--mahjong-tile-width: 60px;
|
|
288
|
+
--mahjong-tile-height: 80px;
|
|
289
|
+
--mahjong-tile-gap: 3px;
|
|
290
|
+
--mahjong-meld-gap: 16px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.mahjong-hand-xlarge {
|
|
294
|
+
--mahjong-tile-width: 75px;
|
|
295
|
+
--mahjong-tile-height: 100px;
|
|
296
|
+
--mahjong-tile-gap: 4px;
|
|
297
|
+
--mahjong-meld-gap: 20px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Responsive adjustments for mobile devices */
|
|
301
|
+
@media (max-width: 768px) {
|
|
302
|
+
.mahjong-hand {
|
|
303
|
+
--mahjong-tile-width: 1.4rem;
|
|
304
|
+
--mahjong-tile-height: 1.87rem;
|
|
305
|
+
--mahjong-tile-gap: 1px;
|
|
306
|
+
--mahjong-meld-gap: 0.5rem;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.mahjong-tile {
|
|
310
|
+
border-width: 1px;
|
|
311
|
+
border-radius: 3px;
|
|
312
|
+
padding: 1px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.mahjong-dora-label {
|
|
316
|
+
font-size: 0.75em;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.mahjong-hand-draw::before {
|
|
320
|
+
font-size: 0.65em;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@media (max-width: 480px) {
|
|
325
|
+
.mahjong-hand {
|
|
326
|
+
--mahjong-tile-width: 1.1rem;
|
|
327
|
+
--mahjong-tile-height: 1.47rem;
|
|
328
|
+
--mahjong-tile-gap: 0.5px;
|
|
329
|
+
--mahjong-meld-gap: 0.35rem;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.mahjong-tile {
|
|
333
|
+
border-width: 1px;
|
|
334
|
+
border-radius: 2px;
|
|
335
|
+
padding: 0.5px;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.mahjong-dora-row {
|
|
339
|
+
gap: 0.75em;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.mahjong-dora-label {
|
|
343
|
+
font-size: 0.65em;
|
|
344
|
+
margin-right: 0.25em;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.mahjong-hand-draw::before {
|
|
348
|
+
font-size: 0.6em;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.mahjong-caption {
|
|
352
|
+
font-size: 0.8em;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@media (max-width: 360px) {
|
|
357
|
+
.mahjong-hand {
|
|
358
|
+
--mahjong-tile-width: 0.9rem;
|
|
359
|
+
--mahjong-tile-height: 1.2rem;
|
|
360
|
+
--mahjong-tile-gap: 0.25px;
|
|
361
|
+
--mahjong-meld-gap: 0.25rem;
|
|
362
|
+
padding: 0.25em 0.25em 0.25em 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.mahjong-tile {
|
|
366
|
+
border-width: 1px;
|
|
367
|
+
border-radius: 2px;
|
|
368
|
+
padding: 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.mahjong-dora-row {
|
|
372
|
+
gap: 0.5em;
|
|
373
|
+
margin-bottom: 0.35em;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.mahjong-dora-label {
|
|
377
|
+
font-size: 0.6em;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.mahjong-hand-draw::before {
|
|
381
|
+
font-size: 0.55em;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.mahjong-caption {
|
|
385
|
+
font-size: 0.75em;
|
|
386
|
+
margin-top: 0.5em;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Print styles */
|
|
391
|
+
@media print {
|
|
392
|
+
.mahjong-hand {
|
|
393
|
+
break-inside: avoid;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* Inline tile styles (emoji-sized) */
|
|
398
|
+
.mahjong-inline {
|
|
399
|
+
display: inline;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.mahjong-inline .mahjong-tile {
|
|
403
|
+
--mahjong-tile-width: 1.8em;
|
|
404
|
+
--mahjong-tile-height: 2.4em;
|
|
405
|
+
display: inline-flex;
|
|
406
|
+
vertical-align: text-bottom;
|
|
407
|
+
margin: 0 0.05em;
|
|
408
|
+
border-width: 1px;
|
|
409
|
+
border-radius: 3px;
|
|
410
|
+
padding: 1px;
|
|
411
|
+
box-shadow: 0 1px 2px var(--mahjong-tile-shadow);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.mahjong-inline .mahjong-tile + .mahjong-tile {
|
|
415
|
+
margin-left: 0;
|
|
416
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Python Markdown extension to render and stylize Mahjong tiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import xml.etree.ElementTree as etree
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import markdown
|
|
10
|
+
from markdown.blockprocessors import BlockProcessor
|
|
11
|
+
|
|
12
|
+
from .inline import INLINE_TILE_PATTERN, MahjongInlineProcessor
|
|
13
|
+
from .parser import MahjongParser, ParseError
|
|
14
|
+
from .renderer import MahjongRenderer
|
|
15
|
+
from .utils import apply_hand_options, parse_block_content
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from markdown import Markdown
|
|
19
|
+
from markdown.blockparser import BlockParser
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MahjongBlockProcessor(BlockProcessor):
|
|
23
|
+
"""Block processor that handles ```mahjong fenced code blocks.
|
|
24
|
+
|
|
25
|
+
Converts mahjong notation blocks into HTML.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Pattern to match the start of a mahjong block
|
|
29
|
+
START_PATTERN = re.compile(r"^`{3,}mahjong\s*$")
|
|
30
|
+
END_PATTERN = re.compile(r"^`{3,}\s*$")
|
|
31
|
+
|
|
32
|
+
md: Markdown
|
|
33
|
+
|
|
34
|
+
def __init__(self, parser: BlockParser, config: dict[str, Any]) -> None:
|
|
35
|
+
"""Initialize the block processor.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
parser: Block parser instance
|
|
39
|
+
config: Extension configuration
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(parser)
|
|
42
|
+
self.config = config
|
|
43
|
+
self.mj_parser = MahjongParser()
|
|
44
|
+
self.renderer = MahjongRenderer(
|
|
45
|
+
theme=config.get("theme", "light"),
|
|
46
|
+
css_class=config.get("css_class", "mahjong-hand"),
|
|
47
|
+
show_labels=config.get("show_labels", True),
|
|
48
|
+
inline_svg=config.get("inline_svg", True),
|
|
49
|
+
assets_path=config.get("assets_path"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def test(self, parent: etree.Element, block: str) -> bool:
|
|
53
|
+
"""Test if this block should be processed.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
parent: Parent element
|
|
57
|
+
block: Block text
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if block starts with ```mahjong
|
|
61
|
+
"""
|
|
62
|
+
return bool(self.START_PATTERN.match(block.split("\n")[0]))
|
|
63
|
+
|
|
64
|
+
def run(self, parent: etree.Element, blocks: list[str]) -> bool:
|
|
65
|
+
"""Process the mahjong block.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
parent: Parent element
|
|
69
|
+
blocks: List of blocks
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if block was processed
|
|
73
|
+
"""
|
|
74
|
+
block = blocks.pop(0)
|
|
75
|
+
lines = block.split("\n")
|
|
76
|
+
|
|
77
|
+
# Find the end of the fenced block
|
|
78
|
+
content_lines: list[str] = []
|
|
79
|
+
fence_closed = False
|
|
80
|
+
|
|
81
|
+
# Skip the opening fence
|
|
82
|
+
for i, line in enumerate(lines[1:], 1):
|
|
83
|
+
if self.END_PATTERN.match(line):
|
|
84
|
+
fence_closed = True
|
|
85
|
+
# Put remaining lines back
|
|
86
|
+
remaining = "\n".join(lines[i + 1 :])
|
|
87
|
+
if remaining.strip():
|
|
88
|
+
blocks.insert(0, remaining)
|
|
89
|
+
break
|
|
90
|
+
content_lines.append(line)
|
|
91
|
+
|
|
92
|
+
# If fence wasn't closed in this block, check subsequent blocks
|
|
93
|
+
if not fence_closed:
|
|
94
|
+
while blocks:
|
|
95
|
+
next_block = blocks.pop(0)
|
|
96
|
+
next_lines = next_block.split("\n")
|
|
97
|
+
for i, line in enumerate(next_lines):
|
|
98
|
+
if self.END_PATTERN.match(line):
|
|
99
|
+
fence_closed = True
|
|
100
|
+
remaining = "\n".join(next_lines[i + 1 :])
|
|
101
|
+
if remaining.strip():
|
|
102
|
+
blocks.insert(0, remaining)
|
|
103
|
+
break
|
|
104
|
+
content_lines.append(line)
|
|
105
|
+
if fence_closed:
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
content = "\n".join(content_lines).strip()
|
|
109
|
+
|
|
110
|
+
# Parse block content
|
|
111
|
+
notation, options = parse_block_content(content)
|
|
112
|
+
|
|
113
|
+
if not notation:
|
|
114
|
+
self._render_error(parent, "No hand notation provided")
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
hand = self.mj_parser.parse(notation)
|
|
119
|
+
except ParseError as e:
|
|
120
|
+
self._render_error(parent, str(e))
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
apply_hand_options(hand, self.mj_parser, options)
|
|
124
|
+
|
|
125
|
+
# Render the hand
|
|
126
|
+
html = self.renderer.render(
|
|
127
|
+
hand,
|
|
128
|
+
title=options.get("title"),
|
|
129
|
+
notation=notation,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Store the raw HTML and use a placeholder that survives processing
|
|
133
|
+
placeholder = self.md.htmlStash.store(html)
|
|
134
|
+
p = etree.SubElement(parent, "div")
|
|
135
|
+
p.text = placeholder
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
def _render_error(self, parent: etree.Element, message: str) -> None:
|
|
140
|
+
"""Render an error message.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
parent: Parent element
|
|
144
|
+
message: Error message
|
|
145
|
+
"""
|
|
146
|
+
div = etree.SubElement(parent, "div")
|
|
147
|
+
div.set("class", "mahjong-error")
|
|
148
|
+
strong = etree.SubElement(div, "strong")
|
|
149
|
+
strong.text = "Mahjong Error: "
|
|
150
|
+
strong.tail = message
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class MahjongExtension(markdown.Extension):
|
|
154
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
155
|
+
"""Initialize the extension with configuration.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
**kwargs: Configuration options
|
|
159
|
+
"""
|
|
160
|
+
# Define configuration options with defaults
|
|
161
|
+
self.config = {
|
|
162
|
+
"theme": ["auto", "Color theme: 'light', 'dark', or 'auto'"],
|
|
163
|
+
"css_class": ["mahjong-hand", "CSS class for container"],
|
|
164
|
+
"show_labels": [True, "Show tile names as title attributes"],
|
|
165
|
+
"inline_svg": [True, "Inline SVG content vs img tags"],
|
|
166
|
+
"assets_path": ["", "Custom path to SVG assets"],
|
|
167
|
+
"enable_inline": [True, "Enable inline tile syntax (:1m:)"],
|
|
168
|
+
}
|
|
169
|
+
super().__init__(**kwargs)
|
|
170
|
+
|
|
171
|
+
def extendMarkdown(self, md: Markdown) -> None:
|
|
172
|
+
"""Register the extension with the Markdown instance.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
md: Markdown instance
|
|
176
|
+
"""
|
|
177
|
+
# Get configuration values
|
|
178
|
+
config = {key: self.getConfig(key) for key in self.config}
|
|
179
|
+
|
|
180
|
+
# Register block processor at high priority (before fenced_code)
|
|
181
|
+
processor = MahjongBlockProcessor(md.parser, config)
|
|
182
|
+
processor.md = md
|
|
183
|
+
md.parser.blockprocessors.register(processor, "mahjong", 110)
|
|
184
|
+
|
|
185
|
+
# Register inline processor if enabled
|
|
186
|
+
# Priority 76 to run before pymdownx.emoji (which uses 75)
|
|
187
|
+
if config.get("enable_inline", True):
|
|
188
|
+
inline_processor = MahjongInlineProcessor(INLINE_TILE_PATTERN, md, config)
|
|
189
|
+
md.inlinePatterns.register(inline_processor, "mahjong_inline", 76)
|
|
190
|
+
|
|
191
|
+
# Reset on each conversion
|
|
192
|
+
md.registerExtension(self)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def makeExtension(**kwargs: Any) -> MahjongExtension:
|
|
196
|
+
"""Create the extension instance.
|
|
197
|
+
|
|
198
|
+
This is the entry point called by Python Markdown.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
**kwargs: Configuration options
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
MahjongExtension instance
|
|
205
|
+
"""
|
|
206
|
+
return MahjongExtension(**kwargs)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Inline processor for Mahjong tile notation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from markdown.inlinepatterns import InlineProcessor
|
|
8
|
+
|
|
9
|
+
from .parser import MahjongParser, ParseError
|
|
10
|
+
from .renderer import MahjongRenderer
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from markdown import Markdown
|
|
16
|
+
|
|
17
|
+
# Pattern matches :123m:, :1z:, :0m: (red dora), etc.
|
|
18
|
+
# Must be valid MPSZ: one or more groups of digits followed by m/p/s/z
|
|
19
|
+
# Examples: :1m:, :123p:, :5z:, :0s:, :123m456p:
|
|
20
|
+
INLINE_TILE_PATTERN = r":([0-9]+[mpsz])+:"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MahjongInlineProcessor(InlineProcessor):
|
|
24
|
+
"""Inline processor for mahjong tile notation.
|
|
25
|
+
|
|
26
|
+
Matches patterns like :1m:, :123m:, :123m456p:
|
|
27
|
+
Renders emoji-sized tiles inline with text.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, pattern: str, md: Markdown, config: dict[str, Any]) -> None:
|
|
31
|
+
"""Initialize the inline processor.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
pattern: Regex pattern to match
|
|
35
|
+
md: Markdown instance
|
|
36
|
+
config: Extension configuration
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(pattern, md)
|
|
39
|
+
self.config = config
|
|
40
|
+
self.parser = MahjongParser()
|
|
41
|
+
self.renderer = MahjongRenderer(
|
|
42
|
+
theme=config.get("theme", "auto"),
|
|
43
|
+
css_class="mahjong-inline",
|
|
44
|
+
show_labels=config.get("show_labels", True),
|
|
45
|
+
inline_svg=config.get("inline_svg", True),
|
|
46
|
+
assets_path=config.get("assets_path"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def handleMatch(self, m: re.Match, data: str) -> tuple[str | None, int | None, int | None]:
|
|
50
|
+
"""Handle matched inline tile notation.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
m: Match object
|
|
54
|
+
data: Full text being processed
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple of (placeholder, start_index, end_index) or (None, None, None) if invalid
|
|
58
|
+
"""
|
|
59
|
+
# Extract notation without colons
|
|
60
|
+
full_match = m.group(0)
|
|
61
|
+
notation = full_match[1:-1] # Strip leading/trailing colons
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
tiles = self.parser.parse_tiles(notation)
|
|
65
|
+
except ParseError:
|
|
66
|
+
# Invalid notation - return None to leave text unchanged
|
|
67
|
+
return None, None, None
|
|
68
|
+
|
|
69
|
+
if not tiles:
|
|
70
|
+
return None, None, None
|
|
71
|
+
|
|
72
|
+
# Render tiles as HTML
|
|
73
|
+
html = self.renderer.render_tiles(tiles)
|
|
74
|
+
|
|
75
|
+
# Store raw HTML and return placeholder
|
|
76
|
+
placeholder = self.md.htmlStash.store(html)
|
|
77
|
+
|
|
78
|
+
return placeholder, m.start(0), m.end(0)
|