diff --git a/src/inc/routes/chan.mjs b/src/inc/routes/chan.mjs
index dbfbdd1..6df6c5a 100644
--- a/src/inc/routes/chan.mjs
+++ b/src/inc/routes/chan.mjs
@@ -48,13 +48,37 @@ export default (router, tpl) => {
function formatComment(com) {
if (!com) return '';
- // Format quote lines
- let formatted = com.replace(/(?:^|
)((?:>|>)((?!>|>)[^\n<]+))/g, '$1>$2');
- // Format post cross-references >>123456
- formatted = formatted.replace(/(?:>|>)>(\d+)/g, '>>$1');
+ let formatted = com;
+ // 1. Normalize existing 4chan intra-thread quotelinks to standard format
+ formatted = formatted.replace(/]*href="#p(\d+)"[^>]*>.*?<\/a>/gi, '___CHANREF_$1___');
+ // 2. Normalize cross-thread/cross-board quotelinks if any
+ formatted = formatted.replace(/]*href="(\/[^"]+)"[^>]*>(.*?)<\/a>/gi, '$2');
+ // 3. Format any unlinked quotes >>123456
+ formatted = formatted.replace(/(?>123456 if plain text was supplied
+ formatted = formatted.replace(/(?\w])>>(\d+)\b/g, '___CHANREF_$1___');
+ // 5. Restore CHANREF placeholders
+ formatted = formatted.replace(/___CHANREF_(\d+)___/g, '>>$1');
+ // 6. Format quote lines (greentext)
+ formatted = formatted.replace(/(^|
)>(?!>)([^\n<]+)/g, '$1>$2');
return formatted;
}
+ function extractQuotes(com) {
+ if (!com) return [];
+ const quotes = new Set();
+ for (const m of com.matchAll(/href="#p(\d+)"/g)) {
+ quotes.add(Number(m[1]));
+ }
+ for (const m of com.matchAll(/(?\w])>>(\d+)\b/g)) {
+ quotes.add(Number(m[1]));
+ }
+ return Array.from(quotes);
+ }
+
// ───────────────────────────────────────────────────────────────────────────
// 0. GET /4 -> redirect to default board catalogue
// ───────────────────────────────────────────────────────────────────────────
@@ -145,6 +169,21 @@ export default (router, tpl) => {
});
function formatPosts(posts, board, rehostMap, userFavSet, targetPostNo = null, lang = 'en') {
+ // Build reply mapping (targetPostNo -> array of replier post numbers)
+ const postNoSet = new Set(posts.map(p => p.no));
+ const replyMap = new Map();
+
+ for (const p of posts) {
+ if (!p.com) continue;
+ const quotes = extractQuotes(p.com);
+ for (const qNo of quotes) {
+ if (postNoSet.has(qNo) && qNo !== p.no) {
+ if (!replyMap.has(qNo)) replyMap.set(qNo, []);
+ replyMap.get(qNo).push(p.no);
+ }
+ }
+ }
+
return posts.map(p => {
const ext = (p.ext || '').toLowerCase();
const isVideo = ['.webm', '.mp4'].includes(ext);
@@ -154,6 +193,7 @@ export default (router, tpl) => {
const localId = rehostInfo ? rehostInfo.id : null;
const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time;
const isoStr = new Date(effectiveTime * 1000).toISOString();
+ const postReplies = replyMap.get(p.no) || [];
return {
no: p.no,
@@ -181,7 +221,9 @@ export default (router, tpl) => {
local_id: localId,
rehosted: !!localId,
user_has_favorited: localId ? userFavSet.has(Number(localId)) : false,
- is_onara_active: targetPostNo ? p.no === targetPostNo : false
+ is_onara_active: targetPostNo ? p.no === targetPostNo : false,
+ replies: postReplies,
+ replies_count: postReplies.length
};
});
}
diff --git a/views/chan/thread.html b/views/chan/thread.html
index f7b2c7d..7cef2a0 100644
--- a/views/chan/thread.html
+++ b/views/chan/thread.html
@@ -56,7 +56,7 @@