MediaWiki:Gadget-dynamicCargoLoading.js

From Angelina Jordan Wiki

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.
/* AJAX load of cargo query templates — pipe-based version */
/* global mw, $ */
( function () {
  'use strict';

  // --- Core utility: computeField ---
  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;
    };

    // --- Conditional processing with |if| ... |else| ... |endif| ---
	function processConditionals(text) {
	  var out = '', pos = 0;
	  while (true) {
	    // allow both {field|if} and {field|if|value}
	    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] !== undefined ? match[2] : null;
	    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 = false;
	
	          if (ref === null) {
	            // {field|if} → true if non-empty
	            ok = val !== null && val !== undefined && val !== '';
	          } else if (ref.indexOf('*') >= 0 || ref.indexOf('?') >= 0) {
	            // wildcard compare
	            var regex = new RegExp('^' + ref.replace(/[.+^${}()|[\]\\]/g, '\\$&')
	                                           .replace(/\*/g, '.*')
	                                           .replace(/\?/g, '.') + '$', 'i');
	            ok = regex.test(String(val));
	          } else {
	            // direct compare (numeric or string)
	            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 {field|transform1|transform2|...} ---
    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) : '';

      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('CargoDynamic: invalid regex or replacement', { pattern: pattern, repl: repl, error: e });
          }
        }
      }
      return value;
    });

    return formula;
  }

  // --- Read config once and cache it ---
  var _configCache = null;
  function readConfig() {
    if (_configCache) return _configCache;
    var div = document.getElementById('cargo-config');
    if (!div) return null;
    try {
      _configCache = JSON.parse(div.textContent);
      return _configCache;
    } catch (e) {
      console.error('CargoDynamic: invalid JSON in #cargo-config', e);
      return null;
    }
  }

  // --- Resource-aware lazy loader ---
  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();
    if (!config || !config.queries || !config.renderers) return;

    var query = config.queries[queryName];
    var renderer = config.renderers[renderName];
    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');
    for (var k in query) {
      if (query.hasOwnProperty(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) ? $.map(data.cargoquery, function(e) { return e.title; }) : [];
      if (!results.length) {
        $content.html('<em>No results found.</em>');
        return;
      }

      var html = [];
      html.push('<table class="wikitable sortable" style="width:100%; margin:0">');
      html.push('<tr>' + $.map(renderer.headers, function(h) { return '<th>' + h + '</th>'; }).join('') + '</tr>');
      for (var i = 0; i < results.length; i++) {
        var r = results[i];
        html.push('<tr>' + $.map(renderer.columns, function(c) {
          return '<td>' + computeField(r, c) + '</td>';
        }).join('') + '</tr>');
      }
      html.push('</table>');
      $content.html(html.join(''));
	  window.refreshYouTubePlayers();
	}).fail(function() {
      $content.html('<em>Error loading data.</em>');
    });
  }

  // --- Initialization ---
  function init() {
    $('.cargo-placeholder').each(function() {
      var $row = $(this);
      var $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);
        loadCargoFor($row);
      }

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

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

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

  $(init);

  // Expose to window for debugging
  window.CargoDynamic = {
    computeField: computeField,
    readConfig: readConfig
  };
}() );