WikiLinks

This commit is contained in:
2026-08-15 03:15:01 +02:00
parent c7cc374cc1
commit b9e081b13d
2 changed files with 19 additions and 53 deletions
+2
View File
@@ -1,5 +1,7 @@
# racket-wiki
Version 0.2.30 tightens implicit wiki links to classic WikiWords: only letter-only CamelCase words with at least two capitalized word segments are treated as implicit links. Version-like identifiers and words containing digits or punctuation are not WikiWords.
Current development version: **0.2.29**.
A small self-hosted wiki with a Racket backend and an HTML5/CSS/JavaScript frontend.
+17 -53
View File
@@ -158,31 +158,21 @@
.replace(/[^\p{L}\p{N}]+/gu, "");
}
function camelCaseParts(text) {
return String(text || "").match(/[A-Z][a-z0-9]+(?:[A-Z][A-Za-z0-9]*)+/g) || [];
}
function splitCamelCase(text) {
function wikiWordParts(text) {
const value = String(text || "");
if (!/^[\p{L}\p{N}]+$/u.test(value)) return [];
const separated = value
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
const parts = separated.split(/\s+/).filter(Boolean);
return parts.length >= 2 ? parts : [];
if (!/^(?:\p{Lu}\p{Ll}+){2,}$/u.test(value)) return [];
return value.match(/\p{Lu}\p{Ll}+/gu) || [];
}
function camelCaseTarget(text, aliases) {
const parts = wikiWordParts(text);
if (parts.length === 0) return null;
const existing = aliases.get(normalizeMentionText(text));
if (existing) {
return { slug: existing.slug, title: existing.title };
}
const parts = splitCamelCase(text);
if (parts.length === 0) return null;
return {
slug: parts.map((part) => part.toLocaleLowerCase()).join("-"),
title: parts.join(" ")
@@ -195,7 +185,9 @@
for (const value of values) {
if (!value) continue;
aliases.add(value);
for (const camel of camelCaseParts(value)) aliases.add(camel);
for (const wikiWord of value.match(/(?:\p{Lu}\p{Ll}+){2,}/gu) || []) {
aliases.add(wikiWord);
}
}
return Array.from(aliases)
.map((value) => ({ text: value, key: normalizeMentionText(value) }))
@@ -268,45 +260,17 @@
}
const protectedRanges = markdownProtectedRanges(line);
const words = Array.from(line.matchAll(/[\p{L}\p{N}]+/gu));
const words = Array.from(line.matchAll(/\p{L}+/gu));
const replacements = [];
let wordIndex = 0;
while (wordIndex < words.length) {
let match = null;
const maxWords = Math.min(5, words.length - wordIndex);
for (const word of words) {
const start = word.index;
const end = start + word[0].length;
if (positionIsProtected(start, end, protectedRanges)) continue;
for (let count = maxWords; count >= 1; count -= 1) {
const first = words[wordIndex];
const last = words[wordIndex + count - 1];
const start = first.index;
const end = last.index + last[0].length;
if (positionIsProtected(start, end, protectedRanges)) continue;
const candidate = line.slice(start, end);
const page = aliases.get(normalizeMentionText(candidate));
if (page) {
match = { start, end, page };
break;
}
if (count === 1) {
const target = camelCaseTarget(candidate, aliases);
if (target) {
match = { start, end, page: target };
break;
}
}
}
if (match) {
replacements.push(match);
while (wordIndex < words.length && words[wordIndex].index < match.end) {
wordIndex += 1;
}
} else {
wordIndex += 1;
const target = camelCaseTarget(word[0], aliases);
if (target) {
replacements.push({ start, end, page: target });
}
}