*/ private $words = []; /** @var int Cache TTL in seconds (30 days) */ private $ttl = 2592000; /** @var string */ private $userAgent = 'DokuWiki-NaturalTitle/1.0 (https://grimore.org)'; public function __construct() { $this->cacheFile = DOKU_INC . 'data/meta/naturaltitle_acronyms.json'; $this->wordsFile = __DIR__ . '/english-words.txt'; $this->loadWordList($this->wordsFile, $this->words); } /** * Build an Overrides object for the tokens in a title. * * @param string $title A pre-normalized title (result of titleCase()) * @return Overrides */ public function buildOverrides(string $title): Overrides { $tokens = preg_split('/\s+/', $title, -1, PREG_SPLIT_NO_EMPTY); $cache = $this->loadCache(); $titleMap = []; foreach ($tokens as $token) { // Strip trailing punctuation so "API," still matches $bare = rtrim($token, '.,;:!?'); if (!preg_match('/^[A-Za-z]{2,8}$/', $bare)) continue; $lower = strtolower($bare); // Common English word? Not an acronym. if (isset($this->words[$lower])) continue; $upper = strtoupper($bare); if (array_key_exists($upper, $cache)) { if ($cache[$upper] === true) { $titleMap[$lower] = $upper; } continue; } $isAcronym = $this->lookupWikidata($upper); $cache[$upper] = $isAcronym; if ($isAcronym) { $titleMap[$lower] = $upper; } } $this->saveCache($cache); return new Overrides($titleMap, [], []); } /** * Load a plain-text word list (one word per line) into the target set. */ private function loadWordList(string $path, array &$target): void { if (!file_exists($path)) return; $raw = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($raw === false) return; foreach ($raw as $line) { $line = trim(strtolower($line)); if ($line === '' || $line[0] === '#') continue; $target[$line] = true; } } /** * Query Wikidata for whether a term is a known acronym (P1813 short name). */ private function lookupWikidata(string $term): bool { $query = sprintf( 'SELECT ?item WHERE { ?item wdt:P1813 "%s"@en . } LIMIT 1', addslashes($term) ); $url = 'https://query.wikidata.org/sparql?format=json&query=' . urlencode($query); $ctx = stream_context_create([ 'http' => [ 'timeout' => 5, 'header' => 'User-Agent: ' . $this->userAgent . "\r\n", ], ]); $response = @file_get_contents($url, false, $ctx); if ($response === false) return false; $data = json_decode($response, true); return !empty($data['results']['bindings']); } private function loadCache(): array { if (!file_exists($this->cacheFile)) return []; $raw = @file_get_contents($this->cacheFile); if ($raw === false) return []; $data = json_decode($raw, true); if (!is_array($data)) return []; $now = time(); $out = []; foreach ($data as $key => $entry) { if (isset($entry['t'], $entry['v']) && ($now - $entry['t']) <= $this->ttl) { $out[$key] = (bool) $entry['v']; } } return $out; } private function saveCache(array $cache): void { $dir = dirname($this->cacheFile); if (!is_dir($dir)) @mkdir($dir, 0755, true); $now = time(); $out = []; foreach ($cache as $k => $v) { $out[$k] = ['v' => (bool) $v, 't' => $now]; } @file_put_contents($this->cacheFile, json_encode($out), LOCK_EX); } }