-- 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.
-- ============================================================================
-- render_page_process.lua - 节点处理子模块（从 render_page.lua 拆分）
-- ============================================================================
-- 文件名: luatex-cn-core-render-page-process.lua
-- 层级: 第三阶段 - 渲染层 (Stage 3: Render Layer)
--
-- 【模块功能 / Module Purpose】
-- 本模块负责单个页面中各节点的坐标赋值和渲染处理：
--   1. handle_glyph_node: 字形节点定位（xoffset/yoffset, 旋转, 缩放）
--   2. handle_block_node: 块级节点定位（kern + shift）
--   3. handle_debug_drawing: 调试网格绘制
--   4. process_page_nodes: 遍历页面所有节点并分发处理
--
-- ============================================================================

-- Load dependencies
local constants = package.loaded['core.luatex-cn-constants'] or
    require('core.luatex-cn-constants')
local D = constants.D
local utils = package.loaded['util.luatex-cn-utils'] or
    require('util.luatex-cn-utils')
local text_position = package.loaded['core.luatex-cn-render-position'] or
    require('luatex-cn-render-position')
local decorate_mod = package.loaded['decorate.luatex-cn-decorate'] or
    require('decorate.luatex-cn-decorate')
local debug = package.loaded['debug.luatex-cn-debug'] or
    require('debug.luatex-cn-debug')
local helpers = package.loaded['core.luatex-cn-layout-grid-helpers'] or
    require('core.luatex-cn-layout-grid-helpers')
local style_registry = package.loaded['util.luatex-cn-style-registry'] or
    require('util.luatex-cn-style-registry')
local setting_stack = package.loaded['util.luatex-cn-setting-stack'] or
    require('util.luatex-cn-setting-stack')

local dbg = debug.get_debugger('render')

-- ============================================================================
-- Node Handling Functions
-- ============================================================================

-- Reusable template tables for calc_grid_position (created once per page in process_page_nodes)
-- glyph_dims: per-glyph dimensions (width, height, depth, char, font)
-- glyph_params: page-constant fields pre-filled, per-glyph fields overwritten each call
local glyph_dims = {}
local glyph_params = {}

