Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

new features and bug fixes - reimplement canonical annotations - implement DOI urls support

+125 -9
+9 -2
backend/internal/oauth/handler.go
··· 571 } 572 573 func (h *Handler) HandleSession(w http.ResponseWriter, r *http.Request) { 574 cookie, err := r.Cookie("margin_session") 575 - if err != nil { 576 w.Header().Set("Content-Type", "application/json") 577 json.NewEncoder(w).Encode(map[string]interface{}{"authenticated": false}) 578 return 579 } 580 581 - did, handle, _, _, _, err := h.db.GetSession(cookie.Value) 582 if err != nil { 583 w.Header().Set("Content-Type", "application/json") 584 json.NewEncoder(w).Encode(map[string]interface{}{"authenticated": false})
··· 571 } 572 573 func (h *Handler) HandleSession(w http.ResponseWriter, r *http.Request) { 574 + sessionID := "" 575 cookie, err := r.Cookie("margin_session") 576 + if err == nil { 577 + sessionID = cookie.Value 578 + } else { 579 + sessionID = r.Header.Get("X-Session-Token") 580 + } 581 + 582 + if sessionID == "" { 583 w.Header().Set("Content-Type", "application/json") 584 json.NewEncoder(w).Encode(map[string]interface{}{"authenticated": false}) 585 return 586 } 587 588 + did, handle, _, _, _, err := h.db.GetSession(sessionID) 589 if err != nil { 590 w.Header().Set("Content-Type", "application/json") 591 json.NewEncoder(w).Encode(map[string]interface{}{"authenticated": false})
+32 -3
extension/src/entrypoints/background.ts
··· 53 return await checkSession(); 54 }); 55 56 - onMessage('getAnnotations', async ({ data }) => { 57 - return await getAnnotations(data.url, [], data.cacheBust); 58 }); 59 60 onMessage('activateOnPdf', async ({ data }) => { ··· 307 return; 308 } 309 310 const result = await createHighlight({ 311 - url: resolveTabUrl(tab.url), 312 title: tab.title, 313 selector: { 314 type: 'TextQuoteSelector',
··· 53 return await checkSession(); 54 }); 55 56 + onMessage('getAnnotations', async ({ data, sender }) => { 57 + let citedUrls: string[] = data.citedUrls ?? []; 58 + 59 + if (data.citedUrls === undefined) { 60 + try { 61 + const tabId = 62 + (sender as any)?.tab?.id ?? 63 + (await browser.tabs.query({ active: true, currentWindow: true }))[0]?.id; 64 + if (tabId !== undefined) { 65 + const res = (await browser.tabs.sendMessage(tabId, { type: 'GET_DOI' })) as 66 + | { doiUrl: string | null } 67 + | undefined; 68 + if (res?.doiUrl) citedUrls = [res.doiUrl]; 69 + } 70 + } catch { 71 + // ignore 72 + } 73 + } 74 + 75 + return await getAnnotations(data.url, citedUrls, data.cacheBust); 76 }); 77 78 onMessage('activateOnPdf', async ({ data }) => { ··· 325 return; 326 } 327 328 + let highlightUrl = resolveTabUrl(tab.url); 329 + try { 330 + const res = (await browser.tabs.sendMessage(tab.id!, { 331 + type: 'GET_CITE_URL', 332 + text: info.selectionText, 333 + })) as { citeUrl: string | null } | undefined; 334 + if (res?.citeUrl) highlightUrl = res.citeUrl; 335 + } catch { 336 + /* ignore */ 337 + } 338 + 339 const result = await createHighlight({ 340 + url: highlightUrl, 341 title: tab.title, 342 selector: { 343 type: 'TextQuoteSelector',
+1 -1
extension/src/utils/messaging.ts
··· 11 interface ProtocolMap { 12 checkSession(): MarginSession; 13 14 - getAnnotations(data: { url: string; cacheBust?: boolean }): Annotation[]; 15 activateOnPdf(data: { tabId: number; url: string }): { redirected: boolean }; 16 createAnnotation(data: { 17 url: string;
··· 11 interface ProtocolMap { 12 checkSession(): MarginSession; 13 14 + getAnnotations(data: { url: string; citedUrls?: string[]; cacheBust?: boolean }): Annotation[]; 15 activateOnPdf(data: { tabId: number; url: string }): { redirected: boolean }; 16 createAnnotation(data: { 17 url: string;
+83 -3
extension/src/utils/overlay.ts
··· 69 return window.location.href; 70 } 71 72 function isPdfContext(): boolean { 73 return !!( 74 document.querySelector('.pdfViewer') || ··· 382 submitBtn.textContent = 'Posting...'; 383 384 try { 385 const res = await sendMessage('createAnnotation', { 386 - url: getPageUrl(), 387 title: document.title, 388 text, 389 selector: { type: 'TextQuoteSelector', exact: quoteText }, ··· 424 const selection = window.getSelection(); 425 const text = selection?.toString().trim() || ''; 426 return Promise.resolve({ text }); 427 } 428 }); 429 ··· 519 } 520 521 try { 522 const annotations = await sendMessage('getAnnotations', { 523 - url: getPageUrl(), 524 cacheBust, 525 }); 526 527 sendMessage('updateBadge', { count: annotations?.length || 0 }); 528 529 if (annotations) { 530 - sendMessage('cacheAnnotations', { url: getPageUrl(), annotations }); 531 } 532 533 if (annotations && annotations.length > 0) {
··· 69 return window.location.href; 70 } 71 72 + function getPageDOIUrl(): string | null { 73 + try { 74 + if (new URL(window.location.href).hostname === 'doi.org') return null; 75 + } catch { 76 + return null; 77 + } 78 + 79 + const metaDOI = 80 + document.querySelector<HTMLMetaElement>('meta[name="citation_doi"]') || 81 + document.querySelector<HTMLMetaElement>('meta[name="dc.identifier"]') || 82 + document.querySelector<HTMLMetaElement>('meta[name="DC.identifier"]'); 83 + if (metaDOI?.content) { 84 + const doi = metaDOI.content.replace(/^doi:/i, '').trim(); 85 + if (doi.startsWith('10.')) return `https://doi.org/${doi}`; 86 + } 87 + 88 + const canonical = document.querySelector<HTMLLinkElement>('link[rel="canonical"]'); 89 + if (canonical?.href) { 90 + try { 91 + if (new URL(canonical.href).hostname === 'doi.org') return canonical.href; 92 + } catch { 93 + /* ignore */ 94 + } 95 + } 96 + 97 + return null; 98 + } 99 + 100 + function getPageCiteUrls(): string[] { 101 + const urls = new Set<string>(); 102 + document.querySelectorAll<Element>('q[cite], blockquote[cite]').forEach((el) => { 103 + const cite = el.getAttribute('cite'); 104 + if (!cite) return; 105 + try { 106 + const abs = new URL(cite, window.location.href).href; 107 + if (abs !== window.location.href) urls.add(abs); 108 + } catch { 109 + /* ignore */ 110 + } 111 + }); 112 + return Array.from(urls); 113 + } 114 + 115 + function getCiteUrlForText(text: string): string | null { 116 + if (!text) return null; 117 + if (!cachedMatcher) cachedMatcher = new DOMTextMatcher(); 118 + const range = cachedMatcher.findRange(text); 119 + if (!range) return null; 120 + 121 + let node: Node | null = range.commonAncestorContainer; 122 + while (node && node !== document.body) { 123 + if (node.nodeType === Node.ELEMENT_NODE) { 124 + const el = node as Element; 125 + if ((el.tagName === 'Q' || el.tagName === 'BLOCKQUOTE') && el.hasAttribute('cite')) { 126 + const cite = el.getAttribute('cite')!; 127 + try { 128 + return new URL(cite, window.location.href).href; 129 + } catch { 130 + return null; 131 + } 132 + } 133 + } 134 + node = node.parentNode; 135 + } 136 + return null; 137 + } 138 + 139 function isPdfContext(): boolean { 140 return !!( 141 document.querySelector('.pdfViewer') || ··· 449 submitBtn.textContent = 'Posting...'; 450 451 try { 452 + const citeUrl = getCiteUrlForText(quoteText); 453 const res = await sendMessage('createAnnotation', { 454 + url: citeUrl || getPageUrl(), 455 title: document.title, 456 text, 457 selector: { type: 'TextQuoteSelector', exact: quoteText }, ··· 492 const selection = window.getSelection(); 493 const text = selection?.toString().trim() || ''; 494 return Promise.resolve({ text }); 495 + } 496 + if (message.type === 'GET_DOI') { 497 + return Promise.resolve({ doiUrl: getPageDOIUrl() }); 498 + } 499 + if (message.type === 'GET_CITE_URL') { 500 + return Promise.resolve({ citeUrl: getCiteUrlForText(message.text || '') }); 501 } 502 }); 503 ··· 593 } 594 595 try { 596 + const pageUrl = getPageUrl(); 597 + const doiUrl = getPageDOIUrl(); 598 + const citeUrls = getPageCiteUrls(); 599 + const citedUrls = [...(doiUrl ? [doiUrl] : []), ...citeUrls]; 600 + 601 const annotations = await sendMessage('getAnnotations', { 602 + url: pageUrl, 603 + citedUrls, 604 cacheBust, 605 }); 606 607 sendMessage('updateBadge', { count: annotations?.length || 0 }); 608 609 if (annotations) { 610 + sendMessage('cacheAnnotations', { url: pageUrl, annotations }); 611 } 612 613 if (annotations && annotations.length > 0) {