% Copyright 2026 Open-Guji (https://github.com/open-guji) % % Licensed under the Apache License, Version 2.0 (the "License"); % you may not use this file except in compliance with the License. % You may obtain a copy of the License at % % http://www.apache.org/licenses/LICENSE-2.0 % % Unless required by applicable law or agreed to in writing, software % distributed under the License is distributed on an "AS IS" BASIS, % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. % See the License for the specific language governing permissions and % limitations under the License. % luatex-cn-textbox.sty % TextBox support for vertical typesetting % This is a subpackage of luatex_cn % % NOTE: This file must be loaded AFTER luatexcn/vertical keys are defined % \RequirePackage{core/luatex-cn-core-base} \RequirePackage{expl3} \RequirePackage{xparse} \ProvidesExplPackage {core/luatex-cn-core-textbox} {2026/02/18} {0.3.0} {TextBox Support for Vertical Typesetting} % Grid Textbox Command (Simplified for Single Column) % Syntax: \GridTextbox[height=N, fill=true/false]{content} % height is in grid units (integers), width is always 1 % Key-value interface for \GridTextbox \bool_new:N \l__luatexcn_textbox_border_bool \tl_new:N \l__luatexcn_textbox_box_align_tl \tl_new:N \l__luatexcn_textbox_inner_gw_tl \tl_new:N \l__luatexcn_textbox_inner_gh_tl \dim_new:N \l__luatexcn_textbox_inner_gw_dim \dim_new:N \l__luatexcn_textbox_inner_gh_dim \bool_new:N \l__luatexcn_textbox_floating_bool \tl_new:N \l__luatexcn_textbox_x_tl \tl_new:N \l__luatexcn_textbox_y_tl \tl_new:N \l__luatexcn_textbox_bg_color_tl \tl_new:N \l__luatexcn_textbox_font_color_tl \tl_new:N \l__luatexcn_textbox_bg_rgb_tl \tl_new:N \l__luatexcn_textbox_font_rgb_tl \tl_new:N \l__luatexcn_textbox_font_size_tl \tl_new:N \l__luatexcn_textbox_floating_paper_width_tl \tl_new:N \l__luatexcn_textbox_border_shape_tl \tl_new:N \l__luatexcn_textbox_border_color_tl \tl_new:N \l__luatexcn_textbox_border_width_tl \tl_new:N \l__luatexcn_textbox_border_margin_tl \tl_new:N \l__luatexcn_textbox_border_rgb_tl \bool_new:N \l__luatexcn_textbox_outer_border_bool \tl_new:N \l__luatexcn_textbox_outer_border_thickness_tl \tl_new:N \l__luatexcn_textbox_outer_border_sep_tl \keys_define:nn { luatexcn / textbox } { height .tl_set:N = \l__luatexcn_textbox_height_tl, height .initial:n = 0, n-cols .int_set:N = \l__luatexcn_textbox_n_cols_int, n-cols .initial:n = 0, outer-cols .int_set:N = \l__luatexcn_textbox_outer_cols_int, outer-cols .initial:n = 0, fill .bool_set:N = \l__luatexcn_textbox_distribute_bool, fill .initial:n = false, box-align .tl_set:N = \l__luatexcn_textbox_box_align_tl, box-align .initial:n = {top}, border .bool_set:N = \l__luatexcn_textbox_border_bool, border .initial:n = false, banxin .bool_set:N = \l__luatexcn_textbox_banxin_bool, column-align .tl_set:N = \l__luatexcn_textbox_col_align_tl, column-align .initial:n = {}, % debug key removed - use global \LtcDebugOn vertical-align .tl_set:N = \l__luatexcn_textbox_box_align_tl, floating .bool_set:N = \l__luatexcn_textbox_floating_bool, floating .initial:n = false, floating-paper-width .tl_set:N = \l__luatexcn_textbox_floating_paper_width_tl, floating-paper-width .initial:n = 0pt, x .tl_set:N = \l__luatexcn_textbox_x_tl, x .initial:n = 0pt, y .tl_set:N = \l__luatexcn_textbox_y_tl, y .initial:n = 0pt, background-color .tl_set:N = \l__luatexcn_textbox_bg_color_tl, background-color .initial:n = {}, font-color .tl_set:N = \l__luatexcn_textbox_font_color_tl, font-color .initial:n = {}, font-size .tl_set:N = \l__luatexcn_textbox_font_size_tl, font-size .initial:n = {}, grid-width .tl_set:N = \l__luatexcn_textbox_inner_gw_tl, grid-width .initial:n = {}, grid-height .tl_set:N = \l__luatexcn_textbox_inner_gh_tl, grid-height .initial:n = {}, % Border shape parameters (rect/octagon/circle) border-shape .tl_set:N = \l__luatexcn_textbox_border_shape_tl, border-shape .initial:n = {none}, border-color .tl_set:N = \l__luatexcn_textbox_border_color_tl, border-color .initial:n = {}, border-width .tl_set:N = \l__luatexcn_textbox_border_width_tl, border-width .initial:n = {0.4pt}, border-margin .tl_set:N = \l__luatexcn_textbox_border_margin_tl, border-margin .initial:n = {0pt}, outer-border .bool_set:N = \l__luatexcn_textbox_outer_border_bool, outer-border .initial:n = false, outer-border-thickness .tl_set:N = \l__luatexcn_textbox_outer_border_thickness_tl, outer-border-thickness .initial:n = {1pt}, outer-border-sep .tl_set:N = \l__luatexcn_textbox_outer_border_sep_tl, outer-border-sep .initial:n = {2pt}, } \NewDocumentCommand{\TextBox}{ O{} +m } { \group_begin: % Inherit debug setting from global environment % No local override anymore \keys_set:nn { luatexcn / textbox } { #1 } % Width is always 1 for outer layout (textbox occupies 1 column externally) \setluatexattribute\cnverticaltextboxwidth{1} % Calculate inner grid width via Lua (replaces TeX-side decision tree) \dim_set:Nn \l__luatexcn_textbox_inner_gw_dim { \lua_now:e { local ~ tb = require('core.luatex-cn-core-textbox') tex.print(tostring(tb.calc_inner_grid_width({ inner_gw = \tl_if_empty:NTF \l__luatexcn_textbox_inner_gw_tl { 0 } { \dim_to_decimal_in_sp:n { \l__luatexcn_textbox_inner_gw_tl } }, column_width = \dim_to_decimal_in_sp:n { \g__luatexcn_column_current_width_dim }, outer_cols = \int_use:N \l__luatexcn_textbox_outer_cols_int, n_cols = \int_use:N \l__luatexcn_textbox_n_cols_int, content_gw = \dim_to_decimal_in_sp:n { \l__luatexcn_content_grid_width_tl }, }))) } sp } \tl_if_empty:NTF \l__luatexcn_textbox_inner_gh_tl { \dim_set:Nn \l__luatexcn_textbox_inner_gh_dim { \l__luatexcn_content_grid_height_tl } } { \dim_set:Nn \l__luatexcn_textbox_inner_gh_dim { \l__luatexcn_textbox_inner_gh_tl } } % Capture content in a box and process it via Lua for inner verticality \vbox_set:Nn \l_tmpa_box { \setluatexattribute\cnverticaltextboxwidth{0} \setluatexattribute\cnverticaltextboxheight{0} % Note: indent is inherited from style stack (no longer reset here) % Reset paragraph spacing to prevent unwanted glues in grid layout \dim_set:Nn \l_tmpa_dim { \l__luatexcn_content_grid_height_tl } % Scale font size \tl_if_empty:NTF \l__luatexcn_textbox_font_size_tl { % Default auto-scaling for multi-column boxes \int_compare:nNnT \l__luatexcn_textbox_n_cols_int > 1 { \dim_set:Nn \l_tmpa_dim { \l__luatexcn_textbox_inner_gw_dim * 85 / 100 } \fontsize{\l_tmpa_dim}{\l__luatexcn_textbox_inner_gw_dim}\selectfont } } { % User-specified font size \dim_set:Nn \l_tmpa_dim { \l__luatexcn_textbox_font_size_tl } \fontsize{\l_tmpa_dim}{\l_tmpa_dim}\selectfont } % Inner line length: if height specified, constrain it; otherwise use max for auto-sizing \tl_if_empty:NTF \l__luatexcn_textbox_height_tl { \dim_set_eq:NN \hsize \c_max_dim } { \exp_args:NnV \regex_match:nnTF { [^0-9\.] } \l__luatexcn_textbox_height_tl { \dim_set:Nn \hsize { \l__luatexcn_textbox_height_tl } } { \dim_set:Nn \hsize { \l__luatexcn_textbox_inner_gh_dim * \l__luatexcn_textbox_height_tl } } } #2 } % Expand variables to ensure they contain the color string, not a macro name % Use o-expansion to preserve spaces (standard x-expansion strips them in ExplSyntax) \tl_set:No \l__luatexcn_textbox_bg_color_tl { \l__luatexcn_textbox_bg_color_tl } \tl_set:No \l__luatexcn_textbox_font_color_tl { \l__luatexcn_textbox_font_color_tl } % Convert background color to RGB \cs_if_exist:cTF { color @ \l__luatexcn_textbox_bg_color_tl } { \exp_args:NV \extractcolorspec \l__luatexcn_textbox_bg_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl { rgb } \l_tmpb_tl \tl_set:Nx \l__luatexcn_textbox_bg_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__luatexcn_textbox_bg_rgb_tl \l__luatexcn_textbox_bg_color_tl } \tl_replace_all:Nnn \l__luatexcn_textbox_bg_rgb_tl { , } { ~ } % Convert font color to RGB \cs_if_exist:cTF { color @ \l__luatexcn_textbox_font_color_tl } { \exp_args:NV \extractcolorspec \l__luatexcn_textbox_font_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl { rgb } \l_tmpb_tl \tl_set:Nx \l__luatexcn_textbox_font_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__luatexcn_textbox_font_rgb_tl \l__luatexcn_textbox_font_color_tl } \tl_replace_all:Nnn \l__luatexcn_textbox_font_rgb_tl { , } { ~ } % Convert border color to RGB \tl_set:No \l__luatexcn_textbox_border_color_tl { \l__luatexcn_textbox_border_color_tl } \cs_if_exist:cTF { color @ \l__luatexcn_textbox_border_color_tl } { \exp_args:NV \extractcolorspec \l__luatexcn_textbox_border_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl { rgb } \l_tmpb_tl \tl_set:Nx \l__luatexcn_textbox_border_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__luatexcn_textbox_border_rgb_tl \l__luatexcn_textbox_border_color_tl } \tl_replace_all:Nnn \l__luatexcn_textbox_border_rgb_tl { , } { ~ } % Setup textbox params in _G.textbox (parsed once, used in Lua) \lua_now:e { vertical_textbox.setup({ column_aligns~=~[=[\luaescapestring{\tl_use:N~\l__luatexcn_textbox_col_align_tl}]=], floating~=~\bool_if:NTF~\l__luatexcn_textbox_floating_bool~{true}~{false}, floating_x~=~[=[\luaescapestring{\l__luatexcn_textbox_x_tl}]=], floating_y~=~[=[\luaescapestring{\l__luatexcn_textbox_y_tl}]=], floating_paper_width~=~[=[\luaescapestring{\l__luatexcn_textbox_floating_paper_width_tl}]=], outer_grid_height~=~\dim_to_decimal_in_sp:n { \l__luatexcn_content_grid_height_tl } }) } \lua_now:e {vertical_textbox.process_inner_box(\int_value:w~\l_tmpa_box,~{ n_cols~=~\int_use:N~\l__luatexcn_textbox_n_cols_int, outer_cols~=~\int_use:N~\l__luatexcn_textbox_outer_cols_int, height~=~[=[\luaescapestring{\tl_use:N \l__luatexcn_textbox_height_tl}]=], grid_width~=~\dim_to_decimal_in_sp:n { \l__luatexcn_textbox_inner_gw_dim }, grid_height~=~\dim_to_decimal_in_sp:n { \l__luatexcn_textbox_inner_gh_dim }, box_align~=~"\luaescapestring{\tl_use:N~\l__luatexcn_textbox_box_align_tl}", border~=~"\bool_if:NTF~\l__luatexcn_textbox_border_bool~{true}{false}", background_color~=~[=[\l__luatexcn_textbox_bg_rgb_tl]=], font_color~=~[=[\l__luatexcn_textbox_font_rgb_tl]=], font_size~=~[=[\luaescapestring{\l__luatexcn_textbox_font_size_tl}]=], border_shape~=~"\luaescapestring{\tl_use:N~\l__luatexcn_textbox_border_shape_tl}", border_color~=~[=[\l__luatexcn_textbox_border_rgb_tl]=], border_width~=~[=[\luaescapestring{\l__luatexcn_textbox_border_width_tl}]=], border_margin~=~[=[\luaescapestring{\l__luatexcn_textbox_border_margin_tl}]=], outer_border~=~\bool_if:NTF~\l__luatexcn_textbox_outer_border_bool~{true}{false}, outer_border_thickness~=~[=[\luaescapestring{\l__luatexcn_textbox_outer_border_thickness_tl}]=], outer_border_sep~=~[=[\luaescapestring{\l__luatexcn_textbox_outer_border_sep_tl}]=] }) } \setluatexattribute\cnverticaltextboxwidth{0} \setluatexattribute\cnverticaltextboxheight{0} \bool_if:NTF \l__luatexcn_textbox_floating_bool { \lua_now:e { vertical_textbox.register_floating_box(\int_value:w~\l_tmpa_box,~{ x~=~[=[\luaescapestring{\l__luatexcn_textbox_x_tl}]=], y~=~[=[\luaescapestring{\l__luatexcn_textbox_y_tl}]=] }) } } { \mode_leave_vertical: \box_use:N \l_tmpa_box } \group_end: } % 填充文本框 - Backwards compatible: accepts either integer (old syntax) or key-value options (new syntax) \NewDocumentCommand{\FillTextBox}{ O{} +m } {% \tl_set:Nn \l_tmpa_tl { #1 } \regex_match:nnTF { ^[0-9]+$ } { #1 } { \TextBox[height=#1, box-align=fill]{#2} } % Old syntax: just an integer height { \TextBox[#1, box-align=fill]{#2} } % New syntax: key-value options } \ExplSyntaxOff% % ============================================================ % Chinese aliases / 中文别名 % ============================================================ % Border shape shortcuts \NewDocumentCommand{\反白}{ m }{\TextBox[background-color={0.2 0.2 0.2}, font-color=white]{#1}} \NewDocumentCommand{\八角框}{ m }{\TextBox[border-shape=octagon]{#1}} \NewDocumentCommand{\带圈}{ m }{\TextBox[border-shape=circle]{#1}} \NewDocumentCommand{\反白八角框}{ m }{\TextBox[background-color={0.2 0.2 0.2}, font-color=white, border-shape=octagon, border-color=white]{#1}} % Simplified Chinese / 简体 \NewCommandCopy{\文本框}{\TextBox} \NewCommandCopy{\填充文本框}{\FillTextBox} % English aliases for Chinese-only commands \NewCommandCopy{\inverted}{\反白} \NewCommandCopy{\octagon}{\八角框} \NewCommandCopy{\circled}{\带圈} \NewCommandCopy{\invertedOctagon}{\反白八角框} % Traditional Chinese / 繁体 (带圈→帶圈) \NewCommandCopy{\帶圈}{\带圈} % ============================================================ % Chinese key aliases / 中文 Key 别名 % ============================================================ \ExplSyntaxOn \keys_define:nn { luatexcn / textbox } { % 简体 高度 .tl_set:N = \l__luatexcn_textbox_height_tl, 列数 .int_set:N = \l__luatexcn_textbox_n_cols_int, 外列数 .int_set:N = \l__luatexcn_textbox_outer_cols_int, 填充 .bool_set:N = \l__luatexcn_textbox_distribute_bool, 框对齐 .tl_set:N = \l__luatexcn_textbox_box_align_tl, 边框 .bool_set:N = \l__luatexcn_textbox_border_bool, 版心 .bool_set:N = \l__luatexcn_textbox_banxin_bool, 列对齐 .meta:n = { column-align = #1 }, 纵对齐 .meta:n = { vertical-align = #1 }, 悬浮 .bool_set:N = \l__luatexcn_textbox_floating_bool, 悬浮纸宽 .tl_set:N = \l__luatexcn_textbox_floating_paper_width_tl, 横位 .tl_set:N = \l__luatexcn_textbox_x_tl, 纵位 .tl_set:N = \l__luatexcn_textbox_y_tl, 底色 .tl_set:N = \l__luatexcn_textbox_bg_color_tl, 字体颜色 .tl_set:N = \l__luatexcn_textbox_font_color_tl, 字号 .tl_set:N = \l__luatexcn_textbox_font_size_tl, 格宽 .meta:n = { grid-width = #1 }, 格高 .meta:n = { grid-height = #1 }, 边框形状 .tl_set:N = \l__luatexcn_textbox_border_shape_tl, 边框色 .tl_set:N = \l__luatexcn_textbox_border_color_tl, 边框宽 .tl_set:N = \l__luatexcn_textbox_border_width_tl, 边框间距 .tl_set:N = \l__luatexcn_textbox_border_margin_tl, 外框 .bool_set:N = \l__luatexcn_textbox_outer_border_bool, 外框粗细 .tl_set:N = \l__luatexcn_textbox_outer_border_thickness_tl, 外框间距 .tl_set:N = \l__luatexcn_textbox_outer_border_sep_tl, % 繁体(与简体不同形的) 列數 .int_set:N = \l__luatexcn_textbox_n_cols_int, 外列數 .int_set:N = \l__luatexcn_textbox_outer_cols_int, 填充 .bool_set:N = \l__luatexcn_textbox_distribute_bool, 框對齊 .tl_set:N = \l__luatexcn_textbox_box_align_tl, 邊框 .bool_set:N = \l__luatexcn_textbox_border_bool, 列對齊 .meta:n = { column-align = #1 }, 縱對齊 .meta:n = { vertical-align = #1 }, 懸浮 .bool_set:N = \l__luatexcn_textbox_floating_bool, 懸浮紙寬 .tl_set:N = \l__luatexcn_textbox_floating_paper_width_tl, 橫位 .tl_set:N = \l__luatexcn_textbox_x_tl, 縱位 .tl_set:N = \l__luatexcn_textbox_y_tl, 字號 .tl_set:N = \l__luatexcn_textbox_font_size_tl, 字體顏色 .tl_set:N = \l__luatexcn_textbox_font_color_tl, 格寬 .meta:n = { grid-width = #1 }, 格高 .meta:n = { grid-height = #1 }, 邊框形狀 .tl_set:N = \l__luatexcn_textbox_border_shape_tl, 邊框色 .tl_set:N = \l__luatexcn_textbox_border_color_tl, 邊框寬 .tl_set:N = \l__luatexcn_textbox_border_width_tl, 邊框間距 .tl_set:N = \l__luatexcn_textbox_border_margin_tl, 外框粗細 .tl_set:N = \l__luatexcn_textbox_outer_border_thickness_tl, 外框間距 .tl_set:N = \l__luatexcn_textbox_outer_border_sep_tl, } \ExplSyntaxOff \endinput%