-- 辅助函数：处理单个字形的定位
local function handle_glyph_node(curr, p_head, pos, params, ctx)
    -- vertical_align now comes from ctx (read from _G.content or params in calculate_render_context)
    local vertical_align = ctx.vertical_align or "center"
    local d = D.getfield(curr, "depth") or 0
    local h = D.getfield(curr, "height") or 0
    local w = D.getfield(curr, "width") or 0

    local v_scale = pos.v_scale or 1.0
    -- Per-glyph horizontal scale: squeeze when char is wider than border available width
    local h_scale = 1.0
    local avail_w = ctx.border_avail_width or 0
    if avail_w > 0 and w > avail_w then
        h_scale = avail_w / w
    end

    -- column_aligns is textbox-specific, still comes from params.visual
    local h_align = "center"
    local visual = params.visual
    if visual and visual.column_aligns and visual.column_aligns[pos.col] then
        h_align = visual.column_aligns[pos.col]
    end

    -- Per-glyph h_align override (e.g. footnote markers use right-align)
    local halign_attr = D.get_attribute(curr, constants.ATTR_HALIGN)
    if halign_attr and halign_attr > 0 then
        if halign_attr == 1 then h_align = "left"
        elseif halign_attr == 3 then h_align = "right"
        end
    end

    -- Fill per-glyph fields into reusable templates (page-constant fields set in process_page_nodes)
    glyph_dims.width = w
    glyph_dims.height = h * v_scale
    glyph_dims.depth = d * v_scale
    glyph_dims.char = D.getfield(curr, "char")
    glyph_dims.font = D.getfield(curr, "font")

    -- P1: read style fields from style_registry, not layout_map
    local glyph_style_id = D.get_attribute(curr, constants.ATTR_STYLE_REG_ID)
    local glyph_style = glyph_style_id and style_registry.get(glyph_style_id)

    glyph_params.v_align = vertical_align
    glyph_params.h_align = h_align
    glyph_params.sub_col = pos.sub_col
    glyph_params.textflow_align = (glyph_style and glyph_style.textflow_align) or ctx.textflow_align
    glyph_params.cell_height = pos.cell_height
    glyph_params.cell_width = pos.cell_width
    glyph_params.y_sp = pos.y_sp
    glyph_params.band_y_offset_sp = pos.band_y_offset_sp or 0
    -- RTL pos.x from layout_map (nil triggers legacy fallback in calc_grid_position)
    glyph_params.pos_x = pos.x
    -- Per-entry col_width from layout_map (set for variable-width columns: Free Mode, TitlePage)
    glyph_params.pos_col_width = pos.col_width
    -- Per-entry content_width override (sum of col_widths for variable-width pages)
    if pos.content_width then
        glyph_params.content_width = pos.content_width
    end

    local final_x, final_y = text_position.calc_grid_position(pos.col, glyph_dims, glyph_params)
    if glyph_style then
        if glyph_style.xshift then
            local xs = constants.resolve_dimen(glyph_style.xshift, ctx.body_font_size or 655360)
            if xs then final_x = final_x - xs end
        end
        if glyph_style.yshift then
            local ys = constants.resolve_dimen(glyph_style.yshift, ctx.body_font_size or 655360)
            if ys then final_y = final_y - ys end
        end
    end

    -- Check if glyph needs vertical rotation (font lacks vertical form)
    local needs_rotate = D.get_attribute(curr, constants.ATTR_VERT_ROTATE) == 1

    if needs_rotate then
        -- Rotate 90° CW and translate glyph to its grid position.
        -- The glyph is at text-space origin (xoffset=yoffset=0).
        -- Mode 0 pdf_literal wraps with T(node)/T(-node), so our matrix
        -- operates in node-relative space.
        -- We need matrix M = [0 -1 1 0 e f] such that glyph center
        -- (gc_x, gc_y) maps to intended center (fx+gc_x, fy+gc_y):
        --   e = fx + gc_x - gc_y
        --   f = fy + gc_x + gc_y
        local sp2bp = utils.sp_to_bp
        local fx = final_x * sp2bp
        local fy = final_y * sp2bp
        local gc_x = (w / 2) * sp2bp           -- glyph center x (from reference point)
        local gc_y = ((h - d) / 2) * sp2bp     -- glyph center y (from reference point)

        D.setfield(curr, "xoffset", 0)
        D.setfield(curr, "yoffset", 0)

        local e = fx + gc_x - gc_y
        local f = fy + gc_x + gc_y
        local literal_str = string.format(
            "q 0 -1 1 0 %.4f %.4f cm", e, f
        )
        local n_start = utils.create_pdf_literal(literal_str)
        local n_end = utils.create_pdf_literal(utils.create_graphics_state_end())

        p_head = D.insert_before(p_head, curr, n_start)
        D.insert_after(p_head, curr, n_end)
    elseif v_scale == 1.0 and h_scale == 1.0 then
        D.setfield(curr, "xoffset", final_x)
        D.setfield(curr, "yoffset", final_y)
    else
        -- Squeeze using PDF matrix: [h_scale 0 0 v_scale x y]
        -- When h_scale < 1, shift x to center the squeezed glyph within its cell
        local h_center_offset = (h_scale < 1.0) and (w * (1.0 - h_scale) / 2) or 0
        local x_bp = (final_x + h_center_offset) * utils.sp_to_bp
        local y_bp = final_y * utils.sp_to_bp
        D.setfield(curr, "xoffset", 0)
        D.setfield(curr, "yoffset", 0)

        local literal_str = string.format("q %.4f 0 0 %.4f %.4f %.4f cm", h_scale, v_scale, x_bp, y_bp)
        local n_start = utils.create_pdf_literal(literal_str)
        local n_end = utils.create_pdf_literal(utils.create_graphics_state_end())

        p_head = D.insert_before(p_head, curr, n_start)
        D.insert_after(p_head, curr, n_end)
    end

    -- Insert negative kern to keep baseline position correct for next nodes
    local k = D.new(constants.KERN)
    D.setfield(k, "kern", -w)
    D.insert_after(p_head, curr, k)

    -- Apply font_color from style_registry (P1: read style directly, not from layout_map)
    -- Skip wrapping when font_color matches the page-level default (avoids redundant
    -- q/Q pairs that introduce ET/cm/BT coordinate transforms and float precision loss)
    local style_id = D.get_attribute(curr, constants.ATTR_STYLE_REG_ID)
    local font_color = style_id and style_registry.get_font_color(style_id)
    if font_color and font_color ~= "" then
        local rgb_str = utils.normalize_rgb(font_color)
        -- Only wrap if color differs from page default (both must be non-nil and different)
        if rgb_str and ctx.text_rgb_str and rgb_str ~= ctx.text_rgb_str then
            -- Different from page default: wrap with q/Q for color isolation
            local color_cmd = utils.create_color_literal(rgb_str, false) -- false = fill color (rg)
            local color_push = utils.create_pdf_literal("q " .. color_cmd)
            local color_pop = utils.create_pdf_literal("Q")
            p_head = D.insert_before(p_head, curr, color_push)
            D.insert_after(p_head, k, color_pop) -- Insert after the kern
        elseif not ctx.text_rgb_str and rgb_str then
            -- No page-level color but char has color: must wrap
            local color_cmd = utils.create_color_literal(rgb_str, false)
            local color_push = utils.create_pdf_literal("q " .. color_cmd)
            local color_pop = utils.create_pdf_literal("Q")
            p_head = D.insert_before(p_head, curr, color_push)
            D.insert_after(p_head, k, color_pop)
        end
        -- When rgb_str == ctx.text_rgb_str: page-level rg already set, no wrapping needed
    end

    return p_head
