feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)

脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
AskaEth committed 2026-11-04 20:02:28 +08:00
1 parent e87ccdf061
commit 7c7d891570
59 files changed
+5305 -590

No files matched your search

+528
View File
@@ -0,0 +1,528 @@
/* core/rdchapter.c — TXT 章节规范解析器实现(见 core/rdchapter.h)。 */
#include "rdchapter.h"
#include "seg.h"
#include "rdtxt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------------- 基础字符工具 ---------------- */
static int ascii_digit_val(uint32_t cp)
{
return (cp >= '0' && cp <= '9') ? (int)(cp - '0') : -1;
}
/* CJK / 全角宽度 = 2,其余 = 1(近似;不含零宽组合符)。 */
static int cp_width(uint32_t cp)
{
if (cp == 0x3000 || cp == 0x3001 || cp == 0x3002) return 2;
if ((cp >= 0x2E80 && cp <= 0x303E) || (cp >= 0x3040 && cp <= 0x30FF) ||
(cp >= 0x3100 && cp <= 0x318F) || (cp >= 0x3400 && cp <= 0x4DBF) ||
(cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xA000 && cp <= 0xA4CF) ||
(cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) ||
(cp >= 0xFE30 && cp <= 0xFE4F) || (cp >= 0xFF00 && cp <= 0xFF60) ||
(cp >= 0xFFE0 && cp <= 0xFFE6)) return 2;
return 1;
}
static int disp_width(const uint32_t *cps, int s, int e)
{
int w = 0;
for (int i = s; i < e; i++) w += cp_width(cps[i]);
return w;
}
/* 章节标记字符集:章 回 节 卷 集 部 篇。 */
static int is_marker(uint32_t cp)
{
return cp == 0x7AE0 || cp == 0x56DE || cp == 0x8282 ||
cp == 0x5377 || cp == 0x96C6 || cp == 0x90E8 || cp == 0x7BC7;
}
/* 卷 部 集(卷/集类章节前缀)。 */
static int is_volmark(uint32_t cp)
{
return cp == 0x5377 /* 卷 */ || cp == 0x90E8 /* 部 */ || cp == 0x96C6 /* 集 */;
}
/* 中文数字字符(含位权)。 */
static int is_cn_num_char(uint32_t cp)
{
return cp == 0x96F6 /* 零 */ || cp == 0x4E00 /* 一 */ || cp == 0x4E8C /* 二 */ ||
cp == 0x4E09 /* 三 */ || cp == 0x56DB /* 四 */ || cp == 0x4E94 /* 五 */ ||
cp == 0x516D /* 六 */ || cp == 0x4E03 /* 七 */ || cp == 0x516B /* 八 */ ||
cp == 0x4E5D /* 九 */ || cp == 0x5341 /* 十 */ || cp == 0x767E /* 百 */ ||
cp == 0x5343 /* 千 */ || cp == 0x4E07 /* 万 */ || cp == 0x4E24 /* 两 */;
}
static int cn_digit_val(uint32_t cp)
{
switch (cp) {
case 0x96F6: return 0; /* 零 */
case 0x4E00: return 1; /* 一 */
case 0x4E8C: return 2; /* 二 */
case 0x4E09: return 3; /* 三 */
case 0x56DB: return 4; /* 四 */
case 0x4E94: return 5; /* 五 */
case 0x516D: return 6; /* 六 */
case 0x4E03: return 7; /* 七 */
case 0x516B: return 8; /* 八 */
case 0x4E5D: return 9; /* 九 */
case 0x4E24: return 2; /* 两 */
default: return -1;
}
}
static int cn_weight_val(uint32_t cp)
{
switch (cp) {
case 0x5341: return 10; /* 十 */
case 0x767E: return 100; /* 百 */
case 0x5343: return 1000; /* 千 */
case 0x4E07: return 10000; /* 万 */
default: return 0;
}
}
/* 中文数字子串 [s,e) → 阿拉伯;非法/空返回 -1。实现:digit 累积 + 位权叠加。 */
static int cn_num_from_cps(const uint32_t *cps, int s, int e)
{
long total = 0, cur = 0, digit = 0;
int any = 0;
for (int i = s; i < e; i++) {
uint32_t cp = cps[i];
int dv = cn_digit_val(cp);
if (dv >= 0) { digit = dv; any = 1; continue; }
int w = cn_weight_val(cp);
if (w > 0) {
cur += (digit ? digit : 1) * (long)w;
digit = 0; any = 1;
continue;
}
return -1; /* 非中文数字字符 */
}
total += cur + digit;
return any ? (int)total : -1;
}
/* 行内正文标点计数:句号/逗号/顿号/分号/问号/感叹号/省略号 + ASCII . , ; ! ?
* 返回计数(区分「极少」:≤1 视为标题,>1 视为正文)。 */
static int sent_punct_count(const uint32_t *cps, int s, int e)
{
int n = 0;
for (int i = s; i < e; i++) {
uint32_t cp = cps[i];
if (cp == 0x3002 /* 。 */ || cp == 0xFF0C /* , */ || cp == 0x3001 /* 、 */ ||
cp == 0xFF1B /* ; */ || cp == 0xFF1F /* ? */ || cp == 0xFF01 /* ! */ ||
cp == 0x2026 /* … */ || cp == '.' || cp == ',' || cp == ';' ||
cp == '!' || cp == '?') n++;
}
return n;
}
/* cps[s..] 是否以 utf8 字面量为前缀(逐码点比较)。 */
static int cps_prefix(const uint32_t *cps, int s, int e, const uint8_t *utf8)
{
int i = s;
const uint8_t *p = utf8;
while (*p && i < e) {
uint32_t cp;
uint8_t b0 = *p;
int n;
if (b0 < 0x80) { cp = b0; n = 1; }
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | (p[1] & 0x3F); n = 2; }
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)(p[1] & 0x3F) << 6) | (p[2] & 0x3F); n = 3; }
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)(p[1] & 0x3F) << 12) | ((uint32_t)(p[2] & 0x3F) << 6) | (p[3] & 0x3F); n = 4; }
if (cps[i] != cp) return 0;
i++; p += n;
}
return (*p == 0) && (i > s);
}
/* ASCII 大小写无关前缀比较(用于 "chapter")。 */
static int cps_prefix_ci(const uint32_t *cps, int s, int e, const char *ascii)
{
int i = s;
for (const char *p = ascii; *p; p++) {
if (i >= e) return 0;
uint32_t cp = cps[i++];
uint32_t lo = (cp >= 'A' && cp <= 'Z') ? cp + 32 : cp;
char ch = *p;
if (lo != (uint32_t)ch) return 0;
}
return 1;
}
/* 罗马数字 → 阿拉伯(ivxl);解析失败 -1。 */
static int roman_to_int(const uint32_t *cps, int s, int e, int *end)
{
struct { uint32_t cp; int v; } tbl[] = {
{ 'i', 1 }, { 'v', 5 }, { 'x', 10 }, { 'l', 50 }, { 'c', 100 },
};
int vals[32], k = 0;
int i = s;
while (i < e && k < 32) {
uint32_t cp = cps[i];
if (cp >= 'A' && cp <= 'Z') cp += 32; /* 小写化 */
int v = 0;
for (int t = 0; t < 5; t++) if (tbl[t].cp == cp) { v = tbl[t].v; break; }
if (v == 0) break;
vals[k++] = v; i++;
}
if (k == 0) return -1;
int total = 0, prev = 0;
for (int j = k - 1; j >= 0; j--) {
int v = vals[j];
if (v < prev) total -= v; else total += v;
prev = v;
}
*end = i;
return total;
}
/* ---- 英文数字词(Chapter one / twenty-one / two hundred fifty ...)→ 阿拉伯 ---- */
static int cps_ascii_word_eq_ci(const uint32_t *cps, int s, int e, const char *w)
{
int len = (int)strlen(w);
if (e - s != len) return 0;
for (int k = 0; k < len; k++) {
uint32_t c = cps[s + k];
if (c >= 'A' && c <= 'Z') c += 32;
if (c != (uint32_t)(unsigned char)w[k]) return 0;
}
return 1;
}
/* 英文数字词一 → 值;不识别返回 -1。覆盖 one..nineteen、twenty..ninety、hundred、thousand。 */
static int en_word_val(const uint32_t *cps, int s, int e)
{
static const struct { const char *w; int v; } tbl[] = {
{ "one", 1 }, { "two", 2 }, { "three", 3 }, { "four", 4 }, { "five", 5 },
{ "six", 6 }, { "seven", 7 }, { "eight", 8 }, { "nine", 9 }, { "ten", 10 },
{ "eleven", 11 }, { "twelve", 12 }, { "thirteen", 13 }, { "fourteen", 14 },
{ "fifteen", 15 }, { "sixteen", 16 }, { "seventeen", 17 }, { "eighteen", 18 },
{ "nineteen", 19 }, { "twenty", 20 }, { "thirty", 30 }, { "forty", 40 },
{ "fifty", 50 }, { "sixty", 60 }, { "seventy", 70 }, { "eighty", 80 },
{ "ninety", 90 }, { "hundred", 100 }, { "thousand", 1000 },
};
for (int t = 0; t < (int)(sizeof tbl / sizeof tbl[0]); t++)
if (cps_ascii_word_eq_ci(cps, s, e, tbl[t].w)) return tbl[t].v;
return -1;
}
/* 解析一段英文数字词(one / twenty-one / two hundred fifty ...)。*end=末字符位置。
* 失败(无词/未知词/越界/数值越界)返回 -1。 */
static int word_num_from_cps(const uint32_t *cps, int s, int e, int *end)
{
long total = 0, cur = 0;
int i = s, words = 0, last_end = s;
while (i < e) {
while (i < e && (cps[i] == ' ' || cps[i] == '-' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
int ws = i;
while (i < e && ((cps[i] >= 'a' && cps[i] <= 'z') || (cps[i] >= 'A' && cps[i] <= 'Z'))) i++;
if (i == ws) break;
int v = en_word_val(cps, ws, i);
if (v < 0) return -1;
if (v == 100 || v == 1000) { long base = (cur == 0) ? 1 : cur; total += base * (long)v; cur = 0; }
else cur += (long)v;
words++;
last_end = i;
}
if (words == 0) return -1;
long n = total + cur;
if (n <= 0 || n > 100000) return -1;
*end = last_end;
return (int)n;
}
/* 把 cps[s..e) 转成 UTF-8 到 out(有界)。 */
static void cps_to_utf8(const uint32_t *cps, int s, int e, char *out, int outsz)
{
int o = 0;
for (int i = s; i < e; i++) {
uint8_t b[4];
int n = rd_cp_to_utf8(cps[i], b);
if (o + n + 1 > outsz) break;
for (int k = 0; k < n; k++) out[o++] = (char)b[k];
}
out[o] = 0;
}
/* 特殊前缀章节(楔子/序章/序言/引子/前言/序幕/尾声/后记/番外/外传/终章/大结局/正文)。 */
static const uint8_t *const g_special[] = {
(const uint8_t *)"\xE6\xA5\x94\xE5\xAD\x90", /* 楔子 */
(const uint8_t *)"\xE5\xBA\x8F\xE7\xAB\xA0", /* 序章 */
(const uint8_t *)"\xE5\xBA\x8F\xE8\xA8\x80", /* 序言 */
(const uint8_t *)"\xE5\xBC\x95\xE5\xAD\x90", /* 引子 */
(const uint8_t *)"\xE5\x89\x8D\xE8\xA8\x80", /* 前言 */
(const uint8_t *)"\xE5\xBA\x8F\xE5\xB9\x95", /* 序幕 */
(const uint8_t *)"\xE5\xB0\xBE\xE5\xA3\xB0", /* 尾声 */
(const uint8_t *)"\xE5\x90\x8E\xE8\xAE\xB0", /* 后记 */
(const uint8_t *)"\xE7\x95\xAA\xE5\xA4\x96", /* 番外 */
(const uint8_t *)"\xE5\xA4\x96\xE4\xBC\xA0", /* 外传 */
(const uint8_t *)"\xE7\xBB\x88\xE7\xAB\xA0", /* 终章 */
(const uint8_t *)"\xE5\xA4\xA7\xE7\xBB\x93\xE5\xB1\x80", /* 大结局 */
(const uint8_t *)"\xE6\xAD\xA3\xE6\x96\x87", /* 正文 */
};
#define G_SPECIAL_N ((int)(sizeof g_special / sizeof g_special[0]))
/* 判断一行(cps[ls..le),ls=首个非空白字符)是否为章节标题行。
* 命中则填 title(完整行)与 *num;返回 1,否则 0。 */
static int chap_match_line(const uint32_t *cps, int ls, int le, char *title, int outsz, int *num)
{
if (ls >= le) return 0;
if (disp_width(cps, ls, le) > RDCHAP_DISP_WMAX) return 0;
/* A/B:`第` + 阿拉伯/中文数字 + 章回节卷集部篇 */
if (cps[ls] == 0x7B2C /* 第 */) {
int i = ls + 1;
int numv = -1;
int dstart = i;
while (i < le && ascii_digit_val(cps[i]) >= 0) i++;
if (i > dstart) {
if (i - dstart > 4) return 0;
numv = 0;
for (int k = dstart; k < i; k++) numv = numv * 10 + ascii_digit_val(cps[k]);
} else {
int cstart = i;
while (i < le && is_cn_num_char(cps[i])) i++;
if (i == cstart) return 0;
if (i - cstart > 8) return 0;
numv = cn_num_from_cps(cps, cstart, i);
if (numv < 0) return 0;
}
if (i >= le || !is_marker(cps[i])) return 0;
i++; /* 消费标记 */
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* C:阿拉伯数字 + 可选分隔符(、.. 空格 全角空格) + 章回节卷集部篇 */
if (ascii_digit_val(cps[ls]) >= 0) {
int i = ls, numv = 0, cnt = 0;
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
if (cnt < 1 || cnt > 4) return 0;
if (i < le && (cps[i] == 0x3001 || cps[i] == '.' || cps[i] == ' ' || cps[i] == 0x3000)) i++;
if (i >= le || !is_marker(cps[i])) return 0;
i++;
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* F:卷/部/集 + 空白 + 中文数字(卷一 / 卷 一 / 集三) */
if (is_volmark(cps[ls])) {
int i = ls + 1;
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000)) i++;
int cstart = i;
while (i < le && is_cn_num_char(cps[i])) i++;
if (i == cstart) return 0;
int numv = cn_num_from_cps(cps, cstart, i);
if (numv < 0) return 0;
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* E:Chapter N(N=阿拉伯 / 罗马数字 / 英文数字词 one..hundred) */
if (cps_prefix_ci(cps, ls, le, "chapter")) {
int i = ls + 7;
if (i >= le) return 0;
if (!(cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) return 0; /* 必须空白:ChapterX 不认 */
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
int numv;
if (ascii_digit_val(cps[i]) >= 0) {
numv = 0; int cnt = 0;
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
if (cnt < 1 || cnt > 5) return 0;
} else {
int endi = i;
numv = roman_to_int(cps, i, le, &endi);
if (numv < 0) { endi = i; numv = word_num_from_cps(cps, i, le, &endi); }
if (numv < 0) return 0;
i = endi;
}
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* D:特殊前缀(楔子/序章/…)——要求整行无正文标点(避免「正文内容。」/「番外正文。」误判) */
for (int t = 0; t < G_SPECIAL_N; t++) {
if (cps_prefix(cps, ls, le, g_special[t])) {
int i = ls;
for (const uint8_t *p = g_special[t]; *p; ) { /* 消费前缀码点 */
uint8_t b0 = *p;
int n = (b0 < 0x80) ? 1 : ((b0 & 0xE0) == 0xC0) ? 2 : ((b0 & 0xF0) == 0xE0) ? 3 : 4;
i++; p += n;
}
if (sent_punct_count(cps, i, le) > 0) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = -1;
return 1;
}
}
return 0;
}
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c)
{
if (!c) return -1;
rdchapter_init(c);
if (!seg_dir || !seg_dir[0]) return 0; /* 无段目录 → 无章节 */
int nsegs = rdseg_n(seg_dir);
if (nsegs < 0) return -1;
if (nsegs == 0) { c->nsegs = 0; return 0; }
c->seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
if (!c->seg_char_start) return -6;
c->nsegs = nsegs;
int ent_cap = 0;
int64_t global = 0;
int err = 0;
uint8_t *raw = NULL; size_t rawcap = 0;
uint8_t *utf8 = NULL; size_t utf8cap = 0;
uint32_t *cps = NULL;
for (int seg = 0; seg < nsegs && !err; seg++) {
size_t rawlen = 0;
if (rdseg_rawlen(seg_dir, seg, &rawlen) != 0) { err = 1; break; }
if (rawlen > rawcap) {
uint8_t *t = (uint8_t *)realloc(raw, rawlen ? rawlen : 1);
if (!t) { err = -6; break; }
raw = t; rawcap = rawlen;
}
size_t rn = 0;
if (rdseg_load(seg_dir, seg, raw, rawlen, &rn) != 0) { err = 2; break; }
size_t ulen = 0;
size_t need = rawlen * 3 / 2 + 16;
if (need < RDTXT_SEG_UTF8_MAX + 8) need = RDTXT_SEG_UTF8_MAX + 8;
if (need > utf8cap) {
uint8_t *t = (uint8_t *)realloc(utf8, need);
if (!t) { err = -6; break; }
utf8 = t; utf8cap = need;
}
if (rd_to_utf8(enc, raw, rn, utf8, utf8cap, &ulen) != RD_ENC_OK) { err = 3; break; }
size_t n = 0;
/* 解码为码点失败(多为内存不足)→ 视作扫描失败:不落盘坏结果(见 rdchapter_scan 头部注释)。 */
if (rd_utf8_to_cp(utf8, ulen, &cps, &n) != RD_ENC_OK) { cps = NULL; err = -6; break; }
c->seg_char_start[seg] = global; /* 本段全局字符起点 */
if (cps && n > 0) {
int ls = 0;
while (ls < (int)n) {
int le = ls;
while (le < (int)n && cps[le] != '\n') le++;
int indent = 0, first = ls;
while (first < le && (cps[first] == ' ' || cps[first] == '\t' || cps[first] == 0x3000)) {
indent++; first++;
}
int te = le;
while (te > first && (cps[te - 1] == ' ' || cps[te - 1] == '\t' ||
cps[te - 1] == '\r' || cps[te - 1] == 0x3000)) te--;
if (indent <= RDCHAP_INDENT_MAX && first < te) {
char tbuf[RDCHAP_TITLE_MAX];
int num = -1;
if (chap_match_line(cps, first, te, tbuf, sizeof tbuf, &num) && c->n < RDCHAP_MAX_ENTRIES) {
if (ent_cap <= c->n) {
int nc = ent_cap ? ent_cap * 2 : 64;
if (nc > RDCHAP_MAX_ENTRIES) nc = RDCHAP_MAX_ENTRIES;
rdchapter_entry *ne = (rdchapter_entry *)realloc(c->entries, (size_t)nc * sizeof(rdchapter_entry));
if (!ne) { err = -6; break; }
c->entries = ne; ent_cap = nc;
}
rdchapter_entry *en = &c->entries[c->n];
memset(en, 0, sizeof *en);
snprintf(en->title, sizeof en->title, "%s", tbuf);
en->char_off = global + first;
en->seg_idx = seg;
en->seg_char = (int32_t)first;
en->number = num;
c->n++;
}
}
if (le >= (int)n) break;
ls = le + 1;
}
free(cps); cps = NULL;
}
global += (int64_t)n;
}
free(cps);
free(raw);
free(utf8);
if (err) { rdchapter_free(c); return err; }
/* 段数恒定:seg_char_start[nsegs] = 全书字符数(尾哨兵)。 */
if (global > (int64_t)0x7fffffffLL) { rdchapter_free(c); return -7; } /* 安全上限 */
c->seg_char_start[nsegs] = global;
/* 卷首/前置:第一章节前有内容 → 作为 entries[0](title=文件名或"卷首",号=-1)。 */
if (c->n > 0 && c->entries[0].char_off > 0) {
if (c->n >= RDCHAP_MAX_ENTRIES) { /* 已满则跳过卷首(理论) */ }
else {
memmove(&c->entries[1], &c->entries[0], (size_t)c->n * sizeof(rdchapter_entry));
rdchapter_entry *pre = &c->entries[0];
memset(pre, 0, sizeof *pre);
const char *bn = (book_basename && book_basename[0]) ? book_basename : "卷首";
strncpy(pre->title, bn, sizeof pre->title - 1);
pre->title[sizeof pre->title - 1] = 0;
pre->char_off = 0;
pre->seg_idx = 0;
pre->seg_char = 0;
pre->number = -1;
c->n++;
c->has_pre = 1;
snprintf(c->pre_title, sizeof c->pre_title, "%s", bn);
}
} else if (book_basename && book_basename[0]) {
snprintf(c->pre_title, sizeof c->pre_title, "%s", book_basename);
} else {
snprintf(c->pre_title, sizeof c->pre_title, "%s", "卷首");
}
return 0;
}
void rdchapter_init(rdchapter *c)
{
if (!c) return;
memset(c, 0, sizeof *c);
}
void rdchapter_free(rdchapter *c)
{
if (!c) return;
free(c->entries); c->entries = NULL;
free(c->seg_char_start); c->seg_char_start = NULL;
c->n = c->nsegs = c->has_pre = 0;
c->pre_title[0] = 0;
}
int rdchapter_cn_num(const char *s, int len)
{
if (!s || len <= 0) return -1;
uint32_t local[16];
int n = 0;
int i = 0;
while (i < len && n < 16) {
uint8_t b0 = (uint8_t)s[i];
uint32_t cp; int nb;
if (b0 < 0x80) { cp = b0; nb = 1; }
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | ((uint8_t)s[i + 1] & 0x3F); nb = 2; }
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 6) | ((uint8_t)s[i + 2] & 0x3F); nb = 3; }
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 12) | ((uint32_t)((uint8_t)s[i + 2] & 0x3F) << 6) | ((uint8_t)s[i + 3] & 0x3F); nb = 4; }
if (!is_cn_num_char(cp)) break;
local[n++] = cp;
i += nb;
}
if (n == 0) return -1;
return cn_num_from_cps(local, 0, n);
}
+61
View File
@@ -0,0 +1,61 @@
/* core/rdchapter.h — TXT 章节规范解析器:扫描已切好的 512KB 码位对齐段文件(core/seg.c),
* 识别章节标题行,输出章节列表(每项:标题、全书字符偏移、所在段/段内字符偏移、阿拉伯章号)。
*
* 设计约束:
* - 逐段流式(`rdseg_load` 读一个段 → `rd_to_utf8` 解码 → `rd_utf8_to_cp` 数码点 → 行扫),
* 不整书驻留;单段内存与 book.c `seg_ensure_built` 同量级(~512KB 原文 + ~3 倍 UTF-8)。
* - 章节标题行约束:行首缩进 ≤4 半角/全角空格;标题显示宽度 ≤ RDCHAP_DISP_WMAX(CJK=2/ASCII=1);
* 行内正文标点 ≤1(句号/逗号/省略号等,容「极少」);行首锚定 + 短 + 无成段标点 → 防误判
* (「他说第三章内容」不居行首即不命中;目录页交错等已知限制,见报告)。
* - 中文数字 → 阿拉伯(`rdchapter_cn_num`,用于排序/显示);按 char_off 升序输出。
* - 开头书名/简介区(第一章节之前的内容)归为「卷首/前置」,作为条目[0](title=文件名或"卷首")。
*
* 纯 libc + rdenc + seg,无 SDK/FreeType;宿主可测。
*/
#ifndef RDCHAPTER_H
#define RDCHAPTER_H
#include "rdenc.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RDCHAP_TITLE_MAX 160 /* 单个标题字符(UTF-8 字节)上限 */
#define RDCHAP_DISP_WMAX 40 /* 标题最大显示宽度(CJK=2 / ASCII=1) */
#define RDCHAP_INDENT_MAX 4 /* 行首缩进上限(空格/全角空格/制表,按字符计) */
#define RDCHAP_MAX_ENTRIES 200000 /* 章节数上限(防失控内存) */
typedef struct rdchapter_entry {
char title[RDCHAP_TITLE_MAX]; /* 完整标题行(如「第一章 风雨」「楔子」),UTF-8 */
int64_t char_off; /* 全书字符偏移(段 0 起,跨段累计;标题首字符) */
int32_t seg_idx; /* 所在 512KB 段下标 */
int32_t seg_char; /* 段内字符偏移(标题首字符) */
int32_t number; /* 阿拉伯章号;-1=无号(如楔子/卷首/序章) */
} rdchapter_entry;
typedef struct rdchapter {
rdchapter_entry *entries;
int n; /* 章节数;0=本书无章节(扫描过) */
int64_t *seg_char_start;/* seg_char_start[seg]=该段在全书中的字符起点;[nsegs]=全书字符数 */
int nsegs; /* 段数(= seg_char_start 数组长 -1) */
char pre_title[RDCHAP_TITLE_MAX]; /* 卷首/前置标题(文件名或"卷首") */
int has_pre; /* 1=存在卷首/前置(第一章节前有内容) */
} rdchapter;
void rdchapter_init(rdchapter *c);
void rdchapter_free(rdchapter *c);
/* 扫描 seg_dir 下全部 512KB 段文件,识别章节标题。book_basename 用于卷首标题(可为 NULL/"卷首")。
* 0=成功(c->n=章节数,可为 0);负=错误(seg_dir 无效/无段/解码失败等)。 */
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c);
/* 中文数字 → 阿拉伯(「二十三」→23,「一百零一」→101,「两」→2)。s/len 为 UTF-8;非法返回 -1。 */
int rdchapter_cn_num(const char *s, int len);
#ifdef __cplusplus
}
#endif
#endif /* RDCHAPTER_H */
+6 -1
View File
@@ -312,7 +312,12 @@ int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n)
size_t cnt = 0, i = 0;
while (i < len) {
uint8_t b = utf8[i];
if (b < 0x80) { arr[cnt++] = b; i++; continue; }
if (b < 0x80) {
/* \r\n → \n、孤 \r → \n:TXT 行尾 \r 若进排版,在无 U+000D 映射的字体里落成
* .notdef(豆腐/占宽);归一化后章节/页表/进度锚同处一个无 \r 码点空间。 */
if (b == '\r') { arr[cnt++] = '\n'; i++; if (i < len && utf8[i] == '\n') i++; continue; }
arr[cnt++] = b; i++; continue;
}
unsigned q, lo, hi;
if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; }
+63
View File
@@ -226,6 +226,12 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
npages++;
page_start = np_start;
cur_y = 0;
/* 孤行上推修正:被推行(l-1)成为新页首行 → 按页首行重算其 y 并计入页内高度,
* 否则它带着上一页的 y 残值(渲染错位)且页填充少算一行高(页底溢出)。 */
if (np_start == l - 1) {
out->lines[l - 1].y = 0;
cur_y = lh;
}
}
/* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */
extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
@@ -245,6 +251,63 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
return RD_LAYOUT_OK;
}
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
rd_line *lines, int max_lines, int *nlines, int32_t *next_start)
{
if (!in || !in->chars || in->n < 0 || !lines || !nlines || !next_start || max_lines <= 0)
return RD_LAYOUT_ERR_INVAL;
if (in->fs <= 0 || in->page_w <= 0 || in->page_h <= 0) return RD_LAYOUT_ERR_INVAL;
if (start < 0) start = 0;
if (start > in->n) start = in->n;
int lh = (in->fs * in->line_scale) / 100;
if (lh < 1) lh = 1;
int pspacing = lh / 2;
if (pspacing < 0) pspacing = 0;
int nl = 0;
int y = 0; /* 页内累计(含段距;规则同 rd_layout 阶段 B 单页填充) */
int32_t ps = start;
int first_para = 1; /* 首段:首行状态由 mid_para 给定(续行不缩进/非段首) */
while (ps < in->n) {
while (ps < in->n && (in->chars[ps] == '\n' || in->chars[ps] == '\r')) ps++;
if (ps >= in->n) break;
int32_t pe = ps;
while (pe < in->n && in->chars[pe] != '\n') pe++;
int32_t pos = ps;
int first_line = first_para ? !mid_para : 1;
first_para = 0;
while (pos < pe) {
rd_line ln;
para_break_line(in, pe, &pos, first_line, &ln);
first_line = 0;
/* 页填充判定(同 rd_layout 阶段 B:页内首行不加段距;溢出即页满) */
int extra = (ln.para_start && nl > 0) ? pspacing : 0;
if (y + lh + extra > in->page_h && nl > 0) {
/* 孤行守卫:页末行是段首行且段未结束 → 该行随段入下一页(同 rd_layout) */
int cut = nl;
if (lines[nl - 1].para_start && !lines[nl - 1].para_end) cut = nl - 1;
*nlines = cut;
*next_start = (cut < nl) ? lines[cut].s : ln.s;
return RD_LAYOUT_OK;
}
if (nl >= max_lines) { *nlines = nl; *next_start = ln.s; return RD_LAYOUT_ERR_OVER; }
ln.y = y + extra;
y += lh + extra;
lines[nl++] = ln;
if (ln.para_end) break;
}
ps = pos;
while (ps < in->n && in->chars[ps] == '\n') ps++;
if (pos >= pe && ps >= in->n) break;
}
*nlines = nl;
*next_start = in->n;
return RD_LAYOUT_OK;
}
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char)
{
if (!out || out->npages <= 0) return 0;
+8
View File
@@ -65,6 +65,14 @@ typedef struct rd_layout_out {
int rd_layout(const rd_layout_in *in, rd_layout_out *out);
/* 页级微排版(Phase 2C):从 chars[start](须为行首:start==0 或 chars[start-1]=='\n')起只排一页,
* 填满 page_h 即停。mid_para=1 表示 start 行是段中续行(不缩进、非段首行);=0 表示段首行
* (缩进 + para_start,与 rd_layout 首行规则一致)。输出 lines[0..*nlines)(内容区坐标,
* 与 rd_layout 同断行/分页/孤行守卫规则);*next_start = 下一页首字符偏移(== in->n 表示文末)。
* 一致性:同一 (chars, start, mid_para) 下产出 == rd_layout 全排版中该页的行区间(逐行 diff 可证)。 */
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
rd_line *lines, int max_lines, int *nlines, int32_t *next_start);
/* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。
* 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char);