// Extract publication identifier from target address (pubkey:d-tag)
const pubParts = targetAddress.split(':');
if (pubParts.length >= 3) {
const pubKey = pubParts[1];
const pubDtag = pubParts[2];
// Find sections that belong to this publication
for (const section of allSections) {
const sectionId = section.id;
const sectionParts = sectionId.split(':');
if (sectionParts.length >= 3 && sectionParts[1] === pubKey) {
// This section belongs to the same publication
matchingSections.push(section as HTMLElement);
}
}
}
if (matchingSections.length > 0) {
// Create a container to search across all matching sections
// We'll search each section individually
console.debug(`[HighlightLayer] Found ${matchingSections.length} sections for publication ${targetAddress}, searching for text: "${text.substring(0,50)}"`);
console.debug(`[HighlightLayer] Text not found in publication sections yet, retrying in ${delay}ms (attempt ${currentRetries+1}/${MAX_TEXT_HIGHLIGHT_RETRIES})`);
console.debug(`[HighlightLayer] Text "${text}" not found in publication sections after ${MAX_TEXT_HIGHLIGHT_RETRIES} retries (content may not be loaded yet)`);
}
return;
}
} else {
// No sections found for this publication - might not be loaded yet
console.debug(`[HighlightLayer] No sections found for publication ${targetAddress}, retrying in ${delay}ms (attempt ${currentRetries+1}/${MAX_TEXT_HIGHLIGHT_RETRIES})`);
// Only warn if we truly couldn't find sections after all retries
console.debug(`[HighlightLayer] No sections found for publication ${targetAddress} after ${MAX_TEXT_HIGHLIGHT_RETRIES} retries (sections may not be loaded yet)`);
return;
}
}
} else {
// Section-scoped highlight - search in specific section element
console.debug(`[HighlightLayer] Section element not found for ${targetAddress}, retrying in ${delay}ms (attempt ${currentRetries+1}/${MAX_TEXT_HIGHLIGHT_RETRIES})`);
// Only warn if we truly couldn't find the section after all retries
console.debug(`[HighlightLayer] Section element not found for ${targetAddress} after ${MAX_TEXT_HIGHLIGHT_RETRIES} retries (section may not be loaded yet)`);
return;
}
}
}
}
// If no target address, use containerRef if available, otherwise document
if (!targetAddress) {
if (containerRef) {
searchRoot = containerRef;
console.debug(`[HighlightLayer] No target address, searching in containerRef for text: "${text.substring(0,50)}"`);
} else {
searchRoot = document;
console.debug(`[HighlightLayer] No target address and no containerRef, searching in document for text: "${text.substring(0,50)}"`);
}
}
if (!searchRoot) {
return;
}
// Use TreeWalker to find all text nodes
const walker = document.createTreeWalker(
searchRoot,
@ -318,39 +645,216 @@
@@ -318,39 +645,216 @@
null,
);
// AI-NOTE: First, check if the text exists in the full content
// This helps us know if we should continue searching
// Normalize whitespace for matching - highlights may have different whitespace than DOM
console.debug(`[HighlightLayer] Section content not loaded yet for ${targetAddress}, retrying in ${delay}ms (attempt ${currentRetries+1}/${MAX_TEXT_HIGHLIGHT_RETRIES})`);
console.debug(`[HighlightLayer] Text "${text}" not found in content yet, retrying in ${delay}ms (attempt ${currentRetries+1}/${MAX_TEXT_HIGHLIGHT_RETRIES}). Full text (first 200 chars): "${fullText.substring(0,200)}"`);
if (regexMatch && regexMatch.index !== undefined) {
actualIndex = regexMatch.index;
// Update the search text to the actual matched text for highlighting
const actualMatchText = regexMatch[0];
// Use the actual matched text length for highlighting
const before = nodeText.substring(0, actualIndex);
const matchedText = actualMatchText;
const after = nodeText.substring(actualIndex + actualMatchText.length);
const parent = textNode.parentNode;
if (!parent) {
console.warn(`[HighlightLayer] Text node has no parent, skipping`);
continue;
}
// Skip if already highlighted
if (
parent.nodeName === "MARK" ||
(parent instanceof Element && parent.classList?.contains("highlight"))
) {
console.debug(`[HighlightLayer] Text node already highlighted, skipping`);
continue;
}
console.debug(`[HighlightLayer] Found match at index ${actualIndex}: "${matchedText}" in node: "${nodeText.substring(0,100)}${nodeText.length>100?'...':''}"`);
// Replace the text node with the highlighted version
const fragment = document.createDocumentFragment();
if (before) fragment.appendChild(document.createTextNode(before));
fragment.appendChild(highlightSpan);
if (after) fragment.appendChild(document.createTextNode(after));
parent.replaceChild(fragment, textNode);
console.debug(
`[HighlightLayer] Successfully highlighted text "${matchedText}" at index ${actualIndex} in node with text: "${nodeText.substring(0,50)}${nodeText.length>50?"...":""}"`,
);
// Clear retry count on success
if (targetAddress) {
const retryKey = `${targetAddress}:${text}`;
textHighlightRetries.delete(retryKey);
}
return; // Only highlight first occurrence to avoid multiple highlights
}
} else {
// Found exact match (case-insensitive) - use it directly
index = actualIndex;
}
}
// Fallback: try exact match (case-sensitive) if normalized match failed
if (index === -1) {
index = nodeText.indexOf(text);
}
// Fallback: try case-insensitive exact match
if (index === -1) {
const lowerNodeText = nodeText.toLowerCase();
const lowerSearchText = text.toLowerCase();
index = lowerNodeText.indexOf(lowerSearchText);
}
if (index !== -1) {
const parent = textNode.parentNode;
if (!parent) continue;
if (!parent) {
console.warn(`[HighlightLayer] Text node has no parent, skipping`);
continue;
}
// Skip if already highlighted
if (
parent.nodeName === "MARK" ||
(parent instanceof Element && parent.classList?.contains("highlight"))
) {
console.debug(`[HighlightLayer] Text node already highlighted, skipping`);
continue;
}
// Find the actual match - use the original text length
const matchLength = text.length;
const before = nodeText.substring(0, index);
const match = nodeText.substring(index, index + text.length);
const after = nodeText.substring(index + text.length);
const match = nodeText.substring(index, index + matchLength);
const after = nodeText.substring(index + matchLength);
console.debug(`[HighlightLayer] Found match at index ${index}: "${match}" in node: "${nodeText.substring(0,100)}${nodeText.length>100?'...':''}"`);
highlightSpan.style.color = "inherit"; // Ensure text color is visible
highlightSpan.style.fontWeight = "inherit"; // Preserve font weight
highlightSpan.textContent = match;
// Replace the text node with the highlighted version
@ -361,35 +865,92 @@
@@ -361,35 +865,92 @@
parent.replaceChild(fragment, textNode);
console.debug(
`[HighlightLayer] Successfully highlighted text "${match}" at index ${index} in node with text: "${nodeText.substring(0,50)}${nodeText.length>50?"...":""}"`,
);
// Clear retry count on success
if (targetAddress) {
const retryKey = `${targetAddress}:${text}`;
textHighlightRetries.delete(retryKey);
}
return; // Only highlight first occurrence to avoid multiple highlights
}
}
// AI-NOTE: If no match found, log for debugging
console.warn(
`[HighlightLayer] Could not find highlight text "${text}" in section. Searched ${textNodes.length} text nodes.`,
);
if (textNodes.length > 0) {
// Log more text nodes and their full content to help debug
const sampleNodes = textNodes.slice(0, 10);
console.debug(
`[HighlightLayer] Sample text nodes (first 10):`,
sampleNodes.map((n, i) => ({
index: i,
text: n.textContent?.substring(0, 100) || "",
fullLength: n.textContent?.length || 0,
parentTag: n.parentElement?.tagName || "unknown",
})),
);
// Also log the full text content of the search root to see what's actually there
if (searchRoot instanceof HTMLElement) {
const fullText = searchRoot.textContent || "";
console.debug(
`[HighlightLayer] Full text content of search root (first 500 chars): "${fullText.substring(0,500)}"`,
);
console.debug(
`[HighlightLayer] Full text contains "${text}": ${fullText.toLowerCase().includes(text.toLowerCase())}`,
);
}
}
}
/**
* Render all highlights on the page
*/
function renderHighlights() {
if (!visible || !containerRef) {
if (!visible) {
return;
}
if (highlights.length === 0) {
if (filteredHighlights.length === 0) {
return;
}
// Clear existing highlights
clearHighlights();
// AI-NOTE: When viewing a section directly (leaf), containerRef might not be set
// But we can still highlight by searching the document for section elements
// Only clear highlights if containerRef exists, otherwise clear from document
console.debug(`[PublicationSection] Found reply ${comment.id?.substring(0,8)} to matching comment ${parentId.substring(0,8)} (NIP-22)`);
break; // Found a match, no need to check other e tags
}
}
}
}
const filtered = Array.from(allMatchingComments);
console.debug(`[PublicationSection] Filtered ${filtered.length} comments (${directComments.length} direct, ${filtered.length-directComments.length} replies) for section ${address} from ${allComments.length} total comments`);
// AI-NOTE: Debug logging to check for nested replies in filtered comments
const filteredCommentIds = new Set(filtered.map(c => c.id?.toLowerCase()).filter(Boolean));
console.debug(`[SectionComments] renderReplies: No replies found for ${normalizedParentId.substring(0,8)}, but found similar IDs:`, similarIds.map(id => id.substring(0, 8)));
} else {
console.debug(`[SectionComments] renderReplies: No replies found for ${normalizedParentId.substring(0,8)}. Map has ${repliesMap.size} entries.`);
{console.debug(`[SectionComments] ✓ Found ${nestedReplies.length}nestedrepliesforreply${reply.id?.substring(0,8)},renderingthemnow`)}
{:else}
{console.debug(`[SectionComments] ✗ No nested replies found for reply ${reply.id?.substring(0,8)}.Maphas${threadStructure.repliesByParent.size}entries.`)}
The app also has a powerful search interface, a composition form, and a universal publisher.
It comes along wtih two other apps, Jumble and Wikistr. <ahref="https://jumble.imwald.eu/notes/naddr1qvzqqqr4tqpzq4ekxjmysc6vhtgs7fz3wasgn63ppyxegplhzh5rc4mmgcg6umkuqyw8wumn8ghj7argv43kjarpv3jkctnwdaehgu339e3k7mf0qpg9g6r9942xzmr994hkvt2sv46x2u3d2fskycnfwskkzmny948hg6r9wfej6cne94px2ct5wf5hst2sda68getj94mz6st4v35k7cn0da4hxttxwfhk6t2vd938yetkdauqrhxcs8"target="_blank">Jumble</a> is a meant as a daily driver, but also supports
basic Alexandria features. <ahref="https://wikistr.imwald.eu/jane-eyre-an-autobiography-by-charlotte-bront%C3%AB-v-3rd-edition*fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1"target="_blank">Wikistr</a> is
a more advanced app, with a focus on wiki pages and collaboration. It contains
the exporting function, utilizing an Asciidoctor server to download publications as Asciidoc, PDFs, EPUB, or HTML files.
</P>
<Pclass="mb-3">
Thank you for your time. Feel free to explore the app and see how it works, and make sure to <button