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.
Files changed (94) hide show
  1. pymdownx_mahjong/__init__.py +23 -0
  2. pymdownx_mahjong/assets/dark/0m.svg +333 -0
  3. pymdownx_mahjong/assets/dark/0p.svg +491 -0
  4. pymdownx_mahjong/assets/dark/0s.svg +678 -0
  5. pymdownx_mahjong/assets/dark/1m.svg +273 -0
  6. pymdownx_mahjong/assets/dark/1p.svg +551 -0
  7. pymdownx_mahjong/assets/dark/1s.svg +764 -0
  8. pymdownx_mahjong/assets/dark/1z.svg +242 -0
  9. pymdownx_mahjong/assets/dark/2m.svg +283 -0
  10. pymdownx_mahjong/assets/dark/2p.svg +396 -0
  11. pymdownx_mahjong/assets/dark/2s.svg +369 -0
  12. pymdownx_mahjong/assets/dark/2z.svg +270 -0
  13. pymdownx_mahjong/assets/dark/3m.svg +289 -0
  14. pymdownx_mahjong/assets/dark/3p.svg +356 -0
  15. pymdownx_mahjong/assets/dark/3s.svg +462 -0
  16. pymdownx_mahjong/assets/dark/3z.svg +248 -0
  17. pymdownx_mahjong/assets/dark/4m.svg +289 -0
  18. pymdownx_mahjong/assets/dark/4p.svg +403 -0
  19. pymdownx_mahjong/assets/dark/4s.svg +570 -0
  20. pymdownx_mahjong/assets/dark/4z.svg +236 -0
  21. pymdownx_mahjong/assets/dark/5m.svg +313 -0
  22. pymdownx_mahjong/assets/dark/5p.svg +450 -0
  23. pymdownx_mahjong/assets/dark/5s.svg +608 -0
  24. pymdownx_mahjong/assets/dark/5z.svg +214 -0
  25. pymdownx_mahjong/assets/dark/6m.svg +294 -0
  26. pymdownx_mahjong/assets/dark/6p.svg +473 -0
  27. pymdownx_mahjong/assets/dark/6s.svg +695 -0
  28. pymdownx_mahjong/assets/dark/6z.svg +324 -0
  29. pymdownx_mahjong/assets/dark/7m.svg +283 -0
  30. pymdownx_mahjong/assets/dark/7p.svg +516 -0
  31. pymdownx_mahjong/assets/dark/7s.svg +628 -0
  32. pymdownx_mahjong/assets/dark/7z.svg +232 -0
  33. pymdownx_mahjong/assets/dark/8m.svg +283 -0
  34. pymdownx_mahjong/assets/dark/8p.svg +566 -0
  35. pymdownx_mahjong/assets/dark/8s.svg +712 -0
  36. pymdownx_mahjong/assets/dark/9m.svg +289 -0
  37. pymdownx_mahjong/assets/dark/9p.svg +609 -0
  38. pymdownx_mahjong/assets/dark/9s.svg +762 -0
  39. pymdownx_mahjong/assets/dark/back.svg +285 -0
  40. pymdownx_mahjong/assets/dark/blank.svg +225 -0
  41. pymdownx_mahjong/assets/dark/front.svg +334 -0
  42. pymdownx_mahjong/assets/light/0m.svg +319 -0
  43. pymdownx_mahjong/assets/light/0p.svg +460 -0
  44. pymdownx_mahjong/assets/light/0s.svg +614 -0
  45. pymdownx_mahjong/assets/light/1m.svg +276 -0
  46. pymdownx_mahjong/assets/light/1p.svg +544 -0
  47. pymdownx_mahjong/assets/light/1s.svg +764 -0
  48. pymdownx_mahjong/assets/light/1z.svg +242 -0
  49. pymdownx_mahjong/assets/light/2m.svg +286 -0
  50. pymdownx_mahjong/assets/light/2p.svg +400 -0
  51. pymdownx_mahjong/assets/light/2s.svg +372 -0
  52. pymdownx_mahjong/assets/light/2z.svg +270 -0
  53. pymdownx_mahjong/assets/light/3m.svg +292 -0
  54. pymdownx_mahjong/assets/light/3p.svg +362 -0
  55. pymdownx_mahjong/assets/light/3s.svg +462 -0
  56. pymdownx_mahjong/assets/light/3z.svg +248 -0
  57. pymdownx_mahjong/assets/light/4m.svg +292 -0
  58. pymdownx_mahjong/assets/light/4p.svg +407 -0
  59. pymdownx_mahjong/assets/light/4s.svg +573 -0
  60. pymdownx_mahjong/assets/light/4z.svg +236 -0
  61. pymdownx_mahjong/assets/light/5m.svg +313 -0
  62. pymdownx_mahjong/assets/light/5p.svg +454 -0
  63. pymdownx_mahjong/assets/light/5s.svg +608 -0
  64. pymdownx_mahjong/assets/light/5z.svg +214 -0
  65. pymdownx_mahjong/assets/light/6m.svg +297 -0
  66. pymdownx_mahjong/assets/light/6p.svg +477 -0
  67. pymdownx_mahjong/assets/light/6s.svg +688 -0
  68. pymdownx_mahjong/assets/light/6z.svg +309 -0
  69. pymdownx_mahjong/assets/light/7m.svg +283 -0
  70. pymdownx_mahjong/assets/light/7p.svg +567 -0
  71. pymdownx_mahjong/assets/light/7s.svg +628 -0
  72. pymdownx_mahjong/assets/light/7z.svg +232 -0
  73. pymdownx_mahjong/assets/light/8m.svg +286 -0
  74. pymdownx_mahjong/assets/light/8p.svg +570 -0
  75. pymdownx_mahjong/assets/light/8s.svg +712 -0
  76. pymdownx_mahjong/assets/light/9m.svg +292 -0
  77. pymdownx_mahjong/assets/light/9p.svg +623 -0
  78. pymdownx_mahjong/assets/light/9s.svg +748 -0
  79. pymdownx_mahjong/assets/light/back.svg +285 -0
  80. pymdownx_mahjong/assets/light/blank.svg +224 -0
  81. pymdownx_mahjong/assets/light/front.svg +334 -0
  82. pymdownx_mahjong/css/mahjong.css +416 -0
  83. pymdownx_mahjong/extension.py +206 -0
  84. pymdownx_mahjong/inline.py +78 -0
  85. pymdownx_mahjong/parser.py +363 -0
  86. pymdownx_mahjong/renderer.py +468 -0
  87. pymdownx_mahjong/superfences.py +126 -0
  88. pymdownx_mahjong/tiles.py +85 -0
  89. pymdownx_mahjong/utils.py +90 -0
  90. pymdownx_mahjong-1.0.0.dist-info/METADATA +44 -0
  91. pymdownx_mahjong-1.0.0.dist-info/RECORD +94 -0
  92. pymdownx_mahjong-1.0.0.dist-info/WHEEL +4 -0
  93. pymdownx_mahjong-1.0.0.dist-info/entry_points.txt +2 -0
  94. 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)