end

-- 辅助函数：处理 HLIST/VLIST（块）的定位
local function handle_block_node(curr, p_head, pos, ctx)
    local h = D.getfield(curr, "height") or 0
    local w = D.getfield(curr, "width") or 0

    local col_width = text_position.get_column_width(pos.col, ctx.col_geom)
    local block_width_sp = (pos.width or 1) * col_width

    local final_x
    if pos.x and (glyph_params.content_width or 0) > 0 then
        -- RTL pos.x path: pos.x is right edge of block, block extends left by block_width_sp
        local shift_x_base = glyph_params.shift_x_base or ctx.shift_x
        final_x = shift_x_base + glyph_params.content_width - pos.x - block_width_sp + ctx.half_thickness
    else
        -- Legacy RTL path
        local rtl_col_left = ctx.p_total_cols - (pos.col + (pos.width or 1))
        final_x = text_position.get_column_x(rtl_col_left, ctx.col_geom)
            + ctx.half_thickness + ctx.shift_x
    end

    -- Sub-column support: textbox inside textflow uses sub-column positioning
    local sub_col = pos.sub_col
    if sub_col and sub_col > 0 then
        -- Recompute base_x using column width (not block_width_sp) to match glyph path
        local base_x
        if pos.x and (glyph_params.content_width or 0) > 0 then
            local shift_x_base = glyph_params.shift_x_base or ctx.shift_x
            base_x = shift_x_base + glyph_params.content_width - pos.x - col_width + ctx.half_thickness
        else
            local rtl_col = ctx.p_total_cols - 1 - pos.col
            base_x = text_position.get_column_x(rtl_col, ctx.col_geom)
                + ctx.half_thickness + ctx.shift_x
        end
        -- Verified via node inspection: textbox vbox has margin_left = 0,
        -- i.e. the inner content (char) sits at vbox.left. So the textbox
        -- position needs to match calculate_sub_column_x_offset's return
        -- value directly (which is where small chars are placed).
        local half_width = col_width / 2
        local inner_padding = half_width * 0.05  -- matches calculate_sub_column_x_offset
        local tb_w_attr = D.get_attribute(curr, constants.ATTR_TEXTBOX_WIDTH)
        local tb_grid_w = D.get_attribute(curr, constants.ATTR_TEXTBOX_GRID_WIDTH)
        local inner_grid_total = (tb_w_attr and tb_w_attr > 0 and tb_grid_w and tb_grid_w > 0)
            and (tb_w_attr * tb_grid_w) or w
        if sub_col == 1 then
            -- sub_col=1 + inward = left align in sub-col: vbox.left at column_center + inner_padding
            final_x = base_x + half_width + inner_padding
        else
            -- sub_col=2 + inward = right align in sub-col: content.right at column_center - inner_padding
            -- content.right = vbox.left + inner_grid_total (since margin_left=0)
            final_x = base_x + half_width - inner_grid_total - inner_padding
        end
    else
        -- Center TextBox grid area within outer column.
        local tb_w_attr = D.get_attribute(curr, constants.ATTR_TEXTBOX_WIDTH)
        if tb_w_attr and tb_w_attr > 0 then
            local tb_content_width = block_width_sp
            local tb_grid_w = D.get_attribute(curr, constants.ATTR_TEXTBOX_GRID_WIDTH)
            if tb_grid_w and tb_grid_w > 0 and tb_grid_w < col_width then
                local inner_grid_total = tb_w_attr * tb_grid_w
                local centering = math.floor((tb_content_width - inner_grid_total) / 2)
                final_x = final_x + centering
            elseif w < tb_content_width then
                local centering = math.floor((tb_content_width - w) / 2)
                final_x = final_x + centering
            end
        end
    end

    -- Apply xshift from sidenote metadata (positive = rightward in page coordinates)
    if pos.xshift and pos.xshift ~= 0 then
        final_x = final_x + pos.xshift
    end

    local final_y_top = -pos.y_sp - (pos.band_y_offset_sp or 0) - ctx.shift_y
    D.setfield(curr, "shift", -final_y_top + h)

    local k_pre = D.new(constants.KERN)
    D.setfield(k_pre, "kern", final_x)

    local k_post = D.new(constants.KERN)
    D.setfield(k_post, "kern", -(final_x + w))

    p_head = D.insert_before(p_head, curr, k_pre)
    D.insert_after(p_head, curr, k_post)
    return p_head
