% 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. % vertical.sty % LuaTeX 竖排排版宏包 % 这是 luatex_cn 的子包 % \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{luatex-cn-vertical}[2026/01/19 v0.1.1 Chinese vertical typesetting for LuaTeX] % Check if LuaTeX is being used \RequirePackage{ifluatex} \ifluatex\else \PackageError{luatex-cn-vertical}{This package requires LuaTeX}{% Please compile your document with LuaLaTeX.} \fi % Load luatexbase for attributes \RequirePackage{luatexbase} \RequirePackage{xcolor} \RequirePackage{enumitem} % 设置 itemize 默认样式:空标签 + 紧凑间距(符合古籍排版风格) \setlist[itemize]{label={}, nosep} \newluatexattribute\cnverticalindent \newluatexattribute\cnverticalrightindent \newluatexattribute\cnverticaltextboxwidth \newluatexattribute\cnverticaltextboxheight \newluatexattribute\cnverticaltextboxdistribute \newluatexattribute\cnverticaljiazhu \newluatexattribute\cnverticaljiazhusub % Load the Lua modules % Split into multiple directlua blocks for better error reporting % Robust Lua loader with error reporting \NewDocumentCommand{\cnvLua}{ +m }{% \directlua{ local status, err = pcall(function() #1 end) if not status then tex.error("vertical Lua error: " .. tostring(err)) end } } % Initialise Lua Namespace \cnvLua{ _G.vertical = _G.vertical or {} _G.vertical.debug = _G.vertical.debug or { enabled = false, verbose_log = true, show_grid = true, show_boxes = true } } % Clear cache \cnvLua{ package.loaded["luatex-cn-vertical-base-constants"] = nil package.loaded["luatex-cn-vertical-base-utils"] = nil package.loaded["luatex-cn-vertical-base-text-utils"] = nil package.loaded["luatex-cn-vertical-flatten-nodes"] = nil package.loaded["luatex-cn-vertical-layout-grid"] = nil package.loaded["luatex-cn-vertical-render-page"] = nil package.loaded["luatex-cn-vertical-render-border"] = nil package.loaded["luatex-cn-vertical-render-background"] = nil package.loaded["luatex-cn-vertical-render-position"] = nil package.loaded["luatex-cn-vertical-core-main"] = nil package.loaded["luatex-cn-vertical-core-textbox"] = nil package.loaded["luatex-cn-vertical-core-textflow"] = nil package.loaded["luatex-cn-vertical-core-sidenote"] = nil } % Load Modules \cnvLua{ vertical_constants = require('luatex-cn-vertical-base-constants') vertical_utils = require('luatex-cn-vertical-base-utils') vertical_text_utils = require('luatex-cn-vertical-base-text-utils') vertical_hooks = require('luatex-cn-vertical-base-hooks') vertical_flatten = require('luatex-cn-vertical-flatten-nodes') vertical_layout = require('luatex-cn-vertical-layout-grid') vertical_text_position = require('luatex-cn-vertical-render-position') vertical_render = require('luatex-cn-vertical-render-page') vertical_border = require('luatex-cn-vertical-render-border') vertical_background = require('luatex-cn-vertical-render-background') } % Load core logic \cnvLua{ vertical_textbox = require('luatex-cn-vertical-core-textbox') vertical_textflow = require('luatex-cn-vertical-core-textflow') vertical_sidenote = require('luatex-cn-vertical-core-sidenote') vertical = require('luatex-cn-vertical-core-main') } % Define keys for SideNode \ExplSyntaxOn \dim_new:N \l_cnv_sidenode_yoffset_dim \dim_new:N \l_cnv_sidenode_grid_height_dim \tl_new:N \l_cnv_sidenode_font_size_tl \tl_new:N \l_cnv_sidenode_color_tl \dim_new:N \l_cnv_sidenode_padding_top_dim \dim_new:N \l_cnv_sidenode_padding_bottom_dim \keys_define:nn { SideNode } { yoffset .dim_set:N = \l_cnv_sidenode_yoffset_dim, yoffset .initial:n = 0pt, grid-height .dim_set:N = \l_cnv_sidenode_grid_height_dim, font-size .tl_set:N = \l_cnv_sidenode_font_size_tl, font-size .initial:n = 10pt, color .tl_set:N = \l_cnv_sidenode_color_tl, color .initial:n = red, border-padding-top .dim_set:N = \l_cnv_sidenode_padding_top_dim, border-padding-top .initial:n = 0pt, border-padding-bottom .dim_set:N = \l_cnv_sidenode_padding_bottom_dim, border-padding-bottom .initial:n = 0pt, } \NewDocumentCommand{\SideNode}{ O{} +m }{% \group_begin: \keys_set:nn { SideNode } { #1 } % Use explicit font size \tl_if_empty:NTF \l_cnv_sidenode_font_size_tl { \dimen0=\f@size pt } { \dimen0=\l_cnv_sidenode_font_size_tl\relax } \fontsize{\dimen0}{\dimen0}\selectfont % Color must be inside the box to be captured in the node list \setbox0=\hbox{\textcolor{\l_cnv_sidenode_color_tl}{#2}}% % Prepare metadata \dimen2=\l_cnv_sidenode_grid_height_dim % If grid-height not set, default to font size (tight packing) \dim_compare:nF { \dimen2 > 0pt } { \dimen2=\dimen0 } \directlua{vertical_sidenote.register_sidenote(0, { yoffset = \number\l_cnv_sidenode_yoffset_dim, grid_height = \number\dimen2, padding_top = \number\l_cnv_sidenode_padding_top_dim, padding_bottom = \number\l_cnv_sidenode_padding_bottom_dim })}% \group_end: } % PiZhu (批注) - Floating annotation box \tl_new:N \l_cnv_pizhu_font_size_tl \tl_new:N \l_cnv_pizhu_color_tl \tl_new:N \l_cnv_pizhu_grid_width_tl \tl_new:N \l_cnv_pizhu_grid_height_tl \tl_new:N \l_cnv_pizhu_x_tl \tl_new:N \l_cnv_pizhu_y_tl \int_new:N \l_cnv_pizhu_height_int \keys_define:nn { PiZhu } { x .tl_set:N = \l_cnv_pizhu_x_tl, y .tl_set:N = \l_cnv_pizhu_y_tl, height .int_set:N = \l_cnv_pizhu_height_int, font-size .tl_set:N = \l_cnv_pizhu_font_size_tl, font-size .initial:n = {18pt}, color .tl_set:N = \l_cnv_pizhu_color_tl, color .initial:n = {1~0~0}, grid-width .tl_set:N = \l_cnv_pizhu_grid_width_tl, grid-width .initial:n = {20pt}, grid-height .tl_set:N = \l_cnv_pizhu_grid_height_tl, grid-height .initial:n = {19pt}, } \NewDocumentCommand{\PiZhu}{ O{} +m }{% \group_begin: \keys_set:nn { PiZhu } { #1 } \TextBox[ floating=true, x=\l_cnv_pizhu_x_tl, y=\l_cnv_pizhu_y_tl, height=\l_cnv_pizhu_height_int, font-color=\l_cnv_pizhu_color_tl, font-size=\l_cnv_pizhu_font_size_tl, grid-width=\l_cnv_pizhu_grid_width_tl, grid-height=\l_cnv_pizhu_grid_height_tl ]{#2} \group_end: } \ExplSyntaxOff % Use l3keys for key-value parameters \RequirePackage{xparse} \ExplSyntaxOn \tl_new:N \l_cnv_banxin_upper_tl \tl_new:N \l_cnv_banxin_middle_tl \tl_new:N \l_cnv_banxin_lower_tl \bool_new:N \g_cnv_debug_bool \bool_set_false:N \g_cnv_debug_bool \bool_new:N \l__cn_vertical_banxin_bool \bool_set_false:N \l__cn_vertical_banxin_bool \NewDocumentCommand{\cnvdebugon}{}{\bool_set_true:N \g_cnv_debug_bool} \NewDocumentCommand{\cnvdebugoff}{}{\bool_set_false:N \g_cnv_debug_bool} % Define keys \keys_define:nn { vertical } { height .tl_set:N = \l__cn_vertical_height_tl, height .initial:n = , spacing-col .tl_set:N = \l__cn_vertical_col_spacing_tl, spacing-col .initial:n = {}, grid-width .tl_set:N = \l__cn_vertical_grid_width_tl, grid-width .initial:n = {1.5em}, grid-height .tl_set:N = \l__cn_vertical_grid_height_tl, grid-height .initial:n = {1.2em}, cols .int_set:N = \l__cn_vertical_cols_int, cols .initial:n = 0, debug .code:n = { \str_if_eq:nnTF { #1 } { true } { \bool_gset_true:N \g_cnv_debug_bool } { \bool_gset_false:N \g_cnv_debug_bool } }, border .bool_set:N = \l__cn_vertical_border_bool, border .initial:n = false, banxin .bool_set:N = \l__cn_vertical_banxin_bool, banxin .initial:n = false, outer-border .bool_set:N = \l__cn_vertical_outer_border_bool, outer-border .initial:n = false, outer-border-thickness .tl_set:N = \l__cn_vertical_outer_border_thickness_tl, outer-border-thickness .initial:n = {2pt}, outer-border-sep .tl_set:N = \l__cn_vertical_outer_border_sep_tl, outer-border-sep .initial:n = {2pt}, border-padding-top .tl_set:N = \l__cn_vertical_border_padding_top_tl, border-padding-top .initial:n = {0.1em}, border-padding-bottom .tl_set:N = \l__cn_vertical_border_padding_bottom_tl, border-padding-bottom .initial:n = {0.1em}, border-thickness .tl_set:N = \l__cn_vertical_border_thickness_tl, border-thickness .initial:n = {0.4pt}, vertical-align .tl_set:N = \l__cn_vertical_valign_tl, vertical-align .initial:n = {center}, n-column .int_set:N = \l__cn_vertical_n_column_int, n-column .initial:n = 8, page-columns .tl_set:N = \l__cn_vertical_page_columns_tl, page-columns .initial:n = {}, border-color .tl_set:N = \l__cn_vertical_border_color_tl, border-color .initial:n = {black}, background-color .tl_set:N = \l__cn_vertical_background_color_tl, background-color .initial:n = {}, font-color .tl_set:N = \l__cn_vertical_font_color_tl, font-color .initial:n = {}, paper-width .tl_set:N = \l__cn_vertical_paper_width_tl, paper-width .initial:n = {0pt}, paper-height .tl_set:N = \l__cn_vertical_paper_height_tl, paper-height .initial:n = {0pt}, margin-top .tl_set:N = \l__cn_vertical_margin_top_tl, margin-top .initial:n = {0pt}, margin-bottom .tl_set:N = \l__cn_vertical_margin_bottom_tl, margin-bottom .initial:n = {0pt}, margin-left .tl_set:N = \l__cn_vertical_margin_left_tl, margin-left .initial:n = {0pt}, margin-right .tl_set:N = \l__cn_vertical_margin_right_tl, margin-right .initial:n = {0pt}, jiazhu-font-size .tl_set:N = \l__cn_vertical_jiazhu_size_tl, jiazhu-font-size .initial:n = {}, % Jiazhu alignment: outward (default), inward, center, left, right jiazhu-align .tl_set:N = \l__cn_vertical_jiazhu_align_tl, jiazhu-align .initial:n = {outward}, % Banxin (版心) configuration banxin-upper-ratio .tl_set:N = \l_cnv_banxin_upper_tl, banxin-upper-ratio .initial:n = {0.28}, banxin-middle-ratio .tl_set:N = \l_cnv_banxin_middle_tl, banxin-middle-ratio .initial:n = {0.56}, % Book Name (formerly Banxin text) book-name .tl_set:N = \l_cnv_book_name_tl, book-name .initial:n = {}, % Banxin padding (版心独立的间距设置) banxin-padding-top .tl_set:N = \l_cnv_banxin_padding_top_tl, banxin-padding-top .initial:n = {2pt}, banxin-padding-bottom .tl_set:N = \l_cnv_banxin_padding_bottom_tl, banxin-padding-bottom .initial:n = {10pt}, % Lower Yuwei (下鱼尾) - optional decoration lower-yuwei .bool_set:N = \l_cnv_lower_yuwei_bool, lower-yuwei .initial:n = true, % Chapter title (章节标题) - displayed in section 2 of banxin chapter-title .tl_set:N = \l_cnv_chapter_title_tl, chapter-title .initial:n = {}, chapter-title-top-margin .tl_set:N = \l_cnv_chapter_title_top_margin_tl, chapter-title-top-margin .initial:n = {20pt}, chapter-title-cols .int_set:N = \l_cnv_chapter_title_cols_int, chapter-title-cols .initial:n = 1, chapter-title-font-size .tl_set:N = \l_cnv_chapter_title_font_size_tl, chapter-title-font-size .initial:n = {}, chapter-title-grid-height .tl_set:N = \l_cnv_chapter_title_grid_height_tl, chapter-title-grid-height .initial:n = {}, book-name-align .tl_set:N = \l_cnv_book_name_align_tl, book-name-align .initial:n = {center}, book-name-grid-height .tl_set:N = \l_cnv_book_name_grid_height_tl, book-name-grid-height .initial:n = {}, upper-yuwei .bool_set:N = \l_cnv_upper_yuwei_bool, upper-yuwei .initial:n = true, banxin-divider .bool_set:N = \l_cnv_banxin_divider_bool, banxin-divider .initial:n = true, page-number-align .tl_set:N = \l_cnv_page_number_align_tl, page-number-align .initial:n = {right-bottom}, page-number-font-size .tl_set:N = \l_cnv_page_number_font_size_tl, page-number-font-size .initial:n = {15pt}, font-size .tl_set:N = \l__cn_vertical_font_size_tl, font-size .initial:n = {12pt}, } \tl_new:N \l__cn_vertical_border_rgb_tl \tl_new:N \l__cn_vertical_background_rgb_tl \tl_new:N \l__cn_vertical_font_rgb_tl % Internal function to call Lua for Grid Layout \cs_new:Npn \__cn_vertical_process_grid:n #1 { % Put content into a vbox to preserve structure (paragraphs) \setbox0=\vbox{\hsize=\maxdimen \hfuzz=\maxdimen \hbadness=10000 \parindent=0pt #1} % Call Lua to transform the box \setlength{\topskip}{0pt} % Support direct RGB input (e.g. 0.1 0.2 0.3 or 0.1,0.2,0.3 or 100,50,0) \tl_if_in:NnTF \l__cn_vertical_border_color_tl { , } { \tl_set_eq:NN \l__cn_vertical_border_rgb_tl \l__cn_vertical_border_color_tl } { \tl_if_in:NnTF \l__cn_vertical_border_color_tl { ~ } { \tl_set_eq:NN \l__cn_vertical_border_rgb_tl \l__cn_vertical_border_color_tl } { % Try extraction, but wrap in safety \cs_if_exist:cTF { color @ \l__cn_vertical_border_color_tl } { \exp_args:NV \extractcolorspec \l__cn_vertical_border_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl {rgb} \l_tmpb_tl \tl_set:Nx \l__cn_vertical_border_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__cn_vertical_border_rgb_tl \l__cn_vertical_border_color_tl } } } \tl_replace_all:Nnn \l__cn_vertical_border_rgb_tl { , } { ~ } \tl_if_empty:NTF \l__cn_vertical_background_color_tl { \tl_set:Nn \l__cn_vertical_background_rgb_tl { nil } } { \tl_if_in:NnTF \l__cn_vertical_background_color_tl { , } { \tl_set_eq:NN \l__cn_vertical_background_rgb_tl \l__cn_vertical_background_color_tl } { \tl_if_in:NnTF \l__cn_vertical_background_color_tl { ~ } { \tl_set_eq:NN \l__cn_vertical_background_rgb_tl \l__cn_vertical_background_color_tl } { \cs_if_exist:cTF { color @ \l__cn_vertical_background_color_tl } { \exp_args:NV \extractcolorspec \l__cn_vertical_background_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl {rgb} \l_tmpb_tl \tl_set:Nx \l__cn_vertical_background_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__cn_vertical_background_rgb_tl \l__cn_vertical_background_color_tl } } } } \tl_replace_all:Nnn \l__cn_vertical_background_rgb_tl { , } { ~ } \tl_if_empty:NTF \l__cn_vertical_font_color_tl { \tl_set:Nn \l__cn_vertical_font_rgb_tl { nil } } { \tl_if_in:NnTF \l__cn_vertical_font_color_tl { , } { \tl_set_eq:NN \l__cn_vertical_font_rgb_tl \l__cn_vertical_font_color_tl } { \tl_if_in:NnTF \l__cn_vertical_font_color_tl { ~ } { \tl_set_eq:NN \l__cn_vertical_font_rgb_tl \l__cn_vertical_font_color_tl } { \cs_if_exist:cTF { color @ \l__cn_vertical_font_color_tl } { \exp_args:NV \extractcolorspec \l__cn_vertical_font_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl {rgb} \l_tmpb_tl \tl_set:Nx \l__cn_vertical_font_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__cn_vertical_font_rgb_tl \l__cn_vertical_font_color_tl } } } } \tl_replace_all:Nnn \l__cn_vertical_font_rgb_tl { , } { ~ } \typeout{DEBUG: banxin-padding-top is \l_cnv_banxin_padding_top_tl} \directlua{ vertical.process_from_tex(0, { height = [=[\luaescapestring{\l__cn_vertical_height_tl}]=], grid_width = [=[\luaescapestring{\l__cn_vertical_grid_width_tl}]=], grid_height = [=[\luaescapestring{\l__cn_vertical_grid_height_tl}]=], col_limit = \int_use:N \l__cn_vertical_cols_int, debug_on = [=[\bool_if:NTF \g_cnv_debug_bool {true} {false}]=], banxin_on = [=[\bool_if:NTF \l__cn_vertical_banxin_bool {true} {false}]=], border_on = [=[\bool_if:NTF \l__cn_vertical_border_bool {true} {false}]=], border_padding_top = [=[\luaescapestring{\l__cn_vertical_border_padding_top_tl}]=], border_padding_bottom = [=[\luaescapestring{\l__cn_vertical_border_padding_bottom_tl}]=], vertical_align = [=[\luaescapestring{\l__cn_vertical_valign_tl}]=], border_thickness = [=[\luaescapestring{\l__cn_vertical_border_thickness_tl}]=], outer_border_on = [=[\bool_if:NTF \l__cn_vertical_outer_border_bool {true} {false}]=], outer_border_thickness = [=[\luaescapestring{\l__cn_vertical_outer_border_thickness_tl}]=], outer_border_sep = [=[\luaescapestring{\l__cn_vertical_outer_border_sep_tl}]=], n_column = \int_use:N \l__cn_vertical_n_column_int, page_columns = [=[\luaescapestring{\l__cn_vertical_page_columns_tl}]=], border_color = [=[\l__cn_vertical_border_rgb_tl]=], background_color = [=[\l__cn_vertical_background_rgb_tl]=], font_color = [=[\l__cn_vertical_font_rgb_tl]=], paper_width = [=[\luaescapestring{\l__cn_vertical_paper_width_tl}]=], paper_height = [=[\luaescapestring{\l__cn_vertical_paper_height_tl}]=], margin_top = [=[\luaescapestring{\l__cn_vertical_margin_top_tl}]=], margin_bottom = [=[\luaescapestring{\l__cn_vertical_margin_bottom_tl}]=], margin_left = [=[\luaescapestring{\l__cn_vertical_margin_left_tl}]=], margin_right = [=[\luaescapestring{\l__cn_vertical_margin_right_tl}]=], banxin_upper_ratio = [=[\luaescapestring{\l_cnv_banxin_upper_tl}]=], banxin_middle_ratio = [=[\luaescapestring{\l_cnv_banxin_middle_tl}]=], book_name = [=[\luaescapestring{\l_cnv_book_name_tl}]=], banxin_padding_top = [=[\luaescapestring{\l_cnv_banxin_padding_top_tl}]=], banxin_padding_bottom = [=[\luaescapestring{\l_cnv_banxin_padding_bottom_tl}]=], lower_yuwei = [=[\bool_if:NTF \l_cnv_lower_yuwei_bool {true} {false}]=], chapter_title = [=[\luaescapestring{\l_cnv_chapter_title_tl}]=], chapter_title_top_margin = [=[\luaescapestring{\l_cnv_chapter_title_top_margin_tl}]=], chapter_title_cols = \int_use:N \l_cnv_chapter_title_cols_int, chapter_title_font_size = [=[\luaescapestring{\l_cnv_chapter_title_font_size_tl}]=], chapter_title_grid_height = [=[\luaescapestring{\l_cnv_chapter_title_grid_height_tl}]=], book_name_align = [=[\luaescapestring{\l_cnv_book_name_align_tl}]=], book_name_grid_height = [=[\luaescapestring{\l_cnv_book_name_grid_height_tl}]=], upper_yuwei = [=[\bool_if:NTF \l_cnv_upper_yuwei_bool {true} {false}]=], banxin_divider = [=[\bool_if:NTF \l_cnv_banxin_divider_bool {true} {false}]=], page_number_align = [=[\luaescapestring{\l_cnv_page_number_align_tl}]=], page_number_font_size = [=[\luaescapestring{\l_cnv_page_number_font_size_tl}]=], jiazhu_font_size = [=[\luaescapestring{\l__cn_vertical_jiazhu_size_tl}]=], jiazhu_align = [=[\luaescapestring{\l__cn_vertical_jiazhu_align_tl}]=], font_size = [=[\luaescapestring{\l__cn_vertical_font_size_tl}]=] }) } } % Define Main Command with Key-Value interface % Syntax: \VerticalRTT[key=val]{text} \NewDocumentCommand{\VerticalRTT}{ O{} +m } { \par\noindent \group_begin: \keys_set:nn { vertical } { #1 } \__cn_vertical_process_grid:n { #2 } \group_end: } % Alias \NewDocumentCommand{\vertical}{ O{} +m } { \VerticalRTT[#1]{#2} } % Grid Textbox Command (Simplified for Single Column) % Syntax: \GridTextbox[height=N, distribute=true/false]{content} % height is in grid units (integers), width is always 1 % Key-value interface for \GridTextbox \bool_new:N \l__cn_vertical_textbox_border_bool \bool_new:N \l__cn_vertical_textbox_debug_bool \tl_new:N \l__cn_vertical_textbox_box_align_tl \tl_new:N \l__cn_vertical_textbox_inner_gw_tl \tl_new:N \l__cn_vertical_textbox_inner_gh_tl \dim_new:N \l__cn_vertical_textbox_inner_gw_dim \dim_new:N \l__cn_vertical_textbox_inner_gh_dim \bool_new:N \l__cn_vertical_textbox_floating_bool \tl_new:N \l__cn_vertical_textbox_x_tl \tl_new:N \l__cn_vertical_textbox_y_tl \tl_new:N \l__cn_vertical_textbox_bg_color_tl \tl_new:N \l__cn_vertical_textbox_font_color_tl \tl_new:N \l__cn_vertical_textbox_bg_rgb_tl \tl_new:N \l__cn_vertical_textbox_font_rgb_tl \tl_new:N \l__cn_vertical_textbox_font_size_tl \keys_define:nn { vertical / textbox } { height .int_set:N = \l__cn_vertical_textbox_height_int, height .initial:n = 1, n-cols .int_set:N = \l__cn_vertical_textbox_n_cols_int, n-cols .initial:n = 0, inner-grid-width .tl_set:N = \l__cn_vertical_textbox_inner_gw_tl, inner-grid-width .initial:n = {}, inner-grid-height .tl_set:N = \l__cn_vertical_textbox_inner_gh_tl, inner-grid-height .initial:n = {}, distribute .bool_set:N = \l__cn_vertical_textbox_distribute_bool, distribute .initial:n = false, box-align .tl_set:N = \l__cn_vertical_textbox_box_align_tl, box-align .initial:n = {top}, border .bool_set:N = \l__cn_vertical_textbox_border_bool, border .initial:n = false, banxin .bool_set:N = \l__cn_vertical_banxin_bool, column-align .tl_set:N = \l__cn_vertical_textbox_col_align_tl, column-align .initial:n = {}, debug .bool_set:N = \l__cn_vertical_textbox_debug_bool, debug .initial:n = false, vertical-align .tl_set:N = \l__cn_vertical_textbox_box_align_tl, floating .bool_set:N = \l__cn_vertical_textbox_floating_bool, floating .initial:n = false, x .tl_set:N = \l__cn_vertical_textbox_x_tl, x .initial:n = 0pt, y .tl_set:N = \l__cn_vertical_textbox_y_tl, y .initial:n = 0pt, background-color .tl_set:N = \l__cn_vertical_textbox_bg_color_tl, background-color .initial:n = {}, font-color .tl_set:N = \l__cn_vertical_textbox_font_color_tl, font-color .initial:n = {}, font-size .tl_set:N = \l__cn_vertical_textbox_font_size_tl, font-size .initial:n = {}, grid-width .tl_set:N = \l__cn_vertical_textbox_inner_gw_tl, grid-height .tl_set:N = \l__cn_vertical_textbox_inner_gh_tl, } \NewDocumentCommand{\TextBox}{ O{} +m } { \group_begin: % Inherit debug setting from global environment \bool_set_eq:NN \l__cn_vertical_textbox_debug_bool \g_cnv_debug_bool \keys_set:nn { vertical / textbox } { #1 } % Width is always 1 for outer layout (textbox occupies 1 column externally) \setluatexattribute\cnverticaltextboxwidth{1} \setluatexattribute\cnverticaltextboxheight{\l__cn_vertical_textbox_height_int} % Calculate inner grid dimensions (hybrid approach) % If inner-grid-width not specified, auto-calculate: outer_width / n_cols \tl_if_empty:NTF \l__cn_vertical_textbox_inner_gw_tl { \int_compare:nNnTF \l__cn_vertical_textbox_n_cols_int > 0 { \dim_set:Nn \l__cn_vertical_textbox_inner_gw_dim { \l__cn_vertical_grid_width_tl / \l__cn_vertical_textbox_n_cols_int } } { \dim_set:Nn \l__cn_vertical_textbox_inner_gw_dim { \l__cn_vertical_grid_width_tl } } } { \dim_set:Nn \l__cn_vertical_textbox_inner_gw_dim { \l__cn_vertical_textbox_inner_gw_tl } } % If inner-grid-height not specified, user outer grid-height directly (for alignment) \tl_if_empty:NTF \l__cn_vertical_textbox_inner_gh_tl { \dim_set:Nn \l__cn_vertical_textbox_inner_gh_dim { \l__cn_vertical_grid_height_tl } } { \dim_set:Nn \l__cn_vertical_textbox_inner_gh_dim { \l__cn_vertical_textbox_inner_gh_tl } } % Capture content in a box and process it via Lua for inner verticality \setbox0=\vbox{ \setluatexattribute\cnverticaltextboxwidth{0} \setluatexattribute\cnverticaltextboxheight{0} % Reset paragraph spacing to prevent unwanted glues in grid layout \parindent=0pt \parskip=0pt \topskip=0pt % Scale font size to match the inner grid width, but slightly smaller (0.85x) to avoid crowding % Scale font size \tl_if_empty:NTF \l__cn_vertical_textbox_font_size_tl { % Default auto-scaling for multi-column boxes \int_compare:nNnT \l__cn_vertical_textbox_n_cols_int > 1 { \fontsize{\dim_eval:n { 0.85 \l__cn_vertical_textbox_inner_gw_dim }}{\l__cn_vertical_textbox_inner_gw_dim}\selectfont } } { % User-specified font size \fontsize{\l__cn_vertical_textbox_font_size_tl}{\l__cn_vertical_textbox_font_size_tl}\selectfont } % Inner line length should be the grid height of the block \hsize=\dim_eval:n { \l__cn_vertical_textbox_inner_gh_dim * \int_use:N \l__cn_vertical_textbox_height_int } #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__cn_vertical_textbox_bg_color_tl { \l__cn_vertical_textbox_bg_color_tl } \tl_set:No \l__cn_vertical_textbox_font_color_tl { \l__cn_vertical_textbox_font_color_tl } % Convert background color to RGB \tl_if_in:NnTF \l__cn_vertical_textbox_bg_color_tl { , } { \tl_set_eq:NN \l__cn_vertical_textbox_bg_rgb_tl \l__cn_vertical_textbox_bg_color_tl } { \tl_if_in:NnTF \l__cn_vertical_textbox_bg_color_tl { ~ } { \tl_set_eq:NN \l__cn_vertical_textbox_bg_rgb_tl \l__cn_vertical_textbox_bg_color_tl } { \cs_if_exist:cTF { color @ \l__cn_vertical_textbox_bg_color_tl } { \exp_args:NV \extractcolorspec \l__cn_vertical_textbox_bg_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl { rgb } \l_tmpb_tl \tl_set:Nx \l__cn_vertical_textbox_bg_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__cn_vertical_textbox_bg_rgb_tl \l__cn_vertical_textbox_bg_color_tl } } } \tl_replace_all:Nnn \l__cn_vertical_textbox_bg_rgb_tl { , } { ~ } % Convert font color to RGB \tl_if_in:NnTF \l__cn_vertical_textbox_font_color_tl { , } { \tl_set_eq:NN \l__cn_vertical_textbox_font_rgb_tl \l__cn_vertical_textbox_font_color_tl } { \tl_if_in:NnTF \l__cn_vertical_textbox_font_color_tl { ~ } { \tl_set_eq:NN \l__cn_vertical_textbox_font_rgb_tl \l__cn_vertical_textbox_font_color_tl } { \cs_if_exist:cTF { color @ \l__cn_vertical_textbox_font_color_tl } { \exp_args:NV \extractcolorspec \l__cn_vertical_textbox_font_color_tl \l_tmpa_tl \exp_args:NNx \convertcolorspec \l_tmpa_tl { rgb } \l_tmpb_tl \tl_set:Nx \l__cn_vertical_textbox_font_rgb_tl { \l_tmpb_tl } } { \tl_set_eq:NN \l__cn_vertical_textbox_font_rgb_tl \l__cn_vertical_textbox_font_color_tl } } } \tl_replace_all:Nnn \l__cn_vertical_textbox_font_rgb_tl { , } { ~ } \directlua{vertical_textbox.process_inner_box(0, { n_cols = \int_use:N \l__cn_vertical_textbox_n_cols_int, height = \int_use:N \l__cn_vertical_textbox_height_int, grid_width = "\dim_to_decimal:n { \l__cn_vertical_textbox_inner_gw_dim } pt", grid_height = "\dim_to_decimal:n { \l__cn_vertical_textbox_inner_gh_dim } pt", box_align = "\luaescapestring{\tl_use:N \l__cn_vertical_textbox_box_align_tl}", column_aligns = "\luaescapestring{\tl_use:N \l__cn_vertical_textbox_col_align_tl}", debug = "\bool_if:NTF \l__cn_vertical_textbox_debug_bool {true}{false}", border = "\bool_if:NTF \l__cn_vertical_textbox_border_bool {true}{false}", background_color = [=[\l__cn_vertical_textbox_bg_rgb_tl]=], font_color = [=[\l__cn_vertical_textbox_font_rgb_tl]=], font_size = [=[\luaescapestring{\l__cn_vertical_textbox_font_size_tl}]=] })} \setluatexattribute\cnverticaltextboxwidth{0} \setluatexattribute\cnverticaltextboxheight{0} \bool_if:NTF \l__cn_vertical_textbox_floating_bool { \directlua{vertical_textbox.register_floating_box(0, { x = [=[\luaescapestring{\l__cn_vertical_textbox_x_tl}]=], y = [=[\luaescapestring{\l__cn_vertical_textbox_y_tl}]=] })} } { \leavevmode\box0 } \group_end: } % TextFlow Command (双行小注/夹注) % Syntax: \TextFlow{content} \makeatletter \NewDocumentCommand{\TextFlow}{ +m } { \group_begin: % Get jiazhu font size: default to 70% of current font size, user can override with explicit size (must include unit like "14pt") % Expand the tl first to check if it's really blank \tl_set:Nx \l_tmpb_tl { \l__cn_vertical_jiazhu_size_tl } \tl_if_empty:NTF \l_tmpb_tl { % Default: 0.7 * current font size \tl_set:Nx \l_tmpa_tl { \fp_eval:n { 0.7 * \f@size } pt } } { % User specified size (must include unit like "14pt") \tl_set_eq:NN \l_tmpa_tl \l_tmpb_tl } % Capture attribute values using edef to avoid register limitations \edef\savedindent{\the\cnverticalindent} \edef\savedblockid{\the\cnverticalblockid} \edef\savedfirst{\the\cnverticalfirstindent} \edef\savedright{\the\cnverticalrightindent} \fontsize{\l_tmpa_tl}{\l_tmpa_tl}\selectfont % CRITICAL: Set jiazhu attribute AFTER selectfont \setluatexattribute\cnverticaljiazhu{1} % Restore \cnverticalindent = \savedindent\relax \cnverticalblockid = \savedblockid\relax \cnverticalfirstindent = \savedfirst\relax \cnverticalrightindent = \savedright\relax #1 \group_end: } \makeatother % Space Command - inserts a space that occupies grid cells in vertical layout % In CJK text, normal ASCII spaces are often absorbed; use this for explicit spacing % Syntax: \Space or \Space[width] % Default width is 1em (one grid cell) \NewDocumentCommand{\Space}{ O{1em} } { \regex_match:nnTF { ^\d+$ } { #1 } { \hspace{\dim_eval:n { \l__cn_vertical_grid_height_tl * #1 }} } { \hspace{#1} } } % Paragraph Environment % Supports block indentation with first-line/hanging indent differentiation % Syntax: \begin{Paragraph}[indent=2, first-indent=0, bottom-indent=1] \newluatexattribute\cnverticalblockid \newluatexattribute\cnverticalfirstindent \newcounter{cnverticalblockid} \setcounter{cnverticalblockid}{0} \keys_define:nn { vertical / paragraph } { indent .int_set:N = \l__cn_vertical_para_indent_int, indent .initial:n = 0, first-indent .int_set:N = \l__cn_vertical_para_first_indent_int, first-indent .initial:n = -1, % -1 means use indent value bottom-indent .int_set:N = \l__cn_vertical_para_bottom_indent_int, bottom-indent .initial:n = 0, } \NewDocumentEnvironment{Paragraph}{ O{} } { \par \stepcounter{cnverticalblockid} \keys_set:nn { vertical / paragraph } { #1 } % Set Block ID \setluatexattribute\cnverticalblockid{\value{cnverticalblockid}} % Set Base Indent (also serves as Hanging Indent) \setluatexattribute\cnverticalindent{\l__cn_vertical_para_indent_int} % Set First Indent \int_compare:nNnTF \l__cn_vertical_para_first_indent_int = {-1} { \setluatexattribute\cnverticalfirstindent{\l__cn_vertical_para_indent_int} } { \setluatexattribute\cnverticalfirstindent{\l__cn_vertical_para_first_indent_int} } % Set Bottom Indent \setluatexattribute\cnverticalrightindent{\l__cn_vertical_para_bottom_indent_int} } { % Reset indent attributes to default BEFORE \par % so subsequent content won't inherit paragraph's indent \setluatexattribute\cnverticalindent{0} \setluatexattribute\cnverticalfirstindent{-1} \setluatexattribute\cnverticalrightindent{0} \par } \ExplSyntaxOff % ============================================================================ % Command Aliases (Defined outside ExplSyntax for CJK support) % ============================================================================ \NewCommandCopy{\竖排}{\VerticalRTT} \NewCommandCopy{\文本框}{\TextBox} \NewDocumentCommand{\填充文本框}{ O{1} +m } {% \TextBox[height=#1, box-align=fill]{#2}% } \NewCommandCopy{\文本流}{\TextFlow} \NewCommandCopy{\空格}{\Space} \NewCommandCopy{\段落}{\Paragraph} \NewCommandCopy{\CePi}{\SideNode} \NewCommandCopy{\侧批}{\SideNode} \NewCommandCopy{\批注}{\PiZhu} \directlua{ _G.vertical.debug.verbose_log = true _G.vertical.debug.enabled = false } \endinput