MediaWiki

Common.js: Difference between revisions

From Illustrations in German Translations of Mark Twain's Works

No edit summary
No edit summary
Line 191: Line 191:
         galleryWindow.document.write('body { font-family: sans-serif; padding: 20px; }');
         galleryWindow.document.write('body { font-family: sans-serif; padding: 20px; }');
         galleryWindow.document.write('img { max-width: 300px; margin: 10px; cursor: pointer; display: inline-block; }');
         galleryWindow.document.write('img { max-width: 300px; margin: 10px; cursor: pointer; display: inline-block; }');
         galleryWindow.document.write('#infoBox { position:absolute; background: rgba(255,255,255,0.95); border:1px solid #ccc; padding:10px; max-width:250px; display:none; z-index:9999; font-size:0.9em; }');
         galleryWindow.document.write('#infoBox { position:absolute; background: rgba(255,255,255,0.95); border:1px solid #ccc; padding:10px; max-width:300px; display:none; z-index:9999; font-size:0.9em; pointer-events:none; }');
         galleryWindow.document.write('</style></head><body>');
         galleryWindow.document.write('</style></head><body>');
         galleryWindow.document.write('<h2>Results</h2>');
         galleryWindow.document.write('<h2>Results</h2>');
Line 202: Line 202:
         galleryWindow.document.write('<div id="infoBox"></div>');
         galleryWindow.document.write('<div id="infoBox"></div>');


         // Script für Hover-Infobox mit Cache
         // Script für Hover-Infobox mit Raw-Text und Cache
         galleryWindow.document.write('<script>' +
         galleryWindow.document.write('<script>' +
         'const imgs = document.querySelectorAll("img");' +
         'const imgs = document.querySelectorAll("img");' +
Line 208: Line 208:
         'const mediaCache = new Map();' +
         'const mediaCache = new Map();' +
         'imgs.forEach(img => {' +
         'imgs.forEach(img => {' +
        '  img.removeAttribute("title");' + // Browser Tooltip deaktivieren
         '  img.addEventListener("mouseenter", async function(e) {' +
         '  img.addEventListener("mouseenter", async function(e) {' +
         '    const fileName = img.src.split("/").pop().replace(".jpg","");' +
         '    const fileName = img.src.split("/").pop().replace(".jpg","");' +
Line 215: Line 216:
         '      return;' +
         '      return;' +
         '    }' +
         '    }' +
         '    const filePage = "File:" + fileName;' +
         '    infoBox.textContent = "Loading ...";' +
        '    infoBox.style.display = "block";' +
         '    try {' +
         '    try {' +
         '      const apiUrl = window.location.origin + "/w/api.php?action=parse&format=json&page=" + encodeURIComponent(filePage) + "&prop=wikitext&origin=*";' +
         '      const resp = await fetch("/index.php?title=File:" + encodeURIComponent(fileName) + ".jpg&action=raw");' +
         '      const res = await fetch(apiUrl);' +
         '      const text = await resp.text();' +
         '      const data = await res.json();' +
         '      const match = text.match(/\\{\\{MediaInfo([\\s\\S]*?)\\}\\}/);' +
         '      const wikitext = data.parse.wikitext["*"];' +
         '      if(!match) { infoBox.textContent = "No Data found."; mediaCache.set(fileName,"No Data found."); return; }' +
         '      const mediaInfo = {};' +
        '      const block = match[1];' +
         '      wikitext.replace(/\\|\\s*(\\w+)\\s*=\\s*(.*)/g, (m,key,value) => { mediaInfo[key.trim()] = value.trim(); });' +
        '      const fields = ["title","chapter","illustration","illustrator","year","tags"];' +
        '      const infoHtml = Object.entries(mediaInfo).map(([k,v])=>`<b>${k}</b>: ${v}`).join("<br>");' +
         '      let html = "<ul style=\'margin:0; padding-left:1em; list-style:none;\'>";' +
         '      mediaCache.set(fileName, infoHtml);' +
         '      fields.forEach(field => {' +
         '      infoBox.innerHTML = infoHtml;' +
        '        const m = block.match(new RegExp("\\\\|\\\\s*" + field + "\\\\s*=([^\\\\n]*)"));' +
        '      infoBox.style.display = "block";' +
        '        if(m) html += `<li><b>${field.charAt(0).toUpperCase() + field.slice(1)}:</b> ${m[1].trim()}</li>`;' +
        '      });' +
        '      html += "</ul>";' +
         '      mediaCache.set(fileName, html);' +
         '      infoBox.innerHTML = html;' +
         '    } catch(err) {' +
         '    } catch(err) {' +
         '      const msg = "No info found";' +
         '      infoBox.textContent = "Error loading info";' +
         '      mediaCache.set(fileName, msg);' +
         '      mediaCache.set(fileName,"Error loading info");' +
         '      infoBox.innerHTML = msg;' +
         '      console.error(err);' +
        '      infoBox.style.display = "block";' +
         '    }' +
         '    }' +
         '  });' +
         '  });' +
Line 247: Line 252:
         galleryWindow.document.close();
         galleryWindow.document.close();
     } else {
     } else {
         alert('Popup wurde blockiert. Bitte erlaube Popups für diese Seite.');
         alert("Popup wurde blockiert. Bitte erlaube Popups für diese Seite.");
     }
     }
}
}

