% 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. \ProvidesExplPackage{core/luatex-cn-core-page}{2026/02/18} {0.3.0}{Page geometry and background support} % Global token lists for page parameters (centralized) \tl_new:N \l__luatexcn_page_paper_width_tl \tl_new:N \l__luatexcn_page_paper_height_tl \tl_new:N \l__luatexcn_page_margin_top_tl \tl_new:N \l__luatexcn_page_margin_bottom_tl \tl_new:N \l__luatexcn_page_margin_left_tl \tl_new:N \l__luatexcn_page_margin_right_tl \tl_new:N \l__luatexcn_page_background_color_tl \tl_new:N \l__luatexcn_page_footskip_tl \tl_new:N \l__luatexcn_page_topskip_tl \bool_new:N \l__luatexcn_page_split_enabled_bool \bool_new:N \l__luatexcn_page_split_right_first_bool \keys_define:nn { luatexcn / page } { paper-width .tl_set:N = \l__luatexcn_page_paper_width_tl, paper-width .initial:n = {210mm}, paper-height .tl_set:N = \l__luatexcn_page_paper_height_tl, paper-height .initial:n = {297mm}, background-color .tl_set:N = \l__luatexcn_page_background_color_tl, background-color .initial:n = {}, margin-top .tl_set:N = \l__luatexcn_page_margin_top_tl, margin-top .initial:n = {0pt}, margin-bottom .tl_set:N = \l__luatexcn_page_margin_bottom_tl, margin-bottom .initial:n = {0pt}, margin-left .tl_set:N = \l__luatexcn_page_margin_left_tl, margin-left .initial:n = {0pt}, margin-right .tl_set:N = \l__luatexcn_page_margin_right_tl, margin-right .initial:n = {0pt}, footskip .tl_set:N = \l__luatexcn_page_footskip_tl, footskip .initial:n = {0pt}, topskip .tl_set:N = \l__luatexcn_page_topskip_tl, topskip .initial:n = {0pt}, % Split page (筒子页) configuration page-split-enabled .bool_set:N = \l__luatexcn_page_split_enabled_bool, page-split-enabled .initial:n = true, page-split-right-first .bool_set:N = \l__luatexcn_page_split_right_first_bool, page-split-right-first .initial:n = true, unknown .code:n = {}, } \NewDocumentCommand{\pageSetup}{ m } { \keys_set:nn { luatexcn / page } { #1 } \lua_now:e { local~page~=~require('core.luatex-cn-core-page') page.setup({ paper_width~=~"\l__luatexcn_page_paper_width_tl", paper_height~=~"\l__luatexcn_page_paper_height_tl", margin_top~=~"\l__luatexcn_page_margin_top_tl", margin_bottom~=~"\l__luatexcn_page_margin_bottom_tl", margin_left~=~"\l__luatexcn_page_margin_left_tl", margin_right~=~"\l__luatexcn_page_margin_right_tl" }) _G.page~=~_G.page~or~{} _G.page.number_style~=~"\l__luatexcn_page_number_style_tl" _G.page.number_font_size~=~"\l__luatexcn_page_number_font_size_tl" } % Apply geometry when called in preamble (sets paper size) \luatexcn_apply_geometry: % Set topskip (only safe in \pageSetup, not in per-page \luatexcn_apply_geometry: % where it can trigger spurious blank pages via shipout hook interactions) \dim_set:Nn \topskip { \l__luatexcn_page_topskip_tl } % Apply split page settings if enabled \__luatexcn_page_apply_split: } % Flag to track if geometry has been initialized (margins set to 0) \bool_new:N \g__luatexcn_geometry_initialized_bool \cs_new:Npn \luatexcn_apply_geometry: { % IMPORTANT: We set ALL geometry margins to 0, and handle margins manually in Lua. % This simplifies coordinate calculations - (0,0) is now at paper edge. % The actual margin values are stored in token lists and passed to Lua for positioning. \if_meaning:w \@onlypreamble \@notprerr % In document body \bool_if:NF \g__luatexcn_geometry_initialized_bool { % First time - need to set margins to 0 via \newgeometry \newgeometry{ top=0pt, bottom=0pt, left=0pt, right=0pt, footskip=0pt } \bool_gset_true:N \g__luatexcn_geometry_initialized_bool } \else % In preamble \geometry{ paperwidth=\l__luatexcn_page_paper_width_tl, paperheight=\l__luatexcn_page_paper_height_tl, top=0pt, bottom=0pt, left=0pt, right=0pt, footskip=0pt } \bool_gset_true:N \g__luatexcn_geometry_initialized_bool \fi % Set paper/page dimensions (common for all branches) % Must set both \paperwidth and \pagewidth for TikZ overlays (e.g. seal stamps) \dim_set:Nn \paperwidth { \l__luatexcn_page_paper_width_tl } \dim_set:Nn \paperheight { \l__luatexcn_page_paper_height_tl } \dim_set:Nn \pagewidth { \l__luatexcn_page_paper_width_tl } \dim_set:Nn \pageheight { \l__luatexcn_page_paper_height_tl } % NOTE: \topskip is intentionally NOT set here. Setting \topskip in the % document body (after \clearpage) interacts with shipout hooks and can % cause spurious blank pages. \topskip is set once in \pageSetup (preamble) % and in \__luatexcn_content_zero_skips: (scoped to Content groups). } % ============================================================================ % Page Environment % Applies new page layout settings for content within the environment % Usage: \begin{page}[paper-width=21cm, paper-height=29.7cm, margin-top=1cm, ...] % Accepts all parameters from luatexcn/page namespace. % After the environment ends, page settings are restored to previous values. % ============================================================================ \RequirePackage{environ} % Helper to sync Lua page state back to TeX token lists \cs_new:Npn \__luatexcn_page_sync_from_lua: { \tl_set:Nx \l__luatexcn_page_paper_width_tl { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_dim_str('paper_width')) } } \tl_set:Nx \l__luatexcn_page_paper_height_tl { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_dim_str('paper_height')) } } \tl_set:Nx \l__luatexcn_page_margin_top_tl { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_dim_str('margin_top')) } } \tl_set:Nx \l__luatexcn_page_margin_bottom_tl { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_dim_str('margin_bottom')) } } \tl_set:Nx \l__luatexcn_page_margin_left_tl { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_dim_str('margin_left')) } } \tl_set:Nx \l__luatexcn_page_margin_right_tl { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_dim_str('margin_right')) } } \str_if_eq:eeTF { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_split_str('enabled')) } } { true } { \bool_set_true:N \l__luatexcn_page_split_enabled_bool } { \bool_set_false:N \l__luatexcn_page_split_enabled_bool } \str_if_eq:eeTF { \lua_now:n { tex.sprint(require('core.luatex-cn-core-page').get_split_str('right_first')) } } { true } { \bool_set_true:N \l__luatexcn_page_split_right_first_bool } { \bool_set_false:N \l__luatexcn_page_split_right_first_bool } } % Helper: globally set paper/page dimensions after restore inside \NewEnviron. % \NewEnviron creates a group; local assignments from split.enable/disable are % undone when the group ends. This must be called inside the \NewEnviron body % AFTER \__luatexcn_page_sync_from_lua: so that the tl variables reflect the % restored state. It respects split mode: when split is enabled, \paperwidth % and \pagewidth are set to half the paper width. \cs_new:Npn \__luatexcn_page_restore_dims_globally: { \bool_if:NTF \l__luatexcn_page_split_enabled_bool { \dim_gset:Nn \paperwidth { \l__luatexcn_page_paper_width_tl / 2 } \dim_gset:Nn \paperheight { \l__luatexcn_page_paper_height_tl } \dim_gset:Nn \pagewidth { \l__luatexcn_page_paper_width_tl / 2 } \dim_gset:Nn \pageheight { \l__luatexcn_page_paper_height_tl } } { \dim_gset:Nn \paperwidth { \l__luatexcn_page_paper_width_tl } \dim_gset:Nn \paperheight { \l__luatexcn_page_paper_height_tl } \dim_gset:Nn \pagewidth { \l__luatexcn_page_paper_width_tl } \dim_gset:Nn \pageheight { \l__luatexcn_page_paper_height_tl } } } \NewEnviron{Page}[1][]{% \clearpage % Save current settings in Lua (includes split state) \lua_now:n { require('core.luatex-cn-core-page').save() } % Apply new settings \keys_set:nn { luatexcn / page } { #1 } % Sync page dimensions to Lua (must be before split enable/disable) \lua_now:e { local~page~=~require('core.luatex-cn-core-page');~ page.setup({ paper_width~=~"\l__luatexcn_page_paper_width_tl", paper_height~=~"\l__luatexcn_page_paper_height_tl", margin_top~=~"\l__luatexcn_page_margin_top_tl", margin_bottom~=~"\l__luatexcn_page_margin_bottom_tl", margin_left~=~"\l__luatexcn_page_margin_left_tl", margin_right~=~"\l__luatexcn_page_margin_right_tl" });~ } \luatexcn_apply_geometry: % Sync split page settings to Lua and apply \__luatexcn_page_split_sync: \bool_if:NTF \l__luatexcn_page_split_enabled_bool { \lua_now:n { require('core.luatex-cn-core-page').split.enable() } } { \lua_now:n { require('core.luatex-cn-core-page').split.disable() } } \BODY \clearpage % Restore previous settings from Lua (includes split state) % Note: page.restore() calls split.enable/disable which sets pagewidth/pageheight directly \lua_now:n { require('core.luatex-cn-core-page').restore() } % Sync restored Lua state back to TeX token lists (for subsequent code that reads them) \__luatexcn_page_sync_from_lua: % Globally restore paper/page dimensions so they survive \NewEnviron group end \__luatexcn_page_restore_dims_globally: } % (CJK alias for Page moved to end of file) % ============================================================================ % Page Split (筒子页) - splits each page into two half-pages % State is stored in _G.page.split (Lua global) for cross-module communication % ============================================================================ % Sync TeX keys to Lua global state \cs_new:Npn \__luatexcn_page_split_sync: { \lua_now:e { _G.page.split.enabled = \bool_if:NTF \l__luatexcn_page_split_enabled_bool {true} {false}; _G.page.split.right_first = \bool_if:NTF \l__luatexcn_page_split_right_first_bool {true} {false}; } } % Enable split page processing \NewDocumentCommand{\enableSplitPage}{ O{} } { \tl_if_empty:nF { #1 } { \keys_set:nn { luatexcn / page } { #1 } } \__luatexcn_page_split_sync: \lua_now:n { require('core.luatex-cn-core-page').split.enable() } } % Disable split page processing \NewDocumentCommand{\disableSplitPage}{ } { \lua_now:n { require('core.luatex-cn-core-page').split.disable() } } % (CJK aliases moved to end of file) % Helper: apply split page if enabled \cs_new:Npn \__luatexcn_page_apply_split: { \bool_if:NT \l__luatexcn_page_split_enabled_bool { \enableSplitPage } } % ============================================================================ % Page Number Style % ============================================================================ % Supported styles: % digits - 逐位中文数字 (915 → 九一五) % chinese - 传统中文数字 (915 → 九百十五) % arabic - 阿拉伯数字 (915) % none - 不显示页码 \tl_new:N \l__luatexcn_page_number_style_tl \tl_new:N \l__luatexcn_page_number_font_size_tl \keys_define:nn { luatexcn / page } { page-number-style .tl_set:N = \l__luatexcn_page_number_style_tl, page-number-style .initial:n = {none}, page-number-font-size .tl_set:N = \l__luatexcn_page_number_font_size_tl, page-number-font-size .initial:n = {9pt}, } % DrawVerticalPageNumber: defined outside ExplSyntax to avoid catcode issues % in shipout hooks. Reads style from _G.page.number_style (synced by \pageSetup). \ExplSyntaxOff \newcommand{\DrawVerticalPageNumber}{% \directlua{ local style = _G.page and _G.page.number_style or "none" if style ~= "none" and style ~= "" then local utils = require('util.luatex-cn-utils') local pg = tex.count[0] local s if style == "digits" then s = utils.to_chinese_digits(pg) elseif style == "chinese" then s = utils.to_chinese_numeral(pg) elseif style == "arabic" then s = tostring(pg) else return end local parts = {} if style == "arabic" then for i = 1, string.len(s) do table.insert(parts, string.sub(s, i, i)) end else for p, c in utf8.codes(s) do table.insert(parts, utf8.char(c)) end end tex.sprint(table.concat(parts, "\\\\\\noexpand\\relax ")) end }% } \ExplSyntaxOn % ============================================================================ % Page Number Commands % ============================================================================ % Command to reset page number to 1 \NewDocumentCommand{\ResetPageNumber}{ } { \lua_now:e { core.reset_page_number(); } } % Command to set page number to arbitrary value \NewDocumentCommand{\SetPageNumber}{ m } { \lua_now:e { core.set_page_number(#1); } } % ============================================================================ % Override page break commands for grid content % ============================================================================ \cs_new_protected:Npn \__luatexcn_page_override_pagebreak: { \RenewDocumentCommand{\newpage}{}{\penalty-10003 } \RenewDocumentCommand{\clearpage}{}{\penalty-10003 } \cs_set:Npn \chapter ##1 { \directlua { core.insert_chapter_marker( "\luaescapestring{ \tl_to_str:n { ##1 } }" ) } \box0\relax } } % ============================================================ % Chinese aliases / 中文别名 % ============================================================ % Simplified Chinese / 简体 \NewEnvironmentCopy{页}{Page} \NewCommandCopy{\页面设置}{\pageSetup} \NewCommandCopy{\开启分页}{\enableSplitPage} \NewCommandCopy{\关闭分页}{\disableSplitPage} \NewCommandCopy{\设置页码}{\SetPageNumber} \NewCommandCopy{\重置页码}{\ResetPageNumber} % Traditional Chinese / 繁体 \NewCommandCopy{\頁面設置}{\pageSetup} \NewCommandCopy{\開啟分頁}{\enableSplitPage} \NewCommandCopy{\關閉分頁}{\disableSplitPage} \NewCommandCopy{\設置頁碼}{\SetPageNumber} \NewCommandCopy{\重置頁碼}{\ResetPageNumber} % ============================================================ % Chinese key aliases / 中文 Key 别名 % ============================================================ \keys_define:nn { luatexcn / page } { % 简体 纸宽 .tl_set:N = \l__luatexcn_page_paper_width_tl, 纸高 .tl_set:N = \l__luatexcn_page_paper_height_tl, 底色 .tl_set:N = \l__luatexcn_page_background_color_tl, 上边距 .tl_set:N = \l__luatexcn_page_margin_top_tl, 下边距 .tl_set:N = \l__luatexcn_page_margin_bottom_tl, 左边距 .tl_set:N = \l__luatexcn_page_margin_left_tl, 右边距 .tl_set:N = \l__luatexcn_page_margin_right_tl, 页脚距 .tl_set:N = \l__luatexcn_page_footskip_tl, 页头距 .tl_set:N = \l__luatexcn_page_topskip_tl, 开启分页 .bool_set:N = \l__luatexcn_page_split_enabled_bool, 右页优先 .bool_set:N = \l__luatexcn_page_split_right_first_bool, 页码样式 .tl_set:N = \l__luatexcn_page_number_style_tl, 页码字号 .tl_set:N = \l__luatexcn_page_number_font_size_tl, % 繁体(与简体不同形的) 紙寬 .tl_set:N = \l__luatexcn_page_paper_width_tl, 紙高 .tl_set:N = \l__luatexcn_page_paper_height_tl, 上邊距 .tl_set:N = \l__luatexcn_page_margin_top_tl, 下邊距 .tl_set:N = \l__luatexcn_page_margin_bottom_tl, 左邊距 .tl_set:N = \l__luatexcn_page_margin_left_tl, 右邊距 .tl_set:N = \l__luatexcn_page_margin_right_tl, 頁腳距 .tl_set:N = \l__luatexcn_page_footskip_tl, 頁頭距 .tl_set:N = \l__luatexcn_page_topskip_tl, 開啟分頁 .bool_set:N = \l__luatexcn_page_split_enabled_bool, 右頁優先 .bool_set:N = \l__luatexcn_page_split_right_first_bool, 頁碼樣式 .tl_set:N = \l__luatexcn_page_number_style_tl, 頁碼字號 .tl_set:N = \l__luatexcn_page_number_font_size_tl, } \endinput