(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

scanash00 071ecbe1 e55e33dd

+125 -9
+9 -2
backend/internal/oauth/handler.go
··· 571 571 } 572 572 573 573 func (h *Handler) HandleSession(w http.ResponseWriter, r *http.Request) { 574 + sessionID := "" 574 575 cookie, err := r.Cookie("margin_session") 575 - if err != nil { 576 + if err == nil { 577 + sessionID = cookie.Value 578 + } else { 579 + sessionID = r.Header.Get("X-Session-Token") 580 + } 581 + 582 + if sessionID == "" { 576 583 w.Header().Set("Content-Type", "application/json") 577 584 json.NewEncoder(w).Encode(map[string]interface{}{"authenticated": false}) 578 585 return 579 586 } 580 587 581 - did, handle, _, _, _, err := h.db.GetSession(cookie.Value) 588 + did, handle, _, _, _, err := h.db.GetSession(sessionID) 582 589 if err != nil { 583 590 w.Header().Set("Content-Type", "application/json") 584 591 json.NewEncoder(w).Encode(map[string]interface{}{"authenticated": false})
+32 -3
extension/src/entrypoints/background.ts
··· 53 53 return await checkSession(); 54 54 }); 55 55 56 - onMessage('getAnnotations', async ({ data }) => { 57 - return await getAnnotations(data.url, [], data.cacheBust); 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); 58 76 }); 59 77 60 78 onMessage('activateOnPdf', async ({ data }) => { ··· 307 325 return; 308 326 } 309 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 + 310 339 const result = await createHighlight({ 311 - url: resolveTabUrl(tab.url), 340 + url: highlightUrl, 312 341 title: tab.title, 313 342 selector: { 314 343 type: 'TextQuoteSelector',
+1 -1
extension/src/utils/messaging.ts
··· 11 11 interface ProtocolMap { 12 12 checkSession(): MarginSession; 13 13 14 - getAnnotations(data: { url: string; cacheBust?: boolean }): Annotation[]; 14 + getAnnotations(data: { url: string; citedUrls?: string[]; cacheBust?: boolean }): Annotation[]; 15 15 activateOnPdf(data: { tabId: number; url: string }): { redirected: boolean }; 16 16 createAnnotation(data: { 17 17 url: string;
+83 -3
extension/src/utils/overlay.ts
··· 69 69 return window.location.href; 70 70 } 71 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 + 72 139 function isPdfContext(): boolean { 73 140 return !!( 74 141 document.querySelector('.pdfViewer') || ··· 382 449 submitBtn.textContent = 'Posting...'; 383 450 384 451 try { 452 + const citeUrl = getCiteUrlForText(quoteText); 385 453 const res = await sendMessage('createAnnotation', { 386 - url: getPageUrl(), 454 + url: citeUrl || getPageUrl(), 387 455 title: document.title, 388 456 text, 389 457 selector: { type: 'TextQuoteSelector', exact: quoteText }, ··· 424 492 const selection = window.getSelection(); 425 493 const text = selection?.toString().trim() || ''; 426 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 || '') }); 427 501 } 428 502 }); 429 503 ··· 519 593 } 520 594 521 595 try { 596 + const pageUrl = getPageUrl(); 597 + const doiUrl = getPageDOIUrl(); 598 + const citeUrls = getPageCiteUrls(); 599 + const citedUrls = [...(doiUrl ? [doiUrl] : []), ...citeUrls]; 600 + 522 601 const annotations = await sendMessage('getAnnotations', { 523 - url: getPageUrl(), 602 + url: pageUrl, 603 + citedUrls, 524 604 cacheBust, 525 605 }); 526 606 527 607 sendMessage('updateBadge', { count: annotations?.length || 0 }); 528 608 529 609 if (annotations) { 530 - sendMessage('cacheAnnotations', { url: getPageUrl(), annotations }); 610 + sendMessage('cacheAnnotations', { url: pageUrl, annotations }); 531 611 } 532 612 533 613 if (annotations && annotations.length > 0) {