Revision as of 22:43, 23 February 2026

/* Any JavaScript here will be loaded for all users on every page load. */
console.log("Common.js läuft");

// ===============================
// Catalog Script
// ===============================



// Canvas prüfen
const canvas = document.getElementById('lineChart');
console.log("Canvas:", canvas);




mw.loader.using('jquery').then(function () {

  console.log('jQuery ist verfügbar:', typeof $);

  $.getScript('https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js')
    .then(() => {
      console.log('DataTables geladen:', $.fn.dataTable);
      return $.getScript('https://cdn.datatables.net/datetime/1.5.1/js/dataTables.dateTime.min.js');
    })
    .then(() => $.getScript('https://cdn.datatables.net/searchbuilder/1.6.0/js/dataTables.searchBuilder.min.js'))
    .then(() => {
      console.log('DataTables + Erweiterungen geladen');
      if ($.fn.dataTable && $.fn.dataTable.SearchBuilder) {
  console.log('SearchBuilder:', $.fn.dataTable.SearchBuilder);
} else {
  console.warn('SearchBuilder nicht verfügbar');
}

      mw.hook('wikipage.content').add(function($content) {
        const $table = $content.find('#catalog');

        if ($table.length && !$.fn.DataTable.isDataTable($table)) {
          $table.DataTable({
            dom: 'Qlfrtip',
            searchBuilder: true,
            paging: true,
            pageLength: 600,
            searching: true,
            ordering: true,
            lengthMenu: [[10, 25, 50, 100, 200, 600], [10, 25, 50, 100, 200, 600]],
            order: [[1, 'asc']],
            language: {
              search: "Search:",
              lengthMenu: "Show _MENU_ Entries",
              zeroRecords: "No Matches",
              info: "Page _PAGE_ of _PAGES_ (showing _TOTAL_ of _MAX_ entries)",
              infoEmpty: "Empty",
              infoFiltered: ""
            },
            initComplete: function () {
              this.api().columns().every(function () {
                var column = this;
                var header = $(column.header());
                var columnTitle = header.text();
                $('<input type="text" placeholder="' + columnTitle + ' ..." style="width: 100%; padding: 5px;"/>')
                  .appendTo(header.empty())
                  .on('keyup change', function () {
                    column.search(this.value).draw();
                  })
                  .on('click', function (e) {
                    e.stopPropagation();
                  });
              });
            }
          });
        }
      });
    })
    .catch((err) => {
      console.error('Fehler beim Laden von DataTables oder Erweiterungen:', err);
    });

});







