[Home] [Overboard] [Catalog] [Search] [Thread list] [Inbox] [Write PM] [Admin]
[Return]

Posting mode: Reply

(for deletion, 8 chars max)
  • Allowed file types are: gif, jpg, jpeg, png, bmp, swf, webm, mp4
  • Maximum file size allowed is 9000 KB.
  • Images greater than 200 * 200 pixels will be thumbnailed.



congratz on setting up the board!


File: 0.jpg
(276 KB, 856x1200)[ImgOps]
276 KB
キタ━━━(゚∀゚)━━━!!
>>
>I never tested what happens if u do this
Test
>>
seems to werk but didn't thoroughly test, teh version with ">" support


// ==UserScript==
// @name Heyuri Backlinks Extended
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Adds backlinks to posts on Heyuri imageboard—including when posts are referenced by filename or partial comment (like >filename.jpg) as well as by post number—for easier navigation.
// @author Your Name
// @match https://*.heyuri.net/*
// @grant none
// ==/UserScript==

(function() {
'use strict';

// Wait for the page to fully load
window.addEventListener('load', function() {

// Collect all posts (both OP and replies) into a map for quick lookup.
const posts = document.querySelectorAll('.post.reply, .post.op');
const postMap = {};
posts.forEach(post => {
// Each post element should have an id like "p48".
postMap[post.id] = post;
});

// --- Helper functions from your preview script ---

// Returns true if the given post appears above the context element.
function isPostAboveContext(post, contextElement) {
const postRect = post.getBoundingClientRect();
const contextRect = contextElement.getBoundingClientRect();
return postRect.top < contextRect.top;
}

// Finds a matching post id by checking the quoted text against previous posts.
// includeUnkfunc is typically true for double-arrow (>>43) quotes and false for single (e.g. >filename.jpg).
function findMatchingPostId(quotedText, contextElement, includeUnkfunc) {
// First, check if the quoted text is in "No. 43" format.
const noMatch = quotedText.match(/^No\. ?(\d+)$/);
if (noMatch) {
const postId = `p${noMatch[1]}`;
const post = document.getElementById(postId);
if (post && isPostAboveContext(post, contextElement)) {
return postId;
}
return 'notFound';
}

// Look only at posts that are above the current (context) post.
const contextRect = contextElement.getBoundingClientRect();
const candidatePosts = Array.from(document.querySelectorAll('.post.reply, .post.op'))
.filter(post => {
const postRect = post.getBoundingClientRect();
return postRect.top < contextRect.top;
})
.reverse(); // Search starting with the most recent

// Try matching based on comment text or filename.
for (const post of candidatePosts) {
const comment = post.querySelector('.comment');
const filenameLink = post.querySelector('.filesize a');
if (comment) {
let textToCheck;
if (!includeUnkfunc) {
// If not including .unkfunc elements (i.e. for non-double quotes), remove them before matching.
const clone = comment.cloneNode(true);
clone.querySelectorAll('.unkfunc').forEach(el => el.remove());
textToCheck = clone.textContent;
} else {
textToCheck = comment.textContent;
}
if (textToCheck.includes(quotedText)) {
return post.id;
}
}
// Also check for an exact filename match.
if (filenameLink) {
const visibleFilename = filenameLink.textContent.trim();
const fullFilename = filenameLink.getAttribute('onmouseover')
?.match(/this\.textContent='([^']+)'/)?.[1] || visibleFilename;
if (fullFilename === quotedText) {
return post.id;
}
}
}
return 'notFound';
}

// --- Backlinks processing ---

// Process each post (the quoting post)
posts.forEach(post => {
// Get the post number from the .postnum .qu element (this labels the backlink).
const quAnchor = post.querySelector('.postnum .qu');
if (!quAnchor) return;
const quotingPostNum = quAnchor.textContent.trim();

// Look for quote triggers inside the comment.
// This covers both standard quotelinks (a.quotelink) and preview triggers (elements with class .unkfunc).
const quoteElements = post.querySelectorAll('.comment a.quotelink, .comment .unkfunc');
quoteElements.forEach(quoteEl => {
const rawText = quoteEl.textContent.trim();
if (!rawText.startsWith('>')) return;

let targetId = null;
// If an href exists and contains a hash, use it (this usually works for numeric quotes like >>43).
const href = quoteEl.getAttribute('href');
if (href && href.indexOf('#') !== -1) {
targetId = href.substring(href.indexOf('#') + 1);
} else {
// Otherwise, use the preview matching logic.
// Remove the first ">"; if an extra ">" is found, mark this as a double-quote.
let quotedText = rawText.slice(1).trim();
let isDoubleQuote = false;
if (quotedText.startsWith('>')) {
isDoubleQuote = true;
quotedText = quotedText.slice(1).trim();
}
// Use the current post as the context element.
targetId = findMatchingPostId(quotedText, post, isDoubleQuote);
}

// Skip if no valid target was found.
if (!targetId || targetId === 'notFound') return;
const targetPost = postMap[targetId];
if (!targetPost) return;

// Look for the backlinks container in the target post.
const backlinksContainer = targetPost.querySelector('.backlinks');
if (!backlinksContainer) return;

// Prevent duplicate backlinks.
if (backlinksContainer.querySelector(`a[href="#${post.id}"]`)) return;

// Create the backlink element.
const backlink = document.createElement('a');
backlink.href = `#${post.id}`;
backlink.textContent = `>>${quotingPostNum}`;
backlink.className = 'backlink';

// If the container already has content, insert a space.
if (backlinksContainer.textContent.trim() !== '') {
backlinksContainer.appendChild(document.createTextNode(' '));
}
backlinksContainer.appendChild(backlink);
});
});
});
})();
>>
seems to be an isolated case of b0rk in the other thread...
>>
test
>>
>seems to be an isolated case of b0rk in the other thread...
I meant teh userscript not thinking >>109 is linking >>104 for some reason, as >>55 does
>>
lolol
>>
huegcolor
>>
ccc[color]ddd[/color]
>>
[color=#ff0000]test[/color=#ff0000][color=#ff0000]
>>
[color=#ff0000][b]test
>>
[color=#ff0000]test
>>
test
>>
test
>>
test
>>
test
>>
test
>>
testtest
>>
I changed NOTHING but I can't get it to b0rk now :unsure:
>>
testtest
>>
testtest
>>
testtest
>>
[bold] [bold and blue] [bold and blue and small] [/bold][bold][color][blue and small] [blue] [/blue][/color][blue][/blue][/bold]
>>
interestan... probably expected that it treats all [text] liek bbcode
maybe it could be changed with more specific bbcode some conditionals so only stuff liek gets treated as bbcode[/text]
>>
(bold) (bold and blue) (bold and blue and small) [color](blue and small) (blue) [/color]
>>
(blue) (blue and small) (blue and small and bold) (small and bold) (bold)
>>
(bold small blue) [color](small blue) [/color][color](blue)[/color]
>>
I finally get it
teh issue happens when bbcode needs to close/reopen the color tags
>>
bold blue[color] blue[/color]
>>
small bold small
>>
bold blue blue
>>
both bold
>>
[1]testlol
>>
a
>>
Test
(function () {
'use strict';

const MIN_WIDTH = 0; // Minimum width of the preview box
const OFFSET_X = 10; // Offset from the mouse cursor
const RIGHT_MARGIN = 30; // Margin from the right edge of the viewport
const PREVIEW_DELAY = 300; // delay before showing preview (ms)
const REMOVAL_DELAY = 50; // delay before removal check (ms)

// Inject style for highlighted quoted text
const highlightStyle = document.createElement('style');
highlightStyle.innerHTML = '.quoteHighlight { background-color: yellow; }';
document.head.appendChild(highlightStyle);

// Each preview object contains:
// { box, trigger, parent, contextPost }
let previewStack = [];

// Creates and appends a preview box.
function createPreviewBox(notFound = false) {
const box = document.createElement('div');
box.classList.add('previewBox');
// Apply basic positioning styles
box.style.position = 'absolute';
box.style.zIndex = '1000';
box.style.boxSizing = 'border-box';
// We use a dynamic min width; for mobile this remains flexible.
box.style.minWidth = `${MIN_WIDTH}px`;
// Set a maxWidth to 90% of viewport width so it doesn’t cover the entire screen.
// Max height is also limited to avoid huge posts overtaking the viewport.
box.style.maxWidth = `${Math.floor(window.innerWidth * 0.9)}px`;
box.style.maxHeight = `${Math.floor(window.innerHeight * 0.9)}px`;
box.style.overflowY = 'auto';
if (notFound) {
box.innerHTML = `
<div class="post reply">
Quote source not found
</div>
`;
}
document.body.appendChild(box);
return box;
}

// Called when hovering over a .unkfunc element.
function startHover(event) {
const trigger = event.currentTarget;
if (trigger.hoverTimeout) return;

// Capture updated mouse position
let latestMouseEvent = event;
function updateMouse(e) {
latestMouseEvent = e;
}
document.addEventListener('mousemove', updateMouse);

trigger.hoverTimeout = setTimeout(() => {
trigger.hoverTimeout = null;
// Remove the mousemove listener now that we have the updated mouse event
document.removeEventListener('mousemove', updateMouse);

const rawText = trigger.textContent;
if (!rawText.startsWith('>')) return;
let quotedText = rawText.slice(1).trim(); // Remove ">" and trim whitespace
const isDoubleQuote = quotedText.startsWith('>');
if (isDoubleQuote) {
quotedText = quotedText.slice(1).trim(); // Remove extra ">" for double >>
}

// Determine proper lookup context:
// If inside a preview, use the original post that was previewed (stored as contextPost).
// Otherwise, use the closest .post element.
let contextElement;
let parentPreviewObj = null;
const parentBox = trigger.closest('.previewBox');
if (parentBox) {
parentPreviewObj = previewStack.find(obj => obj.box === parentBox) || null;
}
if (parentPreviewObj && parentPreviewObj.contextPost) {
contextElement = parentPreviewObj.contextPost;
} else {
contextElement = trigger.closest('.post');
if (!contextElement) return;
}

// Record the parent preview (if any)
let parentPreview = parentPreviewObj || null;

const matchingPostId = findMatchingPostId(quotedText, contextElement, isDoubleQuote);
const post = document.getElementById(matchingPostId);
const isOP = post && post.classList.contains('op');
const previewBox = createPreviewBox(matchingPostId === 'notFound');

// Create a preview object.
// Note: contextPost is the original post element from which the preview was made.
let previewObj = {
box: previewBox,
trigger: trigger,
parent: parentPreview,
contextPost: post ? post : contextElement
};
previewStack.push(previewObj);

if (post && matchingPostId !== 'notFound') {
previewBox.innerHTML = ''; // Clear any existing content

// Clone the entire post and append it to the preview box
const clonedPost = post.cloneNode(true);
clonedPost.removeAttribute('id');
clonedPost.style.margin = '0'; // Remove any margins
previewBox.appendChild(clonedPost);

// Highlight the quoted text inside the preview box
highlightQuotedText(previewBox, quotedText);
}

attachPreviewHoverHandlers(previewObj);

// Apply the hover event listeners to the quotes within the preview box
applyHoverListeners(previewBox);

// Position the preview box according to the mouse position and viewport constraints
positionPreviewBox(previewBox, latestMouseEvent);
previewBox.style.display = 'block';
}, PREVIEW_DELAY);
}

// Clears the hover timeout on the trigger.
function stopHover(event) {
const trigger = event.currentTarget;
if (trigger.hoverTimeout) {
clearTimeout(trigger.hoverTimeout);
trigger.hoverTimeout = null;
}
// Also trigger a global check shortly after the mouse leaves
setTimeout(checkPreviews, REMOVAL_DELAY);
}

// Attach mouseenter/mouseleave handlers to both preview box and trigger.
function attachPreviewHoverHandlers(previewObj) {
const { box, trigger } = previewObj;
box.addEventListener('mouseenter', () => { /* no-op */ });
box.addEventListener('mouseleave', () => {
setTimeout(checkPreviews, REMOVAL_DELAY);
});
trigger.addEventListener('mouseenter', () => { /* no-op */ });
trigger.addEventListener('mouseleave', () => {
setTimeout(checkPreviews, REMOVAL_DELAY);
});
// Update preview position as the mouse moves
trigger.addEventListener('mousemove', (event) => {
positionPreviewBox(box, event);
});
}

// Global removal check:
// Iterate through all previews and remove any (and their descendants)
// that aren’t hovered (or do not have a descendant that is hovered).
function checkPreviews() {
previewStack.slice().forEach(previewObj => {
if (!isPreviewOrDescendantHovered(previewObj)) {
removePreviewAndDescendants(previewObj);
}
});
}

// Recursively checks if this preview or any of its descendants are hovered.
function isPreviewOrDescendantHovered(previewObj) {
if (previewObj.box.matches(':hover') || previewObj.trigger.matches(':hover')) {
return true;
}
const children = previewStack.filter(obj => obj.parent === previewObj);
for (const child of children) {
if (isPreviewOrDescendantHovered(child)) {
return true;
}
}
return false;
}

// Recursively remove the preview and its descendants.
function removePreviewAndDescendants(previewObj) {
const children = previewStack.filter(obj => obj.parent === previewObj);
for (const child of children) {
removePreviewAndDescendants(child);
}
if (previewObj.box.parentNode) {
previewObj.box.parentNode.removeChild(previewObj.box);
}
previewStack = previewStack.filter(obj => obj !== previewObj);
}

// Positions the preview box near the mouse.
function positionPreviewBox(previewBox, event) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

// Update max dimensions to keep the preview from overwhelming the screen.
previewBox.style.maxWidth = `${Math.floor(viewportWidth * 0.9)}px`;
previewBox.style.maxHeight = `${Math.floor(viewportHeight * 0.9)}px`;
previewBox.style.overflowY = 'auto';
previewBox.style.display = 'block'; // Ensure the preview box is displayed for correct measurement

const boxWidth = previewBox.offsetWidth;
let left;
// Decide based on available space on right vs left of the mouse cursor
const availableRight = viewportWidth - event.clientX;
const availableLeft = event.clientX;
if (availableRight > availableLeft) {
left = event.clientX + OFFSET_X;
// Make sure the preview doesn't go off the right edge
if (left + boxWidth > viewportWidth - RIGHT_MARGIN) {
left = viewportWidth - boxWidth - RIGHT_MARGIN;
}
} else {
left = event.clientX - OFFSET_X - boxWidth;
if (left < 0) {
left = 0;
}
}
previewBox.style.left = `${left}px`;

// Determine vertical position
const rect = event.target.getBoundingClientRect();
const previewHeight = previewBox.offsetHeight;
const topBelow = rect.bottom + window.scrollY; // Position below the quote
const topAbove = rect.top + window.scrollY - previewHeight; // Position above the quote

// Check if there's enough space below; if not, use the position above (ensuring it doesn't go off the top)
if (rect.bottom + previewHeight > viewportHeight) {
previewBox.style.top = `${Math.max(topAbove, window.scrollY)}px`;
} else {
previewBox.style.top = `${topBelow}px`;
}
}

// Searches for the matching post based on quoted text.
function findMatchingPostId(quotedText, contextElement, includeUnkfunc) {
// Check if the quoted text matches the "No. xxx" or "No.xxx" format
const noMatch = quotedText.match(/^No\. ?(\d+)$/);
if (noMatch) {
const postId = `p${noMatch[1]}`; // Extract the post number and prepend "p"

// Now try to find the post by ID
const post = document.getElementById(postId);
if (post && isPostAboveContext(post, contextElement)) {
return postId;
}
return 'notFound';
}
const contextRect = contextElement.getBoundingClientRect();

// Find matching post by filename or partial comment match
const posts = Array.from(document.querySelectorAll('.post.reply, .post.op'))
.filter(post => {
const postRect = post.getBoundingClientRect();
return postRect.top < contextRect.top;
})
.reverse(); // Reverse to start searching from the latest
for (const post of posts) {
const comment = post.querySelector('.comment');
const filenameLink = post.querySelector('.filesize a');
if (comment) {
let textToCheck;
if (!includeUnkfunc) {
const clone = comment.cloneNode(true);
clone.querySelectorAll('.unkfunc').forEach(el => el.remove());
textToCheck = clone.textContent;
} else {
textToCheck = comment.textContent; // Match by full comment text (allows partial matches)
}
if (textToCheck.includes(quotedText)) {
return post.id;
}
}

// Match by exact filename
if (filenameLink) {
const visibleFilename = filenameLink.textContent.trim();
const fullFilename = filenameLink.getAttribute('onmouseover')
?.match(/this\.textContent='([^']+)'/)?.[1] || visibleFilename;
if (fullFilename === quotedText) {
return post.id;
}
}
}

// Return a default that will not match anything
return 'notFound';
}

// Returns true if the post is above the context element.
function isPostAboveContext(post, contextElement) {
const postRect = post.getBoundingClientRect();
const contextRect = contextElement.getBoundingClientRect();
return postRect.top < contextRect.top;
}

// Highlight the quoted text inside the preview box's .comment elements.
function highlightQuotedText(container, quotedText) {
if (!quotedText) return;
const comments = container.querySelectorAll('.comment');
const regex = new RegExp(escapeRegExp(quotedText), 'gi');
comments.forEach(comment => {
comment.innerHTML = comment.innerHTML.replace(regex, '<span class="quoteHighlight">$&</span>');
});
}

// Escape special characters for RegExp
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

// Attach hover listeners to all .unkfunc elements within the given element.
function applyHoverListeners(element) {
element.querySelectorAll('.unkfunc').forEach((quoted) => {
quoted.addEventListener('mouseover', startHover);
quoted.addEventListener('mouseout', stopHover);
});
}

// Also run a global check on mousemove.
let mousemoveTimeout;
document.addEventListener('mousemove', () => {
clearTimeout(mousemoveTimeout);
mousemoveTimeout = setTimeout(checkPreviews, REMOVAL_DELAY);
});

applyHoverListeners(document);
})();

Test
>>
s spoiler del
>>
>>
>>
1
>>
>1
>>
>>
lib_post edit change test
>>
sttestseestste


Delete post: []