Skip to content

Commit 5c63e7f

Browse files
committed
chore: sync ORCID publications
1 parent a0c56ea commit 5c63e7f

365 files changed

Lines changed: 8251 additions & 26479 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/orcid-sync.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: ORCID Sync
2+
3+
on:
4+
schedule:
5+
- cron: "0 3 * * 0"
6+
workflow_dispatch:
7+
8+
jobs:
9+
sync:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.11"
19+
20+
- name: Install dependencies
21+
run: python -m pip install --upgrade pip && pip install -r requirements.txt
22+
23+
- name: Sync ORCID
24+
env:
25+
ORCID_ID: "0000-0002-0546-5857"
26+
UNPAYWALL_EMAIL: ${{ secrets.UNPAYWALL_EMAIL }}
27+
run: |
28+
python scripts/orcid_sync.py \
29+
--orcid "$ORCID_ID" \
30+
--bib-file publications/publications.bib \
31+
--publications-dir publications \
32+
--full
33+
34+
- name: Regenerate QMD files
35+
run: |
36+
python scripts/parsePubs.py \
37+
--bib publications/publications.bib \
38+
--out-dir publications \
39+
--include-preprints
40+
41+
- name: Commit changes
42+
run: |
43+
if git status --porcelain | grep -q .; then
44+
git config user.name "github-actions[bot]"
45+
git config user.email "github-actions[bot]@users.noreply.github.com"
46+
git add publications/publications.bib publications publications/preprints
47+
git commit -m "chore: sync ORCID publications"
48+
git push
49+
else
50+
echo "No changes to commit."
51+
fi

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ _site
99
# vim
1010
*~
1111
*swp
12+
13+
**/*.quarto_ipynb

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,81 @@
11
# RSFraLab.github.io
22
The Lab Website built using Quarto:
33
https://rsfralab.github.io/
4+
5+
## Publications sync (ORCID)
6+
7+
This site can generate new publication entries directly from ORCID and Crossref.
8+
9+
### One-time setup
10+
11+
- Install Python dependencies: `pip install requests bibtexparser`
12+
13+
### Sync new publications
14+
15+
```
16+
python scripts/orcid_sync.py --orcid YOUR-ORCID-ID --full
17+
```
18+
19+
This will:
20+
21+
- Read your ORCID works
22+
- Pull metadata from Crossref using DOIs
23+
- Rebuild publications/publications.bib from ORCID
24+
- Optionally create QMD files under publications/ (unless --no-qmd)
25+
26+
### ORCID-only workflow
27+
28+
Use ORCID as the single source of truth and regenerate the BibTeX each time:
29+
30+
```
31+
python scripts/orcid_sync.py --orcid YOUR-ORCID-ID --full
32+
```
33+
34+
To skip QMD creation:
35+
36+
```
37+
python scripts/orcid_sync.py --orcid YOUR-ORCID-ID --full --no-qmd
38+
```
39+
40+
### Preprints
41+
42+
By default, preprints are excluded when generating QMDs from the BibTeX file. To include them:
43+
44+
```
45+
python scripts/parsePubs.py --bib publications/publications.bib --out-dir publications --include-preprints
46+
```
47+
48+
Preprints are written to publications/preprints and shown on preprints.qmd.
49+
50+
### PDF links
51+
52+
If you want a PDF link to appear for a paper, place a PDF at:
53+
54+
```
55+
publications/pdfs/<generated-filename>.pdf
56+
```
57+
58+
The listing template will show a “PDF” link when `materials` is set. You can also manually set `materials` in any publication QMD front matter to an external URL.
59+
60+
### External PDF hosting (recommended for large files)
61+
62+
Use a URL template so new entries get a PDF link automatically:
63+
64+
```
65+
python scripts/orcid_sync.py --orcid YOUR-ORCID-ID --materials-template "https://your-server.org/papers/{slug}.pdf"
66+
```
67+
68+
Template variables:
69+
70+
- `{slug}` = generated filename without `.qmd`
71+
- `{doi}` = DOI
72+
73+
### Auto-discover and download PDFs (OA only)
74+
75+
If you want to auto-find open-access PDFs, use Unpaywall (requires email):
76+
77+
```
78+
python scripts/orcid_sync.py --orcid YOUR-ORCID-ID --unpaywall-email you@caltech.edu --download-pdfs
79+
```
80+
81+
This downloads OA PDFs into publications/pdfs and sets `materials` to that local path.

_quarto.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ website:
2626
# - text: "Trademark"
2727
# href: trademark.qmd
2828
right:
29-
- text: "{{< ai google-scholar >}} Google Scholar"
29+
- text: "{{< iconify academicons google-scholar >}} Google Scholar"
3030
href: https://scholar.google.com/citations?user=7XYDtdYAAAAJ&hl=en
3131
- icon: twitter
3232
href: https://twitter.com/chr_fronge
@@ -51,7 +51,7 @@ website:
5151
text: Blog
5252
right:
5353
- href: https://scholar.google.com/citations?user=7XYDtdYAAAAJ&hl=en
54-
text: "{{< ai google-scholar >}}"
54+
text: "{{< iconify academicons google-scholar >}}"
5555
aria-label: Google Scholar
5656
- icon: github
5757
href: https://github.com/cfranken
@@ -69,6 +69,8 @@ format:
6969
dark: [darkly, custom.scss]
7070
toc: true
7171
css: custom.css
72+
include-in-header:
73+
- text: "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/jpswalsh/academicons@1/css/academicons.min.css\">"
7274
grid:
7375
sidebar-width: 0px
7476
body-width: 1000px

custom.css

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,127 @@
11
.navbar {
22
height: 50px; /* Adjust this value as needed */
3+
}
4+
5+
.pubs-layout {
6+
margin-top: 10px;
7+
position: relative;
8+
display: flex;
9+
gap: 24px;
10+
align-items: flex-start;
11+
}
12+
13+
.pubs-sidebar {
14+
position: sticky;
15+
top: 80px;
16+
align-self: flex-start;
17+
max-height: calc(100vh - 120px);
18+
overflow-y: auto;
19+
padding-left: 16px;
20+
border-left: 1px solid #eee;
21+
background: #fff;
22+
z-index: 10;
23+
pointer-events: auto;
24+
flex: 0 0 180px;
25+
}
26+
27+
.pubs-content {
28+
position: relative;
29+
z-index: 1;
30+
flex: 1 1 auto;
31+
min-width: 0;
32+
}
33+
34+
.pubs-sidebar-title {
35+
font-weight: 600;
36+
margin-bottom: 8px;
37+
}
38+
39+
.pubs-year-list {
40+
list-style: none;
41+
padding-left: 0;
42+
margin: 0;
43+
}
44+
45+
.pubs-year-list li {
46+
margin: 4px 0;
47+
}
48+
49+
.pubs-year-list a {
50+
text-decoration: none;
51+
color: #444;
52+
cursor: pointer;
53+
pointer-events: auto;
54+
}
55+
56+
.pubs-year-list a.active {
57+
color: #FF6C0C;
58+
font-weight: 600;
59+
}
60+
61+
.pubs-type-filters {
62+
margin-bottom: 12px;
63+
}
64+
65+
.pubs-type-option {
66+
display: flex;
67+
align-items: center;
68+
gap: 6px;
69+
margin: 4px 0;
70+
font-size: 0.9rem;
71+
}
72+
73+
.pubs-type-option input {
74+
margin: 0;
75+
}
76+
77+
.pub-list {
78+
padding-left: 0;
79+
}
80+
81+
.pub-list li {
82+
list-style: none;
83+
padding: 8px 0;
84+
border-bottom: 1px solid #f0f0f0;
85+
}
86+
87+
.pub-item {
88+
line-height: 1.35;
89+
}
90+
91+
.pub-title {
92+
font-size: 1.02rem;
93+
font-weight: 600;
94+
color: #2c3e50;
95+
margin-bottom: 2px;
96+
}
97+
98+
.pub-authors {
99+
font-size: 0.92rem;
100+
color: #444;
101+
margin-bottom: 2px;
102+
}
103+
104+
.pub-meta {
105+
font-size: 0.9rem;
106+
color: #666;
107+
margin-bottom: 4px;
108+
}
109+
110+
.pub-links a {
111+
font-size: 0.85rem;
112+
margin-right: 10px;
113+
text-decoration: none;
114+
}
115+
116+
@media (max-width: 768px) {
117+
.pubs-sidebar {
118+
position: static;
119+
max-height: none;
120+
border-left: none;
121+
padding-left: 0;
122+
margin-top: 12px;
123+
}
124+
.pubs-layout {
125+
flex-direction: column;
126+
}
3127
}