/*AUSKLAPPMENÜS*/
$(function () {
  $('.ausklapp-button').click(function () {
    var $button = $(this);
    var $content = $button.next('.ausklapp-inhalt');

    $content.slideToggle(200);
    var expanded = $button.attr('aria-expanded') === 'true';
    $button.attr('aria-expanded', !expanded);
  });
});



// Hover-Preview für Bild-Links in DataTables
$(document).ready(function() {
    // Neuen Image-Container einfügen
   $('body').append('<div id="image-hover-preview" style="display:none; position:absolute; z-index:9999;"><img src="" style="max-width:400px; max-height:400px; border:1px solid #ccc; background:white; padding:5px;"></div>');

    // Auf alle Links in der Tabelle achten
    $('#catalog').on('mouseenter', 'a', function(e) {
        var href = $(this).attr('href');
        if (href && href.includes('/Special:Redirect/file/')) {
            var imgUrl = href; // URL direkt verwenden
            $('#image-hover-preview img').attr('src', imgUrl);
            $('#image-hover-preview').show();
        }
    }).on('mousemove', 'a', function(e) {
        $('#image-hover-preview').css({
            top: e.pageY + 20 + 'px',
            left: e.pageX + 20 + 'px'
        });
    }).on('mouseleave', 'a', function() {
        $('#image-hover-preview').hide();
    });
});

$(document).ready(function () {
  const searchButton = document.getElementById('searchButton');
  const tagInput = document.getElementById('tagInput');

  if (searchButton && tagInput) {
    searchButton.addEventListener('click', function () {
      const tags = tagInput.value.split(',').map(tag => tag.trim());
      searchImagesByTags(tags);
    });
  } else {
    console.warn('searchButton oder tagInput nicht gefunden.');
  }
});

function searchImagesByTags(tags) {
    var url = new URL(window.location.href);
    var apiUrl = url.origin + '/w/api.php';
    
    // Hier wird eine Anfrage an die MediaWiki API gestellt, um nach Bildern zu suchen, die den Tags entsprechen
    fetch(`${apiUrl}?action=query&format=json&list=categorymembers&cmtitle=Category:${tags.join('&cmtitle=Category:')}&cmtype=file`)
        .then(response => response.json())
        .then(data => {
            var images = data.query.categorymembers;
            displayImages(images);
        })
        .catch(error => console.error('Error:', error));
}

