CamelCaseLinks
This commit is contained in:
+110
-32
@@ -162,6 +162,33 @@
|
||||
return String(text || "").match(/[A-Z][a-z0-9]+(?:[A-Z][A-Za-z0-9]*)+/g) || [];
|
||||
}
|
||||
|
||||
function splitCamelCase(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 : [];
|
||||
}
|
||||
|
||||
function camelCaseTarget(text, aliases) {
|
||||
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(" ")
|
||||
};
|
||||
}
|
||||
|
||||
function mentionAliases(page) {
|
||||
const aliases = new Set();
|
||||
const values = [page.title, slugTitle(page.slug), page.slug.replaceAll("-", " ")];
|
||||
@@ -190,72 +217,122 @@
|
||||
return map;
|
||||
}
|
||||
|
||||
function linkUnlinkedMentions(html, currentSlug = null) {
|
||||
const host = document.createElement("div");
|
||||
host.innerHTML = html || "";
|
||||
function markdownProtectedRanges(line) {
|
||||
const ranges = [];
|
||||
const patterns = [
|
||||
/todo\([^()\n]*\)/gi,
|
||||
/!?\[[^\]\n]*\]\([^\)\n]*\)/g,
|
||||
/`+[^`\n]*`+/g,
|
||||
/<https?:\/\/[^>\n]+>/gi,
|
||||
/https?:\/\/[^\s<>()]+/gi
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for (const match of line.matchAll(pattern)) {
|
||||
ranges.push({ start: match.index, end: match.index + match[0].length });
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function positionIsProtected(start, end, ranges) {
|
||||
for (const range of ranges) {
|
||||
if (start < range.end && end > range.start) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function expandWikiMentions(markdown, currentSlug = null) {
|
||||
const aliases = pageMentionMap(currentSlug);
|
||||
if (aliases.size === 0) return host.innerHTML;
|
||||
const lines = String(markdown || "").split("\n");
|
||||
const result = [];
|
||||
let fence = null;
|
||||
|
||||
const walker = document.createTreeWalker(host, NodeFilter.SHOW_TEXT);
|
||||
const textNodes = [];
|
||||
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
||||
for (const line of lines) {
|
||||
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
||||
if (fenceMatch) {
|
||||
const marker = fenceMatch[1].charAt(0);
|
||||
if (fence === null) {
|
||||
fence = marker;
|
||||
} else if (fence === marker) {
|
||||
fence = null;
|
||||
}
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const textNode of textNodes) {
|
||||
const parent = textNode.parentElement;
|
||||
if (!parent || parent.closest("a, code, pre, script, style, textarea")) continue;
|
||||
const text = textNode.nodeValue || "";
|
||||
const words = Array.from(text.matchAll(/[\p{L}\p{N}]+/gu));
|
||||
if (words.length === 0) continue;
|
||||
if (fence !== null || /^\s{4}/.test(line)) {
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const protectedRanges = markdownProtectedRanges(line);
|
||||
const words = Array.from(line.matchAll(/[\p{L}\p{N}]+/gu));
|
||||
const replacements = [];
|
||||
let wordIndex = 0;
|
||||
|
||||
while (wordIndex < words.length) {
|
||||
let match = null;
|
||||
const maxWords = Math.min(5, words.length - wordIndex);
|
||||
|
||||
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;
|
||||
const candidate = text.slice(start, end);
|
||||
|
||||
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;
|
||||
while (wordIndex < words.length && words[wordIndex].index < match.end) {
|
||||
wordIndex += 1;
|
||||
}
|
||||
} else {
|
||||
wordIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (replacements.length === 0) continue;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let position = 0;
|
||||
for (const replacement of replacements) {
|
||||
if (replacement.start > position) fragment.append(document.createTextNode(text.slice(position, replacement.start)));
|
||||
const link = document.createElement("a");
|
||||
link.href = `#/${encodeURIComponent(replacement.page.slug)}`;
|
||||
link.className = "wiki-auto-link";
|
||||
link.title = tr("automatic-wiki-link", "Automatic wiki link");
|
||||
link.textContent = text.slice(replacement.start, replacement.end);
|
||||
fragment.append(link);
|
||||
position = replacement.end;
|
||||
if (replacements.length === 0) {
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
if (position < text.length) fragment.append(document.createTextNode(text.slice(position)));
|
||||
textNode.replaceWith(fragment);
|
||||
|
||||
let expanded = line;
|
||||
for (let index = replacements.length - 1; index >= 0; index -= 1) {
|
||||
const replacement = replacements[index];
|
||||
const link = `[${replacement.page.title}](#/${encodeURIComponent(replacement.page.slug)})`;
|
||||
expanded = expanded.slice(0, replacement.start) + link + expanded.slice(replacement.end);
|
||||
}
|
||||
result.push(expanded);
|
||||
}
|
||||
|
||||
return host.innerHTML;
|
||||
return result.join("\n");
|
||||
}
|
||||
|
||||
function renderMarkdown(markdown, pageSlug = null) {
|
||||
const html = easyMDE.markdown(expandTodoMarkup(markdown || "", pageSlug));
|
||||
const withWikiLinks = expandWikiMentions(markdown || "", pageSlug);
|
||||
const withTodos = expandTodoMarkup(withWikiLinks, pageSlug);
|
||||
const html = easyMDE.markdown(withTodos);
|
||||
const withImages = applyImageWidthMarkup(html);
|
||||
return DOMPurify.sanitize(linkUnlinkedMentions(withImages, pageSlug));
|
||||
return DOMPurify.sanitize(withImages);
|
||||
}
|
||||
|
||||
function slugTitle(slug) {
|
||||
@@ -999,6 +1076,7 @@
|
||||
|
||||
async function savePage() {
|
||||
const title = $("editor-title").value.trim();
|
||||
const currentSlug = state.editingNew ? state.newPageSlug : state.currentPage?.slug;
|
||||
const markdown = easyMDE.value();
|
||||
const tags = parseTags($("editor-tags").value);
|
||||
const summary = $("edit-summary").value.trim();
|
||||
|
||||
Reference in New Issue
Block a user