ejs/article.ejs

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,82 @@
22
```{=html}
33
<ul class="pub-list list">
44
<% for (const item of items) { %>
5+
<% const decodeEntitiesOnce = (value) => (value || '')
6+
.replace(/&lt;/g, '<')
7+
.replace(/&gt;/g, '>')
8+
.replace(/&quot;/g, '"')
9+
.replace(/&#39;/g, "'")
10+
.replace(/&amp;/g, '&')
11+
.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(Number(code)))
12+
.replace(/&#x([0-9a-fA-F]+);/g, (_m, code) => String.fromCharCode(parseInt(code, 16)));
13+
const decodeEntities = (value) => {
14+
let current = value || '';
15+
for (let i = 0; i < 3; i += 1) {
16+
const next = decodeEntitiesOnce(current);
17+
if (next === current) break;
18+
current = next;
19+
}
20+
return current;
21+
};
22+
const titleDecoded = decodeEntities(item.title || '');
23+
const titleHtml = titleDecoded.replace(/\$\\mathrm\{([^}]+)\}\$/g, (_m, formula) => {
24+
const plain = formula.replace(/_\{?(\d+)\}?/g, '$1');
25+
return plain.replace(/([A-Za-z])([0-9]+)/g, '$1<sub>$2</sub>');
26+
}).replace(/\b([A-Z][a-z]?)(\d+)\b/g, '$1<sub>$2</sub>'); %>
27+
<% const formatAuthors = (value) => {
28+
const raw = (value || '').trim();
29+
if (!raw) return '';
30+
const parts = raw.split(/\s+and\s+/i).map((p) => p.trim()).filter(Boolean);
31+
const formatted = parts.map((p) => {
32+
const cleaned = p.replace(/\s+/g, ' ').replace(/[{}]/g, '').trim();
33+
if (!cleaned) return '';
34+
if (cleaned.includes(',')) {
35+
const [last, rest] = cleaned.split(',').map((s) => s.trim());
36+
const initials = (rest || '')
37+
.split(/\s+/)
38+
.map((token) => token.replace(/[^A-Za-z-]/g, ''))
39+
.filter(Boolean)
40+
.map((token) => `${token[0]}.`)
41+
.join(' ');
42+
return `${last}${initials ? `, ${initials}` : ''}`;
43+
}
44+
const tokens = cleaned.split(/\s+/).filter(Boolean);
45+
if (tokens.length === 1) return tokens[0];
46+
const last = tokens[tokens.length - 1];
47+
const initials = tokens.slice(0, -1).map((token) => `${token[0]}.`).join(' ');
48+
return `${last}${initials ? `, ${initials}` : ''}`;
49+
}).filter(Boolean);
50+
if (formatted.length <= 1) return formatted.join('');
51+
return `${formatted.slice(0, -1).join(', ')} and ${formatted[formatted.length - 1]}`;
52+
};
53+
const authorsFormatted = formatAuthors(item.author); %>
554
<li <%= metadataAttrs(item) %>>
6-
<span class="listing-title lead" style="color:orange;font-weight:bold"><%= item.title %></span><br />
7-
<span class="listing-author"><%= item.author %></span>
8-
<span class="listing-publication fst-italic "><%= item.publication %></span>
9-
<span class="listing-year">(<%= item.year %>)</span>
55+
<div class="pub-item">
56+
<div class="listing-title pub-title"><%- titleHtml %></div>
57+
<div class="listing-author pub-authors"><%= authorsFormatted %></div>
58+
<span class="listing-type pub-type" style="display:none"><%= item.type %></span>
59+
<div class="pub-meta">
60+
<span class="listing-publication pub-publication"><%= item.publication %></span>
61+
<span class="listing-year pub-year">(<%= item.year %>)</span>
62+
</div>
63+
<div class="pub-links">
1064
<% if (item.doi) { %>
1165
<a href="https://doi.org/<%- item.doi %>">
1266
DOI
1367
</a>
1468
<% } %>
69+
<% if (item.materials) { %>
70+
<a href="<%- item.materials %>">
71+
PDF
72+
</a>
73+
<% } %>
74+
<% if (item.supplement) { %>
75+
<a href="<%- item.supplement %>">
76+
Supplement
77+
</a>
78+
<% } %>
79+
</div>
80+
</div>
1581
</li>
1682
<% } %>
1783
</ul>

0 commit comments

Comments
 (0)