function displayImages(images) {
    var galleryContainer = document.getElementById('galleryContainer');
    galleryContainer.innerHTML = ''; // Leere den Container, um Platz für neue Bilder zu schaffen

    // Überprüfe, ob Bilder vorhanden sind
    if (images.length === 0) {
        galleryContainer.innerHTML = '<p>No images found.</p>';
        return;
    }

    // Bilder in die Galerie einfügen
    images.forEach(image => {
        var imgElement = document.createElement('img');
        imgElement.src = '/index.php/Special:Redirect/file/' + image.title.replace('Category:', '');
        imgElement.alt = image.title;
        galleryContainer.appendChild(imgElement);
    });
}
// Funktion zur Extraktion der Dateinamen aus der Tabelle und zur Anzeige in der Galerie
function updateGalleryFromTable() {
    // Tabelle durchsuchen und Dateinamen extrahieren
    var rows = document.querySelectorAll('#catalog tbody tr');
    var imageLinks = [];

    rows.forEach(row => {
        var imageCell = row.cells[8];  // Die 9. Zelle enthält den Dateinamen als Link
        var link = imageCell.querySelector('a');
        if (link) {
            var imageName = link.textContent.trim();
            var imageUrl = '/index.php/Special:Redirect/file/' + imageName + '.jpg';
            imageLinks.push(imageUrl);
        }
    });

    // Neue Seite mit Galerie öffnen
    var galleryWindow = window.open('', '_blank');
    if (galleryWindow) {
        galleryWindow.document.write('<html><head><title>Gallery</title>');
        galleryWindow.document.write('<style>');
        galleryWindow.document.write('body { font-family: sans-serif; padding: 20px; }');
        galleryWindow.document.write('img { max-width: 300px; margin: 10px; cursor: pointer; display: inline-block; }');
        galleryWindow.document.write('#infoBox { position:absolute; background: rgba(255,255,255,0.95); border:1px solid #ccc; padding:10px; max-width:300px; display:none; z-index:9999; font-size:0.9em; pointer-events:none; }');
        galleryWindow.document.write('</style></head><body>');
        galleryWindow.document.write('<h2>Results</h2>');

        imageLinks.forEach(url => {
            galleryWindow.document.write('<img src="' + url + '" onclick="window.open(\'' + url + '\')">');
        });

        // Infobox-Container
        galleryWindow.document.write('<div id="infoBox"></div>');

        // Script für Hover-Infobox mit Raw-Text und Cache
        galleryWindow.document.write('<script>' +
        'const imgs = document.querySelectorAll("img");' +
        'const infoBox = document.getElementById("infoBox");' +
        'const mediaCache = new Map();' +
        'imgs.forEach(img => {' +
        '  img.removeAttribute("title");' + // Browser Tooltip deaktivieren
        '  img.addEventListener("mouseenter", async function(e) {' +
        '    const fileName = img.src.split("/").pop().replace(".jpg","");' +
        '    if(mediaCache.has(fileName)) {' +
        '      infoBox.innerHTML = mediaCache.get(fileName);' +
        '      infoBox.style.display = "block";' +
        '      return;' +
        '    }' +
        '    infoBox.textContent = "Loading ...";' +
        '    infoBox.style.display = "block";' +
        '    try {' +
        '      const resp = await fetch("/index.php?title=File:" + encodeURIComponent(fileName) + ".jpg&action=raw");' +
        '      const text = await resp.text();' +
        '      const match = text.match(/\\{\\{MediaInfo([\\s\\S]*?)\\}\\}/);' +
        '      if(!match) { infoBox.textContent = "No Data found."; mediaCache.set(fileName,"No Data found."); return; }' +
        '      const block = match[1];' +
        '      const fields = ["title","chapter","illustration","illustrator","year","tags"];' +
        '      let html = "<ul style=\'margin:0; padding-left:1em; list-style:none;\'>";' +
        '      fields.forEach(field => {' +
        '        const m = block.match(new RegExp("\\\\|\\\\s*" + field + "\\\\s*=([^\\\\n]*)"));' +
        '        if(m) html += `<li><b>${field.charAt(0).toUpperCase() + field.slice(1)}:</b> ${m[1].trim()}</li>`;' +
        '      });' +
        '      html += "</ul>";' +
        '      mediaCache.set(fileName, html);' +
        '      infoBox.innerHTML = html;' +
        '    } catch(err) {' +
        '      infoBox.textContent = "Error loading info";' +
        '      mediaCache.set(fileName,"Error loading info");' +
        '      console.error(err);' +
        '    }' +
        '  });' +
        '  img.addEventListener("mousemove", function(e){' +
        '    infoBox.style.top = (e.pageY + 15) + "px";' +
        '    infoBox.style.left = (e.pageX + 15) + "px";' +
        '  });' +
        '  img.addEventListener("mouseleave", function(){' +
        '    infoBox.style.display = "none";' +
        '  });' +
        '});' +
        '<\/script>');

        galleryWindow.document.write('</body></html>');
        galleryWindow.document.close();
    } else {
        alert("Popup wurde blockiert. Bitte erlaube Popups für diese Seite.");
    }
}

// ===============================
// Comparison / Slideshow Script
// ===============================

// ===============================
// DataTable Initialisierung
// ===============================
$(document).ready(function() {
    if ($('#catalog').length) {
        $('#catalog').DataTable();
    }
});

