- 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>
322 lines
9.0 KiB
C
322 lines
9.0 KiB
C
/*
|
|
* ekp_hyphen.c - Liang hyphenation algorithm implementation
|
|
*
|
|
* 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.
|
|
*
|
|
* Fast, thread-safe hyphenation with pattern caching.
|
|
* Uses FNV-1a hash for O(1) pattern lookup.
|
|
*/
|
|
|
|
#include "ekp_module.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
/* FNV-1a hash constants */
|
|
#define FNV_OFFSET 14695981039346656037ULL
|
|
#define FNV_PRIME 1099511628211ULL
|
|
|
|
static uint64_t fnv1a_hash(const char *data, size_t len)
|
|
{
|
|
uint64_t hash = FNV_OFFSET;
|
|
for (size_t i = 0; i < len; i++) {
|
|
hash ^= (uint8_t)data[i];
|
|
hash *= FNV_PRIME;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
/*
|
|
* Parse a pattern like "hy3ph" into letters and values.
|
|
* Returns true on success.
|
|
*/
|
|
static bool parse_pattern(const char *pat, ekp_pattern_t *out)
|
|
{
|
|
size_t pat_len = strlen(pat);
|
|
if (pat_len == 0 || pat_len >= EKP_MAX_PATTERN_LEN * 2)
|
|
return false;
|
|
|
|
size_t pos = 0;
|
|
size_t letter_idx = 0;
|
|
size_t value_idx = 0;
|
|
|
|
memset(out->values, 0, sizeof(out->values));
|
|
memset(out->letters, 0, sizeof(out->letters));
|
|
|
|
while (pos < pat_len) {
|
|
/* Read optional digit */
|
|
uint8_t digit = 0;
|
|
if (isdigit((unsigned char)pat[pos])) {
|
|
digit = pat[pos] - '0';
|
|
pos++;
|
|
}
|
|
out->values[value_idx++] = digit;
|
|
|
|
/* Read letter if present */
|
|
if (pos < pat_len && !isdigit((unsigned char)pat[pos])) {
|
|
out->letters[letter_idx++] = pat[pos];
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
out->len = letter_idx;
|
|
|
|
/* Find non-zero range */
|
|
size_t start = 0, end = value_idx;
|
|
while (start < end && out->values[start] == 0) start++;
|
|
while (end > start && out->values[end - 1] == 0) end--;
|
|
|
|
out->offset = start;
|
|
|
|
/* Shift values to start */
|
|
if (start > 0) {
|
|
memmove(out->values, out->values + start, end - start);
|
|
memset(out->values + (end - start), 0, start);
|
|
}
|
|
|
|
return letter_idx > 0;
|
|
}
|
|
|
|
/*
|
|
* Load patterns from .dic file
|
|
*/
|
|
ekp_hyphenator_t *ekp_hyphen_create(const char *dict_path)
|
|
{
|
|
FILE *fp = fopen(dict_path, "r");
|
|
if (!fp)
|
|
return NULL;
|
|
|
|
ekp_hyphenator_t *h = calloc(1, sizeof(*h));
|
|
if (!h) {
|
|
fclose(fp);
|
|
return NULL;
|
|
}
|
|
|
|
pthread_rwlock_init(&h->lock, NULL);
|
|
h->left_min = 2;
|
|
h->right_min = 2;
|
|
|
|
/* First pass: count patterns */
|
|
char line[256];
|
|
size_t count = 0;
|
|
|
|
if (!fgets(line, sizeof(line), fp)) {
|
|
/* empty file: no encoding line to skip; count loop sees EOF */
|
|
}
|
|
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
size_t len = strlen(line);
|
|
if (len > 0 && line[len - 1] == '\n')
|
|
line[--len] = '\0';
|
|
|
|
/* Skip empty, comments, HYPHENMIN, patterns with / */
|
|
if (len == 0 || line[0] == '%' || line[0] == '#')
|
|
continue;
|
|
if (strstr(line, "HYPHENMIN") || strchr(line, '/'))
|
|
continue;
|
|
|
|
count++;
|
|
}
|
|
|
|
/* Allocate patterns */
|
|
h->patterns = calloc(count, sizeof(ekp_pattern_t));
|
|
h->hash_size = count * 2; /* load factor 0.5 */
|
|
h->hash_table = calloc(h->hash_size, sizeof(uint32_t));
|
|
|
|
if (!h->patterns || !h->hash_table) {
|
|
ekp_hyphen_destroy(h);
|
|
fclose(fp);
|
|
return NULL;
|
|
}
|
|
|
|
/* Second pass: parse patterns */
|
|
rewind(fp);
|
|
if (!fgets(line, sizeof(line), fp)) {
|
|
/* empty file: no encoding line to skip; parse loop sees EOF */
|
|
}
|
|
|
|
size_t idx = 0;
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
size_t len = strlen(line);
|
|
if (len > 0 && line[len - 1] == '\n')
|
|
line[--len] = '\0';
|
|
|
|
if (len == 0 || line[0] == '%' || line[0] == '#')
|
|
continue;
|
|
if (strstr(line, "HYPHENMIN") || strchr(line, '/'))
|
|
continue;
|
|
|
|
/* Handle ^^XX hex escapes */
|
|
char decoded[256];
|
|
char *dst = decoded;
|
|
const char *src = line;
|
|
while (*src) {
|
|
if (src[0] == '^' && src[1] == '^' &&
|
|
isxdigit((unsigned char)src[2]) &&
|
|
isxdigit((unsigned char)src[3])) {
|
|
char hex[3] = {src[2], src[3], 0};
|
|
*dst++ = (char)strtol(hex, NULL, 16);
|
|
src += 4;
|
|
} else {
|
|
*dst++ = *src++;
|
|
}
|
|
}
|
|
*dst = '\0';
|
|
|
|
if (parse_pattern(decoded, &h->patterns[idx])) {
|
|
/* Insert into hash table */
|
|
uint64_t hash = fnv1a_hash(h->patterns[idx].letters,
|
|
h->patterns[idx].len);
|
|
size_t slot = hash % h->hash_size;
|
|
|
|
while (h->hash_table[slot] != 0) {
|
|
slot = (slot + 1) % h->hash_size;
|
|
}
|
|
h->hash_table[slot] = idx + 1; /* 1-indexed */
|
|
|
|
if (h->patterns[idx].len > h->max_pattern_len)
|
|
h->max_pattern_len = h->patterns[idx].len;
|
|
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
h->pattern_count = idx;
|
|
fclose(fp);
|
|
return h;
|
|
}
|
|
|
|
void ekp_hyphen_destroy(ekp_hyphenator_t *h)
|
|
{
|
|
if (!h) return;
|
|
pthread_rwlock_destroy(&h->lock);
|
|
free(h->patterns);
|
|
free(h->hash_table);
|
|
free(h);
|
|
}
|
|
|
|
/*
|
|
* Find pattern by letters (hash table lookup)
|
|
*/
|
|
static ekp_pattern_t *find_pattern(ekp_hyphenator_t *h,
|
|
const char *letters, size_t len)
|
|
{
|
|
if (len == 0 || len > h->max_pattern_len)
|
|
return NULL;
|
|
|
|
uint64_t hash = fnv1a_hash(letters, len);
|
|
size_t slot = hash % h->hash_size;
|
|
|
|
for (size_t i = 0; i < h->hash_size; i++) {
|
|
uint32_t idx = h->hash_table[slot];
|
|
if (idx == 0)
|
|
return NULL;
|
|
|
|
ekp_pattern_t *p = &h->patterns[idx - 1];
|
|
if (p->len == len && memcmp(p->letters, letters, len) == 0)
|
|
return p;
|
|
|
|
slot = (slot + 1) % h->hash_size;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Compute hyphenation positions for a word
|
|
* Thread-safe (read lock)
|
|
*/
|
|
int ekp_hyphen_word(ekp_hyphenator_t *h, const char *word, size_t len,
|
|
int8_t *positions, size_t max_pos)
|
|
{
|
|
if (!h || !word || len == 0 || len > EKP_MAX_WORD_LEN - 2)
|
|
return 0;
|
|
|
|
/* Check cache first */
|
|
uint64_t word_hash = fnv1a_hash(word, len);
|
|
size_t cache_slot = word_hash % EKP_CACHE_SIZE;
|
|
|
|
pthread_rwlock_rdlock(&h->lock);
|
|
|
|
if (h->cache[cache_slot].hash == word_hash &&
|
|
strncmp(h->cache[cache_slot].word, word, len) == 0) {
|
|
int count = h->cache[cache_slot].pos_count;
|
|
if (count <= (int)max_pos) {
|
|
memcpy(positions, h->cache[cache_slot].positions,
|
|
count * sizeof(int8_t));
|
|
}
|
|
pthread_rwlock_unlock(&h->lock);
|
|
return count;
|
|
}
|
|
|
|
pthread_rwlock_unlock(&h->lock);
|
|
|
|
/* Compute hyphenation */
|
|
char padded[EKP_MAX_WORD_LEN + 2];
|
|
padded[0] = '.';
|
|
for (size_t i = 0; i < len; i++)
|
|
padded[i + 1] = tolower((unsigned char)word[i]);
|
|
padded[len + 1] = '.';
|
|
size_t padded_len = len + 2;
|
|
|
|
uint8_t prio[EKP_MAX_WORD_LEN + 3];
|
|
memset(prio, 0, sizeof(prio));
|
|
|
|
/* Apply matching patterns */
|
|
pthread_rwlock_rdlock(&h->lock);
|
|
|
|
for (size_t i = 0; i < padded_len - 1; i++) {
|
|
for (size_t j = i + 1; j <= padded_len && j <= i + h->max_pattern_len; j++) {
|
|
ekp_pattern_t *pat = find_pattern(h, padded + i, j - i);
|
|
if (pat) {
|
|
size_t val_len = pat->len + 1 - pat->offset;
|
|
for (size_t k = 0; k < val_len && k < sizeof(pat->values); k++) {
|
|
size_t pos = i + pat->offset + k;
|
|
if (pos < sizeof(prio) && pat->values[k] > prio[pos])
|
|
prio[pos] = pat->values[k];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pthread_rwlock_unlock(&h->lock);
|
|
|
|
/* Collect odd positions (subtract 1 for padding offset) */
|
|
int8_t result[EKP_MAX_WORD_LEN];
|
|
int count = 0;
|
|
|
|
for (size_t i = 1; i < padded_len && count < EKP_MAX_WORD_LEN; i++) {
|
|
if (prio[i] & 1) { /* odd = break allowed */
|
|
int pos = (int)i - 1; /* adjust for leading '.' */
|
|
/* Apply margin constraints */
|
|
if (pos >= h->left_min && pos <= (int)len - h->right_min) {
|
|
result[count++] = pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Update cache */
|
|
pthread_rwlock_wrlock(&h->lock);
|
|
|
|
h->cache[cache_slot].hash = word_hash;
|
|
strncpy(h->cache[cache_slot].word, word, len);
|
|
h->cache[cache_slot].word[len] = '\0';
|
|
memcpy(h->cache[cache_slot].positions, result, count * sizeof(int8_t));
|
|
h->cache[cache_slot].pos_count = count;
|
|
|
|
pthread_rwlock_unlock(&h->lock);
|
|
|
|
/* Copy to output */
|
|
int out_count = count < (int)max_pos ? count : (int)max_pos;
|
|
memcpy(positions, result, out_count * sizeof(int8_t));
|
|
return out_count;
|
|
}
|