-- 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. -- ============================================================================ -- banxin_main.lua - 版心模块独立入口(注册钩子) -- ============================================================================ -- 文件名: banxin_main.lua -- 层级: 扩展层 (Extension Layer) - 古籍版心功能 -- -- 【模块功能 / Module Purpose】 -- 本模块作为 banxin 包的独立入口,负责: -- 1. 向 vertical.hooks 系统注册版心相关回调 -- 2. 管理版心配置映射 -- 3. 协调 banxin.render_banxin 和 banxin.render_yuwei 模块 -- -- 【设计原理】 -- banxin 作为一个可选插件,通过覆盖 vertical.hooks 接口实现其功能。 -- -- ============================================================================ -- Ensure core namespace exists (should be loaded by now) _G.core = _G.core or {} _G.core.hooks = _G.core.hooks or {} -- Create banxin namespace for our modules _G.banxin = _G.banxin or {} _G.banxin.enabled = _G.banxin.enabled or false --- Setup global banxin parameters from TeX -- Called by \banxinSetup to pre-set banxin state -- @param params (table) Parameters from TeX keyvals local function banxin_setup(params) params = params or {} if params.enabled ~= nil then _G.banxin.enabled = (params.enabled == true or params.enabled == "true") end end -- Export setup function for early access _G.banxin.setup = banxin_setup --- Store pre-rendered upper section box from TeX -- Called by \__luatexcn_banxin_typeset_sections: in banxin.sty -- @param box_num (number) TeX box register number local function store_upper_section_box(box_num) local box = tex.box[box_num] if box then _G.banxin.upper_section_box = node.copy_list(box) else _G.banxin.upper_section_box = nil end end --- Store pre-rendered middle section box from TeX -- @param box_num (number) TeX box register number local function store_middle_section_box(box_num) local box = tex.box[box_num] if box then _G.banxin.middle_section_box = node.copy_list(box) else _G.banxin.middle_section_box = nil end end --- Store pre-rendered lower section box from TeX -- @param box_num (number) TeX box register number local function store_lower_section_box(box_num) local box = tex.box[box_num] if box then _G.banxin.lower_section_box = node.copy_list(box) else _G.banxin.lower_section_box = nil end end local utils = package.loaded['util.luatex-cn-utils'] or require('util.luatex-cn-utils') local debug = package.loaded['debug.luatex-cn-debug'] or require('debug.luatex-cn-debug') local style_registry = package.loaded['util.luatex-cn-style-registry'] or require('util.luatex-cn-style-registry') local D = node.direct local dbg = debug.get_debugger('banxin') -- ============================================================================= -- Direct TeX Variable Reading -- ============================================================================= -- Read banxin parameters directly from TeX LaTeX3 variables -- This eliminates the need to pass them through the params table --- Read all banxin-related TeX variables -- @return table A table containing all banxin configuration values local function read_banxin_params() local get_tl = utils.get_tex_tl local get_bool = utils.get_tex_bool local get_int = utils.get_tex_int local parse_dim = utils.parse_dim_to_sp return { -- Layout ratios upper_ratio = tonumber(get_tl("l__luatexcn_banxin_upper_ratio_tl")) or 0.28, middle_ratio = tonumber(get_tl("l__luatexcn_banxin_middle_ratio_tl")) or 0.56, -- Padding (from upper-section and page-number sub-namespaces) padding_top = parse_dim(get_tl("l__luatexcn_banxin_upper_section_top_padding_tl")), padding_bottom = parse_dim(get_tl("l__luatexcn_banxin_page_number_bottom_padding_tl")), -- Divider divider = get_bool("l__luatexcn_banxin_divider_bool"), -- Upper section upper_section_text = get_tl("l__luatexcn_banxin_upper_section_text_tl") or "", upper_section_font_size = parse_dim(get_tl("l__luatexcn_banxin_upper_section_font_size_tl")), upper_section_grid_height = parse_dim(get_tl("l__luatexcn_banxin_upper_section_grid_height_tl")), upper_section_align = get_tl("l__luatexcn_banxin_upper_section_align_tl") or "center", upper_section_bottom_padding = parse_dim(get_tl("l__luatexcn_banxin_upper_section_bottom_padding_tl")), upper_section_bg_color = get_tl("l__luatexcn_banxin_upper_section_bg_color_tl") or "", upper_section_font_color = get_tl("l__luatexcn_banxin_upper_section_font_color_tl") or "", -- Middle section middle_section_text = get_tl("l__luatexcn_banxin_middle_section_text_tl") or "", middle_section_top_margin = parse_dim(get_tl("l__luatexcn_banxin_middle_section_top_margin_tl")), middle_section_cols = get_int("l__luatexcn_banxin_middle_section_cols_int"), middle_section_font_size = parse_dim(get_tl("l__luatexcn_banxin_middle_section_font_size_tl")), middle_section_grid_height = parse_dim(get_tl("l__luatexcn_banxin_middle_section_grid_height_tl")), middle_section_align = get_tl("l__luatexcn_banxin_middle_section_align_tl") or "center", middle_section_bg_color = get_tl("l__luatexcn_banxin_middle_section_bg_color_tl") or "", middle_section_font_color = get_tl("l__luatexcn_banxin_middle_section_font_color_tl") or "", -- Yuwei upper_yuwei = get_bool("l__luatexcn_banxin_upper_yuwei_bool"), lower_yuwei = get_bool("l__luatexcn_banxin_lower_yuwei_bool"), -- Page number page_number_show = get_bool("l__luatexcn_banxin_page_number_show_bool"), page_number_align = get_tl("l__luatexcn_banxin_page_number_align_tl") or "right-bottom", page_number_font_size = parse_dim(get_tl("l__luatexcn_banxin_page_number_font_size_tl")), page_number_grid_height = parse_dim(get_tl("l__luatexcn_banxin_page_number_grid_height_tl")), -- Lower section lower_section_text = get_tl("l__luatexcn_banxin_lower_section_text_tl") or "", lower_section_font_size = parse_dim(get_tl("l__luatexcn_banxin_lower_section_font_size_tl")), lower_section_grid_height = parse_dim(get_tl("l__luatexcn_banxin_lower_section_grid_height_tl")), lower_section_bottom_margin = parse_dim(get_tl("l__luatexcn_banxin_lower_section_bottom_margin_tl")), lower_section_align = get_tl("l__luatexcn_banxin_lower_section_align_tl") or "right", lower_section_bg_color = get_tl("l__luatexcn_banxin_lower_section_bg_color_tl") or "", lower_section_font_color = get_tl("l__luatexcn_banxin_lower_section_font_color_tl") or "", -- Banxin-level style overrides style_font_size = get_tl("l__luatexcn_banxin_style_font_size_tl") or "", style_font_color = get_tl("l__luatexcn_banxin_style_font_color_tl") or "", style_font = get_tl("l__luatexcn_banxin_style_font_tl") or "", style_grid_height = get_tl("l__luatexcn_banxin_style_grid_height_tl") or "", style_grid_width = get_tl("l__luatexcn_banxin_style_grid_width_tl") or "", } end -- 1. Load sub-modules using full namespaced paths local render_banxin = package.loaded['banxin.luatex-cn-banxin-render-banxin'] or require('banxin.luatex-cn-banxin-render-banxin') local banxin_layout = package.loaded['banxin.luatex-cn-banxin-layout'] or require('banxin.luatex-cn-banxin-layout') -- 2. Export module table for TeX/other modules local banxin_main = {} --- Initialize Banxin Plugin -- Captures banxin parameters early for use in layout and render stages function banxin_main.initialize(params, engine_ctx) -- Read banxin_on directly from TeX variable local banxin_on = utils.get_tex_bool("l__luatexcn_banxin_on_bool") -- Banxin is active if banxin_on is true or implicitly via n_column local is_active = banxin_on or (tonumber(params.n_column) or 0) > 0 if not is_active then dbg.log("Initialized. Active=false") return { active = false } end -- Capture banxin parameters early (moved from render stage) local bp = read_banxin_params() dbg.log(string.format("Initialized. Active=true, upper_section='%s'", bp.upper_section_text or "")) -- Build banxin style overrides for push/pop in layout/render local banxin_style = {} if bp.style_font_size ~= "" then banxin_style.font_size = constants.to_dimen(bp.style_font_size) end if bp.style_font_color ~= "" then banxin_style.font_color = bp.style_font_color end if bp.style_font ~= "" then banxin_style.font = bp.style_font end if bp.style_grid_height ~= "" then banxin_style.grid_height = constants.to_dimen(bp.style_grid_height) end if bp.style_grid_width ~= "" then banxin_style.grid_width = constants.to_dimen(bp.style_grid_width) end return { active = true, params = bp, -- Store captured parameters layout_cache = {}, -- Will store per-page layout data banxin_style = next(banxin_style) and banxin_style or nil, } end --- Flatten hook (Not used by banxin) function banxin_main.flatten(nodes, engine_ctx, context) return nodes end --- Layout hook for Banxin -- Calculates layout data for all banxin columns across all pages -- and stores in context.layout_cache for the render stage function banxin_main.layout(list, layout_map, engine_ctx, context) if not (context and context.active) then return end if engine_ctx.n_column <= 0 then return end local total_pages = engine_ctx.total_pages or 0 local p_cols = engine_ctx.page_columns or 0 local bp = context.params dbg.log(string.format("Layout hook: calculating for %d pages, %d cols/page", total_pages, p_cols)) -- Push banxin style overrides onto style stack (temporary layer for layout) if context.banxin_style then style_registry.push(context.banxin_style) end -- Calculate layout for each page's reserved columns for page_idx = 0, total_pages - 1 do local reserved_cols = engine_ctx.get_reserved_cols(page_idx, p_cols) context.layout_cache[page_idx] = {} for col = 0, p_cols - 1 do if reserved_cols[col] then local coords = engine_ctx.get_reserved_column_coords(col, p_cols) -- Prepare layout params from captured context params local c_padding_top = bp.padding_top > 0 and bp.padding_top or engine_ctx.c_padding_top local c_padding_bottom = bp.padding_bottom > 0 and bp.padding_bottom or engine_ctx.c_padding_bottom local layout_params = { x = coords.x, y = coords.y, width = coords.width, height = coords.height, border_thickness = engine_ctx.border_thickness, color_str = engine_ctx.border_rgb_str, draw_border = engine_ctx.draw_border, upper_ratio = bp.upper_ratio, middle_ratio = bp.middle_ratio, upper_section_text = bp.upper_section_text, upper_section_font_size = bp.upper_section_font_size > 0 and bp.upper_section_font_size or nil, upper_section_grid_height = bp.upper_section_grid_height > 0 and bp.upper_section_grid_height or nil, upper_section_align = bp.upper_section_align, upper_section_bottom_padding = bp.upper_section_bottom_padding > 0 and bp.upper_section_bottom_padding or nil, upper_section_bg_color = bp.upper_section_bg_color, upper_section_font_color = bp.upper_section_font_color, c_padding_top = c_padding_top, c_padding_bottom = c_padding_bottom, upper_yuwei = bp.upper_yuwei, lower_yuwei = bp.lower_yuwei, banxin_divider = bp.divider, middle_section_text = bp.middle_section_text, middle_section_top_margin = bp.middle_section_top_margin > 0 and bp.middle_section_top_margin or (65536 * 20), middle_section_cols = bp.middle_section_cols > 0 and bp.middle_section_cols or 1, middle_section_font_size = bp.middle_section_font_size > 0 and bp.middle_section_font_size or nil, middle_section_grid_height = bp.middle_section_grid_height > 0 and bp.middle_section_grid_height or nil, middle_section_align = bp.middle_section_align, middle_section_bg_color = bp.middle_section_bg_color, middle_section_font_color = bp.middle_section_font_color, page_number_show = bp.page_number_show, page_number_align = bp.page_number_align, page_number_font_size = bp.page_number_font_size > 0 and bp.page_number_font_size or nil, lower_section_text = bp.lower_section_text, lower_section_font_size = bp.lower_section_font_size > 0 and bp.lower_section_font_size or nil, lower_section_grid_height = bp.lower_section_grid_height > 0 and bp.lower_section_grid_height or nil, lower_section_bottom_margin = bp.lower_section_bottom_margin > 0 and bp.lower_section_bottom_margin or nil, lower_section_align = bp.lower_section_align, lower_section_bg_color = bp.lower_section_bg_color, lower_section_font_color = bp.lower_section_font_color, -- Get font_size from style stack (falls back to default if not set) font_size = style_registry.get_font_size(style_registry.current_id()) or 655360, } -- Calculate layout using the layout module local col_layout = banxin_layout.calculate_column_layout(layout_params) context.layout_cache[page_idx][col] = col_layout dbg.log(string.format(" Page %d, col %d: layout calculated", page_idx, col)) end end end -- Pop banxin style layer (restore style stack) if context.banxin_style then style_registry.pop() end dbg.log(string.format("Layout hook completed: cached %d pages", total_pages)) end --- Render hook for Banxin -- Uses pre-calculated layout from layout stage and resolves runtime content -- @param head (node) Page list head -- @param layout_map (table) Main layout map -- @param params (table) Render parameters -- @param context (table) Plugin context (contains layout_cache) -- @param page_idx (number) Current page index (0-based) -- @param p_total_cols (number) Total columns on this page function banxin_main.render(head, layout_map, params, context, engine_ctx, page_idx, _) if not (context and context.active) then return head end if engine_ctx.n_column <= 0 then return head end local page_layout = context.layout_cache and context.layout_cache[page_idx] if not page_layout then return head end local bp = context.params -- Resolve runtime content: middle section text (may vary per page) -- Only update when \chapter markers exist; walk back to find most recent marker. local middle_section_text = bp.middle_section_text local page_resets = engine_ctx.page_resets if page_resets and next(page_resets) and engine_ctx.page_chapter_titles then for p = page_idx, 0, -1 do if page_resets[p] then local t = engine_ctx.page_chapter_titles[p] if t and t ~= "" then middle_section_text = t end break end end end -- Resolve runtime content: page number -- Support explicit page number string (for digital mode) -- When \chapter resets page number, compute relative page number from last reset. local page_number if page_resets and next(page_resets) then local last_reset_page = -1 for p = page_idx, 0, -1 do if page_resets[p] then last_reset_page = p break end end if last_reset_page >= 0 then page_number = 1 + (page_idx - last_reset_page) else page_number = (params.start_page_number or 1) + page_idx end else page_number = (params.start_page_number or 1) + page_idx end local explicit_page_number = _G.banxin and _G.banxin.explicit_page_number or nil local p_head = D.todirect(head) -- Push banxin style layer for render stage if context.banxin_style then style_registry.push(context.banxin_style) end for _, col_layout in pairs(page_layout) do p_head = render_banxin.draw_from_layout(p_head, col_layout, { middle_section_text = middle_section_text, page_number = page_number, explicit_page_number = explicit_page_number, }) end -- Pop banxin style layer if context.banxin_style then style_registry.pop() end return D.tonode(p_head) end local banxin_plugin = { initialize = banxin_main.initialize, flatten = banxin_main.flatten, layout = banxin_main.layout, render = banxin_main.render, setup = banxin_setup, store_upper_section_box = store_upper_section_box, store_middle_section_box = store_middle_section_box, store_lower_section_box = store_lower_section_box, } -- Registry as plugin if core engine is present if core and core.register_plugin then core.register_plugin("banxin", banxin_plugin) end package.loaded['banxin.luatex-cn-banxin-main'] = banxin_plugin return banxin_plugin