mirror of
				https://github.com/Mabbs/mabbs.github.io
				synced 2025-11-04 10:32:25 +08:00 
			
		
		
		
	Update 5 files
- /.github/PULL_REQUEST_TEMPLATE.yml - /assets/js/rss-feed-preview.js - /links.md - /sitemap.xsl - /.github/PULL_REQUEST_TEMPLATE.md
This commit is contained in:
		@@ -3,234 +3,195 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
(function () {
 | 
			
		||||
    const existingPreviews = document.querySelectorAll('#rss-feed-preview');
 | 
			
		||||
    existingPreviews.forEach(el => el.remove());
 | 
			
		||||
  if (window.rssFeedPreviewInitialized)
 | 
			
		||||
    return;
 | 
			
		||||
  window.rssFeedPreviewInitialized = true;
 | 
			
		||||
 | 
			
		||||
    const CORS_PROXY = 'https://cors-anywhere.mayx.eu.org/?';
 | 
			
		||||
  var CORS_PROXY = 'https://cors-anywhere.mayx.eu.org/?';
 | 
			
		||||
 | 
			
		||||
    const createPreviewElement = () => {
 | 
			
		||||
      const existingPreview = document.getElementById('rss-feed-preview');
 | 
			
		||||
      if (existingPreview) {
 | 
			
		||||
        return existingPreview;
 | 
			
		||||
      }
 | 
			
		||||
  
 | 
			
		||||
      const previewEl = document.createElement('div');
 | 
			
		||||
      previewEl.id = 'rss-feed-preview';
 | 
			
		||||
      previewEl.style.cssText = `
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        display: none;
 | 
			
		||||
        width: 300px;
 | 
			
		||||
        max-height: 400px;
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
        background-color: white;
 | 
			
		||||
        border: 1px solid #ccc;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
			
		||||
        z-index: 1000;
 | 
			
		||||
        font-size: 14px;
 | 
			
		||||
        line-height: 1.4;
 | 
			
		||||
      `;
 | 
			
		||||
      document.body.appendChild(previewEl);
 | 
			
		||||
      return previewEl;
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    const parseRSS = (xmlText) => {
 | 
			
		||||
      const parser = new DOMParser();
 | 
			
		||||
      const xml = parser.parseFromString(xmlText, 'text/xml');
 | 
			
		||||
  
 | 
			
		||||
      const rssItems = xml.querySelectorAll('item');
 | 
			
		||||
      if (rssItems.length > 0) {
 | 
			
		||||
        return Array.from(rssItems).slice(0, 5).map(item => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: item.querySelector('title')?.textContent || 'No title',
 | 
			
		||||
            date: item.querySelector('pubDate')?.textContent || 'No date',
 | 
			
		||||
          };
 | 
			
		||||
  var $previewEl = $('<div>', {
 | 
			
		||||
    id: 'rss-feed-preview'
 | 
			
		||||
  }).css({
 | 
			
		||||
    position: 'fixed',
 | 
			
		||||
    display: 'none',
 | 
			
		||||
    width: '300px',
 | 
			
		||||
    maxHeight: '400px',
 | 
			
		||||
    overflowY: 'auto',
 | 
			
		||||
    backgroundColor: 'white',
 | 
			
		||||
    border: '1px solid #ccc',
 | 
			
		||||
    borderRadius: '5px',
 | 
			
		||||
    padding: '10px',
 | 
			
		||||
    fontSize: '14px',
 | 
			
		||||
    lineHeight: '1.4',
 | 
			
		||||
    zIndex: 1000,
 | 
			
		||||
    boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
 | 
			
		||||
  });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const atomItems = xml.querySelectorAll('entry');
 | 
			
		||||
      if (atomItems.length > 0) {
 | 
			
		||||
        return Array.from(atomItems).slice(0, 5).map(item => {
 | 
			
		||||
  $('body').append($previewEl);
 | 
			
		||||
 | 
			
		||||
  function escapeHTML(str) {
 | 
			
		||||
    return String(str).replace(/[&<>"']/g, function (c) {
 | 
			
		||||
      return {
 | 
			
		||||
            title: item.querySelector('title')?.textContent || 'No title',
 | 
			
		||||
            date: item.querySelector('updated')?.textContent || 'No date',
 | 
			
		||||
          };
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
  
 | 
			
		||||
      return null;
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    const checkFeed = async (url) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await fetch(CORS_PROXY + url);
 | 
			
		||||
        if (!response.ok) {
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
  
 | 
			
		||||
        const text = await response.text();
 | 
			
		||||
        return parseRSS(text);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    const findFeedUrl = async (siteUrl, linkElement) => {
 | 
			
		||||
      if (linkElement && linkElement.hasAttribute('data-feed')) {
 | 
			
		||||
        const dataFeedUrl = linkElement.getAttribute('data-feed');
 | 
			
		||||
        if (dataFeedUrl) {
 | 
			
		||||
          const feedItems = await checkFeed(dataFeedUrl);
 | 
			
		||||
          if (feedItems) {
 | 
			
		||||
            return { url: dataFeedUrl, items: feedItems };
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
  
 | 
			
		||||
      return null;
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    const escapeHTML = (str) => {
 | 
			
		||||
      return String(str).replace(/[&<>"'/]/g, (c) => ({
 | 
			
		||||
        '&': '&',
 | 
			
		||||
        '<': '<',
 | 
			
		||||
        '>': '>',
 | 
			
		||||
        '"': '"',
 | 
			
		||||
        "'": ''',
 | 
			
		||||
        '/': '/'
 | 
			
		||||
      }[c]));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const renderFeedItems = (previewEl, items, siteName) => {
 | 
			
		||||
      if (!items || items.length === 0) {
 | 
			
		||||
        previewEl.innerHTML = '<p>No feed items found.</p>';
 | 
			
		||||
        return;
 | 
			
		||||
        "'": '''
 | 
			
		||||
      }[c];
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      let html = `<h3>Latest from ${siteName}</h3><ul style="list-style: none; padding: 0; margin: 0;">`;
 | 
			
		||||
  function parseRSS(xmlText) {
 | 
			
		||||
    var xml;
 | 
			
		||||
    try {
 | 
			
		||||
      xml = $.parseXML(xmlText);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      items.forEach(item => {
 | 
			
		||||
        const safeTitle = escapeHTML(item.title);
 | 
			
		||||
        const safeDate = escapeHTML(new Date(item.date).toLocaleDateString());
 | 
			
		||||
        html += `
 | 
			
		||||
          <li style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
 | 
			
		||||
            <div style="color: #24292e; font-weight: bold;">
 | 
			
		||||
              ${safeTitle}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div style="color: #586069; font-size: 12px; margin: 3px 0;">
 | 
			
		||||
              ${safeDate}
 | 
			
		||||
            </div>
 | 
			
		||||
          </li>
 | 
			
		||||
        `;
 | 
			
		||||
    var $xml = $(xml);
 | 
			
		||||
    var $items = $xml.find('item');
 | 
			
		||||
    if (!$items.length)
 | 
			
		||||
      $items = $xml.find('entry');
 | 
			
		||||
 | 
			
		||||
    var result = [];
 | 
			
		||||
    $items.slice(0, 5).each(function () {
 | 
			
		||||
      var $el = $(this);
 | 
			
		||||
      result.push({
 | 
			
		||||
        title: $el.find('title').text() || 'No title',
 | 
			
		||||
        date: $el.find('pubDate, updated').text() || 'No date'
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
      html += '</ul>';
 | 
			
		||||
      previewEl.innerHTML = html;
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    const positionPreview = (previewEl, event) => {
 | 
			
		||||
      const viewportWidth = window.innerWidth;
 | 
			
		||||
      const viewportHeight = window.innerHeight;
 | 
			
		||||
  
 | 
			
		||||
      let left = event.clientX + 20;
 | 
			
		||||
      let top = event.clientY + 20;
 | 
			
		||||
  
 | 
			
		||||
      const rect = previewEl.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
      if (left + rect.width > viewportWidth) {
 | 
			
		||||
        left = event.clientX - rect.width - 20;
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      if (top + rect.height > viewportHeight) {
 | 
			
		||||
        top = event.clientY - rect.height - 20;
 | 
			
		||||
  function checkFeed(url, callback) {
 | 
			
		||||
    $.ajax({
 | 
			
		||||
      url: CORS_PROXY + url,
 | 
			
		||||
      type: 'GET',
 | 
			
		||||
      dataType: 'text',
 | 
			
		||||
      success: function (data) {
 | 
			
		||||
        var items = parseRSS(data);
 | 
			
		||||
        callback(items);
 | 
			
		||||
      },
 | 
			
		||||
      error: function () {
 | 
			
		||||
        callback(null);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      left = Math.max(10, left);
 | 
			
		||||
      top = Math.max(10, top);
 | 
			
		||||
  
 | 
			
		||||
      previewEl.style.left = `${left}px`;
 | 
			
		||||
      previewEl.style.top = `${top}px`;
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    const initFeedPreview = () => {
 | 
			
		||||
      const previewEl = createPreviewElement();
 | 
			
		||||
  
 | 
			
		||||
      const tableLinks = document.querySelectorAll('main table tbody tr td a');
 | 
			
		||||
  
 | 
			
		||||
      const feedCache = {};
 | 
			
		||||
  
 | 
			
		||||
      let currentLink = null;
 | 
			
		||||
      let loadingTimeout = null;
 | 
			
		||||
  
 | 
			
		||||
      tableLinks.forEach(link => {
 | 
			
		||||
        link.addEventListener('mouseenter', async (event) => {
 | 
			
		||||
          currentLink = link;
 | 
			
		||||
          const url = link.getAttribute('href');
 | 
			
		||||
          const siteName = link.textContent;
 | 
			
		||||
  
 | 
			
		||||
          previewEl.innerHTML = '<p>Checking for RSS/Atom feed...</p>';
 | 
			
		||||
          previewEl.style.display = 'block';
 | 
			
		||||
          positionPreview(previewEl, event);
 | 
			
		||||
  
 | 
			
		||||
          if (loadingTimeout) {
 | 
			
		||||
            clearTimeout(loadingTimeout);
 | 
			
		||||
          }
 | 
			
		||||
  
 | 
			
		||||
          loadingTimeout = setTimeout(async () => {
 | 
			
		||||
            if (feedCache[url]) {
 | 
			
		||||
              renderFeedItems(previewEl, feedCache[url].items, siteName);
 | 
			
		||||
              positionPreview(previewEl, event); // Reposition after content is loaded
 | 
			
		||||
  function renderFeedItems(items, siteName) {
 | 
			
		||||
    if (!items || !items.length) {
 | 
			
		||||
      $previewEl.html('<p>No feed items found.</p>');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            const feedData = await findFeedUrl(url, link);
 | 
			
		||||
  
 | 
			
		||||
            if (currentLink === link) {
 | 
			
		||||
              if (feedData) {
 | 
			
		||||
                feedCache[url] = feedData;
 | 
			
		||||
                renderFeedItems(previewEl, feedData.items, siteName);
 | 
			
		||||
                positionPreview(previewEl, event); // Reposition after content is loaded
 | 
			
		||||
              } else {
 | 
			
		||||
                previewEl.style.display = 'none';
 | 
			
		||||
    var html = '<h3>Latest from ' + escapeHTML(siteName) + '</h3><ul style="list-style:none; padding:0; margin:0;">';
 | 
			
		||||
    for (var i = 0; i < items.length; i++) {
 | 
			
		||||
      var item = items[i];
 | 
			
		||||
      var dateStr = new Date(item.date).toLocaleDateString();
 | 
			
		||||
      html += '<li style="margin-bottom:10px; padding-bottom:10px; border-bottom:1px solid #eee;">' +
 | 
			
		||||
        '<div style="color:#24292e; font-weight:bold;">' + escapeHTML(item.title) + '</div>' +
 | 
			
		||||
        '<div style="color:#586069; font-size:12px; margin:3px 0;">' + escapeHTML(dateStr) + '</div>' +
 | 
			
		||||
        '</li>';
 | 
			
		||||
    }
 | 
			
		||||
    html += '</ul>';
 | 
			
		||||
    $previewEl.html(html);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function positionPreview(e) {
 | 
			
		||||
    e = e || window.event;
 | 
			
		||||
 | 
			
		||||
    var x = e.clientX;
 | 
			
		||||
    var y = e.clientY;
 | 
			
		||||
 | 
			
		||||
    var offsetWidth = $previewEl.outerWidth();
 | 
			
		||||
    var offsetHeight = $previewEl.outerHeight();
 | 
			
		||||
 | 
			
		||||
    var left = x + 20;
 | 
			
		||||
    var top = y + 20;
 | 
			
		||||
 | 
			
		||||
    if (left + offsetWidth > $(window).width()) {
 | 
			
		||||
      left = x - offsetWidth - 20;
 | 
			
		||||
    }
 | 
			
		||||
    if (top + offsetHeight > $(window).height()) {
 | 
			
		||||
      top = y - offsetHeight - 20;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $previewEl.css({
 | 
			
		||||
      left: Math.max(10, left),
 | 
			
		||||
      top: Math.max(10, top)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  function init() {
 | 
			
		||||
    var cache = {};
 | 
			
		||||
    var currentLink = null;
 | 
			
		||||
    var timeout = null;
 | 
			
		||||
 | 
			
		||||
    $('main table tbody tr td a').each(function () {
 | 
			
		||||
      var $link = $(this);
 | 
			
		||||
 | 
			
		||||
      $link.on('mouseenter', function (e) {
 | 
			
		||||
        currentLink = this;
 | 
			
		||||
        var siteName = $link.text();
 | 
			
		||||
        var url = $link.attr('data-feed');
 | 
			
		||||
        if (!url)
 | 
			
		||||
          return;
 | 
			
		||||
 | 
			
		||||
        $previewEl.html('<p>Checking for RSS/Atom feed...</p>').show();
 | 
			
		||||
        positionPreview(e);
 | 
			
		||||
 | 
			
		||||
        if (timeout)
 | 
			
		||||
          clearTimeout(timeout);
 | 
			
		||||
        timeout = setTimeout(function () {
 | 
			
		||||
          if (cache[url]) {
 | 
			
		||||
            renderFeedItems(cache[url], siteName);
 | 
			
		||||
            positionPreview(e);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (url) {
 | 
			
		||||
            checkFeed(url, function (items) {
 | 
			
		||||
              if (currentLink === $link[0] && items) {
 | 
			
		||||
                cache[url] = items;
 | 
			
		||||
                renderFeedItems(items, siteName);
 | 
			
		||||
                positionPreview(e);
 | 
			
		||||
              } else {
 | 
			
		||||
                $previewEl.hide();
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          } else {
 | 
			
		||||
            $previewEl.hide();
 | 
			
		||||
          }
 | 
			
		||||
        }, 300);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
        link.addEventListener('mousemove', (event) => {
 | 
			
		||||
          if (previewEl.style.display === 'block') {
 | 
			
		||||
            window.requestAnimationFrame(() => {
 | 
			
		||||
              positionPreview(previewEl, event);
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
      $link.on('mousemove', function (e) {
 | 
			
		||||
        if ($previewEl.is(':visible'))
 | 
			
		||||
          positionPreview(e);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
        link.addEventListener('mouseleave', () => {
 | 
			
		||||
          if (loadingTimeout) {
 | 
			
		||||
            clearTimeout(loadingTimeout);
 | 
			
		||||
            loadingTimeout = null;
 | 
			
		||||
          }
 | 
			
		||||
  
 | 
			
		||||
      $link.on('mouseleave', function () {
 | 
			
		||||
        clearTimeout(timeout);
 | 
			
		||||
        timeout = null;
 | 
			
		||||
        currentLink = null;
 | 
			
		||||
          previewEl.style.display = 'none';
 | 
			
		||||
        $previewEl.hide();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
      document.addEventListener('click', (event) => {
 | 
			
		||||
        if (!previewEl.contains(event.target)) {
 | 
			
		||||
          previewEl.style.display = 'none';
 | 
			
		||||
    $(document).on('click', function (e) {
 | 
			
		||||
      if (!$(e.target).closest('#rss-feed-preview').length) {
 | 
			
		||||
        $previewEl.hide();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (!window.rssFeedPreviewInitialized) {
 | 
			
		||||
      window.rssFeedPreviewInitialized = true;
 | 
			
		||||
  
 | 
			
		||||
      if (document.readyState === 'loading') {
 | 
			
		||||
        document.addEventListener('DOMContentLoaded', initFeedPreview);
 | 
			
		||||
  if (document.readyState === 'complete' || document.readyState === 'interactive') {
 | 
			
		||||
    init();
 | 
			
		||||
  } else {
 | 
			
		||||
        initFeedPreview();
 | 
			
		||||
      }
 | 
			
		||||
    $(document).ready(init);
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
  
 | 
			
		||||
		Reference in New Issue
	
	Block a user