% 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-hori.sty - Horizontal Chinese typesetting per W3C clreq (H1).
% Inserts kinsoku penalties and adjustable glues (inter-CJK break points,
% CJK-Western spacing) in pre_linebreak_filter, driven by the shared clreq
% rule kernel in tex/shared/. Usable with any horizontal document class.
%
% Design: docs/CLREQ-HORIZONTAL-PLAN.md (H1)
% Rule kernel contract: ai_must_read/clreq-shared-core.md

\NeedsTeXFormat{LaTeX2e}
\RequirePackage{expl3}
\RequirePackage{l3keys2e}
\ProvidesExplPackage {luatex-cn-hori} {2026/08/07} {0.4.1}
  {Horizontal Chinese typesetting per clreq}

\sys_if_engine_luatex:F
  {
    \msg_new:nnn { luatex-cn-hori } { engine }
      { luatex-cn-hori~requires~LuaTeX.~Loading~aborted. }
    \msg_error:nn { luatex-cn-hori } { engine }
  }

% HR2: luatexja performs its own CJK spacing / kinsoku in the same callbacks
% (JLReq semantics); running both would double-process every paragraph.
\@ifpackageloaded { luatexja }
  {
    \msg_new:nnn { luatex-cn-hori } { luatexja-conflict }
      { luatex-cn-hori~and~luatexja~(also~loaded~by~ctex~under~LuaLaTeX)~
        are~mutually~exclusive:~both~insert~CJK~spacing~and~line-break~
        penalties.~Load~only~one~of~them. }
    \msg_error:nn { luatex-cn-hori } { luatexja-conflict }
  }
  { }

% ============================================================================
% Options
% ============================================================================

\tl_new:N \l__luatexcn_hori_style_tl
\tl_new:N \l__luatexcn_hori_level_tl
\bool_new:N \l__luatexcn_hori_cjkspace_bool
\tl_new:N \l__luatexcn_hori_stretch_tl
\tl_new:N \l__luatexcn_hori_parindent_tl
\bool_new:N \l__luatexcn_hori_lineadjust_bool
\tl_new:N \l__luatexcn_hori_lineendpunct_tl
\bool_new:N \l__luatexcn_hori_hanging_bool
\tl_new:N \l__luatexcn_hori_quotestyle_tl
\bool_new:N \l__luatexcn_hori_orphanchar_bool
\tl_new:N \l__luatexcn_hori_lastline_tl
\tl_new:N \l__luatexcn_hori_adjacentpunct_tl
\tl_new:N \l__luatexcn_hori_linestartbracket_tl
\int_new:N \l__luatexcn_hori_widowpenalty_int
\int_new:N \l__luatexcn_hori_clubpenalty_int
\fp_new:N \l__luatexcn_hori_rubysize_fp
\fp_new:N \l__luatexcn_hori_rubygap_fp

% Ruby defaults come from the shared clreq metrics layer (HR5):
% defines \c__luatexcn_ruby_size_default_tl / \c__luatexcn_ruby_gap_default_tl
\lua_now:n { require('shared.luatex-cn-ruby-metrics').tex_defaults() }

