MediaWiki:Gadget-dynamicCargoLoading.js

From Angelina Jordan Wiki
Revision as of 22:00, 8 November 2025 by Most2dot0 (talk | contribs) (reduce table margin)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* test AJAX load of cargo query templates — pipe-based version */
/* global mw, $ */
window.dynamicCargoLoading = (function() {
  'use strict';

function computeField(item, formula, resolvePath, matchesFilter) {
  resolvePath = resolvePath || function(obj, path) {
    return path.split('.').reduce(function(o, k) {
      return (o && o[k] !== undefined) ? o[k] : null;
    }, obj);
  };
  matchesFilter = matchesFilter || function(value, filter) {
    if (!filter) return true;
    return String(value).indexOf(filter) >= 0;
  };

  function processConditionals(text) {
    var out = '', pos = 0;
    while (true) {
      // Use |if| syntax
      var match = text.slice(pos).match(/\{([^|}]+)\|if\|([^}]+)\}/);
      if (!match) { out += text.slice(pos); break; }
      var si = pos + match.index;
      var sj = si + match[0].length;
      var field = match[1];
      var ref = match[2];
      out += text.slice(pos, si);

      var depth = 1, scan = sj, elseStart = null, elseEnd = null, endPos = null;
      while (scan < text.length) {
        var tagMatch = text.slice(scan).match(/\{([^}]+)\}/);
        if (!tagMatch) break;
        var ti = scan + tagMatch.index;
        var te = ti + tagMatch[0].length;
        var tag = tagMatch[1];
        if (/^[^|}]+\|if\|/.test(tag)) depth++;
        else if (tag === 'else' && depth === 1) { elseStart = ti; elseEnd = te; }
        else if (tag === '/endif') {
          depth--;
          if (depth === 0) {
            endPos = te;
            var thenPart = elseStart ? text.slice(sj, elseStart) : text.slice(sj, ti);
            var elsePart = elseStart ? text.slice(elseEnd, ti) : '';
            var val = resolvePath(item, field);
            var ok;
            if (ref === '') ok = val !== null && val !== undefined && val !== '';
            else {
              var v = (val !== null && val !== undefined) ? String(val) : '';
              var vn = Number(v), rn = Number(ref);
              ok = (!isNaN(vn) && !isNaN(rn)) ? (vn === rn) : (v === ref);
            }
            out += processConditionals(ok ? thenPart : elsePart);
            pos = te;
            break;
          }
        }
        scan = te;
      }
      if (!endPos) { out += text.slice(si); break; }
    }
    return out;
  }

  formula = processConditionals(formula);

  // Replace remaining simple {field|...} placeholders
  formula = formula.replace(/\{([^}]+)\}/g, function(_, content) {
    if (/^[^|}]+\|if\|/.test(content) || content === 'else' || content === '/endif') return '';
    var parts = content.split('|');
    var fieldPath = parts[0];
    var value = resolvePath(item, fieldPath);
    value = (value !== null && value !== undefined) ? String(value) : '';

    // Apply transformations or regex replacements
    for (var i = 1; i < parts.length; i += 2) {
      var pattern = parts[i], repl = parts[i + 1];
      if (pattern === 'toUpper') value = value.toUpperCase();
      else if (pattern === 'toLower') value = value.toLowerCase();
      else if (pattern === 'toTitleCase') value = value.replace(/\b(\w)(\w*)/g, function(_, f, r) { return f.toUpperCase() + r; });
      else if (pattern === 'limit' && repl) {
        var n = Number(repl);
        if (!isNaN(n)) value = value.slice(0, n);
      } 
      else if (repl) {
        try {
          var safePattern = String(pattern).replace(/\\\\/g, '\\').replace(/\\\//g, '/').trim();
          var regex = new RegExp(safePattern, 'g');
          value = String(value).replace(regex, function() {
            var args = Array.prototype.slice.call(arguments);
            return String(repl).replace(/\$(\d+)/g, function(_, n) {
              var idx = parseInt(n, 10);
              return (typeof args[idx] !== 'undefined' && args[idx] !== null) ? args[idx] : '';
            });
          });
        } catch (e) {
          console.error('dynamicCargoLoading: invalid regex or replacement', { pattern: pattern, safePattern: safePattern, repl: repl, error: e });
        }
      }
    }
    return value;
  });

  return formula;
}

function readConfig() {
  var div = document.getElementById('cargo-config');
  if (!div) return null;
  var txt = div.textContent.replace(/<\/?nowiki>/g, '');
  try {
    return JSON.parse(txt);
  } catch (e) {
    console.error('dynamicCargoLoading: invalid JSON in #cargo-config', e);
    return null;
  }
}


function loadCargoFor($row) {
  var $content = $row.find('.mw-collapsible-content, .cargo-content');
  if (!$content.length) {
    $content = $('<div class="cargo-content"></div>').appendTo($row.find('td').first());
  }

  var song = $row.data('song');
  var queryName = $row.data('query');
  var renderName = $row.data('render');
  var config = readConfig();
  var query = (config && config.queries && config.queries[queryName]) || null;
  var renderer = (config && config.renderers && config.renderers[renderName]) || null;

  if (!query || !renderer) {
    $content.html('<em>Missing query or renderer definition.</em>');
    return;
  }

  var apiUrl = new URL(mw.util.wikiScript('api'), location.origin);
  apiUrl.searchParams.set('action', 'cargoquery');
  apiUrl.searchParams.set('format', 'json');
  Object.keys(query).forEach(function(k) {
    var v = String(query[k]).replace(/\$song/g, song || '');
    apiUrl.searchParams.set(k, v);
  });

  $content.html('<em>Loading…</em>');

  $.getJSON(apiUrl.toString()).done(function(data) {
    var results = (data && data.cargoquery) ? data.cargoquery.map(function(e) { return e.title; }) : [];
    if (!results.length) {
      $content.html('<em>No results found.</em>');
      return;
    }

    var rows = results.map(function(r) {
      return '<tr>' + renderer.columns.map(function(c) {
        return '<td>' + computeField(r, c) + '</td>';
      }).join('') + '</tr>';
    });

    var table = '<table class="wikitable sortable" style="width:100%; margin:0;">' +
                '<tr>' + renderer.headers.map(function(h) { return '<th>' + h + '</th>'; }).join('') + '</tr>' +
                rows.join('') + '</table>';

    $content.html(table);
    window.refreshYouTubePlayers();
  }).fail(function() {
    $content.html('<em>Error loading data.</em>');
  });
}

function init() {
  $('.cargo-placeholder').each(function() {
    const $row = $(this);
    let $content = $row.find('.mw-collapsible-content, .cargo-content');
    if (!$content.length) {
      $content = $('<div class="cargo-content"></div>').appendTo($row.find('td').first());
    }

    function triggerLoad() {
      if ($row.data('loaded')) return;
      $row.data('loaded', true);
      $content.html('<em>Loading…</em>');
      loadCargoFor($row);
    }

    const observer = new MutationObserver(() => {
      if ($row.is(':visible')) triggerLoad();
    });
    observer.observe(this, { attributes: true, attributeFilter: ['class','style'], subtree: true });

    $row.on('click', () => {
      setTimeout(() => {
        if ($row.is(':visible')) triggerLoad();
      }, 100);
    });

    if ($row.is(':visible')) triggerLoad();
  });
}

$(init);

return { computeField: computeField, readConfig: readConfig };
})();