// ===============================
// Globale Slideshow-Objekte
// ===============================
const slideshows = {
    A: { images: [], index: 0 },
    B: { images: [], index: 0 },
    C: { images: [], index: 0 },
    D: { images: [], index: 0 },
};

// ===============================
// Hilfsfunktionen
// ===============================

// Gefilterte Bild-Links aus DataTable holen
function getFilteredImageLinks() {
    const rows = document.querySelectorAll('#catalog tbody tr');
    const images = [];
    rows.forEach(row => {
        if (row.style.display !== 'none') {
            const cell = row.cells[8];
            const link = cell ? cell.querySelector('a') : null;
            if (link) {
                const name = link.textContent.trim();
                const url = '/index.php/Special:Redirect/file/' + name + '.jpg';
                images.push({ url: url, name: name + '.jpg' });
            }
        }
    });
    return images;
}

// Counter-Update
function updateCounter(target) {
    const data = slideshows[target];
    const total = data.images.length;
    const current = total > 0 ? data.index + 1 : 0;
    const counterEl = document.getElementById("counter" + target);
    if (counterEl) counterEl.textContent = current + "/" + total;
}

// ===============================
// MediaInfo Funktionen (aus HTML-Text der File-Seite)
// ===============================
function loadMediaInfo(target, filename) {
    const infoBox = document.getElementById('mediainfo' + target);
    if (!infoBox) return;
    infoBox.textContent = "Loading ...";

    fetch('/index.php?title=File:' + encodeURIComponent(filename) + '&action=raw')
    .then(resp => resp.text())
    .then(text => {
        const match = text.match(/\{\{MediaInfo([\s\S]*?)\}\}/);
        if (!match) {
            infoBox.textContent = "No Data found.";
            return;
        }

        const block = match[1];

        // Felder, die angezeigt werden sollen
        const fields = ["title","chapter","illustration","illustrator","year","tags"];
        let htmlList = "<ul style='margin:0; padding-left:1em; list-style:none;'>";

        fields.forEach(field => {
            const m = block.match(new RegExp("\\|\\s*" + field + "\\s*=([^\\n]*)"));
            if (m) {
                // Label groß schreiben
                const label = field.charAt(0).toUpperCase() + field.slice(1);
                htmlList += `<li><b>${label}:</b> ${m[1].trim()}</li>`;
            }
        });

        htmlList += "</ul>";
        infoBox.innerHTML = htmlList;

        // Optional: etwas CSS stylen
        infoBox.style.backgroundColor = "#f9f9f9";
        infoBox.style.border = "1px solid #ccc";
        infoBox.style.padding = "0.5em 1em";
        infoBox.style.marginTop = "0.5em";
        infoBox.style.borderRadius = "4px";
        infoBox.style.fontSize = "0.9em";
    })
    .catch(err => {
        console.error(err);
        infoBox.textContent = "Error";
    });
}




// ===============================
// Slideshow-Funktionen
// ===============================
function updateSlide(target) {
    const data = slideshows[target];
    const imgEl = document.getElementById('slide' + target);

    if (data.images.length === 0) {
        imgEl.src = '';
        imgEl.alt = 'No Images';
        imgEl.onclick = null;
        updateCounter(target);
        const infoBox = document.getElementById('mediainfo' + target);
        if(infoBox) infoBox.textContent = "";
        return;
    }

    if (data.index < 0) data.index = data.images.length - 1;
    if (data.index >= data.images.length) data.index = 0;

    const current = data.images[data.index];
    imgEl.src = current.url;
    imgEl.alt = current.name;
    imgEl.style.cursor = "pointer";
    imgEl.onclick = () => window.open('/File:' + current.name, '_blank');

    updateCounter(target);
    loadMediaInfo(target, current.name);
}

