% Copyright 2026 Open-Guji (https://github.com/open-guji) % ... (License header omitted for brevity) % luatex-cn-guji-meipi.sty % MeiPi (眉批) auto-positioned annotation support for vertical typesetting % This is a subpackage of luatex_cn % % NOTE: This file must be loaded AFTER TextBox and PiZhu are defined % \RequirePackage{expl3} \RequirePackage{xparse} \ProvidesExplPackage {guji/luatex-cn-guji-meipi} {2026/02/18} {0.3.0} {MeiPi Auto-Positioned Annotation Support} % Load the Lua module \lua_now:n { require('guji.luatex-cn-guji-meipi') } % MeiPi (眉批) - Auto-positioned floating annotation box \tl_new:N \l__luatexcn_meipi_font_size_tl \tl_new:N \l__luatexcn_meipi_color_tl \tl_new:N \l__luatexcn_meipi_grid_width_tl \tl_new:N \l__luatexcn_meipi_grid_height_tl \tl_new:N \l__luatexcn_meipi_x_tl \tl_new:N \l__luatexcn_meipi_y_tl \tl_new:N \l__luatexcn_meipi_height_tl \tl_new:N \l__luatexcn_meipi_spacing_tl \tl_new:N \l__luatexcn_meipi_gap_tl \keys_define:nn { luatexcn / meipi } { x .tl_set:N = \l__luatexcn_meipi_x_tl, x .initial:n = {}, y .tl_set:N = \l__luatexcn_meipi_y_tl, y .initial:n = {}, height .tl_set:N = \l__luatexcn_meipi_height_tl, height .initial:n = {7}, font-size .tl_set:N = \l__luatexcn_meipi_font_size_tl, font-size .initial:n = {18pt}, color .tl_set:N = \l__luatexcn_meipi_color_tl, color .initial:n = {1~0~0}, grid-width .tl_set:N = \l__luatexcn_meipi_grid_width_tl, grid-width .initial:n = {20pt}, grid-height .tl_set:N = \l__luatexcn_meipi_grid_height_tl, grid-height .initial:n = {19pt}, spacing .tl_set:N = \l__luatexcn_meipi_spacing_tl, spacing .initial:n = {10pt}, gap .tl_set:N = \l__luatexcn_meipi_gap_tl, gap .initial:n = {0pt}, } \NewDocumentCommand{\meipiSetup}{ m } { \keys_set:nn { luatexcn / meipi } { #1 } \lua_now:e { local~meipi~=~require('guji.luatex-cn-guji-meipi') meipi.setup({ spacing~=~[=[\luaescapestring{\l__luatexcn_meipi_spacing_tl}]=], gap~=~[=[\luaescapestring{\l__luatexcn_meipi_gap_tl}]=] }) } } % Internal: Calculate annotation dimensions by typesetting content in a temp box \dim_new:N \l__luatexcn_meipi_box_width_dim \dim_new:N \l__luatexcn_meipi_box_height_dim \box_new:N \l__luatexcn_meipi_temp_box \tl_new:N \l__luatexcn_meipi_width_sp_tl \tl_new:N \l__luatexcn_meipi_height_sp_tl \NewDocumentCommand{\MeiPi}{ O{} +m }{% \group_begin: \keys_set:nn { luatexcn / meipi } { #1 } % Ensure grid dimensions are set to defaults if empty (defensive) \tl_if_empty:NT \l__luatexcn_meipi_grid_width_tl { \tl_set:Nn \l__luatexcn_meipi_grid_width_tl { 20pt } } \tl_if_empty:NT \l__luatexcn_meipi_grid_height_tl { \tl_set:Nn \l__luatexcn_meipi_grid_height_tl { 19pt } } % Setup Lua parameters for gap and spacing \lua_now:e { local~meipi~=~require('guji.luatex-cn-guji-meipi') meipi.setup({ spacing~=~[=[\luaescapestring{\l__luatexcn_meipi_spacing_tl}]=], gap~=~[=[\luaescapestring{\l__luatexcn_meipi_gap_tl}]=] }) } % Pre-calculate X position for center gap detection during TextBox layout % This ensures TextBox knows its floating position before layout happens \tl_if_empty:NTF \l__luatexcn_meipi_x_tl { % No user-specified x, estimate based on accumulated meipi positions \tl_set:Nx \l__luatexcn_meipi_x_tl { \lua_now:e { tex.sprint(require('guji.luatex-cn-guji-meipi').get_next_x_pt()) } } } { } % 1. Create TextBox with floating=false but pass x for center gap detection during layout % We use \use:x to expand variables before passing to TextBox % This ensures that \regex_match_... inside TextBox sees actual values (digits) not macro names \hbox_set:Nn \l__luatexcn_meipi_temp_box { \use:x { \exp_not:N \TextBox [ height = \l__luatexcn_meipi_height_tl, font-size = \l__luatexcn_meipi_font_size_tl, font-color = \l__luatexcn_meipi_color_tl, grid-width = \l__luatexcn_meipi_grid_width_tl, grid-height = \l__luatexcn_meipi_grid_height_tl, floating = false, x = \l__luatexcn_meipi_x_tl ] { \exp_not:n { #2 } } } } % 2. Measure actual dimensions \dim_set:Nn \l__luatexcn_meipi_box_width_dim { \wd\l__luatexcn_meipi_temp_box } % Calculate height for Y positioning % Safe calculation: if contains 'pt' or units, use as is. Else use FP math. \str_if_in:NnTF \l__luatexcn_meipi_height_tl { p } { \dim_set:Nn \l__luatexcn_meipi_box_height_dim { \l__luatexcn_meipi_height_tl } } { \tl_if_empty:NTF \l__luatexcn_meipi_height_tl { % Fallback if height not specified: 5 lines of grid height \dim_set:Nn \l__luatexcn_meipi_box_height_dim { \fp_to_dim:n { 5 * \dim_to_fp:n { \l__luatexcn_meipi_grid_height_tl } } } } { % Use FP math to be absolutely safe (avoid dim expressions * operator issues) \dim_set:Nn \l__luatexcn_meipi_box_height_dim { \fp_to_dim:n { \l__luatexcn_meipi_height_tl * \dim_to_fp:n { \l__luatexcn_meipi_grid_height_tl } } } } } % 3. Calculate coordinates based on what's provided % Prepare dimension values in sp for Lua \tl_set:Nx \l__luatexcn_meipi_width_sp_tl { \dim_to_decimal_in_sp:n { \l__luatexcn_meipi_box_width_dim } } \tl_set:Nx \l__luatexcn_meipi_height_sp_tl { \dim_to_decimal_in_sp:n { \l__luatexcn_meipi_box_height_dim } } \tl_if_empty:NTF \l__luatexcn_meipi_x_tl { % X not provided \tl_if_empty:NTF \l__luatexcn_meipi_y_tl { % Case 1: Neither X nor Y provided - calculate both \lua_now:e { require('guji.luatex-cn-guji-meipi').calculate_and_store( "\l__luatexcn_meipi_width_sp_tl", "\l__luatexcn_meipi_height_sp_tl" ) } \tl_set:Nx \l__luatexcn_meipi_x_tl { \lua_now:e { tex.sprint(_G.meipi_x) } } \tl_set:Nx \l__luatexcn_meipi_y_tl { \lua_now:e { tex.sprint(_G.meipi_y) } } } { % Case 2: Only Y provided - calculate X (keep spacing from previous meipi) \lua_now:e { require('guji.luatex-cn-guji-meipi').calculate_x_and_store( "\l__luatexcn_meipi_width_sp_tl", "\l__luatexcn_meipi_height_sp_tl", "\l__luatexcn_meipi_y_tl" ) } \tl_set:Nx \l__luatexcn_meipi_x_tl { \lua_now:e { tex.sprint(_G.meipi_x) } } } } { % X is provided \tl_if_empty:NTF \l__luatexcn_meipi_y_tl { % Case 3: Only X provided - calculate Y and register annotation \lua_now:e { require('guji.luatex-cn-guji-meipi').calculate_y_and_store( "\l__luatexcn_meipi_width_sp_tl", "\l__luatexcn_meipi_height_sp_tl", "\l__luatexcn_meipi_x_tl" ) } \tl_set:Nx \l__luatexcn_meipi_y_tl { \lua_now:e { tex.sprint(_G.meipi_y) } } } { % Case 4: Both X and Y provided - register annotation for tracking \lua_now:e { require('guji.luatex-cn-guji-meipi').register_with_fixed_xy( "\l__luatexcn_meipi_width_sp_tl", "\l__luatexcn_meipi_height_sp_tl", "\l__luatexcn_meipi_x_tl", "\l__luatexcn_meipi_y_tl" ) } } } % 4. Register the captured box as a floating box \lua_now:e { local~vertical_textbox~=~require('core.luatex-cn-core-textbox') local~box_num~=~\int_use:N \l__luatexcn_meipi_temp_box vertical_textbox.register_floating_box(box_num, { x~=~[=[\luaescapestring{\l__luatexcn_meipi_x_tl}]=], y~=~[=[\luaescapestring{\l__luatexcn_meipi_y_tl}]=] }) } \group_end: } % Reset MeiPi registry at the start of each document \AtBeginDocument{ \lua_now:n { require('guji.luatex-cn-guji-meipi').reset() } } \ExplSyntaxOff% % ============================================================================ % CJK Aliases (中文别名) % ============================================================================ % Simplified Chinese / 简体 \NewCommandCopy{\眉批}{\MeiPi} \NewCommandCopy{\眉批设置}{\meipiSetup} % Traditional Chinese / 繁体 \NewCommandCopy{\眉批設置}{\meipiSetup} % ============================================================ % Chinese key aliases / 中文 Key 别名 % ============================================================ \ExplSyntaxOn \keys_define:nn { luatexcn / meipi } { % 简体 横位 .tl_set:N = \l__luatexcn_meipi_x_tl, 纵位 .tl_set:N = \l__luatexcn_meipi_y_tl, 高度 .tl_set:N = \l__luatexcn_meipi_height_tl, 字号 .tl_set:N = \l__luatexcn_meipi_font_size_tl, 颜色 .tl_set:N = \l__luatexcn_meipi_color_tl, 格宽 .tl_set:N = \l__luatexcn_meipi_grid_width_tl, 格高 .tl_set:N = \l__luatexcn_meipi_grid_height_tl, 间距 .tl_set:N = \l__luatexcn_meipi_spacing_tl, 间隔 .tl_set:N = \l__luatexcn_meipi_gap_tl, % 繁体(与简体不同形的) 橫位 .tl_set:N = \l__luatexcn_meipi_x_tl, 縱位 .tl_set:N = \l__luatexcn_meipi_y_tl, 字號 .tl_set:N = \l__luatexcn_meipi_font_size_tl, 顏色 .tl_set:N = \l__luatexcn_meipi_color_tl, 格寬 .tl_set:N = \l__luatexcn_meipi_grid_width_tl, 間距 .tl_set:N = \l__luatexcn_meipi_spacing_tl, 間隔 .tl_set:N = \l__luatexcn_meipi_gap_tl, } \ExplSyntaxOff \endinput%