<script>
import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import { Select } from '$lib/components/ui/select/index.js'
import { Card, CardContent } from '$lib/components/ui/card/index.js'
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '$lib/components/ui/table/index.js'
import { setupI18n } from '$lib/i18n.js'
import { _, locale } from '@sveltia/i18n'
import Icon from '@iconify/svelte'
import orcidSvg from './assets/orcid.svg'
import rorSvg from './assets/ror.svg'
import katex from 'katex'
import 'katex/dist/katex.min.css'
// ── App config ────────────────────────────────────────────────────────────
let isDark = $state(document.documentElement.classList.contains('dark'))
function toggleDark() {
isDark = !isDark
document.documentElement.classList.toggle('dark', isDark)
localStorage.setItem('app-dark', String(isDark))
}
// ── Bibliography ──────────────────────────────────────────────────────────
let bibDoi = $state('')
let bibStyle = $state(localStorage.getItem('dragoman-style') ?? 'apa')
const initialLocale = localStorage.getItem('dragoman-locale') ?? (() => {
const supported = ['en-US','de-DE','fr-FR','es-ES','it-IT','ja-JP','ko-KR','nl-NL','pt-BR','sv-SE','zh-CN']
const lang = navigator.language
return supported.find(v => v === lang)
?? supported.find(v => v.startsWith(lang.split('-')[0] + '-'))
?? 'en-US'
})()
setupI18n(initialLocale)
const BIB_MAX = 10
let bibLocale = $state(initialLocale)
let bibliography = $state((() => {
try { return JSON.parse(localStorage.getItem('dragoman-bibliography') ?? '[]') }
catch { return [] }
})())
let bibLoading = $state(false)
let bibFetchingRemote = $state(false)
let bibError = $state('')
let _remoteTimer = null
let randomLoading = $state(false)
let hasDb = $state(false)
fetch('/status').then(r => r.json()).then(d => { hasDb = d.db }).catch(() => {})
let copiedCiteId = $state(null)
let copiedPreIdx = $state(null)
let dragId = $state(null)
let dropPosition = $state(null) // { id, before: boolean }
let dragHandleActive = $state(false)
$effect(() => { localStorage.setItem('dragoman-bibliography', JSON.stringify(bibliography)) })
$effect(() => { localStorage.setItem('dragoman-style', bibStyle) })
$effect(() => { localStorage.setItem('dragoman-locale', bibLocale); locale.set(bibLocale) })
// ── Export ────────────────────────────────────────────────────────────────
let expFormat = $state('citation')
let expError = $state('')
let expLoading = $state(false)
let expCopied = $state(false)
// ── Style picker ──────────────────────────────────────────────────────────
let stylePickerOpen = $state(false)
let styleSearch = $state('')
// ── Data ──────────────────────────────────────────────────────────────────
const commonStyles = [
{ value: 'apa', label: 'American Psychological Association 7th edition' },
{ value: 'chicago-author-date', label: 'Chicago Manual of Style 18th Edition (Author-Date)' },
{ value: 'harvard-cite-them-right', label: 'Cite Them Right 12th Edition (Harvard)' },
{ value: 'ieee', label: 'IEEE' },
{ value: 'modern-language-association', label: 'MLA Handbook 9th Edition' },
{ value: 'vancouver', label: 'Vancouver' },
]
const allCitationStyles = [
{ value: 'apa', label: 'American Psychological Association 7th edition' },
{ value: 'alphanumeric', label: 'Alphanumeric' },
{ value: 'american-anthropological-association', label: 'American Anthropological Association' },
{ value: 'american-chemical-society', label: 'American Chemical Society' },
{ value: 'american-geophysical-union', label: 'American Geophysical Union' },
{ value: 'american-institute-of-aeronautics-and-astronautics', label: 'American Institute of Aeronautics and Astronautics' },
{ value: 'american-institute-of-physics', label: 'American Institute of Physics' },
{ value: 'american-medical-association', label: 'American Medical Association' },
{ value: 'american-meteorological-society', label: 'American Meteorological Society' },
{ value: 'american-physics-society', label: 'American Physical Society' },
{ value: 'american-physiological-society', label: 'American Physiological Society' },
{ value: 'american-political-science-association', label: 'American Political Science Association' },
{ value: 'american-society-for-microbiology', label: 'American Society for Microbiology' },
{ value: 'american-society-of-civil-engineers', label: 'American Society of Civil Engineers' },
{ value: 'american-society-of-mechanical-engineers', label: 'American Society of Mechanical Engineers' },
{ value: 'american-sociological-association', label: 'American Sociological Association' },
{ value: 'angewandte-chemie', label: 'Angewandte Chemie International Edition' },
{ value: 'annual-reviews', label: 'Annual Reviews' },
{ value: 'annual-reviews-author-date', label: 'Annual Reviews (Author-Date)' },
{ value: 'associacao-brasileira-de-normas-tecnicas', label: 'Associação Brasileira de Normas Técnicas' },
{ value: 'association-for-computing-machinery', label: 'Association for Computing Machinery' },
{ value: 'biomed-central', label: 'BioMed Central' },
{ value: 'bmj', label: 'BMJ' },
{ value: 'bristol-university-press', label: 'Bristol University Press' },
{ value: 'cell', label: 'Cell' },
{ value: 'chicago-author-date', label: 'Chicago Manual of Style 18th Edition (Author-Date)' },
{ value: 'chicago-notes', label: 'Chicago Manual of Style 18th Edition (Notes & Bibliography)' },
{ value: 'chicago-shortened-notes', label: 'Chicago Manual of Style 18th Edition (Shortened Notes)' },
{ value: 'copernicus', label: 'Copernicus Publications' },
{ value: 'council-of-science-editors', label: 'Council of Science Editors (Citation-Sequence)' },
{ value: 'council-of-science-editors-author-date', label: 'Council of Science Editors (Name-Year)' },
{ value: 'current-opinion', label: 'Current Opinion' },
{ value: 'deutsche-gesellschaft-für-psychologie', label: 'Deutsche Gesellschaft für Psychologie (Deutsch)' },
{ value: 'deutsche-sprache', label: 'Deutsche Sprache (Deutsch)' },
{ value: 'elsevier-harvard', label: 'Elsevier (Harvard)' },
{ value: 'elsevier-vancouver', label: 'Elsevier (Vancouver)' },
{ value: 'elsevier-with-titles', label: 'Elsevier (Numeric, With Titles)' },
{ value: 'frontiers', label: 'Frontiers' },
{ value: 'future-medicine', label: 'Future Medicine' },
{ value: 'future-science', label: 'Future Science Group' },
{ value: 'gb-7714-2005-numeric', label: 'GB/T 7714-2005 (Numeric)' },
{ value: 'gb-7714-2015-author-date', label: 'GB/T 7714-2015 (Author-Date)' },
{ value: 'gb-7714-2015-note', label: 'GB/T 7714-2015 (Note)' },
{ value: 'gb-7714-2015-numeric', label: 'GB/T 7714-2015 (Numeric)' },
{ value: 'gost-r-705-2008-numeric', label: 'GOST R 7.0.5-2008 (Numeric)' },
{ value: 'harvard-cite-them-right', label: 'Cite Them Right 12th Edition (Harvard)' },
{ value: 'ieee', label: 'IEEE' },
{ value: 'institute-of-physics-numeric', label: 'Institute of Physics (Numeric)' },
{ value: 'iso-690-author-date', label: 'ISO 690 (Author-Date)' },
{ value: 'iso-690-numeric', label: 'ISO 690 (Numeric)' },
{ value: 'karger', label: 'Karger' },
{ value: 'mary-ann-liebert-vancouver', label: 'Mary Ann Liebert (Vancouver)' },
{ value: 'modern-humanities-research-association', label: 'MHRA Style Guide 4th Edition (Notes)' },
{ value: 'mla', label: 'MLA Handbook 9th Edition' },
{ value: 'multidisciplinary-digital-publishing-institute', label: 'Multidisciplinary Digital Publishing Institute' },
{ value: 'nature', label: 'Nature' },
{ value: 'vancouver', label: 'Vancouver' },
{ value: 'vancouver-superscript', label: 'NLM/Vancouver (Superscript)' },
{ value: 'pensoft', label: 'Pensoft Journals' },
{ value: 'plos', label: 'Public Library of Science' },
{ value: 'royal-society-of-chemistry', label: 'Royal Society of Chemistry' },
{ value: 'sage-vancouver', label: 'SAGE (Vancouver)' },
{ value: 'sist02', label: 'SIST02' },
{ value: 'spie', label: 'SPIE' },
{ value: 'springer-basic', label: 'Springer Basic (Numeric)' },
{ value: 'springer-basic-author-date', label: 'Springer Basic (Author-Date)' },
{ value: 'springer-fachzeitschriften-medizin-psychologie', label: 'Springer Fachzeitschriften Medizin Psychologie (Deutsch)' },
{ value: 'springer-humanities-author-date', label: 'Springer Humanities (Author-Date)' },
{ value: 'springer-lecture-notes-in-computer-science', label: 'Springer Lecture Notes in Computer Science' },
{ value: 'springer-mathphys', label: 'Springer MathPhys (Numeric)' },
{ value: 'springer-socpsych-author-date', label: 'Springer SocPsych (Author-Date)' },
{ value: 'springer-vancouver', label: 'Springer (Vancouver)' },
{ value: 'taylor-and-francis-chicago-author-date', label: 'Taylor & Francis (Chicago Author-Date)' },
{ value: 'taylor-and-francis-national-library-of-medicine', label: 'Taylor & Francis (NLM/Vancouver)' },
{ value: 'the-institution-of-engineering-and-technology', label: 'Institution of Engineering and Technology' },
{ value: 'the-lancet', label: 'The Lancet' },
{ value: 'thieme', label: 'Thieme' },
{ value: 'trends', label: 'Trends' },
{ value: 'turabian-author-date', label: 'Chicago Manual of Style 17th Edition (Author-Date)' },
{ value: 'turabian-fullnote-8', label: 'Chicago Manual of Style 17th Edition (Notes & Bibliography)' },
]
const locales = [
{ value: 'en-US', label: 'English (US)' },
{ value: 'de-DE', label: 'Deutsch' },
{ value: 'fr-FR', label: 'Français' },
{ value: 'es-ES', label: 'Español' },
{ value: 'it-IT', label: 'Italiano' },
{ value: 'ja-JP', label: '日本語' },
{ value: 'ko-KR', label: '한국어' },
{ value: 'nl-NL', label: 'Nederlands' },
{ value: 'pt-BR', label: 'Português (Brasil)' },
{ value: 'sv-SE', label: 'Svenska' },
{ value: 'zh-CN', label: '中文(简体)' },
]
const expFormats = [
{ value: 'citation', i18n: 'format.citation' },
{ value: 'commonmeta', label: 'Commonmeta' },
{ value: 'bibtex', label: 'BibTeX' },
{ value: 'csl', label: 'CSL-JSON' },
{ value: 'crossref', label: 'Crossref' },
{ value: 'crossref_xml', label: 'Crossref XML' },
{ value: 'datacite', label: 'DataCite JSON' },
{ value: 'datacite_xml', label: 'DataCite XML' },
{ value: 'inveniordm', label: 'InvenioRDM' },
{ value: 'ris', label: 'RIS' },
{ value: 'schemaorg', label: 'Schema.org' },
]
// ── Helpers ───────────────────────────────────────────────────────────────
const TYPE_LABELS = {
'de-DE': {
Article: 'Artikel', Audiovisual: 'Audiovisuell', BlogPost: 'Blogbeitrag',
Book: 'Buch', BookChapter: 'Buchkapitel', BookSeries: 'Buchreihe',
Collection: 'Sammlung', Component: 'Komponente', ComputationalNotebook: 'Computational Notebook',
DataPaper: 'Datenpapier', Dataset: 'Datensatz', Dissertation: 'Dissertation',
Document: 'Dokument', Entry: 'Eintrag', Event: 'Veranstaltung',
Figure: 'Abbildung', Grant: 'Förderantrag', Image: 'Bild',
Instrument: 'Instrument', InteractiveResource: 'Interaktive Ressource',
Journal: 'Zeitschrift', JournalArticle: 'Zeitschriftenartikel',
LegalDocument: 'Rechtsdokument', Manuscript: 'Manuskript', Map: 'Karte',
Model: 'Modell', Other: 'Sonstiges', OutputManagementPlan: 'Datenmanagementplan',
Patent: 'Patent', PeerReview: 'Fachgutachten', Performance: 'Aufführung',
PersonalCommunication: 'Persönliche Mitteilung', PhysicalObject: 'Physisches Objekt',
Poster: 'Poster', Presentation: 'Präsentation', Preprint: 'Preprint',
ProceedingsArticle: 'Konferenzbeitrag', Report: 'Bericht', Service: 'Dienst',
Software: 'Software', Sound: 'Ton', Standard: 'Norm',
StudyRegistration: 'Studienregistrierung', WebPage: 'Webseite',
},
'fr-FR': {
Article: 'Article', Audiovisual: 'Audiovisuel', BlogPost: 'Article de blog',
Book: 'Livre', BookChapter: 'Chapitre de livre', BookSeries: 'Série de livres',
Collection: 'Collection', Component: 'Composant', ComputationalNotebook: 'Carnet de calcul',
DataPaper: 'Article de données', Dataset: 'Ensemble de données', Dissertation: 'Thèse',
Document: 'Document', Entry: 'Entrée', Event: 'Événement',
Figure: 'Figure', Grant: 'Subvention', Image: 'Image',
Instrument: 'Instrument', InteractiveResource: 'Ressource interactive',
Journal: 'Revue', JournalArticle: 'Article de revue',
LegalDocument: 'Document juridique', Manuscript: 'Manuscrit', Map: 'Carte',
Model: 'Modèle', Other: 'Autre', OutputManagementPlan: 'Plan de gestion des données',
Patent: 'Brevet', PeerReview: 'Évaluation par les pairs', Performance: 'Représentation',
PersonalCommunication: 'Communication personnelle', PhysicalObject: 'Objet physique',
Poster: 'Affiche', Presentation: 'Présentation', Preprint: 'Préimpression',
ProceedingsArticle: 'Article de conférence', Report: 'Rapport', Service: 'Service',
Software: 'Logiciel', Sound: 'Son', Standard: 'Norme',
StudyRegistration: "Enregistrement d'étude", WebPage: 'Page web',
},
'es-ES': {
Article: 'Artículo', Audiovisual: 'Audiovisual', BlogPost: 'Entrada de blog',
Book: 'Libro', BookChapter: 'Capítulo de libro', BookSeries: 'Serie de libros',
Collection: 'Colección', Component: 'Componente', ComputationalNotebook: 'Cuaderno computacional',
DataPaper: 'Artículo de datos', Dataset: 'Conjunto de datos', Dissertation: 'Tesis',
Document: 'Documento', Entry: 'Entrada', Event: 'Evento',
Figure: 'Figura', Grant: 'Subvención', Image: 'Imagen',
Instrument: 'Instrumento', InteractiveResource: 'Recurso interactivo',
Journal: 'Revista', JournalArticle: 'Artículo de revista',
LegalDocument: 'Documento legal', Manuscript: 'Manuscrito', Map: 'Mapa',
Model: 'Modelo', Other: 'Otro', OutputManagementPlan: 'Plan de gestión de datos',
Patent: 'Patente', PeerReview: 'Revisión por pares', Performance: 'Actuación',
PersonalCommunication: 'Comunicación personal', PhysicalObject: 'Objeto físico',
Poster: 'Póster', Presentation: 'Presentación', Preprint: 'Preimpresión',
ProceedingsArticle: 'Artículo de conferencia', Report: 'Informe', Service: 'Servicio',
Software: 'Software', Sound: 'Sonido', Standard: 'Norma',
StudyRegistration: 'Registro de estudio', WebPage: 'Página web',
},
'it-IT': {
Article: 'Articolo', Audiovisual: 'Audiovisivo', BlogPost: 'Articolo di blog',
Book: 'Libro', BookChapter: 'Capitolo di libro', BookSeries: 'Serie di libri',
Collection: 'Raccolta', Component: 'Componente', ComputationalNotebook: 'Quaderno computazionale',
DataPaper: 'Articolo di dati', Dataset: 'Set di dati', Dissertation: 'Tesi',
Document: 'Documento', Entry: 'Voce', Event: 'Evento',
Figure: 'Figura', Grant: 'Finanziamento', Image: 'Immagine',
Instrument: 'Strumento', InteractiveResource: 'Risorsa interattiva',
Journal: 'Rivista', JournalArticle: 'Articolo di rivista',
LegalDocument: 'Documento legale', Manuscript: 'Manoscritto', Map: 'Mappa',
Model: 'Modello', Other: 'Altro', OutputManagementPlan: 'Piano di gestione dei dati',
Patent: 'Brevetto', PeerReview: 'Revisione paritaria', Performance: 'Rappresentazione',
PersonalCommunication: 'Comunicazione personale', PhysicalObject: 'Oggetto fisico',
Poster: 'Poster', Presentation: 'Presentazione', Preprint: 'Preprint',
ProceedingsArticle: 'Atti di conferenza', Report: 'Rapporto', Service: 'Servizio',
Software: 'Software', Sound: 'Suono', Standard: 'Norma',
StudyRegistration: 'Registrazione dello studio', WebPage: 'Pagina web',
},
'ja-JP': {
Article: '論文', Audiovisual: '映像・音声', BlogPost: 'ブログ記事',
Book: '図書', BookChapter: '書籍の章', BookSeries: '叢書',
Collection: 'コレクション', Component: 'コンポーネント', ComputationalNotebook: '計算ノートブック',
DataPaper: 'データ論文', Dataset: 'データセット', Dissertation: '学位論文',
Document: '文書', Entry: '項目', Event: 'イベント',
Figure: '図', Grant: '研究助成', Image: '画像',
Instrument: '機器', InteractiveResource: 'インタラクティブリソース',
Journal: '学術誌', JournalArticle: '学術論文',
LegalDocument: '法的文書', Manuscript: '原稿', Map: '地図',
Model: 'モデル', Other: 'その他', OutputManagementPlan: 'データ管理計画',
Patent: '特許', PeerReview: '査読', Performance: 'パフォーマンス',
PersonalCommunication: '個人通信', PhysicalObject: '物理的対象',
Poster: 'ポスター', Presentation: 'プレゼンテーション', Preprint: 'プレプリント',
ProceedingsArticle: '会議論文', Report: '報告書', Service: 'サービス',
Software: 'ソフトウェア', Sound: '音声', Standard: '規格',
StudyRegistration: '研究登録', WebPage: 'ウェブページ',
},
'ko-KR': {
Article: '논문', Audiovisual: '시청각 자료', BlogPost: '블로그 게시물',
Book: '도서', BookChapter: '책 챕터', BookSeries: '도서 시리즈',
Collection: '컬렉션', Component: '컴포넌트', ComputationalNotebook: '계산 노트북',
DataPaper: '데이터 논문', Dataset: '데이터셋', Dissertation: '학위논문',
Document: '문서', Entry: '항목', Event: '이벤트',
Figure: '그림', Grant: '연구 지원금', Image: '이미지',
Instrument: '기기', InteractiveResource: '대화형 자료',
Journal: '학술지', JournalArticle: '학술 논문',
LegalDocument: '법률 문서', Manuscript: '원고', Map: '지도',
Model: '모델', Other: '기타', OutputManagementPlan: '데이터 관리 계획',
Patent: '특허', PeerReview: '동료 심사', Performance: '공연',
PersonalCommunication: '개인 통신', PhysicalObject: '물리적 객체',
Poster: '포스터', Presentation: '발표', Preprint: '프리프린트',
ProceedingsArticle: '학술대회 논문', Report: '보고서', Service: '서비스',
Software: '소프트웨어', Sound: '음성', Standard: '표준',
StudyRegistration: '연구 등록', WebPage: '웹 페이지',
},
'nl-NL': {
Article: 'Artikel', Audiovisual: 'Audiovisueel', BlogPost: 'Blogbericht',
Book: 'Boek', BookChapter: 'Boekhoofdstuk', BookSeries: 'Boekreeks',
Collection: 'Collectie', Component: 'Component', ComputationalNotebook: 'Berekeningsnotebook',
DataPaper: 'Datapaper', Dataset: 'Dataset', Dissertation: 'Proefschrift',
Document: 'Document', Entry: 'Invoer', Event: 'Evenement',
Figure: 'Figuur', Grant: 'Subsidie', Image: 'Afbeelding',
Instrument: 'Instrument', InteractiveResource: 'Interactieve bron',
Journal: 'Tijdschrift', JournalArticle: 'Tijdschriftartikel',
LegalDocument: 'Juridisch document', Manuscript: 'Manuscript', Map: 'Kaart',
Model: 'Model', Other: 'Overig', OutputManagementPlan: 'Data-managementplan',
Patent: 'Octrooi', PeerReview: 'Collegiale toetsing', Performance: 'Uitvoering',
PersonalCommunication: 'Persoonlijke communicatie', PhysicalObject: 'Fysiek object',
Poster: 'Poster', Presentation: 'Presentatie', Preprint: 'Preprint',
ProceedingsArticle: 'Congresartikel', Report: 'Rapport', Service: 'Dienst',
Software: 'Software', Sound: 'Geluid', Standard: 'Norm',
StudyRegistration: 'Studieregistratie', WebPage: 'Webpagina',
},
'pt-BR': {
Article: 'Artigo', Audiovisual: 'Audiovisual', BlogPost: 'Postagem de blog',
Book: 'Livro', BookChapter: 'Capítulo de livro', BookSeries: 'Série de livros',
Collection: 'Coleção', Component: 'Componente', ComputationalNotebook: 'Caderno computacional',
DataPaper: 'Artigo de dados', Dataset: 'Conjunto de dados', Dissertation: 'Dissertação',
Document: 'Documento', Entry: 'Entrada', Event: 'Evento',
Figure: 'Figura', Grant: 'Financiamento de pesquisa', Image: 'Imagem',
Instrument: 'Instrumento', InteractiveResource: 'Recurso interativo',
Journal: 'Periódico', JournalArticle: 'Artigo de periódico',
LegalDocument: 'Documento legal', Manuscript: 'Manuscrito', Map: 'Mapa',
Model: 'Modelo', Other: 'Outro', OutputManagementPlan: 'Plano de gestão de dados',
Patent: 'Patente', PeerReview: 'Revisão por pares', Performance: 'Apresentação artística',
PersonalCommunication: 'Comunicação pessoal', PhysicalObject: 'Objeto físico',
Poster: 'Pôster', Presentation: 'Apresentação', Preprint: 'Preprint',
ProceedingsArticle: 'Artigo de conferência', Report: 'Relatório', Service: 'Serviço',
Software: 'Software', Sound: 'Som', Standard: 'Norma',
StudyRegistration: 'Registro de estudo', WebPage: 'Página web',
},
'sv-SE': {
Article: 'Artikel', Audiovisual: 'Audiovisuellt material', BlogPost: 'Blogginlägg',
Book: 'Bok', BookChapter: 'Bokkapitel', BookSeries: 'Bokserie',
Collection: 'Samling', Component: 'Komponent', ComputationalNotebook: 'Beräkningsnotatbok',
DataPaper: 'Datapaper', Dataset: 'Dataset', Dissertation: 'Avhandling',
Document: 'Dokument', Entry: 'Post', Event: 'Evenemang',
Figure: 'Figur', Grant: 'Anslag', Image: 'Bild',
Instrument: 'Instrument', InteractiveResource: 'Interaktiv resurs',
Journal: 'Tidskrift', JournalArticle: 'Tidskriftsartikel',
LegalDocument: 'Juridiskt dokument', Manuscript: 'Manuskript', Map: 'Karta',
Model: 'Modell', Other: 'Övrigt', OutputManagementPlan: 'Datahanteringsplan',
Patent: 'Patent', PeerReview: 'Kollegial granskning', Performance: 'Föreställning',
PersonalCommunication: 'Personlig kommunikation', PhysicalObject: 'Fysiskt objekt',
Poster: 'Poster', Presentation: 'Presentation', Preprint: 'Preprint',
ProceedingsArticle: 'Konferensartikel', Report: 'Rapport', Service: 'Tjänst',
Software: 'Programvara', Sound: 'Ljud', Standard: 'Standard',
StudyRegistration: 'Studieregistrering', WebPage: 'Webbsida',
},
'zh-CN': {
Article: '文章', Audiovisual: '视听资料', BlogPost: '博客文章',
Book: '图书', BookChapter: '书目章节', BookSeries: '丛书',
Collection: '集合', Component: '组件', ComputationalNotebook: '计算笔记本',
DataPaper: '数据论文', Dataset: '数据集', Dissertation: '学位论文',
Document: '文档', Entry: '条目', Event: '活动',
Figure: '图表', Grant: '科研经费', Image: '图像',
Instrument: '仪器', InteractiveResource: '交互式资源',
Journal: '期刊', JournalArticle: '期刊文章',
LegalDocument: '法律文件', Manuscript: '手稿', Map: '地图',
Model: '模型', Other: '其他', OutputManagementPlan: '数据管理计划',
Patent: '专利', PeerReview: '同行评审', Performance: '表演',
PersonalCommunication: '个人通信', PhysicalObject: '实体对象',
Poster: '海报', Presentation: '演示文稿', Preprint: '预印本',
ProceedingsArticle: '会议论文', Report: '报告', Service: '服务',
Software: '软件', Sound: '音频', Standard: '标准',
StudyRegistration: '研究注册', WebPage: '网页',
},
}
function typeLabel(type, locale) {
if (!type) return ''
return TYPE_LABELS[locale]?.[type] ?? type.replace(/([A-Z])/g, ' $1').trim()
}
function cleanDoi(raw) {
return raw.trim().replace(/^https?:\/\/(?:dx\.)?doi\.org\//, '')
}
// Returns { type, id } for the given raw identifier string, or null if empty.
// Supports CURIEs (openalex:, doi:, pmid:, pmcid:, arxiv:) and full URLs.
function parseIdentifier(raw) {
const s = raw.trim()
if (!s) return null
// OpenAlex: openalex:Wxxx or https://openalex.org/[works/]Wxxx or bare Wxxx
const oaM = s.match(/^(?:openalex:|https?:\/\/openalex\.org\/(?:works\/)?)?(W\d+)$/i)
if (oaM) return { type: 'openalex', id: oaM[1].toUpperCase() }
// PMID: pmid:xxx or https://pubmed.ncbi.nlm.nih.gov/xxx
const pmidM = s.match(/^(?:pmid:|https?:\/\/pubmed\.ncbi\.nlm\.nih\.gov\/)(\d+)\/?$/)
if (pmidM) return { type: 'pmid', id: pmidM[1] }
// PMCID: pmcid:PMCxxx or https://www.ncbi.nlm.nih.gov/pmc/articles/PMCxxx
const pmcM = s.match(/^(?:pmcid:|https?:\/\/(?:www\.)?ncbi\.nlm\.nih\.gov\/pmc\/articles\/)(PMC\d+)\/?$/i)
if (pmcM) return { type: 'pmcid', id: pmcM[1].toUpperCase() }
// arXiv: arxiv:XXXX.XXXXX or https://arxiv.org/abs/XXXX.XXXXX
const arxivM = s.match(/^(?:arxiv:|https?:\/\/arxiv\.org\/(?:abs|pdf)\/)(\d{4}\.\d{4,}(?:v\d+)?)\/?$/i)
if (arxivM) return { type: 'arxiv', id: arxivM[1] }
// DOI: doi:xxx or URL or bare 10.xxx
const doi = s.replace(/^doi:/i, '').replace(/^https?:\/\/(?:dx\.)?doi\.org\//, '')
return { type: 'doi', id: doi }
}
function stripHtml(html) {
const doc = new DOMParser().parseFromString(html, 'text/html')
return doc.body.textContent ?? ''
}
const INLINE_TAGS = new Set(['b', 'i', 'strong', 'em', 'sup', 'sub'])
function sanitizePlain(text) {
if (!text) return ''
const doc = new DOMParser().parseFromString(text, 'text/html')
function walk(node) {
if (node.nodeType === 3) {
return node.textContent
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
}
if (node.nodeType !== 1) return ''
const tag = node.tagName.toLowerCase()
const children = Array.from(node.childNodes).map(walk).join('')
return INLINE_TAGS.has(tag) ? `<${tag}>${children}</${tag}>` : children
}
return Array.from(doc.body.childNodes).map(walk).join('')
}
// Renders a string that may contain LaTeX math ($...$, $$...$$) and inline
// HTML (b, em, sup, sub). Math regions are typeset by KaTeX; text regions
// are sanitized to only the INLINE_TAGS allowlist.
function renderContent(text) {
if (!text) return ''
// Fix malformed bare numeric refs (&39; → ') before any splitting.
const s = text.replace(/&(\d+);/g, '&#$1;')
const parts = []
let i = 0
while (i < s.length) {
// Display math: $$...$$
if (s[i] === '$' && s[i + 1] === '$') {
const end = s.indexOf('$$', i + 2)
if (end !== -1) {
parts.push(katex.renderToString(s.slice(i + 2, end), { displayMode: true, throwOnError: false }))
i = end + 2
continue
}
}
// Inline math: $...$
if (s[i] === '$') {
const end = s.indexOf('$', i + 1)
if (end !== -1) {
parts.push(katex.renderToString(s.slice(i + 1, end), { displayMode: false, throwOnError: false }))
i = end + 1
continue
}
}
// Plain text until next $
const next = s.indexOf('$', i)
parts.push(sanitizePlain(next === -1 ? s.slice(i) : s.slice(i, next)))
i = next === -1 ? s.length : next
}
return parts.join('')
}
function formatDate(dateStr, locale) {
if (!dateStr) return ''
const parts = dateStr.split('-').map(Number)
if (parts.length >= 3) {
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long', day: 'numeric' })
.format(new Date(parts[0], parts[1] - 1, parts[2]))
} else if (parts.length === 2) {
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long' })
.format(new Date(parts[0], parts[1] - 1, 1))
}
return String(parts[0])
}
function languageLabel(lang, locale) {
if (!lang) return ''
try {
return new Intl.DisplayNames([locale], { type: 'language' }).of(lang) ?? lang
} catch {
return lang
}
}
function getCCIcons(id) {
if (!id) return []
const u = id.toUpperCase()
if (!u.startsWith('CC')) return []
const icons = ['fa-brands:creative-commons']
if (u.startsWith('CC0') || u.includes('ZERO')) {
icons.push('fa-brands:creative-commons-zero')
} else {
if (u.includes('-BY')) icons.push('fa-brands:creative-commons-by')
if (u.includes('-SA')) icons.push('fa-brands:creative-commons-sa')
if (u.includes('-NC')) icons.push('fa-brands:creative-commons-nc')
if (u.includes('-ND')) icons.push('fa-brands:creative-commons-nd')
}
return icons
}
function parseMeta(cmData) {
try {
const d = JSON.parse(cmData)
// Build deduplicated affiliation list across all authors
const affMap = new Map() // key → 1-based index
const affiliations = [] // [{name, ror}]
function affIndex(aff) {
const key = aff.id || aff.name
if (!key) return null
if (!affMap.has(key)) {
affiliations.push({ name: aff.name || aff.id, ror: aff.id?.startsWith('https://ror.org/') ? aff.id : '' })
affMap.set(key, affiliations.length)
}
return affMap.get(key)
}
const authorList = (d.contributors ?? [])
.filter(c => !c.roles?.length || c.roles.includes('Author'))
.map(c => {
if (c.type === 'Person' && c.person) {
const p = c.person
const name = [p.given_name, p.family_name].filter(Boolean).join(' ')
if (!name) return null
const orcid = p.id?.startsWith('https://orcid.org/') ? p.id : ''
const affIndices = (p.affiliations ?? []).map(affIndex).filter(Boolean)
return { name, orcid, affIndices }
} else {
const name = c.organization?.name ?? ''
if (!name) return null
return { name, orcid: '', affIndices: [] }
}
})
.filter(Boolean)
const funders = (d.funding_references ?? [])
.filter(f => f.funder_name || f.funder_id)
const cid = d.container?.identifier ?? ''
const cidType = (d.container?.identifier_type ?? '').toUpperCase()
const containerUrl = cidType === 'DOI' ? `https://doi.org/${cid}`
: cidType === 'ISSN' ? `https://portal.issn.org/resource/ISSN/${cid}`
: ''
return {
title: d.title ?? '',
type: d.type ?? '',
version: d.version ?? '',
datePublished: d.date_published ?? '',
containerTitle: d.container?.title ?? '',
containerUrl,
authorList,
affiliations,
funders,
description: d.description ?? '',
subjects: (d.subjects ?? []).map(s => s.subject).filter(Boolean),
referenceCount: (d.references ?? []).length,
language: d.language ?? '',
licenseTitle: d.license?.title ?? '',
licenseId: d.license?.id ?? '',
licenseUrl: d.license?.url ?? '',
id: d.id ?? '',
}
} catch { return null }
}
// ── Bibliography actions ──────────────────────────────────────────────────
async function addToBib(e) {
e.preventDefault()
const parsed = parseIdentifier(bibDoi)
if (!parsed?.id || bibliography.length >= BIB_MAX) return
const routePrefix = {
openalex: '/openalex',
pmid: '/pmid',
pmcid: '/pmcid',
arxiv: '/arxiv',
}
const prefix = routePrefix[parsed.type] ?? ''
const fetchUrl = prefix
? `${prefix}/${parsed.id}?format=commonmeta`
: `/${parsed.id}?format=commonmeta`
bibLoading = true
bibError = ''
bibFetchingRemote = false
clearTimeout(_remoteTimer)
_remoteTimer = setTimeout(() => { bibFetchingRemote = true }, 300)
try {
// Fetch commonmeta JSON once; stored so style changes need no external API calls.
const dataResp = await fetch(fetchUrl)
if (!dataResp.ok) {
const text = await dataResp.text().catch(() => dataResp.statusText)
throw new Error(`${dataResp.status}: ${text}`)
}
const cmData = await dataResp.text()
const entryId = crypto.randomUUID()
const entryUrl = (() => { try { return JSON.parse(cmData).url ?? null } catch { return null } })()
const html = await formatItems([{ id: entryId, data: cmData }], bibStyle, bibLocale)
.then(items => items[0]?.html ?? '')
bibliography = [...bibliography, { id: entryId, doi: parsed.id, data: cmData, url: entryUrl, html }]
bibDoi = ''
} catch (err) {
bibError = String(err)
} finally {
bibLoading = false
bibFetchingRemote = false
clearTimeout(_remoteTimer)
}
}
async function pickRandom() {
randomLoading = true
bibError = ''
try {
const resp = await fetch('/random')
if (!resp.ok) throw new Error(await resp.text().catch(() => resp.statusText))
const { id } = await resp.json()
bibDoi = id
} catch (err) {
bibError = String(err)
} finally {
randomLoading = false
}
}
async function formatItems(items, style, locale) {
const resp = await fetch('/bibliography', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items, style, locale }),
})
if (!resp.ok) {
const text = await resp.text().catch(() => resp.statusText)
throw new Error(`${resp.status}: ${text}`)
}
return (await resp.json()).items
}
function onBibStyleChange(e) {
const val = e.target.value
if (val === '__more__') {
e.target.value = bibStyle
stylePickerOpen = true
styleSearch = ''
return
}
bibStyle = val
if (bibliography.length > 0) reformatBibliography(val, bibLocale)
}
function pickMoreStyle(style) {
bibStyle = style.value
stylePickerOpen = false
styleSearch = ''
if (bibliography.length > 0) reformatBibliography(style.value, bibLocale)
}
function onBibLocaleChange(e) {
const locale = e.target.value
bibLocale = locale
if (bibliography.length > 0) reformatBibliography(bibStyle, locale)
}
async function reformatBibliography(style, locale) {
bibLoading = true
try {
const formatted = await formatItems(
bibliography.map(e => ({ id: e.id, data: e.data })),
style,
locale,
)
const byId = Object.fromEntries(formatted.map(i => [i.id, i.html]))
bibliography = bibliography.map(e => ({ ...e, html: byId[e.id] ?? e.html }))
} catch (err) {
bibError = String(err)
} finally {
bibLoading = false
}
}
function onDragStart(e, entry) {
dragId = entry.id
e.dataTransfer.effectAllowed = 'move'
}
function onDragOver(e, entry) {
if (!dragId || dragId === entry.id) return
e.preventDefault()
const rect = e.currentTarget.getBoundingClientRect()
dropPosition = { id: entry.id, before: e.clientY < rect.top + rect.height / 2 }
}
function onDragLeave(e) {
if (!e.currentTarget.contains(e.relatedTarget)) dropPosition = null
}
function onDrop(e, entry) {
e.preventDefault()
if (!dragId || !dropPosition || dragId === entry.id) return
const from = bibliography.findIndex(b => b.id === dragId)
const moved = bibliography[from]
const rest = bibliography.filter(b => b.id !== dragId)
const toIdx = rest.findIndex(b => b.id === dropPosition.id)
rest.splice(dropPosition.before ? toIdx : toIdx + 1, 0, moved)
bibliography = rest
dragId = null
dropPosition = null
}
function onDragEnd() {
dragId = null
dropPosition = null
dragHandleActive = false
}
function deleteEntry(id) {
bibliography = bibliography.filter(e => e.id !== id)
}
function deleteBibliography() {
bibliography = []
}
async function copyCitation(entry) {
await navigator.clipboard.writeText(stripHtml(entry.html))
copiedCiteId = entry.id
setTimeout(() => { copiedCiteId = null }, 2000)
}
async function copyPre(text, idx) {
await navigator.clipboard.writeText(text)
copiedPreIdx = idx
setTimeout(() => { copiedPreIdx = null }, 2000)
}
// ── Export actions ────────────────────────────────────────────────────────
const expExtensions = { citation: 'txt', bibtex: 'bib', ris: 'ris', csl: 'json', commonmeta: 'json',
crossref: 'json', crossref_xml: 'xml', datacite: 'json', datacite_xml: 'xml',
inveniordm: 'json', schemaorg: 'json' }
const jsonFormats = new Set(['csl', 'commonmeta', 'crossref', 'datacite', 'inveniordm', 'schemaorg'])
async function fetchBibExport() {
if (expFormat === 'citation') {
return bibliography.map(e => stripHtml(e.html)).join('\n\n')
}
const results = await Promise.all(
bibliography.map(async entry => {
const resp = await fetch(`/${entry.doi}?format=${expFormat}`)
if (!resp.ok) { const t = await resp.text().catch(() => resp.statusText); throw new Error(`${resp.status}: ${t}`) }
return resp.text()
})
)
return jsonFormats.has(expFormat)
? JSON.stringify(results.map(r => JSON.parse(r)), null, 2)
: results.join('\n\n')
}
async function exportBibliography() {
if (!bibliography.length) return
expLoading = true
expError = ''
try {
const combined = await fetchBibExport()
const ext = expExtensions[expFormat] ?? 'txt'
const url = URL.createObjectURL(new Blob([combined], { type: 'text/plain' }))
Object.assign(document.createElement('a'), { href: url, download: `bibliography.${ext}` }).click()
URL.revokeObjectURL(url)
} catch (err) { expError = String(err) }
finally { expLoading = false }
}
async function copyBibExport() {
if (!bibliography.length) return
expLoading = true
expError = ''
expCopied = false
try {
const combined = await fetchBibExport()
await navigator.clipboard.writeText(combined)
expCopied = true
setTimeout(() => { expCopied = false }, 2000)
} catch (err) { expError = String(err) }
finally { expLoading = false }
}
</script>
<div class="min-h-screen flex flex-col bg-background text-foreground antialiased">
<!-- Header -->
<header class="bg-[#f2f2f2] dark:bg-gray-800 shrink-0 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-4xl mx-auto px-6 h-14 flex items-center justify-between gap-4">
<div class="flex items-center gap-2 min-w-0">
<!-- Lightbulb icon matching python.commonmeta.org -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 text-primary shrink-0">
<path d="M12 .75a8.25 8.25 0 0 0-4.135 15.39c.686.398 1.115 1.143 1.115 1.942V18h5.25v-.008c0-.799.43-1.544 1.115-1.942A8.25 8.25 0 0 0 12 .75Z" />
<path fill-rule="evenodd" d="M9.013 19.9a.75.75 0 0 1 .877-.597 11.319 11.319 0 0 0 4.22 0 .75.75 0 1 1 .28 1.473 12.819 12.819 0 0 1-4.78 0 .75.75 0 0 1-.597-.876ZM9 22.5a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 9 22.5Z" clip-rule="evenodd" />
</svg>
<span class="text-base font-bold tracking-tight shrink-0 text-gray-900 dark:text-white">Commonmeta</span>
<span class="text-sm text-gray-600 dark:text-gray-400 truncate">{_('app.description')}</span>
</div>
<div class="flex items-center gap-3 shrink-0">
<button
type="button"
onclick={toggleDark}
aria-label="Toggle dark mode"
class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors"
>
{#if isDark}
<!-- Sun -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
{:else}
<!-- Moon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
{/if}
</button>
<Select bind:value={bibLocale} onchange={onBibLocaleChange}
class="h-7 w-36 text-xs py-0">
{#each locales as l}
<option value={l.value}>{l.label}</option>
{/each}
</Select>
</div>
</div>
</header>
<!-- Main -->
<main class="flex-1 max-w-4xl w-full mx-auto px-6 py-10 space-y-12">
<!-- ── Bibliography ─────────────────────────────────────────────────── -->
<section>
<h2 class="text-xl font-bold text-primary mb-4">{_('bibliography.title')}</h2>
<!-- DOI input card -->
<Card>
<CardContent class="p-5">
<form onsubmit={addToBib} class="flex gap-3">
<div class="relative flex-1">
<Input
type="text"
bind:value={bibDoi}
placeholder={_('bibliography.placeholder')}
autocomplete="off"
autocorrect="off"
autocapitalize="none"
spellcheck="false"
class="font-mono w-full {hasDb ? 'pr-9' : ''}"
/>
{#if hasDb}
<button
type="button"
onclick={pickRandom}
disabled={randomLoading}
aria-label={_('bibliography.random')}
class="group absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40"
>
{#if randomLoading}
<span class="block w-4 h-4 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground animate-spin"></span>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/>
<circle cx="7.5" cy="7.5" r="0.5" fill="currentColor"/>
<circle cx="12" cy="7.5" r="0.5" fill="currentColor"/>
<circle cx="16.5" cy="7.5" r="0.5" fill="currentColor"/>
<circle cx="7.5" cy="16.5" r="0.5" fill="currentColor"/>
<circle cx="12" cy="16.5" r="0.5" fill="currentColor"/>
<circle cx="16.5" cy="16.5" r="0.5" fill="currentColor"/>
</svg>
{/if}
<span class="pointer-events-none absolute z-50 whitespace-nowrap rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground shadow-md bottom-full right-0 mb-2 opacity-0 group-hover:opacity-100 transition-opacity">
{_('bibliography.random')}
</span>
</button>
{/if}
</div>
<Button type="submit" disabled={bibLoading || !bibDoi.trim() || bibliography.length >= BIB_MAX}>
{#if bibLoading}
<span class="w-4 h-4 rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground animate-spin"></span>
{:else}
{_('bibliography.add')}
{/if}
</Button>
</form>
{#if bibFetchingRemote}
<div class="mt-3 h-1 w-full overflow-hidden rounded-full bg-muted" aria-live="polite">
<div class="fetch-progress h-full w-1/4 rounded-full bg-primary"></div>
</div>
{/if}
{#if bibError}
<div class="mt-3 px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-md text-sm text-destructive" role="alert">
{bibError}
</div>
{/if}
</CardContent>
</Card>
<!-- Bibliography list -->
{#if bibliography.length > 0}
<div class="mt-4 border border-border rounded-md overflow-hidden">
<ol class="divide-y divide-border">
{#each bibliography as entry, i (entry.id)}
{@const meta = parseMeta(entry.data)}
<li
draggable={dragHandleActive}
onpointerdown={() => { dragHandleActive = false }}
ondragstart={e => { if (!dragHandleActive) { e.preventDefault(); return } onDragStart(e, entry) }}
ondragover={e => onDragOver(e, entry)}
ondragleave={onDragLeave}
ondrop={e => onDrop(e, entry)}
ondragend={onDragEnd}
class="flex items-start gap-3 px-5 py-4 bg-background transition-opacity
{dragId === entry.id ? 'opacity-30' : ''}
{dropPosition?.id === entry.id && dropPosition.before ? 'border-t-2 border-primary' : ''}
{dropPosition?.id === entry.id && !dropPosition.before ? 'border-b-2 border-primary' : ''}"
>
<!-- Grip handle -->
<div
onpointerdown={e => { e.stopPropagation(); dragHandleActive = true }}
class="shrink-0 pt-1 cursor-grab active:cursor-grabbing text-muted-foreground/30 hover:text-muted-foreground/60">
<svg viewBox="0 0 8 14" fill="currentColor" class="w-2 h-3.5">
<circle cx="2" cy="2" r="1.5"/>
<circle cx="6" cy="2" r="1.5"/>
<circle cx="2" cy="7" r="1.5"/>
<circle cx="6" cy="7" r="1.5"/>
<circle cx="2" cy="12" r="1.5"/>
<circle cx="6" cy="12" r="1.5"/>
</svg>
</div>
<span class="text-xs text-muted-foreground w-5 shrink-0 pt-0.5 text-right">{i + 1}.</span>
<!-- Structured metadata -->
<div class="flex-1 min-w-0 space-y-0.5">
{#if meta}
{#if meta.title}
<p class="text-lg font-semibold leading-snug">{@html renderContent(meta.title)}</p>
{/if}
{@const formattedDate = formatDate(meta.datePublished, bibLocale)}
{@const typeLbl = typeLabel(meta.type, bibLocale)}
{@const langLbl = languageLabel(meta.language, bibLocale)}
{@const line2 = [
typeLbl,
meta.version ? `${_('bibliography.version')} ${meta.version}` : '',
formattedDate ? `${_('bibliography.published')} ${formattedDate}` : '',
meta.containerTitle ? `${_('bibliography.in')} ${meta.containerTitle}` : '',
langLbl ? `${_('bibliography.in')} ${langLbl}` : '',
].filter(Boolean).join(' ')}
{#if line2}
<p class="text-sm text-muted-foreground">{line2}</p>
{/if}
{#if meta.authorList.length > 0}
<p class="text-sm text-muted-foreground leading-relaxed">
{#each meta.authorList as author, ai}
{#if ai > 0}, {/if}<span class="inline-flex items-baseline gap-px">{author.name}{#if author.affIndices.length > 0}<sup class="text-[0.65em] leading-none ml-px">{author.affIndices.join(',')}</sup>{/if}{#if author.orcid}<a href={author.orcid} target="_blank" rel="noreferrer" aria-label="ORCID" class="ml-0.5 inline-block align-middle opacity-70 hover:opacity-100 shrink-0"><img src={orcidSvg} width="14" height="14" alt="ORCID" /></a>{/if}</span>
{/each}
</p>
{#if meta.affiliations.length > 0}
<p class="text-sm text-muted-foreground/60">
{#each meta.affiliations as aff, i}
{#if i > 0} · {/if}<sup class="text-[0.65em] leading-none">{i + 1}</sup> {aff.name}{#if aff.ror}<a href={aff.ror} target="_blank" rel="noreferrer" aria-label="ROR" class="ml-0.5 inline-block align-middle opacity-70 hover:opacity-100 shrink-0"><img src={rorSvg} width="14" height="14" alt="ROR" /></a>{/if}
{/each}
</p>
{/if}
{/if}
{#if meta.funders.length > 0}
<p class="text-sm text-muted-foreground/60">
{_('bibliography.funded_by')}
{#each meta.funders as f, i}{#if i > 0}, {/if}{f.funder_name || f.funder_id}{#if f.funder_id?.startsWith('http')}<a href={f.funder_id} target="_blank" rel="noreferrer" aria-label="ROR" class="ml-0.5 inline-block align-middle opacity-70 hover:opacity-100 shrink-0"><img src={rorSvg} width="14" height="14" alt="ROR" /></a>{/if}{#if f.award_title || f.award_number}{@const awardLabel = f.award_title ? (f.award_number ? `${f.award_title} (${f.award_number})` : f.award_title) : f.award_number}: {#if f.award_id?.startsWith('http')}<a href={f.award_id} target="_blank" rel="noreferrer" class="hover:underline">{awardLabel}</a>{:else}{awardLabel}{/if}{/if}{/each}
</p>
{/if}
{#if meta.description}
<p class="text-sm text-muted-foreground line-clamp-3 mt-1 mb-1">{@html renderContent(meta.description)}</p>
{/if}
{#if meta.referenceCount > 0}
<p class="text-sm text-muted-foreground/60">{meta.referenceCount} {_('bibliography.references')}</p>
{/if}
{#if meta.licenseTitle || meta.licenseId}
{@const ccIcons = getCCIcons(meta.licenseId)}
<p class="text-sm text-muted-foreground/60 inline-flex flex-wrap items-center gap-0.5">
{_('bibliography.license')} {meta.licenseTitle}{#if meta.licenseId}{meta.licenseTitle ? ' ' : ''}({#if meta.licenseUrl}<a href={meta.licenseUrl} target="_blank" rel="noreferrer" class="hover:font-semibold hover:underline">{meta.licenseId}</a>{:else}{meta.licenseId}{/if}){/if}.{#if ccIcons.length > 0} {#each ccIcons as icon}<Icon {icon} width="16" height="16" />{/each}{/if}
</p>
{/if}
{#if meta.subjects.length > 0}
<div class="flex flex-wrap gap-1 my-1">
{#each meta.subjects.slice(0, 5) as subject}
<span class="inline-flex items-center rounded-full border border-border px-2.5 py-0.5 text-xs font-medium text-muted-foreground">{subject}</span>
{/each}
</div>
{/if}
{#if meta.id}
<a href={meta.id} target="_blank" rel="noreferrer"
class="text-sm text-primary hover:underline inline-flex items-center gap-1.5 pt-0.5"
>{#if meta.id.startsWith('https://doi.org/')}<Icon icon="academicons:doi" width="16" height="16" color="#fab608" />{/if}{meta.id}</a>
{/if}
{/if}
</div>
<!-- Per-entry icon buttons -->
<div class="flex flex-col gap-0.5 shrink-0">
{#if entry.url}
<a href={entry.url} target="_blank" rel="noreferrer"
class="inline-flex items-center justify-center h-7 w-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
aria-label="Open URL"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
{/if}
<Button type="button" variant="ghost" size="icon"
onclick={() => copyCitation(entry)}
aria-label={_('bibliography.copy_citation')}
class="h-7 w-7 text-muted-foreground hover:text-foreground"
>
{#if copiedCiteId === entry.id}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>
{/if}
</Button>
<Button type="button" variant="ghost" size="icon"
onclick={() => deleteEntry(entry.id)}
aria-label={_('bibliography.delete_entry')}
class="h-7 w-7 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</Button>
</div>
</li>
{/each}
</ol>
</div>
<!-- Citation style selector — between list and export actions -->
<div class="mt-2 relative">
<Select value={bibStyle} onchange={onBibStyleChange} class="w-full">
{#if !commonStyles.some(s => s.value === bibStyle)}
<option value={bibStyle}>{allCitationStyles.find(s => s.value === bibStyle)?.label ?? bibStyle}</option>
<option disabled>──────────────────────</option>
{/if}
{#each commonStyles as s}
<option value={s.value}>{s.label}</option>
{/each}
<option disabled>──────────────────────</option>
<option value="__more__">More styles…</option>
</Select>
{#if stylePickerOpen}
<div class="fixed inset-0 z-40" role="presentation" onclick={() => { stylePickerOpen = false; styleSearch = '' }}></div>
<div class="absolute top-full left-0 right-0 z-50 mt-1 bg-background border border-border rounded-md shadow-lg">
<div class="p-2 border-b border-border">
<!-- svelte-ignore a11y_autofocus -->
<input
bind:value={styleSearch}
placeholder="Search styles…"
autofocus
class="w-full text-sm px-3 py-1.5 rounded border border-input bg-background outline-none focus:ring-1 focus:ring-ring"
onkeydown={e => { if (e.key === 'Escape') { stylePickerOpen = false; styleSearch = '' } }}
/>
</div>
<ul class="max-h-72 overflow-y-auto py-1">
{#each allCitationStyles.filter(s => !styleSearch || s.label.toLowerCase().includes(styleSearch.toLowerCase())) as s}
<li>
<button
type="button"
class="w-full text-left px-3 py-1.5 text-sm hover:bg-muted {s.value === bibStyle ? 'font-semibold text-primary' : ''}"
onclick={() => pickMoreStyle(s)}
>{s.label}</button>
</li>
{/each}
{#if allCitationStyles.filter(s => !styleSearch || s.label.toLowerCase().includes(styleSearch.toLowerCase())).length === 0}
<li class="px-3 py-2 text-sm text-muted-foreground">No styles found</li>
{/if}
</ul>
</div>
{/if}
</div>
<!-- Bibliography-level actions -->
<div class="mt-2 border border-border rounded-md bg-muted px-4 py-3 flex items-center gap-2 flex-wrap">
<Select bind:value={expFormat} class="w-40 h-8 text-xs py-0">
{#each expFormats as f}
<option value={f.value}>{f.i18n ? _(f.i18n) : f.label}</option>
{/each}
</Select>
<Button variant="default" onclick={exportBibliography} disabled={expLoading} class="h-8 gap-1.5 text-xs px-3">
{#if expLoading}
<span class="w-3.5 h-3.5 rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground animate-spin"></span>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
{/if}
{_('bibliography.export')}
</Button>
<Button variant="outline" onclick={copyBibExport} disabled={expLoading} class="h-8 gap-1.5 text-xs px-3">
{#if expCopied}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
{_('bibliography.copied')}
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>
{_('bibliography.copy')}
{/if}
</Button>
<Button
variant="outline"
onclick={deleteBibliography}
class="h-8 gap-1.5 text-xs px-3 ml-auto text-destructive border-destructive/30 hover:bg-destructive/10 hover:text-destructive"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
{_('bibliography.delete')}
</Button>
{#if expError}
<p class="w-full text-xs text-destructive">{expError}</p>
{/if}
</div>
{/if}
</section>
<!-- ── Docs ─────────────────────────────────────────────────────────── -->
<!-- Supported Metadata Formats table -->
<div class="mb-8">
<h2 class="text-xl font-bold text-primary border-b border-border pb-1 mb-4">{_('docs.formats.title')}</h2>
<div class="rounded-md border border-border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Format</TableHead>
<TableHead>Name</TableHead>
<TableHead>Content Type</TableHead>
<TableHead class="text-center">Read</TableHead>
<TableHead class="text-center">Write</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Commonmeta</TableCell>
<TableCell class="font-mono text-xs">commonmeta</TableCell>
<TableCell class="font-mono text-xs">application/vnd.commonmeta+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="http://en.wikipedia.org/wiki/BibTeX" class="text-primary hover:underline">BibTeX</a></TableCell>
<TableCell class="font-mono text-xs">bibtex</TableCell>
<TableCell class="font-mono text-xs">application/x-bibtex</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://citation-file-format.github.io/" class="text-primary hover:underline">Citation File Format (CFF)</a></TableCell>
<TableCell class="font-mono text-xs">cff</TableCell>
<TableCell class="font-mono text-xs">application/vnd.cff+yaml</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center text-muted-foreground">later</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://codemeta.github.io/" class="text-primary hover:underline">Codemeta</a></TableCell>
<TableCell class="font-mono text-xs">codemeta</TableCell>
<TableCell class="font-mono text-xs">application/vnd.codemeta.ld+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center text-muted-foreground">later</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://api.crossref.org" class="text-primary hover:underline">Crossref</a></TableCell>
<TableCell class="font-mono text-xs">crossref</TableCell>
<TableCell class="font-mono text-xs">application/vnd.crossref+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://www.crossref.org/schema/documentation/unixref1.1/unixref1.1.html" class="text-primary hover:underline">CrossRef XML</a></TableCell>
<TableCell class="font-mono text-xs">crossref_xml</TableCell>
<TableCell class="font-mono text-xs">application/vnd.crossref.unixref+xml</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://citationstyles.org/" class="text-primary hover:underline">CSL-JSON</a></TableCell>
<TableCell class="font-mono text-xs">csl</TableCell>
<TableCell class="font-mono text-xs">application/vnd.citationstyles.csl+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://api.datacite.org/" class="text-primary hover:underline">DataCite</a></TableCell>
<TableCell class="font-mono text-xs">datacite</TableCell>
<TableCell class="font-mono text-xs">application/vnd.datacite.datacite+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://datacite-metadata-schema.readthedocs.io/en/4.7/" class="text-primary hover:underline">DataCite XML</a></TableCell>
<TableCell class="font-mono text-xs">datacite_xml</TableCell>
<TableCell class="font-mono text-xs">application/vnd.datacite.datacite+xml</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://citationstyles.org/" class="text-primary hover:underline">Formatted Citation</a></TableCell>
<TableCell class="font-mono text-xs">citation</TableCell>
<TableCell class="font-mono text-xs">text/x-bibliography</TableCell>
<TableCell class="text-center text-muted-foreground">n/a</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://inveniordm.docs.cern.ch/reference/metadata/" class="text-primary hover:underline">InvenioRDM</a></TableCell>
<TableCell class="font-mono text-xs">inveniordm</TableCell>
<TableCell class="font-mono text-xs">application/vnd.inveniordm.v1+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://www.jsonfeed.org/" class="text-primary hover:underline">JSON Feed</a></TableCell>
<TableCell class="font-mono text-xs">jsonfeed</TableCell>
<TableCell class="font-mono text-xs">application/feed+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center text-muted-foreground">later</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="https://www.openalex.org/" class="text-primary hover:underline">OpenAlex</a></TableCell>
<TableCell class="font-mono text-xs">openalex</TableCell>
<TableCell class="font-mono text-xs">n/a</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center text-muted-foreground">later</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="http://en.wikipedia.org/wiki/RIS_(file_format)" class="text-primary hover:underline">RIS</a></TableCell>
<TableCell class="font-mono text-xs">ris</TableCell>
<TableCell class="font-mono text-xs">application/x-research-info-systems</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
<TableRow>
<TableCell><a href="http://schema.org/" class="text-primary hover:underline">Schema.org (JSON-LD)</a></TableCell>
<TableCell class="font-mono text-xs">schemaorg</TableCell>
<TableCell class="font-mono text-xs">application/vnd.schemaorg.ld+json</TableCell>
<TableCell class="text-center">✓</TableCell>
<TableCell class="text-center">✓</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
{#snippet codeBlock(code, idx)}
<div class="relative group my-3">
<pre class="!my-0"><code>{code}</code></pre>
<button
type="button"
onclick={() => copyPre(code, idx)}
aria-label="Copy"
class="absolute top-1.5 right-1.5 h-7 w-7 flex items-center justify-center rounded
text-muted-foreground hover:text-foreground
opacity-0 group-hover:opacity-100 transition-opacity"
>
{#if copiedPreIdx === idx}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>
{/if}
</button>
</div>
{/snippet}
<div class="prose prose-sm prose-slate max-w-none
[&_h2]:text-primary [&_h2]:border-b [&_h2]:border-border [&_h2]:pb-1
[&_a]:text-primary [&_pre]:bg-muted [&_pre]:border [&_pre]:border-border
[&_pre]:text-sm [&_pre]:text-gray-800 dark:[&_pre]:text-gray-200">
<h2>{_('docs.usage.title')}</h2>
<p>{_('docs.doi_resolution.intro')}</p>
{@render codeBlock('curl https://commonmeta.org/10.1371/journal.pcbi.1000204', 0)}
<h2>{_('docs.content_negotiation.title')}</h2>
<p>{@html _('docs.content_negotiation.intro')}</p>
<p>{_('docs.content_negotiation.bibtex_label')}</p>
{@render codeBlock(`curl -H "Accept: application/x-bibtex" \\\n https://commonmeta.org/10.1371/journal.pcbi.1000204`, 1)}
<p>{@html _('docs.content_negotiation.format_param_label')}</p>
{@render codeBlock('curl https://commonmeta.org/10.1371/journal.pcbi.1000204?format=bibtex', 2)}
<p>{@html _('docs.content_negotiation.citation_style_label')}</p>
{@render codeBlock(`curl -H "Accept: text/x-bibliography; style=vancouver; locale=de-DE" \\\n https://commonmeta.org/10.1371/journal.pcbi.1000204`, 3)}
<p>{_('docs.content_negotiation.query_params_label')}</p>
{@render codeBlock('curl "https://commonmeta.org/10.1371/journal.pcbi.1000204?format=citation&style=vancouver&locale=de-DE"', 4)}
<p>{@html _('docs.content_negotiation.multiple_types_label')}</p>
{@render codeBlock(`curl -H "Accept: application/vnd.citationstyles.csl+json;q=0.9, application/x-bibtex" \\\n https://commonmeta.org/10.1371/journal.pcbi.1000204`, 5)}
</div>
</main>
<!-- Footer -->
<footer class="py-4 text-xs text-gray-700 dark:text-gray-300">
<div class="max-w-4xl mx-auto px-6 flex items-center justify-between">
<span class="[&_a]:hover:text-gray-900 dark:[&_a]:hover:text-gray-100 [&_a]:transition-colors">
{@html _('footer.copyright')}
</span>
<span class="flex items-center gap-3">
<a href="mailto:info@front-matter.de" aria-label="Email"
class="hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg>
</a>
<a href="https://hachyderm.io/@mfenner" target="_blank" rel="noreferrer" aria-label="Mastodon"
class="hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a4 4 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522q0-1.288.66-2.046c.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764q.662.757.661 2.046z"/>
</svg>
</a>
</span>
</div>
</footer>
</div>
<style>
@keyframes fetch-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(500%); }
}
.fetch-progress {
animation: fetch-slide 1.2s ease-in-out infinite;
}
</style>