function populateSlideshow(target) {
    const images = getFilteredImageLinks();
    slideshows[target].images = images;
    slideshows[target].index = 0;

    const imgEl = document.getElementById('slide' + target);
    if(images.length > 0){
        imgEl.src = images[0].url;
        imgEl.alt = images[0].name;
        imgEl.style.cursor = "pointer";
        imgEl.onclick = () => window.open('/File:' + images[0].name, '_blank');
        loadMediaInfo(target, images[0].name);
    } else {
        imgEl.src = '';
        imgEl.alt = 'No Images';
        imgEl.onclick = null;
        const infoBox = document.getElementById('mediainfo' + target);
        if(infoBox) infoBox.textContent = "";
    }
    updateCounter(target);
}

function nextSlideX(target){
    slideshows[target].index++;
    updateSlide(target);
}

function prevSlideX(target){
    slideshows[target].index--;
    updateSlide(target);
}

function openSlideshowInNewTab(target){
    const data = slideshows[target];
    if(!data.images || data.images.length===0){
        alert("No images to display for slideshow " + target);
        return;
    }
    const newTab = window.open();
    if(!newTab){ alert("Popup blocked! Bitte Popups erlauben."); return; }
    let html = "<html><head><title>Slideshow " + target + "</title></head><body style='font-family:sans-serif'>";
    html += "<h2>Slideshow " + target + " (" + data.images.length + " images)</h2>";
    html += "<div style='display:flex; flex-wrap:wrap; gap:10px;'>";
    data.images.forEach(img=>{
        html += "<div><a href='/File:" + img.name + "' target='_blank'>";
        html += "<img src='" + img.url + "' style='max-width:300px; height:auto;'></a></div>";
    });
    html += "</div></body></html>";
    newTab.document.open();
    newTab.document.write(html);
    newTab.document.close();
}



// Chapter Script
importScript('MediaWiki:ChapterSlides.js');

// Chapter Script
importScript('MediaWiki:ChapterSlidesAlt.js');

// Filter Scripts, character pages 
importScript('MediaWiki:CharacterFilter.js');

// CharacterDistribution
// Chart.js laden
mw.loader.load('https://cdn.jsdelivr.net/npm/chart.js');

// Chart initialisieren
function initChart() {
    if (typeof Chart === 'undefined') {
        setTimeout(initChart, 50);
        return;
    }

    const canvas = document.getElementById('lineChart');
    if (!canvas) return;

    const ctx = canvas.getContext('2d');

    // HiDPI / Retina: Pixelratio berücksichtigen
    const dpr = window.devicePixelRatio || 1;
    canvas.width = canvas.offsetWidth * dpr;
    canvas.height = canvas.offsetHeight * dpr;
    ctx.scale(dpr, dpr);

    const data = {
        labels: ['Kemble (1885)', 'Schröder (1898)', 'Hirth (1920)', 'Trier (1936)', 'Harder (1938)', 'Kellerer (1938)', 'Busoni (1940)','Bebié (1944)'],
        datasets: [
            { label: 'Huck', data: [47.40,50.98,72.22,54.29,93.10,50.00,54.10,69.81], backgroundColor:'red', borderColor:'red', borderWidth:1, barPercentage:0.8 },
            { label: 'Jim', data: [17.92,29.41,22.22,28.57,44.83,18.18,26.23,17.00], backgroundColor:'blue', borderColor:'blue', borderWidth:1, barPercentage:0.8 },
            { label: 'Pap Finn', data: [5.78,7.84,5.56,5.71,6.90,9.09,4.92,1.88], backgroundColor:'green', borderColor:'green', borderWidth:1, barPercentage:0.8 },
            { label: 'King/Duke', data: [13.29,13.73,16.67,20.00,17.24,9.09,16.39,3.77], backgroundColor:'orange', borderColor:'orange', borderWidth:1, barPercentage:0.8 },
            { label: 'Tom', data: [11.56,7.84,19.44,11.43,24.14,9.09,11.48,13.21], backgroundColor:'black', borderColor:'black', borderWidth:1, barPercentage:0.8 },
            { label: 'Others', data: [38.73,29.41,36.11,31.43,17.24,31.82,22.95,13.21], backgroundColor:'pink', borderColor:'pink', borderWidth:1, barPercentage:0.8 },
            { type: 'scatter', label:'Total # of Illustrations', data:[173,51,36,35,29,22,61,53], borderColor:'purple', borderWidth:3, xAxisID:'x2', pointStyle:'cross', pointRadius:8, pointHoverRadius:10, showLine:false }
        ]
    };

    const config = {
        type:'bar',
        data:data,
        options:{
            responsive:true,
            maintainAspectRatio:false, // damit Chart die Containerhöhe nutzt
            indexAxis:'y',
            plugins:{ legend:{ display:true }, tooltip:{ mode:'index', intersect:false } },
            interaction:{ mode:'nearest', axis:'y', intersect:false },
            scales:{
                y:{ display:true, title:{ display:true, text:'Illustrator (Year)' }, stacked:false, barThickness:30 },
                x:{ display:true, position:'top', title:{ display:true, text:'% of Illustrations Featuring Character' }, min:0, max:100 },
                x2:{ display:true, position:'bottom', title:{ display:true, text:'Total # of Illustrations' }, min:0, max:180, grid:{ drawOnChartArea:false } }
            }
        }
    };

    new Chart(ctx, config);
    console.log('Chart erfolgreich initialisiert!');
}