end

-- 辅助函数：绘制调试网格/框
-- 通过 setting_stack 中的 debug 标志控制
local function handle_debug_drawing(curr, p_head, pos, ctx)
    local show_me = setting_stack.get("debug")

    local color_str = pos.is_block and "1 0 0 RG" or "0 0 1 RG"

    if show_me then
        local col_widths = glyph_params.col_widths
        local tx_sp, tw_sp
        -- X: compute column left edge (LTR) and width
        if glyph_params.content_width and glyph_params.content_width > 0 and pos.x then
            -- RTL pos.x path: convert to LTR
            local shift_x_base = glyph_params.shift_x_base or ctx.shift_x
            if col_widths and #col_widths > 0 then
                tw_sp = text_position.get_column_width_var(pos.col, col_widths)
            else
                tw_sp = text_position.get_column_width(pos.col, ctx.col_geom)
            end
            tx_sp = shift_x_base + glyph_params.content_width - pos.x - tw_sp + (ctx.half_thickness or 0)
            if col_widths and #col_widths > 0 then
                local sp_bottom = glyph_params.col_spacing_bottom and glyph_params.col_spacing_bottom[pos.col + 1] or 0
                local sp_top = glyph_params.col_spacing_top and glyph_params.col_spacing_top[pos.col + 1] or 0
                if sp_bottom > 0 or sp_top > 0 then
                    tx_sp = tx_sp + sp_bottom
                    tw_sp = tw_sp - sp_bottom - sp_top
                end
            end
        elseif col_widths and #col_widths > 0 then
            -- Legacy variable-width columns
            local total_cols = ctx.p_total_cols
            local rtl_col = total_cols - 1 - pos.col
            tx_sp = text_position.get_column_x_var(rtl_col, col_widths, total_cols)
                + (ctx.half_thickness or 0) + (ctx.shift_x or 0)
            tw_sp = text_position.get_column_width_var(pos.col, col_widths)
            local sp_bottom = glyph_params.col_spacing_bottom and glyph_params.col_spacing_bottom[pos.col + 1] or 0
            local sp_top = glyph_params.col_spacing_top and glyph_params.col_spacing_top[pos.col + 1] or 0
            if sp_bottom > 0 or sp_top > 0 then
                tx_sp = tx_sp + sp_bottom
                tw_sp = tw_sp - sp_bottom - sp_top
            end
        else
            -- Legacy uniform-width columns
            _, tx_sp = text_position.calculate_rtl_position(pos.col, ctx.p_total_cols, ctx.col_geom,
                ctx.half_thickness, ctx.shift_x)
            tw_sp = text_position.get_column_width(pos.col, ctx.col_geom)
        end
        -- P2: pos.y = y_sp + band_y_offset_sp + shift_y (Y has no p_total_cols issue)
        local ty_sp = -(pos.y)
        local th_sp = -(pos.cell_height or ctx.grid_height)

        if pos.sub_col and pos.sub_col > 0 then
            tw_sp = ctx.col_geom.grid_width / 2
            if pos.sub_col == 1 then
                tx_sp = tx_sp + tw_sp
            end
        end

        if pos.is_block then
            tw_sp = pos.width * ctx.col_geom.grid_width
            th_sp = -pos.height * ctx.grid_height
        end
        return utils.draw_debug_rect(p_head, curr, tx_sp, ty_sp, tw_sp, th_sp, color_str)
    end
    return p_head