\keys_define:nn { luatexcn / hori }
  {
    % Punctuation style preset: mainland | taiwan | none
    style .choices:nn = { mainland , taiwan , none }
      { \tl_set:Nn \l__luatexcn_hori_style_tl { #1 } },
    style .initial:n = { mainland },

    % Kinsoku level: none | basic | gb | strict (clreq: basic recommended)
    kinsoku-level .choices:nn = { none , basic , gb , strict }
      { \tl_set:Nn \l__luatexcn_hori_level_tl { #1 } },
    kinsoku-level .initial:n = { basic },

    % CJK-Western 1/4 em spacing on/off
    cjk-latin-space .bool_set:N = \l__luatexcn_hori_cjkspace_bool,
    cjk-latin-space .initial:n = { true },

    % Inter-CJK stretch of last resort (em ratio)
    inter-cjk-stretch .tl_set:N = \l__luatexcn_hori_stretch_tl,
    inter-cjk-stretch .initial:n = { 0.05 },

    % First-line indent (clreq: 段首缩排以两个汉字的空间为标准).
    % Applied at \begin{document} so 1em = the main CJK font size.
    % Set to `keep' to leave \parindent untouched.
    paragraph-indent .tl_set:N = \l__luatexcn_hori_parindent_tl,
    paragraph-indent .initial:n = { 2em },

    % H2: per-line priority redistribution (clreq 挤压 7 级 / 拉伸 2 级)
    line-adjust .bool_set:N = \l__luatexcn_hori_lineadjust_bool,
    line-adjust .initial:n = { true },

    % H2: line-final punctuation blank (clreq 挤压第 1 级: 行末标点半字宽).
    % compress = always reclaim the blank and re-give it to the line;
    % natural  = reclaim only as far as the line's tightness demands.
    line-end-punct .choices:nn = { compress , natural }
      { \tl_set:Nn \l__luatexcn_hori_lineendpunct_tl { #1 } },
    line-end-punct .initial:n = { compress },

    % H2: 行尾点号悬挂 (clreq, opt-in). 行末点号整字悬于版口外；
    % 港台式点号居中不宜悬挂，故默认关闭。
    hanging-punct .bool_set:N = \l__luatexcn_hori_hanging_bool,
    hanging-punct .initial:n = { false },

    % 连续标点缩减 (clreq: 夹注符号连排「无论何种风格都应该」把 2 字宽
    % 缩为 1.5；风格可进一步到 1). natural = 仅按行紧度挤压
    adjacent-punct .choices:nn = { 1.5 , 1 , natural }
      { \tl_set:Nn \l__luatexcn_hori_adjacentpunct_tl { #1 } },
    adjacent-punct .initial:n = { 1.5 },

    % 行首开始夹注符号 (clreq: 可以缩减始侧半字，含段首缩进首行)
    line-start-bracket .choices:nn = { trim , natural }
      { \tl_set:Nn \l__luatexcn_hori_linestartbracket_tl { #1 } },
    line-start-bracket .initial:n = { trim },

    % H1: 引号体例转换 (clreq: 简体横排弯引号先双后单 / 台湾传统引号
    % 先单后双). 默认 keep 不改动来稿用字；auto 按 style 选择；
    % curly/corner 显式指定目标体例（嵌套深度经一一映射保持）。
    quote-style .choices:nn = { keep , auto , curly , corner }
      { \tl_set:Nn \l__luatexcn_hori_quotestyle_tl { #1 } },
    quote-style .initial:n = { keep },

    % H5: 段末孤字避免 (clreq: 段落最后一行不宜只剩一个汉字——前行借字)
    orphan-char .bool_set:N = \l__luatexcn_hori_orphanchar_bool,
    orphan-char .initial:n = { true },

    % H5: 段末行对齐 (clreq 单行对齐): left(默认)|center|right|justify
    last-line .choices:nn = { left , center , right , justify }
      { \tl_set:Nn \l__luatexcn_hori_lastline_tl { #1 } },
    last-line .initial:n = { left },

    % H5: 孤行 (clreq: 避免页首孤行/页尾孤行；10000=禁止)
    widow-penalty .int_set:N = \l__luatexcn_hori_widowpenalty_int,
    widow-penalty .initial:n = { 10000 },
    club-penalty .int_set:N = \l__luatexcn_hori_clubpenalty_int,
    club-penalty .initial:n = { 10000 },

    % H4: annotation size ratio / row gap (defaults from shared/ruby-metrics)
    ruby-size .fp_set:N = \l__luatexcn_hori_rubysize_fp,
    ruby-size .initial:x = { \c__luatexcn_ruby_size_default_tl },
    ruby-gap .fp_set:N = \l__luatexcn_hori_rubygap_fp,
    ruby-gap .initial:x = { \c__luatexcn_ruby_gap_default_tl },
  }

% Push current option values to the Lua pipeline
\cs_new_protected:Npn \__luatexcn_hori_sync_options:
  {
    \lua_now:e
      {
        local~p = require('hori.luatex-cn-hori-pipeline')
        p.setup({
          style = '\l__luatexcn_hori_style_tl',
          level = '\l__luatexcn_hori_level_tl',
          cjk_latin_space = \bool_if:NTF \l__luatexcn_hori_cjkspace_bool { true } { false },
          inter_cjk_stretch = \l__luatexcn_hori_stretch_tl,
          line_adjust = \bool_if:NTF \l__luatexcn_hori_lineadjust_bool { true } { false },
          line_end_punct = '\l__luatexcn_hori_lineendpunct_tl',
          hanging_punct = \bool_if:NTF \l__luatexcn_hori_hanging_bool { true } { false },
          quote_style = '\l__luatexcn_hori_quotestyle_tl',
          avoid_orphan_char = \bool_if:NTF \l__luatexcn_hori_orphanchar_bool { true } { false },
          last_line = '\l__luatexcn_hori_lastline_tl',
          adjacent_punct = '\l__luatexcn_hori_adjacentpunct_tl',
          line_start_bracket = '\l__luatexcn_hori_linestartbracket_tl',
        })
      }
  }

\ProcessKeysOptions { luatexcn / hori }

% ============================================================================
% User commands
% ============================================================================

\NewDocumentCommand { \horiSetup } { m }
  {
    \keys_set:nn { luatexcn / hori } { #1 }
    \__luatexcn_hori_sync_options:
  }
\NewCommandCopy { \横排设置 } { \horiSetup }

% ============================================================================
% H3: Inter-line indication marks (行间标号, 单面装 — drawn below the text)
% ============================================================================

% tex.setattribute is group-local: everything typeset inside the group
% carries the mark attribute; drawing happens per line in post_linebreak.
\cs_new_protected:Npn \__luatexcn_hori_linemark:nn #1#2
  {
    \group_begin:
    \lua_now:n { require('hori.luatex-cn-hori-linemark').mark_on(#1) }
    #2
    \group_end:
  }

% Kind codes match linemark.KIND_* (1 专名 / 2 书名甲 / 3 着重)
\NewDocumentCommand { \专名 } { m } { \__luatexcn_hori_linemark:nn { 1 } { #1 } }
\NewDocumentCommand { \书名号甲 } { m } { \__luatexcn_hori_linemark:nn { 2 } { #1 } }
\NewCommandCopy { \书名甲 } { \书名号甲 }
\NewDocumentCommand { \着重 } { m } { \__luatexcn_hori_linemark:nn { 3 } { #1 } }

% ============================================================================
% H4: Inter-line annotations (行间注: 拼音标音 / 中外文对照)
% ============================================================================

\box_new:N \l__luatexcn_hori_ruby_base_box
\box_new:N \l__luatexcn_hori_ruby_ann_box
\seq_new:N \l__luatexcn_hori_ruby_ann_seq
\bool_new:N \l__luatexcn_hori_ruby_first_bool

% Annotation font: the current font scaled by ruby-size
\cs_new_protected:Npn \__luatexcn_hori_ruby_annfont:
  {
    \fontsize
      { \fp_eval:n { \l__luatexcn_hori_rubysize_fp * \f@size } }
      { \fp_eval:n { \l__luatexcn_hori_rubysize_fp * \f@size } }
    \selectfont
  }

% Item separator: skipped before the first item, an #1-wide skip after that
\cs_new_protected:Npn \__luatexcn_hori_ruby_sep:n #1
  {
    \bool_if:NTF \l__luatexcn_hori_ruby_first_bool
      { \bool_set_false:N \l__luatexcn_hori_ruby_first_bool }
      { \skip_horizontal:n { #1 } }
  }

% Row contents with #1 inner gaps. Measurement uses the SAME constructions
% with 0pt gaps, so measured and rebuilt widths agree exactly (a plain
% concatenation would keep inter-item font kerns the separators suppress).
\cs_new_protected:Npn \__luatexcn_hori_ruby_base_content:nn #1#2
  {
    \bool_set_true:N \l__luatexcn_hori_ruby_first_bool
    \tl_map_inline:nn { #2 } { \__luatexcn_hori_ruby_sep:n { #1 } ##1 }
  }
\cs_new_protected:Npn \__luatexcn_hori_ruby_ann_content:n #1
  {
    \__luatexcn_hori_ruby_annfont:
    \bool_set_true:N \l__luatexcn_hori_ruby_first_bool
    \seq_map_inline:Nn \l__luatexcn_hori_ruby_ann_seq
      { \__luatexcn_hori_ruby_sep:n { #1 } ##1 }
  }

% \__luatexcn_hori_ruby:nn {基文} {注文}
% clreq 词对齐 solved by shared/luatex-cn-ruby-metrics.lua (HR5): the shorter
% row is spread across the block width (n slots: half at each edge), the
% longer row is set solid. Annotation syllables split on spaces (分词连写).
\cs_new_protected:Npn \__luatexcn_hori_ruby:nn #1#2
  {
    \group_begin:
    \seq_set_split:Nnn \l__luatexcn_hori_ruby_ann_seq { ~ } { #2 }
    \hbox_set:Nn \l__luatexcn_hori_ruby_base_box
      { \__luatexcn_hori_ruby_base_content:nn { 0pt } { #1 } }
    \hbox_set:Nn \l__luatexcn_hori_ruby_ann_box
      { \__luatexcn_hori_ruby_ann_content:n { 0pt } }
    % Solve the block layout in the shared layer → \l__luatexcn_hori_ruby_*_tl
    \lua_now:e
      {
        require('shared.luatex-cn-ruby-metrics').tex_layout(
          \dim_to_decimal_in_sp:n { \box_wd:N \l__luatexcn_hori_ruby_base_box },
          \dim_to_decimal_in_sp:n { \box_wd:N \l__luatexcn_hori_ruby_ann_box },
          \tl_count:n { #1 },
          \seq_count:N \l__luatexcn_hori_ruby_ann_seq )
      }
    \mode_leave_vertical:
    % Zero glues on both sides restore the break opportunities a box boundary
    % would otherwise remove (break before/after the annotated word)
    \skip_horizontal:n { 0pt }
    \vbox:n
      {
        \hbox_to_wd:nn { \l__luatexcn_hori_ruby_width_tl }
          {
            \skip_horizontal:n { \l__luatexcn_hori_ruby_ann_edge_tl }
            \__luatexcn_hori_ruby_ann_content:n { \l__luatexcn_hori_ruby_ann_inner_tl }
            \skip_horizontal:n { \l__luatexcn_hori_ruby_ann_edge_tl }
          }
        \nointerlineskip
        \skip_vertical:n { \fp_use:N \l__luatexcn_hori_rubygap_fp em }
        \hbox_to_wd:nn { \l__luatexcn_hori_ruby_width_tl }
          {
            \skip_horizontal:n { \l__luatexcn_hori_ruby_base_edge_tl }
            \__luatexcn_hori_ruby_base_content:nn
              { \l__luatexcn_hori_ruby_base_inner_tl } { #1 }
            \skip_horizontal:n { \l__luatexcn_hori_ruby_base_edge_tl }
          }
      }
    \skip_horizontal:n { 0pt }
    \group_end:
  }

\NewDocumentCommand { \ruby } { m m } { \__luatexcn_hori_ruby:nn { #1 } { #2 } }
\NewCommandCopy { \拼音 } { \ruby }
\NewCommandCopy { \对照 } { \ruby }

% ============================================================================
% 均排 (clreq: 字均匀分布于指定宽度，两端字紧贴边界——用于标题、表格
% 单元格等；行级的拉伸兜底均分由 H2 的分配器完成)
% ============================================================================

\bool_new:N \l__luatexcn_hori_evenspread_first_bool

\NewDocumentCommand { \均排 } { O{\linewidth} m }
  {
    \mode_leave_vertical:
    \hbox_to_wd:nn { #1 }
      {
        \bool_set_true:N \l__luatexcn_hori_evenspread_first_bool
        \tl_map_inline:nn { #2 }
          {
            \bool_if:NTF \l__luatexcn_hori_evenspread_first_bool
              { \bool_set_false:N \l__luatexcn_hori_evenspread_first_bool }
              { \skip_horizontal:n { 0pt plus 1fil } }
            ##1
          }
      }
  }
\NewCommandCopy { \EvenSpread } { \均排 }

% ============================================================================
% H5: 段落级排式
% ============================================================================

% 凸排（clreq: 首行顶格、第二行起固定量缩排——词典释义、参考文献等）。
% 环境内每个段落生效；缩排量默认 2em。
\NewDocumentEnvironment { 凸排 } { O{2em} }
  {
    \par
    \dim_set:Nn \parindent { 0pt }
    \everypar { \hangindent #1 \hangafter 1 }
  }
  { \par }

% 段落缩排（clreq: 整段较版心缩进）。可选参数：左缩、右缩（默认 2em/0pt）。
\NewDocumentEnvironment { 段落缩排 } { O{2em} O{0pt} }
  {
    \par
    \dim_add:Nn \leftskip { #1 }
    \dim_add:Nn \rightskip { #2 }
  }
  { \par }

% 号数字号（clreq「字号」节的号数制；表在 shared/luatex-cn-type-sizes.lua）。
% \字号{五号} / \字号{小四}[1.7]（可选参数为行距倍数，默认取共享层常量）
\msg_new:nnn { luatex-cn-hori } { unknown-zihao }
  { 未知号数「#1」——可用值见 shared/luatex-cn-type-sizes.lua }

\NewDocumentCommand { \字号 } { m o }
  {
    \lua_now:e
      {
        require('shared.luatex-cn-type-sizes').tex_lookup(
          '\luaescapestring{#1}' \IfValueT {#2} { , #2 } )
      }
    \tl_if_empty:NTF \l__luatexcn_hori_zihao_size_tl
      { \msg_error:nnn { luatex-cn-hori } { unknown-zihao } { #1 } }
      {
        \fontsize { \l__luatexcn_hori_zihao_size_tl }
                  { \l__luatexcn_hori_zihao_base_tl }
        \selectfont
      }
  }

% ============================================================================
% Activation
% ============================================================================

\__luatexcn_hori_sync_options:
\lua_now:n { require('hori.luatex-cn-hori-pipeline').enable() }

% First-line indent, resolved against the document's main font at
% \begin{document} time (2em of a CJK main font = two hanzi widths)
\hook_gput_code:nnn { begindocument } { luatexcn-hori }
  {
    \tl_if_eq:NnF \l__luatexcn_hori_parindent_tl { keep }
      {
        \dim_set:Nn \parindent { \l__luatexcn_hori_parindent_tl }
      }
    % H5 孤行控制（clreq: 避免页首孤行 / 页尾孤行）
    \int_set:Nn \widowpenalty { \l__luatexcn_hori_widowpenalty_int }
    \int_set:Nn \clubpenalty { \l__luatexcn_hori_clubpenalty_int }
  }