$(document).ready(initChart);


// initChart beim Laden starten
$(document).ready(initChart);




// Media Viewer Adjustments to Display Title and Tags
importScript('MediaWiki:MediaViewerDisplay.js');

mw.loader.using(['mediawiki.util'], function () {

  function cleanFileLink(a) {
    // a.href = /index.php?title=Hf_1885_kmb_ch001_ill1.jpg&action=edit&redlink=1
    const title = a.textContent.trim();
    const file = title
      .replace(/\s+/g, '_')
      .replace(/\.(jpg|png|jpeg)$/i, '');

    return `<a href="/index.php/Special:Redirect/file/${file}.jpg">${file}</a>`;
  }

  document.addEventListener('click', function (e) {
    if (e.target.id !== 'generateCatalogHTML') return;

    const dpl = document.getElementById('DPL');
    if (!dpl) {
      alert('DPL table not found');
      return;
    }

    const rows = dpl.querySelectorAll('tbody tr');
    if (!rows.length) {
      alert('No data rows found');
      return;
    }

    let html = '';
    rows.forEach(row => {
      const cells = row.querySelectorAll('td');
      if (cells.length < 8) return;

      const publication  = cells[0].textContent.trim();
      const year         = cells[1].textContent.trim();
      const illustrator  = cells[2].textContent.trim();
      const chapter      = cells[3].textContent.trim();
      const illustration = cells[4].textContent.trim();
      const title        = cells[5].textContent.trim();
      const tags         = cells[6].textContent.trim();
      const link         = cells[7].querySelector('a');

      if (!link) return;

      html +=
`<tr>
<td align="left"><font face="Liberation Serif">${publication}</font></td>
<td align="right"><font face="Liberation Serif">${year}</font></td>
<td align="left"><font face="Liberation Serif">${illustrator}</font></td>
<td align="center"><font face="Liberation Serif">${chapter}</font></td>
<td align="center"><font face="Liberation Serif">${chapter}</font></td>
<td align="center"><font face="Liberation Serif">${illustration}</font></td>
<td align="left"><font face="Liberation Serif">${title}</font></td>
<td align="left"><font face="Liberation Serif">-${tags.replace(/,\s*/g,' -')}</font></td>
<td align="left">${cleanFileLink(link)}</td>
</tr>\n`;
    });

    const output = document.getElementById('catalogOutput');
    if (!output) {
      alert('Output textarea not found');
      return;
    }

    output.value = html;
    output.focus();
    output.select();

    alert('Static catalog HTML generated.');
  });

});