end

-- 辅助函数：处理单个页面的所有节点
local function process_page_nodes(p_head, layout_map, params, ctx)
    local curr = p_head
    -- Initialize last_font_id with current fallback font
    ctx.last_font_id = ctx.last_font_id or params.font_id or font.current()
    -- Initialize line mark collection for this page
    ctx.line_mark_entries = ctx.line_mark_entries or {}

    -- Initialize page-constant fields in glyph_params template (per-glyph fields set in handle_glyph_node)
    glyph_params.total_cols = ctx.p_total_cols
    glyph_params.shift_x = ctx.shift_x
    glyph_params.shift_y = ctx.shift_y
    glyph_params.half_thickness = ctx.half_thickness
    glyph_params.col_geom = ctx.col_geom
    glyph_params.body_font_size = ctx.body_font_size
    -- P2: col_widths from col_widths_sp (Free Mode / TitlePage \行[width=...])
    -- TextBox has its own grid_width; never use outer content's col_widths
    if ctx.is_textbox then
        glyph_params.col_widths = nil
    else
        glyph_params.col_widths = ctx.page_col_widths_sp
    end
    -- RTL pos.x support: content_width and shift_x_base for coordinate conversion.
    -- All pages (uniform, Free Mode, TitlePage) use pos_x path.
    glyph_params.content_width = ctx.content_width
    glyph_params.shift_x_base = ctx.shift_x_base
    -- Column spacing for glyph offset within columns that have spacing
    glyph_params.col_spacing_top = ctx.page_col_spacing_top_sp
    glyph_params.col_spacing_bottom = ctx.page_col_spacing_bottom_sp

    while curr do
        local next_curr = D.getnext(curr)
        local id = D.getid(curr)

        if id == constants.GLYPH or id == constants.HLIST or id == constants.VLIST then
            local pos = layout_map[curr]
            if pos then
                if not pos.col or pos.col < 0 then
                    dbg.log(string.format("  [render] SKIP Node=%s ID=%d (invalid col=%s)", tostring(curr),
                        id, tostring(pos.col)))
                else
                    if id == constants.GLYPH then
                        local dec_id = D.get_attribute(curr, constants.ATTR_DECORATE_ID)
                        if dec_id and dec_id > 0 then
                            -- Dispatch by decoration type
                            local dec_reg = _G.decorate_registry and _G.decorate_registry[dec_id]
                            if dec_reg and dec_reg.type == "side_text" then
                                p_head = decorate_mod.handle_side_text_node(curr, p_head, pos, params, ctx, dec_id)
                            else
                                p_head = decorate_mod.handle_node(curr, p_head, pos, params, ctx, dec_id)
                            end
                            -- Remove the original marker node to prevent ghost rendering at (0,0)
                            p_head = D.remove(p_head, curr)
                            node.flush_node(D.tonode(curr))
                        else
                            -- Track the font from regular glyphs for decoration fallback
                            ctx.last_font_id = D.getfield(curr, "font")
                            p_head = handle_glyph_node(curr, p_head, pos, params, ctx)
                            p_head = handle_debug_drawing(curr, p_head, pos, ctx)
                            -- Collect line mark entries for batch rendering
                            if pos.line_mark_id then
                                -- Always use glyph's actual positioned center
                                -- (handles Free Mode, variable-width columns, sub-columns, etc.)
                                local gw = D.getfield(curr, "width") or 0
                                local gx = D.getfield(curr, "xoffset") or 0
                                local x_center = gx + gw / 2
                                -- P1: read font_size from style_registry, not layout_map
                                local lm_style_id = D.get_attribute(curr, constants.ATTR_STYLE_REG_ID)
                                local lm_font_size = lm_style_id and style_registry.get_font_size(lm_style_id)
                                ctx.line_mark_entries[#ctx.line_mark_entries + 1] =
                                    helpers.create_linemark_entry({
                                        group_id = pos.line_mark_id,
                                        col = pos.col,
                                        y_sp = pos.y_sp,
                                        cell_height = pos.cell_height,
                                        font_size = lm_font_size,
                                        sub_col = pos.sub_col,
                                        x_center_sp = x_center,
                                    })
                            end
                        end
                    else
                        p_head = handle_block_node(curr, p_head, pos, ctx)
                        p_head = handle_debug_drawing(curr, p_head, pos, ctx)
                    end
                end
            else
                -- CRITICAL DEBUG: If it has Jiazhu attribute but no pos, it's a bug!
                if setting_stack.get("debug") then
                    local has_jiazhu = (D.get_attribute(curr, constants.ATTR_JIAZHU) == 1)
                    if has_jiazhu then
                        dbg.log(string.format("  [render] DISCARDED JIAZHU NODE=%s (not in layout_map!) char=%s",
                            tostring(curr), (id == constants.GLYPH and tostring(D.getfield(curr, "char")) or "N/A")))
                    end
                end
            end
        elseif id == constants.GLUE then
            local pos = layout_map[curr]
            if pos and pos.col and pos.col >= 0 then
                -- This is a positioned space (user glue with width)
                -- Zero out the natural glue width and insert kern for positioning
                local glue_width = D.getfield(curr, "width") or 0
                D.setfield(curr, "width", 0)
                D.setfield(curr, "stretch", 0)
                D.setfield(curr, "shrink", 0)

                -- Calculate grid position via calc_grid_position (same RTL→LTR as glyph)
                local glue_dims = { width = glue_width, height = 0, depth = 0 }
                local glue_p = {
                    total_cols = ctx.p_total_cols,
                    shift_x = ctx.shift_x,
                    shift_y = ctx.shift_y,
                    half_thickness = ctx.half_thickness,
                    col_geom = ctx.col_geom,
                    h_align = "left",
                    v_align = "top",
                    y_sp = pos.y_sp,
                    band_y_offset_sp = pos.band_y_offset_sp or 0,
                    pos_x = pos.x,
                    pos_col_width = pos.col_width,
                    content_width = pos.content_width or glyph_params.content_width,
                    shift_x_base = glyph_params.shift_x_base,
                    col_widths = glyph_params.col_widths,
                }
                local final_x, final_y = text_position.calc_grid_position(pos.col, glue_dims, glue_p)

                -- Insert kern to move to correct position, then kern back
                local k_pre = D.new(constants.KERN)
                D.setfield(k_pre, "kern", final_x)
                local k_post = D.new(constants.KERN)
                D.setfield(k_post, "kern", -final_x)

                p_head = D.insert_before(p_head, curr, k_pre)
                D.insert_after(p_head, curr, k_post)

                dbg.log(string.format("  [render] GLUE (space) [c:%d, y_sp:%.0f]", pos.col, pos.y_sp or 0))
                p_head = handle_debug_drawing(curr, p_head, pos, ctx)
            else
                -- Not positioned - zero out (baseline/lineskip glue)
                D.setfield(curr, "width", 0)
                D.setfield(curr, "stretch", 0)
                D.setfield(curr, "shrink", 0)
            end
        elseif id == constants.KERN then
            local subtype = D.getfield(curr, "subtype")
            if subtype ~= 1 then
                D.setfield(curr, "kern", 0)
            end
        elseif id == constants.WHATSIT then
            local uid = D.getfield(curr, "user_id")
            if uid == constants.SIDENOTE_USER_ID or uid == constants.FLOATING_TEXTBOX_USER_ID then
                p_head = D.remove(p_head, curr)
                node.flush_node(D.tonode(curr))
            end
        end
        curr = next_curr
    end

    return p_head
end

-- ============================================================================
-- Module Export
-- ============================================================================

local M = {
    handle_glyph_node = handle_glyph_node,
    handle_block_node = handle_block_node,
    handle_debug_drawing = handle_debug_drawing,
    handle_decorate_node = decorate_mod.handle_node,
    process_page_nodes = process_page_nodes,
}

package.loaded['core.luatex-cn-core-render-page-process'] = M
return M
