PageSourceSearch

https://wcowin.work/javascripts/github-repo-card.js

js wcowin.work collected 2026-09-28 05:46:15 UTC 11,842 bytes, 316 lines download raw bytes

1(function () {
2  const CACHE_DURATION = 2 * 60 * 60 * 1000;
3  const CACHE_KEY_PREFIX = 'github-repo-card-';
4
5  function parseRepoFromHref(href) {
6    if (!href) return null;
7    const match = href.match(/github\.com\/([^/]+)\/([^/?#]+)/i);
8    if (!match) return null;
9    return `${match[1]}/${match[2]}`;
10  }
11
12  function buildCardInnerHTML() {
13    return `
14      <div class="github-repo-card-inner">
15        <div class="github-repo-main">
16          <div class="github-repo-left">
17            <div class="github-repo-avatar-wrapper">
18              <div class="github-repo-avatar"></div>
19            </div>
20            <div class="github-repo-title">
21              <span class="github-repo-owner"></span>
22              <span class="github-repo-slash">/</span>
23              <span class="github-repo-name"></span>
24            </div>
25          </div>
26          <div class="github-repo-logo">
27            <span class="github-repo-logo-inner">
28              <svg viewBox="0 0 24 24" aria-hidden="true">
29                <path
30                  d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.21 11.39.6.11.79-.26.79-.58v-2.23c-3.34.73-4.03-1.42-4.03-1.42-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.21.08 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.3 3.49.99.11-.78.42-1.3.76-1.6-2.66-.31-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.11-3.17 0 0 1.01-.32 3.31 1.23A11.5 11.5 0 0 1 12 4.8c1.02.01 2.04.14 3 .41 2.3-1.55 3.3-1.23 3.3-1.23.66 1.65.24 2.87.12 3.17.77.84 1.23 1.91 1.23 3.22 0 4.61-2.81 5.62-5.49 5.93.43.37.82 1.1.82 2.23v3.29c0 .32.19.7.8.58C20.57 21.8 24 17.3 24 12 24 5.37 18.63 0 12 0Z"
31                />
32              </svg>
33            </span>
34          </div>
35        </div>
36        <p class="github-repo-description"></p>
37        <div class="github-repo-meta">
38          <span class="github-repo-meta-item">
39            <span class="github-repo-meta-icon">
40              <svg viewBox="0 0 24 24" aria-hidden="true">
41                <polygon
42                  points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
43                />
44              </svg>
45            </span>
46            <span class="github-repo-stars"></span>
47          </span>
48          <span class="github-repo-meta-item">
49            <span class="github-repo-meta-icon">
50              <svg viewBox="0 0 24 24" aria-hidden="true">
51                <line x1="6" y1="3" x2="6" y2="15"></line>
52                <circle cx="6" cy="18" r="3"></circle>
53                <circle cx="18" cy="6" r="3"></circle>
54                <path d="M18 9a9 9 0 0 1-9 9"></path>
55              </svg>
56            </span>
57            <span class="github-repo-forks"></span>
58          </span>
59          <span class="github-repo-meta-item">
60            <span class="github-repo-meta-icon">
61              <svg viewBox="0 0 24 24" aria-hidden="true">
62                <path d="M7 20h10" />
63                <path d="M6 6h12" />
64                <path d="M6 6 3 12h6Z" />
65                <path d="m21 12-3-6-3 6Z" />
66                <path d="M12 6V2" />
67                <path d="M12 12v8" />
68              </svg>
69            </span>
70            <span class="github-repo-license"></span>
71          </span>
72        </div>
73      </div>
74    `;
75  }
76
77  function getCachedData(repo) {
78    try {
79      const cacheKey = CACHE_KEY_PREFIX + repo;
80      const cached = localStorage.getItem(cacheKey);
81      const cacheTime = localStorage.getItem(cacheKey + '-time');
82
83      if (cached && cacheTime) {
84        const age = Date.now() - parseInt(cacheTime, 10);
85        if (age < CACHE_DURATION) {
86          return JSON.parse(cached);
87        }
88      }
89    } catch (e) {
90      // 缓存读取失败时静默处理
91    }
92    return null;
93  }
94
95  function setCachedData(repo, data) {
96    try {
97      const cacheKey = CACHE_KEY_PREFIX + repo;
98      localStorage.setItem(cacheKey, JSON.stringify(data));
99      localStorage.setItem(cacheKey + '-time', Date.now().toString());
100    } catch (e) {
101      // 缓存写入失败时静默处理(如存储空间不足)
102    }
103  }
104
105  function clearCachedData(repo) {
106    try {
107      const cacheKey = CACHE_KEY_PREFIX + repo;
108      localStorage.removeItem(cacheKey);
109      localStorage.removeItem(cacheKey + '-time');
110    } catch (e) {
111      // 缓存清理失败时静默处理
112    }
113  }
114
115  function hasCustomAttributes(card) {
116    const attrs = ['data-owner', 'data-name', 'data-description', 'data-avatar', 'data-stars', 'data-forks', 'data-license'];
117    return attrs.some(attr => {
118      const val = card.getAttribute(attr);
119      return val !== null && val.trim() !== '';
120    });
121  }
122
123  // 检查是否所有必要数据都有自定义值(不需要 API 调用)
124  function hasAllCustomData(card, owner, name) {
125    // 检查 description
126    const hasDescription = card.getAttribute('data-description') !== null && 
127                          card.getAttribute('data-description').trim() !== '';
128    
129    // 检查 owner 和 name:要么有 data-owner/data-name,要么能从 repo 解析出有效的
130    const customOwner = card.getAttribute('data-owner');
131    const customName = card.getAttribute('data-name');
132    const hasOwnerName = (customOwner !== null && customOwner.trim() !== '') ||
133                         (owner !== null && owner.trim() !== '');
134    const hasRepoName = (customName !== null && customName.trim() !== '') ||
135                        (name !== null && name.trim() !== '');
136    
137    // 同时至少有一个统计信息(stars 或 forks)
138    const hasStats = ['data-stars', 'data-forks'].some(attr => {
139      const val = card.getAttribute(attr);
140      return val !== null && val.trim() !== '';
141    });
142    
143    return hasDescription && hasOwnerName && hasRepoName && hasStats;
144  }
145
146  function getCustomValue(card, attr) {
147    const val = card.getAttribute(attr);
148    // 返回 null 表示使用 API 数据
149    if (val === null || val.trim() === '') return null;
150    return val.trim();
151  }
152
153  function fillCardData(card, data, owner, name) {
154    const ownerEl = card.querySelector('.github-repo-owner');
155    const nameEl = card.querySelector('.github-repo-name');
156    const descEl = card.querySelector('.github-repo-description');
157    const avatarEl = card.querySelector('.github-repo-avatar');
158    const starsEl = card.querySelector('.github-repo-stars');
159    const forksEl = card.querySelector('.github-repo-forks');
160    const licenseEl = card.querySelector('.github-repo-license');
161    
162    const customOwner = getCustomValue(card, 'data-owner');
163    const customName = getCustomValue(card, 'data-name');
164    const customDescription = getCustomValue(card, 'data-description');
165    const customAvatar = getCustomValue(card, 'data-avatar');
166    const customStars = getCustomValue(card, 'data-stars');
167    const customForks = getCustomValue(card, 'data-forks');
168    const customLicense = getCustomValue(card, 'data-license');
169
170    // Owner 和 Name:优先使用自定义,否则使用从 repo 解析的
171    if (ownerEl) ownerEl.textContent = customOwner || owner || '';
172    if (nameEl) nameEl.textContent = customName || name || '';
173
174    // Stars:自定义优先,其次是 API 数据
175    if (starsEl) {
176      if (customStars !== null) {
177        starsEl.textContent = customStars === '0' ? '' : customStars;
178      } else if (typeof data.stargazers_count === 'number') {
179        starsEl.textContent = data.stargazers_count > 0 ? data.stargazers_count.toString() : '';
180      }
181    }
182
183    // Forks
184    if (forksEl) {
185      if (customForks !== null) {
186        forksEl.textContent = customForks === '0' ? '' : customForks;
187      } else if (typeof data.forks_count === 'number') {
188        forksEl.textContent = data.forks_count > 0 ? data.forks_count.toString() : '';
189      }
190    }
191
192    // License
193    if (licenseEl) {
194      const licenseText = customLicense !== null 
195        ? customLicense 
196        : ((data.license && (data.license.spdx_id || data.license.name)) || '');
197      licenseEl.textContent = licenseText;
198      const metaItem = licenseEl.closest('.github-repo-meta-item');
199      if (metaItem) {
200        if (!licenseText) {
201          metaItem.classList.add('github-repo-license-empty');
202        } else {
203          metaItem.classList.remove('github-repo-license-empty');
204        }
205      }
206    }
207
208    // Description
209    if (descEl) {
210      descEl.textContent = customDescription !== null 
211        ? customDescription 
212        : (data.description || '');
213    }
214
215    // Avatar
216    if (avatarEl) {
217      if (customAvatar) {
218        avatarEl.style.backgroundImage = `url(${customAvatar})`;
219        avatarEl.setAttribute('aria-label', 'Custom avatar');
220        avatarEl.setAttribute('role', 'img');
221      } else if (data.owner && data.owner.avatar_url) {
222        avatarEl.style.backgroundImage = `url(${data.owner.avatar_url})`;
223        avatarEl.setAttribute('aria-label', data.owner.login || 'Repository owner avatar');
224        avatarEl.setAttribute('role', 'img');
225      }
226    }
227  }
228
229  async function fetchRepoData(repo) {
230    const apiUrl = `https://api.github.com/repos/${repo}`;
231    const res = await fetch(apiUrl, { headers: { Accept: 'application/vnd.github+json' } });
232    if (!res.ok) {
233      throw new Error('GitHub API error: ' + res.status);
234    }
235    return await res.json();
236  }
237
238  async function initCard(card) {
239    if (card.dataset.githubCardInitialized === 'true') return;
240    card.dataset.githubCardInitialized = 'true';
241
242    const href = card.getAttribute('href') || '';
243    const explicitRepo = card.getAttribute('data-repo');
244    const repo = explicitRepo || parseRepoFromHref(href);
245    if (!repo) return;
246
247    // 验证 repo 格式
248    const [owner, name] = repo.split('/');
249    if (!owner || !name) {
250      console.warn('Invalid repo format:', repo);
251      return;
252    }
253
254    card.innerHTML = buildCardInnerHTML();
255
256    // 检查是否有所有必要数据的自定义值
257    const hasAllCustom = hasAllCustomData(card, owner, name);
258    
259    // 如果有任何自定义属性,先显示自定义数据
260    const hasCustom = hasCustomAttributes(card);
261    if (hasCustom) {
262      fillCardData(card, {}, owner, name);
263    }
264
265    // 如果所有必要数据都有自定义值,跳过 API 调用
266    if (hasAllCustom) {
267      return;
268    }
269
270    // 尝试获取缓存数据
271    const cachedData = getCachedData(repo);
272
273    if (cachedData) {
274      // 有缓存,用缓存数据填充(如果有自定义属性,自定义值会覆盖缓存值)
275      fillCardData(card, cachedData, owner, name);
276
277      // 后台刷新数据
278      fetchRepoData(repo)
279        .then(data => {
280          setCachedData(repo, data);
281          fillCardData(card, data, owner, name);
282        })
283        .catch(() => {
284          // 获取失败,清除过期缓存
285          clearCachedData(repo);
286        });
287    } else {
288      // 无缓存,尝试获取 API 数据
289      try {
290        const data = await fetchRepoData(repo);
291        setCachedData(repo, data);
292        fillCardData(card, data, owner, name);
293      } catch (e) {
294        console.warn('加载 GitHub 仓库信息失败:', repo, e);
295        // API 失败且没有自定义属性,显示基本信息
296        if (!hasCustom) {
297          fillCardData(card, {}, owner, name);
298        }
299      }
300    }
301  }
302
303  async function initGitHubRepoCards() {
304    const cards = document.querySelectorAll('.github-repo-card');
305    if (!cards.length) return;
306    await Promise.all(Array.from(cards).map((card) => initCard(card)));
307  }
308
309  // Render cards that are already in the document immediately. Pages can load
310  // this script next to their cards to reserve the final layout before anchor
311  // scrolling runs; the DOMContentLoaded pass picks up any later cards.
312  initGitHubRepoCards();
313  if (document.readyState === 'loading') {
314    document.addEventListener('DOMContentLoaded', initGitHubRepoCards);
315  }
316})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.