ekp/ekp_c/ekp_paragraph.c
Kinneyzhang 9dcaeb0ba3 chore: licensing, customize support, and packaging hygiene
- add COPYING (GPL-3.0-or-later) and license headers to all elisp
  and C sources; real author/maintainer info replaces placeholders
- convert user-facing options to defcustom under new group `ekp'
  (spacing internals managed by ekp-param-set stay defvars)
- autoload user commands: ekp-param-reset, ekp-clear-caches,
  ekp-c-module-load, ekp-c-module-build
- ekp--load-dicts: a missing dictionaries/ directory now only
  disables hyphenation instead of breaking (require 'ekp);
  ekp--split-with-hyphen degrades gracefully (and resolves the
  hyphenator once per call instead of once per word)
- package summary no longer redundantly says "for Emacs"
- Makefile: drop personal Windows EMACS_ROOT default; add guidance
- ekp_c/README.md: fix stale 1.1 version references (module is 1.4)
- DEVELOPER*.md: remove archive/ from file map (not in the repo)
- remove unreferenced 10 MB demo GIF

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 00:52:37 +08:00

409 lines
12 KiB
C

/*
* ekp_paragraph.c - Text preprocessing and box/glue construction
*
* Copyright (C) 2024-2026 Kinney Zhang
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of emacs-kp, which is free software: you can
* redistribute it and/or modify it under the terms of the GNU General
* Public License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
* It is distributed WITHOUT ANY WARRANTY; see the GNU General Public
* License (COPYING) for details.
*
* The boring part that makes everything else fast.
* Get the data layout right, and the algorithm sings.
*/
#include "ekp_module.h"
#include <stdlib.h>
#include <string.h>
/* Box types */
#define BOX_LATIN 0
#define BOX_CJK 1
#define BOX_CJK_PUNCT 2
#define BOX_SPACE 3
/* Glue types */
#define GLUE_NONE 0
#define GLUE_LWS 1 /* Latin word space */
#define GLUE_MWS 2 /* Mixed (Latin-CJK) */
#define GLUE_CWS 3 /* CJK character space */
/* UTF-8 helpers */
static inline uint32_t utf8_decode(const char *s, int *len)
{
unsigned char c = s[0];
*len = 1;
if ((c & 0x80) == 0)
return c;
if ((c & 0xE0) == 0xC0) {
*len = 2;
return ((c & 0x1F) << 6) | (s[1] & 0x3F);
}
if ((c & 0xF0) == 0xE0) {
*len = 3;
return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F);
}
if ((c & 0xF8) == 0xF0) {
*len = 4;
return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) |
((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
}
return c;
}
/* Character classification */
static inline bool is_cjk(uint32_t cp)
{
/* CJK Unified Ideographs and related blocks */
return (cp >= 0x4E00 && cp <= 0x9FFF) || /* CJK Unified */
(cp >= 0x3400 && cp <= 0x4DBF) || /* CJK Ext A */
(cp >= 0x20000 && cp <= 0x2A6DF) || /* CJK Ext B */
(cp >= 0x2A700 && cp <= 0x2B73F) || /* CJK Ext C */
(cp >= 0x2B740 && cp <= 0x2B81F) || /* CJK Ext D */
(cp >= 0xF900 && cp <= 0xFAFF) || /* CJK Compat */
(cp >= 0x3000 && cp <= 0x303F) || /* CJK Symbols */
(cp >= 0x3040 && cp <= 0x309F) || /* Hiragana */
(cp >= 0x30A0 && cp <= 0x30FF) || /* Katakana */
(cp >= 0xAC00 && cp <= 0xD7AF); /* Hangul */
}
static inline bool is_cjk_punct(uint32_t cp)
{
return (cp >= 0x3000 && cp <= 0x303F) || /* CJK Symbols */
(cp >= 0xFF00 && cp <= 0xFF60) || /* Fullwidth Forms */
cp == 0x201C || cp == 0x201D || /* " " */
cp == 0x2018 || cp == 0x2019; /* ' ' */
}
static inline bool is_whitespace(uint32_t cp)
{
return cp == ' ' || cp == '\t' || cp == '\n' || cp == '\r' ||
cp == 0x00A0 || cp == 0x3000; /* NBSP, ideographic space */
}
/*
* Determine box type from codepoint
*/
static uint8_t classify_char(uint32_t cp)
{
if (is_whitespace(cp))
return BOX_SPACE;
if (is_cjk_punct(cp))
return BOX_CJK_PUNCT;
if (is_cjk(cp))
return BOX_CJK;
return BOX_LATIN;
}
/*
* Determine glue type between two boxes
*/
static uint8_t glue_between(uint8_t prev_end, uint8_t curr_start)
{
if (prev_end == BOX_SPACE || curr_start == BOX_SPACE)
return GLUE_NONE;
bool prev_latin = (prev_end == BOX_LATIN);
bool curr_latin = (curr_start == BOX_LATIN);
if (prev_latin && curr_latin)
return GLUE_LWS;
if (!prev_latin && !curr_latin)
return GLUE_CWS;
return GLUE_MWS;
}
/*
* Split text into boxes with hyphenation
*/
ekp_paragraph_t *ekp_para_create(const char *text, size_t len,
ekp_hyphenator_t *h,
int32_t (*measure_fn)(const char *, size_t))
{
if (!text || len == 0)
return NULL;
ekp_paragraph_t *p = calloc(1, sizeof(*p));
if (!p)
return NULL;
/* Copy text */
p->text = malloc(len + 1);
if (!p->text) {
free(p);
return NULL;
}
memcpy(p->text, text, len);
p->text[len] = '\0';
p->text_len = len;
/* Compute hash for caching */
uint64_t hash = 14695981039346656037ULL;
for (size_t i = 0; i < len; i++) {
hash ^= (uint8_t)text[i];
hash *= 1099511628211ULL;
}
p->hash = hash;
/* First pass: count boxes (rough estimate) */
size_t max_boxes = len + 1;
/* Temporary arrays for first pass */
size_t *box_starts = malloc(max_boxes * sizeof(size_t));
size_t *box_lens = malloc(max_boxes * sizeof(size_t));
uint8_t *box_types = malloc(max_boxes * sizeof(uint8_t));
if (!box_starts || !box_lens || !box_types) {
free(box_starts);
free(box_lens);
free(box_types);
ekp_para_destroy(p);
return NULL;
}
/* Tokenize into boxes */
size_t box_count = 0;
size_t pos = 0;
size_t word_start = 0;
bool in_latin_word = false;
while (pos < len) {
int char_len;
uint32_t cp = utf8_decode(text + pos, &char_len);
uint8_t type = classify_char(cp);
if (in_latin_word) {
if (type != BOX_LATIN) {
/* End Latin word */
box_starts[box_count] = word_start;
box_lens[box_count] = pos - word_start;
box_types[box_count] = BOX_LATIN;
box_count++;
in_latin_word = false;
}
}
if (type == BOX_LATIN) {
if (!in_latin_word) {
word_start = pos;
in_latin_word = true;
}
} else {
/* Non-Latin: each character is its own box */
box_starts[box_count] = pos;
box_lens[box_count] = char_len;
box_types[box_count] = type;
box_count++;
}
pos += char_len;
}
/* Flush final Latin word */
if (in_latin_word) {
box_starts[box_count] = word_start;
box_lens[box_count] = pos - word_start;
box_types[box_count] = BOX_LATIN;
box_count++;
}
/* Hyphenation: expand Latin words */
size_t *hyphen_pos = malloc(max_boxes * sizeof(size_t));
size_t hyphen_count = 0;
/* Estimate expanded size */
size_t expanded_boxes = box_count * 2;
ekp_box_t *boxes = calloc(expanded_boxes, sizeof(ekp_box_t));
if (!boxes || !hyphen_pos) {
free(box_starts);
free(box_lens);
free(box_types);
free(hyphen_pos);
free(boxes);
ekp_para_destroy(p);
return NULL;
}
size_t final_count = 0;
for (size_t i = 0; i < box_count; i++) {
const char *box_text = text + box_starts[i];
size_t box_len = box_lens[i];
uint8_t type = box_types[i];
if (type == BOX_LATIN && h && box_len > 4) {
/* Try hyphenation */
int8_t positions[EKP_MAX_WORD_LEN];
int pos_count = ekp_hyphen_word(h, box_text, box_len,
positions, EKP_MAX_WORD_LEN);
if (pos_count > 0) {
/* Split at hyphenation points */
size_t prev_split = 0;
for (int j = 0; j < pos_count; j++) {
size_t split = positions[j];
if (split <= prev_split || split >= box_len)
continue;
boxes[final_count].text = box_text + prev_split;
boxes[final_count].text_len = split - prev_split;
boxes[final_count].box_type = BOX_LATIN;
boxes[final_count].start_type = BOX_LATIN;
boxes[final_count].end_type = BOX_LATIN;
boxes[final_count].pixel_width =
measure_fn ? measure_fn(boxes[final_count].text,
boxes[final_count].text_len) : 0;
hyphen_pos[hyphen_count++] = final_count;
final_count++;
prev_split = split;
}
/* Final segment */
if (prev_split < box_len) {
boxes[final_count].text = box_text + prev_split;
boxes[final_count].text_len = box_len - prev_split;
boxes[final_count].box_type = BOX_LATIN;
boxes[final_count].start_type = BOX_LATIN;
boxes[final_count].end_type = BOX_LATIN;
boxes[final_count].pixel_width =
measure_fn ? measure_fn(boxes[final_count].text,
boxes[final_count].text_len) : 0;
final_count++;
}
continue;
}
}
/* No hyphenation */
boxes[final_count].text = box_text;
boxes[final_count].text_len = box_len;
boxes[final_count].box_type = type;
boxes[final_count].start_type = type;
boxes[final_count].end_type = type;
boxes[final_count].pixel_width =
measure_fn ? measure_fn(box_text, box_len) : 0;
final_count++;
}
free(box_starts);
free(box_lens);
free(box_types);
/* Build final arrays */
p->boxes = boxes;
p->box_count = final_count;
/* Hyphenation positions */
p->hyphen_positions = malloc(hyphen_count * sizeof(int32_t));
if (p->hyphen_positions) {
for (size_t i = 0; i < hyphen_count; i++) {
p->hyphen_positions[i] = hyphen_pos[i];
}
p->hyphen_count = hyphen_count;
}
free(hyphen_pos);
/* Hyphen width */
p->hyphen_width = measure_fn ? measure_fn("-", 1) : 5;
/* Build glues */
p->glues = calloc(final_count, sizeof(ekp_glue_t));
if (!p->glues) {
ekp_para_destroy(p);
return NULL;
}
if (!ekp_global) {
ekp_para_destroy(p);
return NULL;
}
ekp_spacing_t *sp = &ekp_global->spacing;
for (size_t i = 0; i < final_count; i++) {
/* Check if after hyphenation point */
bool after_hyphen = false;
for (size_t j = 0; j < p->hyphen_count; j++) {
if ((size_t)(p->hyphen_positions[j] + 1) == i) {
after_hyphen = true;
break;
}
}
if (after_hyphen || i == 0) {
p->glues[i].type = GLUE_NONE;
continue;
}
uint8_t prev_end = boxes[i - 1].end_type;
uint8_t curr_start = boxes[i].start_type;
uint8_t gtype = glue_between(prev_end, curr_start);
p->glues[i].type = gtype;
switch (gtype) {
case GLUE_LWS:
p->glues[i].ideal = sp->lws_ideal;
p->glues[i].stretch = sp->lws_stretch;
p->glues[i].shrink = sp->lws_shrink;
break;
case GLUE_MWS:
p->glues[i].ideal = sp->mws_ideal;
p->glues[i].stretch = sp->mws_stretch;
p->glues[i].shrink = sp->mws_shrink;
break;
case GLUE_CWS:
p->glues[i].ideal = sp->cws_ideal;
p->glues[i].stretch = sp->cws_stretch;
p->glues[i].shrink = sp->cws_shrink;
break;
default:
break;
}
}
/* Build prefix sums for O(1) range queries */
p->ideal_prefix = calloc(final_count + 1, sizeof(int32_t));
p->min_prefix = calloc(final_count + 1, sizeof(int32_t));
p->max_prefix = calloc(final_count + 1, sizeof(int32_t));
if (!p->ideal_prefix || !p->min_prefix || !p->max_prefix) {
ekp_para_destroy(p);
return NULL;
}
for (size_t i = 0; i < final_count; i++) {
int32_t box_w = boxes[i].pixel_width;
int32_t glue_ideal = p->glues[i].ideal;
int32_t glue_stretch = p->glues[i].stretch;
int32_t glue_shrink = p->glues[i].shrink;
p->ideal_prefix[i + 1] = p->ideal_prefix[i] + box_w + glue_ideal;
p->min_prefix[i + 1] = p->min_prefix[i] + box_w + (glue_ideal - glue_shrink);
p->max_prefix[i + 1] = p->max_prefix[i] + box_w + (glue_ideal + glue_stretch);
}
return p;
}
void ekp_para_destroy(ekp_paragraph_t *p)
{
if (!p)
return;
free(p->text);
free(p->boxes);
free(p->glues);
free(p->hyphen_positions);
free(p->ideal_prefix);
free(p->min_prefix);
free(p->max_prefix);
free(p);
}