Skip to content

Commit eca3f82

Browse files
author
NoUsernameAvailable
committed
add embed widget, added latex rendering, fix duplicate crossref entries, unified links into the entry title
1 parent 2588d11 commit eca3f82

File tree

7 files changed

+712
-13
lines changed

7 files changed

+712
-13
lines changed

embed.js

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
/**
2+
* BibTeX Embed Widget
3+
* Usage: <script src="https://roars.dev/bibtex/embed.js?bib=URL_TO_BIB_FILE"></script>
4+
*/
5+
(function () {
6+
'use strict';
7+
8+
// Get script parameters
9+
const currentScript = document.currentScript;
10+
const scriptSrc = currentScript ? currentScript.src : '';
11+
const params = new URLSearchParams(scriptSrc.split('?')[1] || '');
12+
let bibUrl = params.get('bib') || '';
13+
14+
if (!bibUrl) {
15+
console.error('[BibTeX Embed] No bib URL provided. Usage: embed.js?bib=URL');
16+
return;
17+
}
18+
19+
// Convert GitHub blob URL to raw
20+
if (bibUrl.includes('github.com') && bibUrl.includes('/blob/')) {
21+
bibUrl = bibUrl.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/');
22+
}
23+
24+
// Create container
25+
const container = document.createElement('div');
26+
container.className = 'bibtex-embed';
27+
container.innerHTML = '<div class="bibtex-loading">Loading publications...</div>';
28+
currentScript.parentNode.insertBefore(container, currentScript);
29+
30+
// Inject styles
31+
const style = document.createElement('style');
32+
style.textContent = `
33+
.bibtex-embed {
34+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
35+
font-size: 14px;
36+
line-height: 1.6;
37+
color: #333;
38+
}
39+
.bibtex-embed * { box-sizing: border-box; }
40+
.bibtex-loading {
41+
padding: 1rem;
42+
color: #666;
43+
font-style: italic;
44+
}
45+
.bibtex-error {
46+
padding: 1rem;
47+
color: #c00;
48+
background: #fee;
49+
border: 1px solid #fcc;
50+
border-radius: 4px;
51+
}
52+
.bibtex-group {
53+
margin-bottom: 1.5rem;
54+
}
55+
.bibtex-group-title {
56+
font-size: 1.1em;
57+
font-weight: 600;
58+
color: #111;
59+
border-bottom: 2px solid #333;
60+
padding-bottom: 0.25rem;
61+
margin-bottom: 0.75rem;
62+
}
63+
.bibtex-pub {
64+
padding: 0.75rem 0;
65+
border-bottom: 1px solid #eee;
66+
}
67+
.bibtex-pub:last-child { border-bottom: none; }
68+
.bibtex-title {
69+
font-weight: 500;
70+
color: #000;
71+
margin-bottom: 0.25rem;
72+
}
73+
.bibtex-title a {
74+
color: inherit;
75+
text-decoration: none;
76+
}
77+
.bibtex-title a:hover { text-decoration: underline; }
78+
.bibtex-authors {
79+
color: #555;
80+
font-size: 0.9em;
81+
}
82+
.bibtex-venue {
83+
color: #666;
84+
font-size: 0.85em;
85+
font-style: italic;
86+
}
87+
.bibtex-links {
88+
margin-top: 0.25rem;
89+
}
90+
.bibtex-links a {
91+
display: inline-block;
92+
font-size: 0.8em;
93+
color: #0066cc;
94+
margin-right: 0.75rem;
95+
text-decoration: none;
96+
}
97+
.bibtex-links a:hover { text-decoration: underline; }
98+
.bibtex-badge {
99+
display: inline-block;
100+
font-size: 0.7em;
101+
padding: 0.15em 0.4em;
102+
border-radius: 3px;
103+
background: #f0f0f0;
104+
color: #666;
105+
margin-left: 0.5rem;
106+
text-transform: uppercase;
107+
font-weight: 500;
108+
}
109+
.bibtex-footer {
110+
margin-top: 1rem;
111+
padding-top: 0.5rem;
112+
border-top: 1px solid #ddd;
113+
font-size: 0.75em;
114+
color: #999;
115+
}
116+
.bibtex-footer a { color: #666; }
117+
`;
118+
document.head.appendChild(style);
119+
120+
function parseBibTeX(content) {
121+
const entries = [];
122+
const stringDefs = {};
123+
124+
const stringPattern = /@string\s*\{\s*(\w+)\s*=\s*\{([^}]*)\}\s*\}/gi;
125+
let match;
126+
while ((match = stringPattern.exec(content)) !== null) {
127+
stringDefs[match[1].toLowerCase()] = match[2].trim();
128+
}
129+
130+
// Extract entries
131+
const entryPattern = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g;
132+
while ((match = entryPattern.exec(content)) !== null) {
133+
const type = match[1].toLowerCase();
134+
const key = match[2];
135+
const startPos = match.index + match[0].length;
136+
137+
if (['preamble', 'string', 'comment'].includes(type)) continue;
138+
139+
let braceCount = 1, pos = startPos;
140+
while (pos < content.length && braceCount > 0) {
141+
if (content[pos] === '{') braceCount++;
142+
else if (content[pos] === '}') braceCount--;
143+
pos++;
144+
}
145+
146+
if (braceCount === 0) {
147+
const fieldsContent = content.slice(startPos, pos - 1);
148+
const fields = parseFields(fieldsContent);
149+
entries.push({ type, key, fields });
150+
}
151+
}
152+
153+
const entriesMap = new Map(entries.map(e => [e.key, e]));
154+
const usedCrossrefs = new Set();
155+
156+
return entries.map(entry => {
157+
let fields = entry.fields;
158+
if (fields.crossref) {
159+
const parent = entriesMap.get(fields.crossref);
160+
if (parent) {
161+
usedCrossrefs.add(fields.crossref);
162+
fields = { ...parent.fields, ...fields };
163+
}
164+
}
165+
return normalizeEntry(entry.type, entry.key, fields, stringDefs);
166+
}).filter(e => e.title && e.title !== 'Untitled' && !usedCrossrefs.has(e.key));
167+
}
168+
169+
function parseFields(content) {
170+
const fields = {};
171+
const pattern = /(\w+)\s*=\s*(?:\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}|"([^"]*)"|(\w+))/g;
172+
let match;
173+
while ((match = pattern.exec(content)) !== null) {
174+
const key = match[1].toLowerCase();
175+
const value = match[2] || match[3] || match[4] || '';
176+
fields[key] = cleanLatex(value.trim());
177+
}
178+
return fields;
179+
}
180+
181+
function cleanLatex(text) {
182+
if (!text) return '';
183+
return text
184+
.replace(/\$\^[\{]?([^\$\}]+)[\}]?\$/g, '<sup>$1</sup>')
185+
.replace(/\^\{([^}]+)\}/g, '<sup>$1</sup>')
186+
.replace(/\$_[\{]?([^\$\}]+)[\}]?\$/g, '<sub>$1</sub>')
187+
.replace(/_\{([^}]+)\}/g, '<sub>$1</sub>')
188+
.replace(/\\href\{([^}]*)\}\{([^}]*)\}/g, '$2')
189+
.replace(/\\url\{([^}]*)\}/g, '$1')
190+
.replace(/\\textit\{([^}]*)\}/g, '<em>$1</em>')
191+
.replace(/\\textbf\{([^}]*)\}/g, '<strong>$1</strong>')
192+
.replace(/\\emph\{([^}]*)\}/g, '<em>$1</em>')
193+
.replace(/\\&/g, '&')
194+
.replace(/\\\\/g, '')
195+
.replace(/[\{\}]/g, '')
196+
.replace(/\$/g, '')
197+
.replace(/\s+/g, ' ')
198+
.trim();
199+
}
200+
201+
function normalizeEntry(type, key, fields, stringDefs) {
202+
let venue = fields.booktitle || fields.journal || '';
203+
const venueKey = venue.toLowerCase();
204+
if (stringDefs[venueKey]) venue = stringDefs[venueKey];
205+
206+
let url = fields.url || null;
207+
if (fields.note) {
208+
const urlMatch = fields.note.match(/https?:\/\/[^\s}]+/i);
209+
if (urlMatch) url = urlMatch[0];
210+
}
211+
212+
let pubType = 'misc';
213+
if (type === 'inproceedings' || type === 'conference') pubType = 'conference';
214+
else if (type === 'article') pubType = 'journal';
215+
else if ((type === 'misc' || type === 'unpublished') && fields.eprint) pubType = 'preprint';
216+
217+
return {
218+
key,
219+
type: pubType,
220+
title: fields.title || 'Untitled',
221+
authors: formatAuthors(fields.author || ''),
222+
year: parseInt(fields.year) || 0,
223+
venue: cleanLatex(venue),
224+
doi: fields.doi || null,
225+
url
226+
};
227+
}
228+
229+
function formatAuthors(str) {
230+
if (!str) return '';
231+
return str.split(/\s+and\s+/i).map(a => {
232+
a = a.replace(/\$\^[^$]*\$/g, '').trim();
233+
if (a.includes(',')) {
234+
const parts = a.split(',').map(p => p.trim());
235+
return parts.length >= 2 ? `${parts[1]} ${parts[0]}` : a;
236+
}
237+
return a;
238+
}).join(', ');
239+
}
240+
241+
// Render functions
242+
function render(publications) {
243+
const grouped = {};
244+
publications.forEach(pub => {
245+
const year = pub.year || 'Unknown';
246+
if (!grouped[year]) grouped[year] = [];
247+
grouped[year].push(pub);
248+
});
249+
250+
const years = Object.keys(grouped).sort((a, b) => (parseInt(b) || 0) - (parseInt(a) || 0));
251+
252+
let html = '';
253+
years.forEach(year => {
254+
html += `<div class="bibtex-group">
255+
<div class="bibtex-group-title">${year}</div>`;
256+
grouped[year].forEach(pub => {
257+
html += renderPub(pub);
258+
});
259+
html += '</div>';
260+
});
261+
262+
html += `<div class="bibtex-footer">
263+
Powered by <a href="https://roars.dev/bibtex/" target="_blank">BibTeX Parser</a>
264+
</div>`;
265+
266+
container.innerHTML = html;
267+
}
268+
269+
function renderPub(pub) {
270+
const typeLabels = { conference: 'Conf', journal: 'Journal', preprint: 'Preprint' };
271+
const badge = typeLabels[pub.type] ? `<span class="bibtex-badge">${typeLabels[pub.type]}</span>` : '';
272+
273+
let titleHtml = pub.title;
274+
if (pub.url) {
275+
titleHtml = `<a href="${pub.url}" target="_blank">${pub.title}</a>`;
276+
} else if (pub.doi) {
277+
titleHtml = `<a href="https://doi.org/${pub.doi}" target="_blank">${pub.title}</a>`;
278+
}
279+
280+
let links = '';
281+
// if (pub.url) links += `<a href="${pub.url}" target="_blank">📄 PDF</a>`;
282+
if (pub.doi) links += `<a href="https://doi.org/${pub.doi}" target="_blank">🔗 DOI</a>`;
283+
284+
return `<div class="bibtex-pub">
285+
<div class="bibtex-title">${titleHtml}${badge}</div>
286+
<div class="bibtex-authors">${pub.authors}</div>
287+
<div class="bibtex-venue">${pub.venue}${pub.year ? ` (${pub.year})` : ''}</div>
288+
${links ? `<div class="bibtex-links">${links}</div>` : ''}
289+
</div>`;
290+
}
291+
292+
// Fetch and parse
293+
async function load() {
294+
try {
295+
let response;
296+
try {
297+
response = await fetch(bibUrl);
298+
if (!response.ok) throw new Error('Direct fetch failed');
299+
} catch (e) {
300+
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(bibUrl)}`;
301+
response = await fetch(proxyUrl);
302+
}
303+
304+
if (!response.ok) {
305+
throw new Error(`Failed to fetch: ${response.status}`);
306+
}
307+
308+
const content = await response.text();
309+
const publications = parseBibTeX(content);
310+
311+
if (publications.length === 0) {
312+
container.innerHTML = '<div class="bibtex-error">No publications found in the BibTeX file.</div>';
313+
return;
314+
}
315+
316+
render(publications);
317+
} catch (err) {
318+
container.innerHTML = `<div class="bibtex-error">Error loading publications: ${err.message}</div>`;
319+
console.error('[BibTeX Embed]', err);
320+
}
321+
}
322+
323+
load();
324+
})();

index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ <h1>BibTeX Parser</h1>
7171
</select>
7272
</div>
7373
</div>
74+
<div class="filter-group">
75+
<a href="./start.html" class="embed-link">Embed on your site →</a>
76+
</div>
7477
</div>
7578
</div>
7679
</div>
@@ -100,7 +103,7 @@ <h1>BibTeX Parser</h1>
100103
</div>
101104

102105
<footer>
103-
<a href="https://github.com/dynaroars/bibTex" target="_blank">GitHub</a>
106+
<a href="https://github.com/dynaroars/bibtex" target="_blank">GitHub</a>
104107
</footer>
105108

106109
<div id="loadingOverlay" class="modal hidden">

src/main.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@ function init() {
2727
setupEventListeners();
2828

2929
const params = new URLSearchParams(window.location.search);
30-
const bibUrl = params.get('bib') || DEFAULT_BIB_URL;
30+
const customBibUrl = params.get('bib');
31+
const bibUrl = customBibUrl || DEFAULT_BIB_URL;
32+
33+
if (customBibUrl) {
34+
document.body.classList.add('show-mode');
35+
const uploadBox = document.querySelector('.upload-box');
36+
if (uploadBox) uploadBox.style.display = 'none';
37+
}
38+
3139
urlInput.value = bibUrl;
3240
loadFromUrl(bibUrl);
3341
}
@@ -137,10 +145,8 @@ async function loadFromUrl(url) {
137145
return;
138146
}
139147

140-
// Convert GitHub blob URL to raw
141148
if (url.includes('github.com') && url.includes('/blob/')) {
142149
url = url.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/');
143-
// Update input to show the converted raw URL (optional, helps user understand)
144150
urlInput.value = url;
145151
}
146152

0 commit comments

Comments
 (0)