% \iffalse meta-comment % % mahjong-tiles.dtx -- documented source for mahjong-tiles % % Copyright (c) 2021 Daniel Schmitz % Copyright (c) 2026 Lilia Chen May wither % % This file is distributed under the MIT Licence; see LICENCE. % %<*driver> \ProvidesFile{mahjong-tiles.dtx}[2026/07/01 v2.2.0 mahjong-tiles documented source] \documentclass{ltxdoc} \usepackage[T1]{fontenc} \usepackage{lmodern} \usepackage{xcolor} \usepackage{hyperref} \hypersetup{ pdftitle={mahjong-tiles documented source}, pdfauthor={Lilia Chen May wither}, pdfsubject={Typesetting Riichi Mahjong hands and discard rivers}, pdfkeywords={LaTeX, Mahjong, Riichi, MPSZ, CTAN} } \EnableCrossrefs \CodelineIndex \RecordChanges \begin{document} \DocInput{mahjong-tiles.dtx} \end{document} % % \fi % % \GetFileInfo{mahjong-tiles.dtx} % % \title{The \textsf{mahjong-tiles} Package: Documented Source} % \author{Lilia Chen May wither} % \date{Version 2.2.0, 2026-07-01} % \maketitle % % \begin{abstract} % \textsf{mahjong-tiles} typesets Japanese/Riichi Mahjong hands and discard rivers % from compact MPSZ notation. The user-facing manual is distributed as % \texttt{doc/mahjong-tiles-doc.pdf}; this file is the docstrip source used to % generate \texttt{mahjong-tiles.sty}. % \end{abstract} % % \section{Installation source} % % Run \texttt{latex mahjong-tiles.ins} to extract \texttt{mahjong-tiles.sty} from this % file. The extracted style file must be installed together with the % \texttt{tiles/} directory, because the package reads the PDF tile artwork at % runtime. % % \section{Licence and attribution} % % The LaTeX package code is distributed under the MIT Licence. Portions are % based on Daniel Schmitz's \textsf{mahjong} package, copyright 2021 Daniel % Schmitz, also under the MIT Licence. % % The tile artwork in \texttt{tiles/} is derived from FluffyStuff's % \texttt{riichi-mahjong-tiles} project. The upstream Licence file places that % work in the public domain/CC0. Attribution is retained in \texttt{README.md} % and \texttt{LICENCE}. % % \StopEventually{\PrintChanges\PrintIndex} % % \section{Implementation} % % The following code is extracted by docstrip with the \texttt{package} guard. % % \begin{macrocode} %<*package> %% mahjong-tiles.sty -- Typeset Japanese/Riichi Mahjong tiles from compact MPSZ notation. %% %% This package is derived from and extends Daniel Schmitz's mahjong package. %% Copyright (c) 2021 Daniel Schmitz. %% mahjong-tiles modifications are copyright (c) 2026 Lilia Chen May wither. %% %% The package name and file prefix are mahjong-tiles; the LaTeX3 namespace is mahjongtiles. %% The public hand command is \mahjong for compact document markup. %% Distributed under the MIT Licence; see LICENCE. %% Tile artwork in tiles/ is credited to FluffyStuff/riichi-mahjong-tiles; %% see README.md and LICENCE for details. \NeedsTeXFormat{LaTeX2e}[2020/10/01] \RequirePackage{expl3} \ProvidesExplPackage{mahjong-tiles}{2026/07/01}{2.2.0}{Typeset Japanese/Riichi Mahjong hands and discard rivers} \RequirePackage{xparse} \RequirePackage{l3keys2e} \RequirePackage{graphicx} \RequirePackage{xcolor} \definecolor{mahjongtilesbackred}{rgb}{1,0.215576,0.215576} \RequirePackage{tikz} \RequirePackage{stackengine} \ExplSyntaxOn % ----------------------------------------------------------------------------- % Messages % ----------------------------------------------------------------------------- \msg_new:nnnn { mahjong-tiles } { invalid-token } { Token~'#1'~is~not~valid~in~Mahjong~MPSZ~notation. } { Valid~tokens~are~0--9,~m,~p,~s,~z,~x,~?,~-,~',~",~*,~+,~[text],~\mj\{tile\},~and~spaces. } \msg_new:nnnn { mahjong-tiles } { unknown-tile } { I~cannot~find~tile~'#1'. } { Check~that~the~tile~PDF~exists~under~the~configured~tile-dir. } \msg_new:nnnn { mahjong-tiles } { unknown-orientation } { Orientation~'#1'~is~unknown. } { This~is~an~internal~error;~valid~orientations~are~0,~1,~and~2. } \msg_new:nnnn { mahjong-tiles } { missing-suit } { Tile~digit(s)~'#1'~do~not~have~a~suit. } { Add~m,~p,~s,~or~z~after~the~digit~group,~or~use~N-~for~a~separator. } \msg_new:nnnn { mahjong-tiles } { missing-rotation-target } { Rotation~marker~'#1'~has~no~tile~to~rotate. } { Put~the~marker~after~a~digit,~x,~?,~or~a~completed~tile~group. } \msg_new:nnnn { mahjong-tiles } { missing-overlay-target } { Overlay~text~'#1'~has~no~tile~to~annotate. } { Put~[text]~after~a~digit,~x,~?,~or~a~completed~tile~group. } \msg_new:nnnn { mahjong-tiles } { unclosed-overlay } { Overlay~text~was~started~with~'['~but~no~closing~']'~was~found. } { Add~a~closing~']'.~Nested~literal~square~brackets~are~not~supported. } \msg_new:nnnn { mahjong-tiles } { concealed-kong-overlay } { Overlay~text~'#1'~is~attached~to~the~final~back~tile~of~a~concealed~kong. } { This~warning~confirms~that~the~postfix~annotation~targets~the~expanded~kong~tail. } \msg_new:nnnn { mahjong-tiles } { nested-mj } { The~\mj~command~cannot~be~nested. } { mahjong-tiles~only~expands~one~level~of~\mj\{...\}. } \msg_new:nnnn { mahjong-tiles } { inline-mj-single-tile } { Inline~\mj\{#1\}~must~contain~exactly~one~Mahjong~tile. } { Use~one~simple~tile~such~as~\mj\{2z\}.~Do~not~put~a~hand,~gap,~or~meld~inside~inline~\mj. } \msg_new:nnnn { mahjong-tiles } { inline-mj-tile-only } { Inline~\mj\{#1\}~did~not~produce~a~plain~tile. } { Inline~\mj~inside~\mahjong\{...\}~is~reserved~for~one~drawn~tile. } \msg_new:nnnn { mahjong-tiles } { invalid-bool } { Boolean~option~'#1'~must~be~one~of~true,~false,~1,~0,~yes,~no,~on,~or~off. } { Use~for~example~no-aka=1~to~replace~red~fives~with~regular~fives. } % ----------------------------------------------------------------------------- % Package options and state % ----------------------------------------------------------------------------- \dim_new:N \g__mahjongtiles_default_height_dim \fp_new:N \g__mahjongtiles_default_scale_fp \tl_new:N \g__mahjongtiles_tile_dir_tl \int_new:N \g__mahjongtiles_river_cols_int \dim_new:N \g__mahjongtiles_river_row_gap_dim \dim_gset:Nn \g__mahjongtiles_default_height_dim { \baselineskip } \fp_gset:Nn \g__mahjongtiles_default_scale_fp { 0.75 } \tl_gset:Nn \g__mahjongtiles_tile_dir_tl { tiles } \int_gset:Nn \g__mahjongtiles_river_cols_int { 6 } \dim_gset:Nn \g__mahjongtiles_river_row_gap_dim { 0pt } \tl_new:N \g__mahjongtiles_back_color_tl \tl_new:N \g__mahjongtiles_overlay_style_tl \bool_new:N \g__mahjongtiles_back_recolor_bool \bool_new:N \g__mahjongtiles_no_aka_bool \tl_gclear:N \g__mahjongtiles_back_color_tl \tl_gset:Nn \g__mahjongtiles_overlay_style_tl { font = \scriptsize, align = center, anchor = south, inner~sep = 0.2pt, outer~sep = 0pt, yshift = 0.35em } \bool_gset_false:N \g__mahjongtiles_back_recolor_bool \bool_gset_false:N \g__mahjongtiles_no_aka_bool \cs_new_protected:Npn \__mahjongtiles_set_back_color_global:n #1 { \tl_if_blank:nTF {#1} { \tl_gclear:N \g__mahjongtiles_back_color_tl \bool_gset_false:N \g__mahjongtiles_back_recolor_bool } { \str_if_eq:nnTF {#1} { none } { \tl_gclear:N \g__mahjongtiles_back_color_tl \bool_gset_false:N \g__mahjongtiles_back_recolor_bool } { \tl_gset:Nn \g__mahjongtiles_back_color_tl {#1} \bool_gset_true:N \g__mahjongtiles_back_recolor_bool } } } \cs_new_protected:Npn \__mahjongtiles_set_back_color_local:n #1 { \tl_if_blank:nTF {#1} { \tl_clear:N \g__mahjongtiles_back_color_tl \bool_set_false:N \g__mahjongtiles_back_recolor_bool } { \str_if_eq:nnTF {#1} { none } { \tl_clear:N \g__mahjongtiles_back_color_tl \bool_set_false:N \g__mahjongtiles_back_recolor_bool } { \tl_set:Nn \g__mahjongtiles_back_color_tl {#1} \bool_set_true:N \g__mahjongtiles_back_recolor_bool } } } \cs_new_protected:Npn \__mahjongtiles_set_no_aka_global:n #1 { \str_case:nnF {#1} { { true } { \bool_gset_true:N \g__mahjongtiles_no_aka_bool } { 1 } { \bool_gset_true:N \g__mahjongtiles_no_aka_bool } { yes } { \bool_gset_true:N \g__mahjongtiles_no_aka_bool } { on } { \bool_gset_true:N \g__mahjongtiles_no_aka_bool } { false } { \bool_gset_false:N \g__mahjongtiles_no_aka_bool } { 0 } { \bool_gset_false:N \g__mahjongtiles_no_aka_bool } { no } { \bool_gset_false:N \g__mahjongtiles_no_aka_bool } { off } { \bool_gset_false:N \g__mahjongtiles_no_aka_bool } } { \msg_error:nnn { mahjong-tiles } { invalid-bool } { no-aka=#1 } } } \cs_new_protected:Npn \__mahjongtiles_set_no_aka_local:n #1 { \str_case:nnF {#1} { { true } { \bool_set_true:N \g__mahjongtiles_no_aka_bool } { 1 } { \bool_set_true:N \g__mahjongtiles_no_aka_bool } { yes } { \bool_set_true:N \g__mahjongtiles_no_aka_bool } { on } { \bool_set_true:N \g__mahjongtiles_no_aka_bool } { false } { \bool_set_false:N \g__mahjongtiles_no_aka_bool } { 0 } { \bool_set_false:N \g__mahjongtiles_no_aka_bool } { no } { \bool_set_false:N \g__mahjongtiles_no_aka_bool } { off } { \bool_set_false:N \g__mahjongtiles_no_aka_bool } } { \msg_error:nnn { mahjong-tiles } { invalid-bool } { no-aka=#1 } } } \keys_define:nn { mahjong-tiles } { height .dim_gset:N = \g__mahjongtiles_default_height_dim, scale .fp_gset:N = \g__mahjongtiles_default_scale_fp, tile-dir .tl_gset:N = \g__mahjongtiles_tile_dir_tl, river-cols .int_gset:N = \g__mahjongtiles_river_cols_int, river-cols .value_required:n = true, river-row-gap .dim_gset:N = \g__mahjongtiles_river_row_gap_dim, river-row-gap .value_required:n = true, overlay-style .tl_gset:N = \g__mahjongtiles_overlay_style_tl, overlay-style .value_required:n = true, color .code:n = { \__mahjongtiles_set_back_color_global:n {#1} }, color .value_required:n = true, no-aka .code:n = { \__mahjongtiles_set_no_aka_global:n {#1} }, no-aka .default:n = true, } \ProcessKeysOptions { mahjong-tiles } \dim_new:N \l__mahjongtiles_tile_height_dim \dim_new:N \l__mahjongtiles_tile_width_dim \dim_new:N \l__mahjongtiles_symbol_height_dim \dim_new:N \l__mahjongtiles_baseline_offset_dim \fp_new:N \l__mahjongtiles_tile_scale_fp \int_new:N \l__mahjongtiles_river_cols_int \dim_new:N \l__mahjongtiles_river_row_gap_dim \tl_new:N \l__mahjongtiles_overlay_style_tl \int_new:N \l__mahjongtiles_river_col_int \int_new:N \l__mahjongtiles_river_seen_tiles_int \int_new:N \l__mahjongtiles_river_total_tiles_int \seq_new:N \l__mahjongtiles_tiles_seq \seq_new:N \l__mahjongtiles_pending_seq \tl_new:N \l__mahjongtiles_pending_digits_tl \tl_new:N \l__mahjongtiles_tmp_tl \tl_new:N \l__mahjongtiles_tmpa_tl \tl_new:N \l__mahjongtiles_tmpb_tl \tl_new:N \l__mahjongtiles_first_face_tl \tl_new:N \l__mahjongtiles_file_tl \box_new:N \l__mahjongtiles_tile_box \int_new:N \l__mahjongtiles_tmp_int \int_new:N \l__mahjongtiles_mj_depth_int \bool_new:N \l__mahjongtiles_concealed_kong_bool \bool_new:N \l__mahjongtiles_last_concealed_kong_tail_bool \tl_new:N \g__mahjongtiles_inline_mj_face_tl \tl_new:N \g__mahjongtiles_inline_mj_suit_tl \bool_new:N \g__mahjongtiles_inline_mj_valid_bool \keys_define:nn { mahjong-tiles / local } { height .dim_set:N = \l__mahjongtiles_tile_height_dim, scale .fp_set:N = \l__mahjongtiles_tile_scale_fp, river-cols .int_set:N = \l__mahjongtiles_river_cols_int, river-cols .value_required:n = true, river-row-gap .dim_set:N = \l__mahjongtiles_river_row_gap_dim, river-row-gap .value_required:n = true, overlay-style .tl_set:N = \l__mahjongtiles_overlay_style_tl, overlay-style .value_required:n = true, color .code:n = { \__mahjongtiles_set_back_color_local:n {#1} }, color .value_required:n = true, no-aka .code:n = { \__mahjongtiles_set_no_aka_local:n {#1} }, no-aka .default:n = true, } % ----------------------------------------------------------------------------- % Tuple helpers % Pending tuple: {face}{orientation}{overlay} % Output tuple: {kind}{a}{b}{c}{overlay} % tile = {tile}{face}{suit}{orientation}{overlay}; suit may be empty for x and ? % draw = {draw}{face}{suit}{ }{ }; drawn tile rendered sideways above the hand row % gap = {gap}{n}{ }{ }{ }; n means n/7 of one tile width % ----------------------------------------------------------------------------- \cs_new_protected:Npn \__mahjongtiles_pending_put:nn #1#2 { \seq_put_right:Nn \l__mahjongtiles_pending_seq { {#1} {#2} { } } } \cs_new_protected:Npn \__mahjongtiles_tile_put:nnnn #1#2#3#4 { \seq_put_right:Nn \l__mahjongtiles_tiles_seq { { tile } {#1} {#2} {#3} {#4} } \bool_set_false:N \l__mahjongtiles_last_concealed_kong_tail_bool } \cs_new_protected:Npn \__mahjongtiles_tile_put:nnn #1#2#3 { \__mahjongtiles_tile_put:nnnn {#1} {#2} {#3} { } } \cs_generate_variant:Nn \__mahjongtiles_tile_put:nnn { Vnn } \cs_generate_variant:Nn \__mahjongtiles_tile_put:nnnn { Vnnn } \cs_new_protected:Npn \__mahjongtiles_tile_put_aka_aware:nnnn #1#2#3#4 { \bool_if:NTF \g__mahjongtiles_no_aka_bool { \str_if_eq:nnTF {#1} {0} { \tl_if_in:nnTF { mps } {#2} { \__mahjongtiles_tile_put:nnnn {5} {#2} {#3} {#4} } { \__mahjongtiles_tile_put:nnnn {#1} {#2} {#3} {#4} } } { \__mahjongtiles_tile_put:nnnn {#1} {#2} {#3} {#4} } } { \__mahjongtiles_tile_put:nnnn {#1} {#2} {#3} {#4} } } \cs_new_protected:Npn \__mahjongtiles_tile_put_aka_aware:nnn #1#2#3 { \__mahjongtiles_tile_put_aka_aware:nnnn {#1} {#2} {#3} { } } \cs_generate_variant:Nn \__mahjongtiles_tile_put_aka_aware:nnn { Vnn } \cs_generate_variant:Nn \__mahjongtiles_tile_put_aka_aware:nnnn { Vnnn } \cs_new_protected:Npn \__mahjongtiles_gap_put:n #1 { \seq_put_right:Nn \l__mahjongtiles_tiles_seq { { gap } {#1} { } { } { } } \bool_set_false:N \l__mahjongtiles_last_concealed_kong_tail_bool } \cs_new_protected:Npn \__mahjongtiles_draw_put:nn #1#2 { \seq_put_right:Nn \l__mahjongtiles_tiles_seq { { draw } {#1} {#2} { } { } } \bool_set_false:N \l__mahjongtiles_last_concealed_kong_tail_bool } \cs_generate_variant:Nn \__mahjongtiles_draw_put:nn { VV } \cs_new_protected:Npn \__mahjongtiles_pending_set_last_orientation:n #1 { \seq_pop_right:NNTF \l__mahjongtiles_pending_seq \l__mahjongtiles_tmp_tl { \exp_last_unbraced:NV \__mahjongtiles_pending_set_last_orientation_aux:nnnn \l__mahjongtiles_tmp_tl {#1} } { \msg_error:nnn { mahjong-tiles } { missing-rotation-target } {#1} } } \cs_new_protected:Npn \__mahjongtiles_pending_set_last_orientation_aux:nnnn #1#2#3#4 { \seq_put_right:Nn \l__mahjongtiles_pending_seq { {#1} {#4} {#3} } } \cs_new_protected:Npn \__mahjongtiles_output_set_last_orientation:n #1 { \seq_pop_right:NNTF \l__mahjongtiles_tiles_seq \l__mahjongtiles_tmp_tl { \exp_last_unbraced:NV \__mahjongtiles_output_set_last_orientation_aux:nnnnnn \l__mahjongtiles_tmp_tl {#1} } { \msg_error:nnn { mahjong-tiles } { missing-rotation-target } {#1} } } \cs_new_protected:Npn \__mahjongtiles_output_set_last_orientation_aux:nnnnnn #1#2#3#4#5#6 { \str_if_eq:nnTF {#1} { tile } { \seq_put_right:Nn \l__mahjongtiles_tiles_seq { { tile } {#2} {#3} {#6} {#5} } } { \seq_put_right:Nn \l__mahjongtiles_tiles_seq { {#1} {#2} {#3} {#4} {#5} } \msg_error:nnn { mahjong-tiles } { missing-rotation-target } {#6} } } \cs_new_protected:Npn \__mahjongtiles_set_last_orientation:n #1 { \seq_if_empty:NTF \l__mahjongtiles_pending_seq { \__mahjongtiles_output_set_last_orientation:n {#1} } { \__mahjongtiles_pending_set_last_orientation:n {#1} } } \cs_new_protected:Npn \__mahjongtiles_pending_set_last_overlay:n #1 { \seq_pop_right:NNTF \l__mahjongtiles_pending_seq \l__mahjongtiles_tmp_tl { \exp_last_unbraced:NV \__mahjongtiles_pending_set_last_overlay_aux:nnnn \l__mahjongtiles_tmp_tl {#1} } { \msg_error:nnn { mahjong-tiles } { missing-overlay-target } {#1} } } \cs_new_protected:Npn \__mahjongtiles_pending_set_last_overlay_aux:nnnn #1#2#3#4 { \seq_put_right:Nn \l__mahjongtiles_pending_seq { {#1} {#2} {#4} } } \cs_new_protected:Npn \__mahjongtiles_output_set_last_overlay:n #1 { \seq_pop_right:NNTF \l__mahjongtiles_tiles_seq \l__mahjongtiles_tmp_tl { \exp_last_unbraced:NV \__mahjongtiles_output_set_last_overlay_aux:nnnnnn \l__mahjongtiles_tmp_tl {#1} } { \msg_error:nnn { mahjong-tiles } { missing-overlay-target } {#1} } } \cs_generate_variant:Nn \__mahjongtiles_output_set_last_overlay:n { V } \cs_new_protected:Npn \__mahjongtiles_output_set_last_overlay_aux:nnnnnn #1#2#3#4#5#6 { \str_if_eq:nnTF {#1} { tile } { \tl_if_blank:nF {#6} { \bool_if:NT \l__mahjongtiles_last_concealed_kong_tail_bool { \msg_warning:nnn { mahjong-tiles } { concealed-kong-overlay } {#6} } } \seq_put_right:Nn \l__mahjongtiles_tiles_seq { { tile } {#2} {#3} {#4} {#6} } } { \seq_put_right:Nn \l__mahjongtiles_tiles_seq { {#1} {#2} {#3} {#4} {#5} } \msg_error:nnn { mahjong-tiles } { missing-overlay-target } {#6} } } \cs_new_protected:Npn \__mahjongtiles_set_last_overlay:n #1 { \seq_if_empty:NTF \l__mahjongtiles_pending_seq { \__mahjongtiles_output_set_last_overlay:n {#1} } { \__mahjongtiles_pending_set_last_overlay:n {#1} } } % ----------------------------------------------------------------------------- % Parser % ----------------------------------------------------------------------------- \quark_new:N \q__mahjongtiles_parse_stop \quark_new:N \q__mahjongtiles_parse_done \cs_new_protected:Npn \__mahjongtiles_parse:n #1 { \seq_clear:N \l__mahjongtiles_tiles_seq \seq_clear:N \l__mahjongtiles_pending_seq \bool_set_false:N \l__mahjongtiles_last_concealed_kong_tail_bool \__mahjongtiles_parse_loop:w #1 \q__mahjongtiles_parse_stop ] \q__mahjongtiles_parse_done \seq_if_empty:NF \l__mahjongtiles_pending_seq { \tl_clear:N \l__mahjongtiles_pending_digits_tl \seq_map_inline:Nn \l__mahjongtiles_pending_seq { \__mahjongtiles_collect_pending_digits:nnn ##1 } \msg_error:nnV { mahjong-tiles } { missing-suit } \l__mahjongtiles_pending_digits_tl } } \cs_new_protected:Npn \__mahjongtiles_parse_loop:w #1 { \tl_if_eq:nnTF {#1} { \q__mahjongtiles_parse_stop } { \__mahjongtiles_parse_finish:w } { \token_if_eq_meaning:NNTF #1 \mj { \int_compare:nNnTF { \l__mahjongtiles_mj_depth_int } > {0} { \__mahjongtiles_parse_nested_mj:n } { \__mahjongtiles_parse_inline_mj:n } } { \str_if_eq:nnTF {#1} { [ } { \__mahjongtiles_parse_overlay:w } { \__mahjongtiles_parse_token:n {#1} \__mahjongtiles_parse_loop:w } } } } \cs_new_protected:Npn \__mahjongtiles_parse_nested_mj:n #1 { \msg_error:nnn { mahjong-tiles } { nested-mj } {#1} \__mahjongtiles_parse_loop:w } \cs_new_protected:Npn \__mahjongtiles_parse_inline_mj:n #1 { \seq_if_empty:NF \l__mahjongtiles_pending_seq { \tl_clear:N \l__mahjongtiles_pending_digits_tl \seq_map_inline:Nn \l__mahjongtiles_pending_seq { \__mahjongtiles_collect_pending_digits:nnn ##1 } \msg_error:nnV { mahjong-tiles } { missing-suit } \l__mahjongtiles_pending_digits_tl \seq_clear:N \l__mahjongtiles_pending_seq } \__mahjongtiles_draw_put_from_notation:n {#1} \__mahjongtiles_parse_loop:w } \cs_new_protected:Npn \__mahjongtiles_draw_put_from_notation:n #1 { \bool_gset_false:N \g__mahjongtiles_inline_mj_valid_bool \tl_gclear:N \g__mahjongtiles_inline_mj_face_tl \tl_gclear:N \g__mahjongtiles_inline_mj_suit_tl \group_begin: \int_incr:N \l__mahjongtiles_mj_depth_int \__mahjongtiles_parse:n {#1} \int_compare:nNnTF { \seq_count:N \l__mahjongtiles_tiles_seq } = {1} { \seq_get_left:NN \l__mahjongtiles_tiles_seq \l__mahjongtiles_tmp_tl \exp_last_unbraced:NV \__mahjongtiles_extract_inline_mj_tile:nnnnnn \l__mahjongtiles_tmp_tl {#1} } { \msg_error:nnn { mahjong-tiles } { inline-mj-single-tile } {#1} } \group_end: \bool_if:NT \g__mahjongtiles_inline_mj_valid_bool { \__mahjongtiles_draw_put:VV \g__mahjongtiles_inline_mj_face_tl \g__mahjongtiles_inline_mj_suit_tl } } \cs_new_protected:Npn \__mahjongtiles_extract_inline_mj_tile:nnnnnn #1#2#3#4#5#6 { \str_if_eq:nnTF {#1} { tile } { \tl_gset:Nn \g__mahjongtiles_inline_mj_face_tl {#2} \tl_gset:Nn \g__mahjongtiles_inline_mj_suit_tl {#3} \bool_gset_true:N \g__mahjongtiles_inline_mj_valid_bool } { \msg_error:nnn { mahjong-tiles } { inline-mj-tile-only } {#6} } } \cs_new_protected:Npn \__mahjongtiles_parse_finish:w #1 \q__mahjongtiles_parse_done { } \cs_new_protected:Npn \__mahjongtiles_parse_overlay:w #1 ] { \tl_if_in:nnTF {#1} { \q__mahjongtiles_parse_stop } { \msg_error:nn { mahjong-tiles } { unclosed-overlay } \__mahjongtiles_parse_finish:w } { \__mahjongtiles_set_last_overlay:n {#1} \__mahjongtiles_parse_loop:w } } \cs_new_protected:Npn \__mahjongtiles_collect_pending_digits:nnn #1#2#3 { \tl_put_right:Nn \l__mahjongtiles_pending_digits_tl {#1} } \cs_new_protected:Npn \__mahjongtiles_parse_token:n #1 { \tl_if_blank:nTF {#1} { } { \tl_set:Nx \l__mahjongtiles_tmp_tl { \text_lowercase:n {#1} } \exp_args:NV \__mahjongtiles_parse_normalized_token:n \l__mahjongtiles_tmp_tl } } \cs_new_protected:Npn \__mahjongtiles_parse_normalized_token:n #1 { \tl_if_in:nnTF { 0123456789 } {#1} { \__mahjongtiles_pending_put:nn {#1} {0} } { \tl_if_in:nnTF { mpsz } {#1} { \__mahjongtiles_flush_suit:n {#1} } { \str_case:nnF {#1} { { - } { \__mahjongtiles_handle_dash: } { x } { \__mahjongtiles_tile_put:nnn { x } { } {0} } { ? } { \__mahjongtiles_tile_put:nnn { ? } { } {0} } { ' } { \__mahjongtiles_set_last_orientation:n {1} } { * } { \__mahjongtiles_set_last_orientation:n {1} } { " } { \__mahjongtiles_set_last_orientation:n {2} } { + } { \__mahjongtiles_set_last_orientation:n {2} } } { \msg_error:nnn { mahjong-tiles } { invalid-token } {#1} } } } } \cs_new_protected:Npn \__mahjongtiles_handle_dash: { \seq_if_empty:NTF \l__mahjongtiles_pending_seq { \__mahjongtiles_gap_put:n {7} } { \tl_clear:N \l__mahjongtiles_pending_digits_tl \seq_map_inline:Nn \l__mahjongtiles_pending_seq { \__mahjongtiles_collect_pending_digits:nnn ##1 } \__mahjongtiles_gap_put:V \l__mahjongtiles_pending_digits_tl \seq_clear:N \l__mahjongtiles_pending_seq } } \cs_generate_variant:Nn \__mahjongtiles_gap_put:n { V } \cs_new_protected:Npn \__mahjongtiles_flush_suit:n #1 { \seq_if_empty:NF \l__mahjongtiles_pending_seq { \__mahjongtiles_detect_concealed_kong: \bool_if:NTF \l__mahjongtiles_concealed_kong_bool { \__mahjongtiles_flush_concealed_kong:n {#1} } { \seq_map_inline:Nn \l__mahjongtiles_pending_seq { \__mahjongtiles_flush_pending_item:nnnn ##1 {#1} } } \seq_clear:N \l__mahjongtiles_pending_seq } } \cs_new_protected:Npn \__mahjongtiles_flush_pending_item:nnnn #1#2#3#4 { \__mahjongtiles_tile_put_aka_aware:nnnn {#1} {#4} {#2} {#3} } \cs_new_protected:Npn \__mahjongtiles_detect_concealed_kong: { \bool_set_false:N \l__mahjongtiles_concealed_kong_bool \int_compare:nNnT { \seq_count:N \l__mahjongtiles_pending_seq } = {5} { \seq_get_left:NN \l__mahjongtiles_pending_seq \l__mahjongtiles_tmp_tl \exp_last_unbraced:NV \__mahjongtiles_remember_first_pending:nnn \l__mahjongtiles_tmp_tl \bool_set_true:N \l__mahjongtiles_concealed_kong_bool \seq_map_inline:Nn \l__mahjongtiles_pending_seq { \__mahjongtiles_check_concealed_kong_item:nnn ##1 } } } \cs_new_protected:Npn \__mahjongtiles_remember_first_pending:nnn #1#2#3 { \tl_set:Nn \l__mahjongtiles_first_face_tl {#1} } \cs_new_protected:Npn \__mahjongtiles_check_concealed_kong_item:nnn #1#2#3 { \str_if_eq:VnF \l__mahjongtiles_first_face_tl {#1} { \bool_set_false:N \l__mahjongtiles_concealed_kong_bool } \int_compare:nNnF {#2} = {0} { \bool_set_false:N \l__mahjongtiles_concealed_kong_bool } } \cs_new_protected:Npn \__mahjongtiles_remember_pending_overlay:nnn #1#2#3 { \tl_if_blank:nF {#3} { \tl_set:Nn \l__mahjongtiles_tmpb_tl {#3} } } \cs_new_protected:Npn \__mahjongtiles_flush_concealed_kong:n #1 { % Five repeated digits followed by a suit are rendered as a concealed kong: % back, two visible tiles, back. For 55555m/p/s the visible pair is red 5 % plus normal 5 by default; no-aka=1 turns that visible pair into two % regular fives. \tl_clear:N \l__mahjongtiles_tmpb_tl \seq_map_inline:Nn \l__mahjongtiles_pending_seq { \__mahjongtiles_remember_pending_overlay:nnn ##1 } \__mahjongtiles_tile_put:nnn { x } { } {0} \str_if_eq:VnTF \l__mahjongtiles_first_face_tl {5} { \tl_if_in:nnTF { mps } {#1} { \__mahjongtiles_tile_put_aka_aware:nnn {0} {#1} {0} \__mahjongtiles_tile_put_aka_aware:nnn {5} {#1} {0} } { \__mahjongtiles_tile_put:nnn {5} {#1} {0} \__mahjongtiles_tile_put:nnn {5} {#1} {0} } } { \__mahjongtiles_tile_put_aka_aware:Vnn \l__mahjongtiles_first_face_tl {#1} {0} \__mahjongtiles_tile_put_aka_aware:Vnn \l__mahjongtiles_first_face_tl {#1} {0} } \__mahjongtiles_tile_put:nnn { x } { } {0} \bool_set_true:N \l__mahjongtiles_last_concealed_kong_tail_bool \tl_if_blank:VF \l__mahjongtiles_tmpb_tl { \__mahjongtiles_output_set_last_overlay:V \l__mahjongtiles_tmpb_tl } } % ----------------------------------------------------------------------------- % Rendering % ----------------------------------------------------------------------------- \cs_new:Npn \__mahjongtiles_file:n #1 { \g__mahjongtiles_tile_dir_tl / mahjong-tiles-#1.pdf } \cs_new_protected:Npn \__mahjongtiles_render_hand:n #1 { \dim_set:Nn \l__mahjongtiles_symbol_height_dim { \fp_to_decimal:N \l__mahjongtiles_tile_scale_fp \l__mahjongtiles_tile_height_dim } \dim_set:Nn \l__mahjongtiles_baseline_offset_dim { ( \l__mahjongtiles_tile_height_dim - \l__mahjongtiles_symbol_height_dim ) / 2 } \dim_set:Nn \l__mahjongtiles_tile_width_dim { 0.75 \l__mahjongtiles_tile_height_dim } \__mahjongtiles_parse:n {#1} \leavevmode \raisebox { -\l__mahjongtiles_baseline_offset_dim } { \seq_map_inline:Nn \l__mahjongtiles_tiles_seq { \__mahjongtiles_render_item:nnnnn ##1 } } } \cs_new_protected:Npn \__mahjongtiles_render_river:n #1 { \dim_set:Nn \l__mahjongtiles_symbol_height_dim { \fp_to_decimal:N \l__mahjongtiles_tile_scale_fp \l__mahjongtiles_tile_height_dim } \dim_set:Nn \l__mahjongtiles_baseline_offset_dim { ( \l__mahjongtiles_tile_height_dim - \l__mahjongtiles_symbol_height_dim ) / 2 } \dim_set:Nn \l__mahjongtiles_tile_width_dim { 0.75 \l__mahjongtiles_tile_height_dim } \__mahjongtiles_parse:n {#1} \int_zero:N \l__mahjongtiles_river_total_tiles_int \seq_map_inline:Nn \l__mahjongtiles_tiles_seq { \__mahjongtiles_count_river_tile:nnnnn ##1 } \leavevmode \vtop \bgroup \offinterlineskip \int_zero:N \l__mahjongtiles_river_seen_tiles_int \int_zero:N \l__mahjongtiles_river_col_int \hbox \bgroup \seq_map_inline:Nn \l__mahjongtiles_tiles_seq { \__mahjongtiles_render_river_item:nnnnn ##1 } \egroup \egroup } \cs_new_protected:Npn \__mahjongtiles_count_river_tile:nnnnn #1#2#3#4#5 { \str_if_eq:nnF {#1} { gap } { \int_incr:N \l__mahjongtiles_river_total_tiles_int } } \cs_new_protected:Npn \__mahjongtiles_render_river_item:nnnnn #1#2#3#4#5 { \str_if_eq:nnTF {#1} { gap } { \__mahjongtiles_render_gap:n {#2} } { \str_if_eq:nnTF {#1} { draw } { \__mahjongtiles_render_draw_tile:nn {#2} {#3} } { \__mahjongtiles_render_tile_with_overlay:nnnn {#2} {#3} {#4} {#5} } \int_incr:N \l__mahjongtiles_river_seen_tiles_int \int_incr:N \l__mahjongtiles_river_col_int \int_compare:nT { \l__mahjongtiles_river_cols_int > 0 } { \int_compare:nT { \l__mahjongtiles_river_col_int >= \l__mahjongtiles_river_cols_int } { \int_compare:nT { \l__mahjongtiles_river_seen_tiles_int < \l__mahjongtiles_river_total_tiles_int } { \egroup \vskip \l__mahjongtiles_river_row_gap_dim \hbox \bgroup \int_zero:N \l__mahjongtiles_river_col_int } } } } } \cs_new_protected:Npn \__mahjongtiles_render_item:nnnnn #1#2#3#4#5 { \str_case:nnF {#1} { { gap } { \__mahjongtiles_render_gap:n {#2} } { draw } { \__mahjongtiles_render_draw_tile:nn {#2} {#3} } } { \__mahjongtiles_render_tile_with_overlay:nnnn {#2} {#3} {#4} {#5} } } \cs_new_protected:Npn \__mahjongtiles_render_gap:n #1 { \hspace { \dim_eval:n { #1 \l__mahjongtiles_tile_width_dim / 7 } } } \cs_new_protected:Npn \__mahjongtiles_render_tile_with_overlay:nnnn #1#2#3#4 { \tl_if_blank:nTF {#4} { \__mahjongtiles_render_tile:nnn {#1} {#2} {#3} } { \__mahjongtiles_make_overlay_tile:nnnn {#1} {#2} {#3} {#4} } } \cs_new_protected:Npn \__mahjongtiles_make_overlay_tile:nnnn #1#2#3#4 { \hbox_set:Nn \l__mahjongtiles_tile_box { \__mahjongtiles_render_tile:nnn {#1} {#2} {#3} } \tikz [ baseline = (base.base), inner~sep = 0pt, outer~sep = 0pt ] { \node [ inner~sep = 0pt, outer~sep = 0pt ] (base) { \box_use:N \l__mahjongtiles_tile_box }; \exp_args:NV \__mahjongtiles_make_overlay_node:nn \l__mahjongtiles_overlay_style_tl {#4} \path [ use~as~bounding~box ] (base.south~west) rectangle (base.north~east |- mahjongtilesoverlay.north); } } \cs_new_protected:Npn \__mahjongtiles_make_overlay_node:nn #1#2 { \node [ #1, overlay ] (mahjongtilesoverlay) at (base.north) {#2}; } \cs_new_protected:Npn \__mahjongtiles_render_draw_tile:nn #1#2 { \hbox_to_wd:nn { 0pt } { \tikz [ baseline = (base.base), inner~sep = 0pt, outer~sep = 0pt ] { % This phantom only provides the hand-height reference. \node [ inner~sep = 0pt, outer~sep = 0pt ] (base) { \phantom { \__mahjongtiles_render_tile:nnn {#1} {#2} {0} } }; \node [ inner~sep = 0pt, outer~sep = 0pt, anchor = south~west ] at (base.north~west) { \__mahjongtiles_render_tile:nnn {#1} {#2} {1} }; } \hss } } \cs_new_protected:Npn \__mahjongtiles_render_tile:nnn #1#2#3 { \str_case:nnF {#1} { { x } { \__mahjongtiles_make_back_tile:n {#3} } { ? } { \__mahjongtiles_make_symbol_tile:nn { \__mahjongtiles_file:n { Blank } } {#3} } } { \__mahjongtiles_make_symbol_tile:nn { \__mahjongtiles_file:n { #1#2 } } {#3} } } \cs_new_protected:Npn \__mahjongtiles_make_symbol_tile:nn #1#2 { \file_if_exist:nTF {#1} { \int_case:nnF {#2} { {0} { \stackinset { c } { 0pt } { c } { 0pt } { \includegraphics [ height = \l__mahjongtiles_symbol_height_dim ] {#1} } { \includegraphics [ height = \l__mahjongtiles_tile_height_dim ] { \__mahjongtiles_file:n { Front } } } } {1} { \stackinset { c } { 0pt } { c } { 0pt } { \includegraphics [ angle = 90, width = \l__mahjongtiles_symbol_height_dim ] {#1} } { \includegraphics [ angle = 90, width = \l__mahjongtiles_tile_height_dim ] { \__mahjongtiles_file:n { Front } } } } {2} { \stackon [0pt] { \__mahjongtiles_make_symbol_tile:nn {#1} {1} } { \__mahjongtiles_make_symbol_tile:nn {#1} {1} } } } { \msg_fatal:nnn { mahjong-tiles } { unknown-orientation } {#2} } } { \msg_error:nnn { mahjong-tiles } { unknown-tile } {#1} } } \cs_new_protected:Npn \__mahjongtiles_make_back_tile:n #1 { \int_case:nnF {#1} { {0} { \__mahjongtiles_make_back_tile_upright: } {1} { \rotatebox { 90 } { \__mahjongtiles_make_back_tile_upright: } } {2} { \stackon [0pt] { \__mahjongtiles_make_back_tile:n {1} } { \__mahjongtiles_make_back_tile:n {1} } } } { \msg_fatal:nnn { mahjong-tiles } { unknown-orientation } {#1} } } \cs_new_protected:Npn \__mahjongtiles_make_back_tile_upright: { \bool_if:NTF \g__mahjongtiles_back_recolor_bool { \__mahjongtiles_make_back_tile_upright_recolored: } { \includegraphics [ height = \l__mahjongtiles_tile_height_dim ] { \__mahjongtiles_file:n { Back } } } } % The back tile PDF is a single pure-red vector shape on a transparent % background. Recoloring by blending a rectangle over the whole PDF box leaks % into the transparent rounded corners, so we redraw the exact vector outline as % a clipping mask and fill only the visible shape. \cs_new_protected:Npn \__mahjongtiles_back_clip_path: { \path [ clip ] (30,300.059) -- (195.043,300.059) .. controls (211.664,300.059) and (225.043,286.68) .. (225.043,270.059) -- (225.043,29.477) .. controls (225.043,12.8551) and (211.664,-0.523047) .. (195.043,-0.523047) -- (30,-0.523047) .. controls (13.3789,-0.523047) and (0,12.8551) .. (0,29.477) -- (0,270.059) .. controls (0,286.68) and (13.3789,300.059) .. (30,300.059) -- cycle; } \cs_new_protected:Npn \__mahjongtiles_make_back_tile_upright_recolored: { \tikz [ baseline = 0pt, x = { \dim_eval:n { \l__mahjongtiles_tile_width_dim / 225 } }, y = { \dim_eval:n { \l__mahjongtiles_tile_height_dim / 300 } }, inner~sep = 0pt, outer~sep = 0pt ] { \path [ use~as~bounding~box ] (0,0) rectangle (225,300); \begin{scope} \__mahjongtiles_back_clip_path: \fill [ \tl_use:N \g__mahjongtiles_back_color_tl, opacity = 1 ] (0,-1) rectangle (226,301); \end{scope} } } % ----------------------------------------------------------------------------- % Public commands % ----------------------------------------------------------------------------- \cs_new_protected:Npn \__mahjongtiles_set_local_defaults: { \dim_set_eq:NN \l__mahjongtiles_tile_height_dim \g__mahjongtiles_default_height_dim \fp_set_eq:NN \l__mahjongtiles_tile_scale_fp \g__mahjongtiles_default_scale_fp \tl_set_eq:NN \l__mahjongtiles_overlay_style_tl \g__mahjongtiles_overlay_style_tl \int_set_eq:NN \l__mahjongtiles_river_cols_int \g__mahjongtiles_river_cols_int \dim_set_eq:NN \l__mahjongtiles_river_row_gap_dim \g__mahjongtiles_river_row_gap_dim } \cs_new_protected:Npn \__mahjongtiles_apply_local_options:nnnn #1#2#3#4 { \__mahjongtiles_set_local_defaults: \tl_if_in:nnTF {#1} { = } { \keys_set:nn { mahjong-tiles / local } {#1} } { \dim_set:Nn \l__mahjongtiles_tile_height_dim {#1} \fp_set:Nn \l__mahjongtiles_tile_scale_fp {#2} \IfNoValueF {#3} { \__mahjongtiles_set_back_color_local:n {#3} } } \tl_if_blank:nF {#4} { \keys_set:nn { mahjong-tiles / local } {#4} } } \cs_new_protected:Npn \__mahjongtiles_ensure_local_defaults: { \dim_compare:nNnT { \l__mahjongtiles_tile_height_dim } = { 0pt } { \__mahjongtiles_set_local_defaults: } } \cs_new_protected:Npn \__mahjongtiles_mj:n #1 { \int_compare:nNnTF { \l__mahjongtiles_mj_depth_int } > {0} { \msg_error:nnn { mahjong-tiles } { nested-mj } {#1} } { \group_begin: \__mahjongtiles_ensure_local_defaults: \int_incr:N \l__mahjongtiles_mj_depth_int \__mahjongtiles_render_hand:n {#1} \group_end: } } \cs_new_protected:Npn \mahjongtiles_typeset_hand:n #1 { \group_begin: \__mahjongtiles_set_local_defaults: \__mahjongtiles_render_hand:n {#1} \group_end: } \cs_new_protected:Npn \mahjongtiles:n #1 { \mahjongtiles_typeset_hand:n {#1} } \cs_new_protected:Npn \mahjongtiles_typeset_river:n #1 { \group_begin: \__mahjongtiles_set_local_defaults: \__mahjongtiles_render_river:n {#1} \group_end: } \cs_new_protected:Npn \mahjongtilesriver:n #1 { \mahjongtiles_typeset_river:n {#1} } \NewDocumentCommand \mj { m } { \__mahjongtiles_mj:n {#1} } \NewDocumentCommand \mahjong { O{\g__mahjongtiles_default_height_dim} O{\g__mahjongtiles_default_scale_fp} o O{} m } { \group_begin: \__mahjongtiles_apply_local_options:nnnn {#1} {#2} {#3} {#4} \__mahjongtiles_render_hand:n {#5} \group_end: } \NewDocumentCommand \mahjongriver { O{\g__mahjongtiles_default_height_dim} O{\g__mahjongtiles_default_scale_fp} o O{} m } { \group_begin: \__mahjongtiles_apply_local_options:nnnn {#1} {#2} {#3} {#4} \__mahjongtiles_render_river:n {#5} \group_end: } \NewDocumentCommand \mahjongtilessetup { m } { \keys_set:nn { mahjong-tiles } {#1} } \ExplSyntaxOff %\endinput % % \end{macrocode} % % \Finale \endinput