% 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-core-table.sty % Inline table environment for vertical typesetting. % Tables are embedded within \begin{正文}...\end{正文} (BodyText). % Provides \begin{Table} / \begin{表格} environment and \Cell / \单元格 command. \NeedsTeXFormat{LaTeX2e} \ProvidesExplPackage{core/luatex-cn-core-table}{2026/08/07} {0.4.1} {Inline table environment for luatex-cn vertical typesetting} \ExplSyntaxOn % ============================================================================ % Table Parameters % ============================================================================ \int_new:N \l__luatexcn_table_n_bands_int \tl_new:N \l__luatexcn_table_band_gap_tl \tl_new:N \l__luatexcn_table_band_heights_tl \tl_new:N \l__luatexcn_table_n_cols_tl \int_new:N \l__luatexcn_table_header_rows_int \tl_new:N \l__luatexcn_cell_col_width_tl \tl_new:N \l__luatexcn_cell_valign_tl \tl_new:N \l__luatexcn_cell_column_border_tl \tl_new:N \l__luatexcn_table_column_border_tl \tl_new:N \l__luatexcn_table_band_border_tl \tl_new:N \l__luatexcn_table_column_fill_tl % Map Chinese column-fill values to English \cs_new_protected:Nn \__luatexcn_table_set_column_fill:n { \str_case:nnF {#1} { {页} { \tl_set:Nn \l__luatexcn_table_column_fill_tl { page } } {頁} { \tl_set:Nn \l__luatexcn_table_column_fill_tl { page } } {半页} { \tl_set:Nn \l__luatexcn_table_column_fill_tl { half-page } } {半頁} { \tl_set:Nn \l__luatexcn_table_column_fill_tl { half-page } } } { \tl_set:Nn \l__luatexcn_table_column_fill_tl {#1} } } \keys_define:nn { luatexcn / table } { n-bands .int_set:N = \l__luatexcn_table_n_bands_int, n-bands .initial:n = 2, band-gap .tl_set:N = \l__luatexcn_table_band_gap_tl, band-gap .initial:n = {0pt}, band-heights .tl_set:N = \l__luatexcn_table_band_heights_tl, band-heights .initial:n = {}, n-columns .tl_set:N = \l__luatexcn_table_n_cols_tl, n-columns .initial:n = {}, header-rows .int_set:N = \l__luatexcn_table_header_rows_int, header-rows .initial:n = 0, column-border .tl_set:N = \l__luatexcn_table_column_border_tl, column-border .initial:n = {}, band-border .tl_set:N = \l__luatexcn_table_band_border_tl, band-border .initial:n = {}, column-fill .code:n = { \__luatexcn_table_set_column_fill:n {#1} }, column-fill .initial:n = {}, % Forward unknown keys: try settings first, then style unknown .code:n = { \exp_args:Nnx \keys_if_exist:nnTF { luatexcn / settings } { \l_keys_key_str } { \keys_set:nx { luatexcn / settings } { \l_keys_key_str = { \exp_not:n {#1} } } } { \exp_args:Nnx \keys_if_exist:nnTF { luatexcn / style } { \l_keys_key_str } { \keys_set:nx { luatexcn / style } { \l_keys_key_str = { \exp_not:n {#1} } } } { } } }, } \keys_define:nn { luatexcn / cell } { col-width .tl_set:N = \l__luatexcn_cell_col_width_tl, col-width .initial:n = {1}, vertical-align .tl_set:N = \l__luatexcn_cell_valign_tl, vertical-align .initial:n = {}, column-border .tl_set:N = \l__luatexcn_cell_column_border_tl, column-border .initial:n = {}, % Forward unknown keys: try settings first, then style unknown .code:n = { \exp_args:Nnx \keys_if_exist:nnTF { luatexcn / settings } { \l_keys_key_str } { \keys_set:nx { luatexcn / settings } { \l_keys_key_str = { \exp_not:n {#1} } } } { \exp_args:Nnx \keys_if_exist:nnTF { luatexcn / style } { \l_keys_key_str } { \keys_set:nx { luatexcn / style } { \l_keys_key_str = { \exp_not:n {#1} } } } { } } }, } % ============================================================================ % Internal: build band_heights Lua table from comma-separated dimensions % ============================================================================ \tl_new:N \l__luatexcn_table_band_heights_lua_tl \tl_new:N \l__luatexcn_table_n_cols_lua_tl \cs_new_protected:Nn \__luatexcn_table_build_params: { % Convert band-heights comma list to Lua table: {h1_sp, h2_sp, ...} \tl_if_empty:NTF \l__luatexcn_table_band_heights_tl { \tl_set:Nn \l__luatexcn_table_band_heights_lua_tl { nil } } { \tl_set:Nn \l__luatexcn_table_band_heights_lua_tl { \c_left_brace_str } \bool_set_true:N \l_tmpa_bool \exp_args:NV \clist_map_function:nN \l__luatexcn_table_band_heights_tl \__luatexcn_table_heights_item:n \tl_put_right:Nn \l__luatexcn_table_band_heights_lua_tl { \c_right_brace_str } } % Convert n-columns to Lua number or nil \tl_if_empty:NTF \l__luatexcn_table_n_cols_tl { \tl_set:Nn \l__luatexcn_table_n_cols_lua_tl { nil } } { \tl_set_eq:NN \l__luatexcn_table_n_cols_lua_tl \l__luatexcn_table_n_cols_tl } } \cs_new_protected:Nn \__luatexcn_table_heights_item:n { \bool_if:NTF \l_tmpa_bool { \bool_set_false:N \l_tmpa_bool } { \tl_put_right:Nn \l__luatexcn_table_band_heights_lua_tl { ,~ } } \tl_put_right:Nx \l__luatexcn_table_band_heights_lua_tl { \dim_to_decimal_in_sp:n { #1 } } } % ============================================================================ % Table Environment (inline within BodyText) % ============================================================================ % Tables emit PENALTY_TABLE_START at begin and PENALTY_TABLE_END at end. % The layout engine dynamically switches to band mode for the table section. \NewDocumentEnvironment{Table}{ O{} } { % Initialize style variables before key parsing (unknown keys forward to style) \__luatexcn_style_init_and_parse:n {} \keys_set:nn { luatexcn / table } { #1 } \__luatexcn_table_build_params: \lua_now:e { require('core.luatex-cn-core-table').init({ n_bands~=~\int_use:N \l__luatexcn_table_n_bands_int, band_gap_sp~=~\dim_to_decimal_in_sp:n { \l__luatexcn_table_band_gap_tl }, band_heights~=~\l__luatexcn_table_band_heights_lua_tl, n_columns~=~\l__luatexcn_table_n_cols_lua_tl, column_border~=~\tl_if_empty:NTF \l__luatexcn_table_column_border_tl {nil} {\l__luatexcn_table_column_border_tl}, band_border~=~\tl_if_empty:NTF \l__luatexcn_table_band_border_tl {nil} {\l__luatexcn_table_band_border_tl}, column_fill~=~\tl_if_empty:NTF \l__luatexcn_table_column_fill_tl {nil} {"\l__luatexcn_table_column_fill_tl"}, }) } % Apply style if any style keys were forwarded \__luatexcn_style_maybe_apply_and_push: } { \__luatexcn_style_maybe_pop: \lua_now:n { require('core.luatex-cn-core-table').cleanup() } } % ============================================================================ % Cell Command % ============================================================================ \NewDocumentCommand{\Cell}{ O{} +g } { \tl_set:Nn \l__luatexcn_cell_col_width_tl {1} \tl_clear:N \l__luatexcn_cell_valign_tl \tl_clear:N \l__luatexcn_cell_column_border_tl % Initialize style variables before key parsing (unknown keys forward to style) \__luatexcn_style_init_and_parse:n {} \keys_set:nn { luatexcn / cell } { #1 } \lua_now:e { require('core.luatex-cn-core-table').begin_cell( \str_if_eq:VnTF \l__luatexcn_cell_col_width_tl {nil} {0} {\l__luatexcn_cell_col_width_tl}, \tl_if_empty:NTF \l__luatexcn_cell_valign_tl {nil} {"\l__luatexcn_cell_valign_tl"}, \tl_if_empty:NTF \l__luatexcn_cell_column_border_tl {nil} {\l__luatexcn_cell_column_border_tl} ) } \luatexcnRestoreTempIndent % Apply style if any style keys were forwarded, scoped to cell content \IfValueTF{#2} { \group_begin: \__luatexcn_style_maybe_apply_and_push: #2 \__luatexcn_style_maybe_pop: \group_end: } { \__luatexcn_style_maybe_apply_and_push: \__luatexcn_style_maybe_pop: } } % ============================================================================ % Band Format Command % ============================================================================ % \BandFormat / \栏格式 sets per-band formatting options. % Must be used at the beginning of a band (before any \Cell). \tl_new:N \l__luatexcn_bandformat_column_border_tl \tl_new:N \l__luatexcn_bandformat_padding_top_tl \tl_new:N \l__luatexcn_bandformat_padding_bottom_tl \keys_define:nn { luatexcn / band-format } { column-border .tl_set:N = \l__luatexcn_bandformat_column_border_tl, column-border .initial:n = {}, padding-top .tl_set:N = \l__luatexcn_bandformat_padding_top_tl, padding-top .initial:n = {}, padding-bottom .tl_set:N = \l__luatexcn_bandformat_padding_bottom_tl, padding-bottom .initial:n = {}, padding .meta:n = { padding-top = #1, padding-bottom = #1 }, % Forward unknown keys: try settings first, then style unknown .code:n = { \exp_args:Nnx \keys_if_exist:nnTF { luatexcn / settings } { \l_keys_key_str } { \keys_set:nx { luatexcn / settings } { \l_keys_key_str = { \exp_not:n {#1} } } } { \exp_args:Nnx \keys_if_exist:nnTF { luatexcn / style } { \l_keys_key_str } { \keys_set:nx { luatexcn / style } { \l_keys_key_str = { \exp_not:n {#1} } } } { } } }, } \NewDocumentCommand{\BandFormat}{ O{} } { \tl_clear:N \l__luatexcn_bandformat_column_border_tl \tl_clear:N \l__luatexcn_bandformat_padding_top_tl \tl_clear:N \l__luatexcn_bandformat_padding_bottom_tl % Initialize style variables before key parsing (unknown keys forward to style) \__luatexcn_style_init_and_parse:n {} \keys_set:nn { luatexcn / band-format } { #1 } \lua_now:e { require('core.luatex-cn-core-table').set_band_format({ column_border~=~\tl_if_empty:NTF \l__luatexcn_bandformat_column_border_tl {nil} {\l__luatexcn_bandformat_column_border_tl}, padding_top~=~\tl_if_empty:NTF \l__luatexcn_bandformat_padding_top_tl {nil} {[=[\luaescapestring{\l__luatexcn_bandformat_padding_top_tl}]=]}, padding_bottom~=~\tl_if_empty:NTF \l__luatexcn_bandformat_padding_bottom_tl {nil} {[=[\luaescapestring{\l__luatexcn_bandformat_padding_bottom_tl}]=]}, }) } % Apply style if any style keys were forwarded \__luatexcn_style_maybe_apply_and_push: } % ============================================================================ % Chinese Aliases % ============================================================================ \NewEnvironmentCopy{表格}{Table} \NewCommandCopy{\单元格}{\Cell} \NewCommandCopy{\單元格}{\Cell} \NewCommandCopy{\栏格式}{\BandFormat} \NewCommandCopy{\欄格式}{\BandFormat} \keys_define:nn { luatexcn / band-format } { 行边框 .tl_set:N = \l__luatexcn_bandformat_column_border_tl, 行邊框 .tl_set:N = \l__luatexcn_bandformat_column_border_tl, 上填充 .tl_set:N = \l__luatexcn_bandformat_padding_top_tl, 下填充 .tl_set:N = \l__luatexcn_bandformat_padding_bottom_tl, 填充 .meta:n = { padding-top = #1, padding-bottom = #1 }, } \keys_define:nn { luatexcn / table } { 分栏 .int_set:N = \l__luatexcn_table_n_bands_int, 栏间距 .tl_set:N = \l__luatexcn_table_band_gap_tl, 栏高度 .tl_set:N = \l__luatexcn_table_band_heights_tl, 行数 .tl_set:N = \l__luatexcn_table_n_cols_tl, 表头栏数 .int_set:N = \l__luatexcn_table_header_rows_int, 行边框 .tl_set:N = \l__luatexcn_table_column_border_tl, 栏边框 .tl_set:N = \l__luatexcn_table_band_border_tl, 分欄 .int_set:N = \l__luatexcn_table_n_bands_int, 欄間距 .tl_set:N = \l__luatexcn_table_band_gap_tl, 欄高度 .tl_set:N = \l__luatexcn_table_band_heights_tl, 行數 .tl_set:N = \l__luatexcn_table_n_cols_tl, 表頭欄數 .int_set:N = \l__luatexcn_table_header_rows_int, 行邊框 .tl_set:N = \l__luatexcn_table_column_border_tl, 欄邊框 .tl_set:N = \l__luatexcn_table_band_border_tl, 行填充 .code:n = { \__luatexcn_table_set_column_fill:n {#1} }, 行填滿 .code:n = { \__luatexcn_table_set_column_fill:n {#1} }, } \keys_define:nn { luatexcn / cell } { 行宽 .tl_set:N = \l__luatexcn_cell_col_width_tl, 行寬 .tl_set:N = \l__luatexcn_cell_col_width_tl, 纵对齐 .tl_set:N = \l__luatexcn_cell_valign_tl, 縱對齊 .tl_set:N = \l__luatexcn_cell_valign_tl, 行边框 .tl_set:N = \l__luatexcn_cell_column_border_tl, 行邊框 .tl_set:N = \l__luatexcn_cell_column_border_tl, } \ExplSyntaxOff \endinput