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:
1 parent
e87ccdf061
commit
7c7d891570
59 files changed
+5305
-590
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user