Jump to content

MediaWiki:Gadget-TciTest.js: Difference between revisions

Find traditional instrumental music
WikiSysop (talk | contribs)
No edit summary
Tag: Reverted
WikiSysop (talk | contribs)
No edit summary
Tag: Manual revert
Line 1: Line 1:
// Gadget-TciTest.js — TCI Calculator (VISIBLE version fix)
// Gadget-TciTest.js — TCI Calculator (LIVE VERSION — fixed parseABC scope)
(function ($, mw, OO) {
(function ($, mw, OO) {
   "use strict";
   "use strict";
Line 15: Line 15:
       { action: 'calculate', label: 'Calculate TCI', flags: ['primary', 'progressive'] }
       { action: 'calculate', label: 'Calculate TCI', flags: ['primary', 'progressive'] }
     ];
     ];
    const majorScales = {
      C: ['C', 'D', 'E', 'F', 'G', 'A', 'B'],
      G: ['G', 'A', 'B', 'C', 'D', 'E', 'F#'],
      D: ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'],
      A: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'],
      E: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'],
      B: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'],
      F: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E']
    };
    const minorScales = {
      Am: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
      Em: ['E', 'F#', 'G', 'A', 'B', 'C', 'D'],
      Dm: ['D', 'E', 'F', 'G', 'A', 'Bb', 'C'],
      Bm: ['B', 'C#', 'D', 'E', 'F#', 'G', 'A'],
      Gm: ['G', 'A', 'Bb', 'C', 'D', 'Eb', 'F'],
      Fsm: ['F#', 'G#', 'A', 'B', 'C#', 'D', 'E']
    };
    const parallelMajors = {
      Em: 'E', Am: 'A', Dm: 'D', Bm: 'B', Gm: 'G', Fsm: 'F#'
    };
    function normalizeKey(key) {
      return key.replace(/[^a-zA-Z#bm]/g, '').trim();
    }
    function getScaleForKey(keyRaw) {
      const key = normalizeKey(keyRaw);
      return majorScales[key] || minorScales[key] || majorScales['D'];
    }
    function getComparativeMajorScale(keyRaw) {
      const key = normalizeKey(keyRaw);
      return majorScales[parallelMajors[key]] || majorScales['D'];
    }
    function normalizeNote(note) {
      return note.replace(/=/g, '').replace(/\^/g, '#').replace(/_/g, 'b');
    }
    function parseABC(abcText, octaveMode) {
      const lines = abcText.split(/\r?\n/);
      const keyLine = lines.find(line => line.startsWith('K:')) || 'K:D';
      const lengthLine = lines.find(line => line.startsWith('L:')) || 'L:1/8';
      const key = keyLine.split(':')[1].trim();
      const [lNum, lDen] = lengthLine.split(':')[1].split('/').map(Number);
      const beatDuration = lNum / lDen;
      const scale = getScaleForKey(key);
      const refScale = getComparativeMajorScale(key) || scale;
      const beats4 = [], beats2 = [], debug4 = [], debug2 = [];
      const tokenRegex = /((=|\^|_)?[a-gA-GzZ][',/]*)(\d*\/?\d*)/g;
      let match;
      let beatAcc = 0, beatAlt = 0;
      function parseDuration(str) {
        if (!str) return beatDuration;
        if (str.includes('/')) {
          const [n, d] = str.split('/').map(Number);
          return n / d;
        }
        return parseFloat(str);
      }
      function code(note) {
        const baseRaw = normalizeNote(note.replace(/[',0-9\/]/g, ''));
        const base = baseRaw.toUpperCase();
        const refIdx = refScale.findIndex(n => n.replace(/[#b]/g, '') === base);
        const idxInMinor = scale.findIndex(n => n.replace(/[#b]/g, '') === base);
        let c = refIdx !== -1 ? (refIdx + 1).toString() : '?';
        if (refIdx !== -1 && (idxInMinor === -1 || scale[idxInMinor] !== refScale[refIdx])) {
          if (refScale[refIdx].includes('#') && (!scale[idxInMinor] || !scale[idxInMinor].includes('#'))) c += 'b';
          else if (refScale[refIdx].includes('b') && (!scale[idxInMinor] || !scale[idxInMinor].includes('b'))) c += '#';
        }
        const isHigh = /[a-g]/.test(note);
        const isLow = /,/.test(note);
        if (octaveMode === 'shiftUp') return c + 'H';
        if (octaveMode === 'shiftDown') return c + 'L';
        if (octaveMode === 'visual') return isHigh ? c + 'H' : isLow ? c + 'L' : c;
        if (octaveMode === 'standard') return isHigh ? c + 'H' : c;
        return c;
      }
      while ((match = tokenRegex.exec(abcText)) !== null && (beatAcc < 8 || beatAlt < 4)) {
        const [full, note, , durStr] = match;
        const dur = parseDuration(durStr || `${lNum}/${lDen}`);
        const val = /z/i.test(note) ? '0' : code(note);
        const plain = note.replace(/\d.*$/, '');
        console.log(`NOTE: ${plain}, duration: ${dur}`);
        if (beatAcc < 8) {
          beats4.push(val);
          debug4.push(`B${beatAcc + 1}: ${plain} → ${val}`);
          beatAcc++;
        }
        if (beatAlt < 4) {
          beats2.push(val);
          debug2.push(`B${beatAlt + 1}: ${plain} → ${val}`);
          beatAlt++;
        }
      }
      return {
        normal: beats4.join(' '),
        dual: beats2.join(' '),
        debug: '4-beat mode:\n' + debug4.join('\n') + '\n\n2-beat mode:\n' + debug2.join('\n')
      };
    }


     TCICalculatorTestDialog.prototype.initialize = function () {
     TCICalculatorTestDialog.prototype.initialize = function () {
       TCICalculatorTestDialog.super.prototype.initialize.call(this);
       TCICalculatorTestDialog.super.prototype.initialize.call(this);
 
       this.content = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
       this.content = new OO.ui.PanelLayout({
        padded: true,
        expanded: true,
        scrollable: true
      });
       this.$body.append(this.content.$element);
       this.$body.append(this.content.$element);


       this.abcInput = new OO.ui.MultilineTextInputWidget({
       this.abcInput = new OO.ui.MultilineTextInputWidget({ placeholder: 'Paste ABC notation here...', autosize: true, rows: 10 });
        placeholder: 'Paste ABC notation here...',
        autosize: true,
        rows: 10,
        classes: ['tci-abc-input']
      });


       this.octaveChoice = new OO.ui.DropdownInputWidget({
       this.octaveChoice = new OO.ui.DropdownInputWidget({
Line 42: Line 147:
       });
       });


       this.resultOutput = new OO.ui.MultilineTextInputWidget({
       this.resultOutput = new OO.ui.MultilineTextInputWidget({ readOnly: true, autosize: true, rows: 4 });
        readOnly: true,
        autosize: true,
        rows: 6,
        classes: ['tci-output']
      });


       this.content.$element.append(
       this.content.$element.append(
Line 61: Line 161:
     };
     };


     // CSS fix
     TCICalculatorTestDialog.prototype.getActionProcess = function (action) {
    mw.util.addCSS(`
       if (action === 'calculate') {
       .tci-abc-input textarea {
        return new OO.ui.Process(() => {
         min-height: 150px;
          const abc = this.abcInput.getValue();
          const mode = this.octaveChoice.getValue();
 
          console.log('📥 ABC from textarea:\n', abc);
 
          const result = parseABC(abc, mode);
          this.resultOutput.setValue(
            'Standard (4 beats): ' + result.normal +
            '\nAlternate (2 beats): ' + result.dual +
            '\n\n' + result.debug
          );
        });
      } else if (action === 'cancel') {
         return new OO.ui.Process(() => this.close({ action: action }));
       }
       }
       .tci-output textarea {
       return TCICalculatorTestDialog.super.prototype.getActionProcess.call(this, action);
        min-height: 100px;
    };
        background: #f5f5f5;
      }
      .oo-ui-window-body {
        max-height: 90vh !important;
        overflow-y: auto;
      }
    `);
 
    // ... (rest of script unchanged, parser + logs)


     function openTCICalculatorTestDialog() {
     function openTCICalculatorTestDialog() {

Revision as of 10:45, 11 April 2025

// Gadget-TciTest.js — TCI Calculator (LIVE VERSION — fixed parseABC scope)
(function ($, mw, OO) {
  "use strict";

  mw.loader.using(["oojs-ui-core", "oojs-ui-widgets", "oojs-ui-windows"]).done(function () {
    function TCICalculatorTestDialog(config) {
      TCICalculatorTestDialog.super.call(this, config);
    }
    OO.inheritClass(TCICalculatorTestDialog, OO.ui.ProcessDialog);

    TCICalculatorTestDialog.static.name = 'tciCalculatorTestDialog';
    TCICalculatorTestDialog.static.title = 'Theme Code Index Calculator — LIVE VERSION';
    TCICalculatorTestDialog.static.actions = [
      { action: 'cancel', label: 'Cancel', flags: 'safe' },
      { action: 'calculate', label: 'Calculate TCI', flags: ['primary', 'progressive'] }
    ];

    const majorScales = {
      C: ['C', 'D', 'E', 'F', 'G', 'A', 'B'],
      G: ['G', 'A', 'B', 'C', 'D', 'E', 'F#'],
      D: ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'],
      A: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'],
      E: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'],
      B: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'],
      F: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E']
    };

    const minorScales = {
      Am: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
      Em: ['E', 'F#', 'G', 'A', 'B', 'C', 'D'],
      Dm: ['D', 'E', 'F', 'G', 'A', 'Bb', 'C'],
      Bm: ['B', 'C#', 'D', 'E', 'F#', 'G', 'A'],
      Gm: ['G', 'A', 'Bb', 'C', 'D', 'Eb', 'F'],
      Fsm: ['F#', 'G#', 'A', 'B', 'C#', 'D', 'E']
    };

    const parallelMajors = {
      Em: 'E', Am: 'A', Dm: 'D', Bm: 'B', Gm: 'G', Fsm: 'F#'
    };

    function normalizeKey(key) {
      return key.replace(/[^a-zA-Z#bm]/g, '').trim();
    }

    function getScaleForKey(keyRaw) {
      const key = normalizeKey(keyRaw);
      return majorScales[key] || minorScales[key] || majorScales['D'];
    }

    function getComparativeMajorScale(keyRaw) {
      const key = normalizeKey(keyRaw);
      return majorScales[parallelMajors[key]] || majorScales['D'];
    }

    function normalizeNote(note) {
      return note.replace(/=/g, '').replace(/\^/g, '#').replace(/_/g, 'b');
    }

    function parseABC(abcText, octaveMode) {
      const lines = abcText.split(/\r?\n/);
      const keyLine = lines.find(line => line.startsWith('K:')) || 'K:D';
      const lengthLine = lines.find(line => line.startsWith('L:')) || 'L:1/8';

      const key = keyLine.split(':')[1].trim();
      const [lNum, lDen] = lengthLine.split(':')[1].split('/').map(Number);
      const beatDuration = lNum / lDen;

      const scale = getScaleForKey(key);
      const refScale = getComparativeMajorScale(key) || scale;
      const beats4 = [], beats2 = [], debug4 = [], debug2 = [];

      const tokenRegex = /((=|\^|_)?[a-gA-GzZ][',/]*)(\d*\/?\d*)/g;
      let match;
      let beatAcc = 0, beatAlt = 0;

      function parseDuration(str) {
        if (!str) return beatDuration;
        if (str.includes('/')) {
          const [n, d] = str.split('/').map(Number);
          return n / d;
        }
        return parseFloat(str);
      }

      function code(note) {
        const baseRaw = normalizeNote(note.replace(/[',0-9\/]/g, ''));
        const base = baseRaw.toUpperCase();
        const refIdx = refScale.findIndex(n => n.replace(/[#b]/g, '') === base);
        const idxInMinor = scale.findIndex(n => n.replace(/[#b]/g, '') === base);

        let c = refIdx !== -1 ? (refIdx + 1).toString() : '?';
        if (refIdx !== -1 && (idxInMinor === -1 || scale[idxInMinor] !== refScale[refIdx])) {
          if (refScale[refIdx].includes('#') && (!scale[idxInMinor] || !scale[idxInMinor].includes('#'))) c += 'b';
          else if (refScale[refIdx].includes('b') && (!scale[idxInMinor] || !scale[idxInMinor].includes('b'))) c += '#';
        }

        const isHigh = /[a-g]/.test(note);
        const isLow = /,/.test(note);
        if (octaveMode === 'shiftUp') return c + 'H';
        if (octaveMode === 'shiftDown') return c + 'L';
        if (octaveMode === 'visual') return isHigh ? c + 'H' : isLow ? c + 'L' : c;
        if (octaveMode === 'standard') return isHigh ? c + 'H' : c;
        return c;
      }

      while ((match = tokenRegex.exec(abcText)) !== null && (beatAcc < 8 || beatAlt < 4)) {
        const [full, note, , durStr] = match;
        const dur = parseDuration(durStr || `${lNum}/${lDen}`);
        const val = /z/i.test(note) ? '0' : code(note);
        const plain = note.replace(/\d.*$/, '');

        console.log(`NOTE: ${plain}, duration: ${dur}`);

        if (beatAcc < 8) {
          beats4.push(val);
          debug4.push(`B${beatAcc + 1}: ${plain}${val}`);
          beatAcc++;
        }
        if (beatAlt < 4) {
          beats2.push(val);
          debug2.push(`B${beatAlt + 1}: ${plain}${val}`);
          beatAlt++;
        }
      }

      return {
        normal: beats4.join(' '),
        dual: beats2.join(' '),
        debug: '4-beat mode:\n' + debug4.join('\n') + '\n\n2-beat mode:\n' + debug2.join('\n')
      };
    }

    TCICalculatorTestDialog.prototype.initialize = function () {
      TCICalculatorTestDialog.super.prototype.initialize.call(this);
      this.content = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
      this.$body.append(this.content.$element);

      this.abcInput = new OO.ui.MultilineTextInputWidget({ placeholder: 'Paste ABC notation here...', autosize: true, rows: 10 });

      this.octaveChoice = new OO.ui.DropdownInputWidget({
        options: [
          { data: 'standard', label: 'Standard fiddle range (default)' },
          { data: 'visual', label: 'Based on ABC visual octave' },
          { data: 'shiftUp', label: 'Force high octave (+1)' },
          { data: 'shiftDown', label: 'Force low octave (-1)' }
        ]
      });

      this.resultOutput = new OO.ui.MultilineTextInputWidget({ readOnly: true, autosize: true, rows: 4 });

      this.content.$element.append(
        new OO.ui.FieldsetLayout({
          label: 'TCI Calculator Options',
          items: [
            new OO.ui.FieldLayout(this.abcInput, { label: 'ABC Notation', align: 'top' }),
            new OO.ui.FieldLayout(this.octaveChoice, { label: 'Octave Interpretation', align: 'top' }),
            new OO.ui.FieldLayout(this.resultOutput, { label: 'Theme Code Index (TCI)', align: 'top' })
          ]
        }).$element
      );
    };

    TCICalculatorTestDialog.prototype.getActionProcess = function (action) {
      if (action === 'calculate') {
        return new OO.ui.Process(() => {
          const abc = this.abcInput.getValue();
          const mode = this.octaveChoice.getValue();

          console.log('📥 ABC from textarea:\n', abc);

          const result = parseABC(abc, mode);
          this.resultOutput.setValue(
            'Standard (4 beats): ' + result.normal +
            '\nAlternate (2 beats): ' + result.dual +
            '\n\n' + result.debug
          );
        });
      } else if (action === 'cancel') {
        return new OO.ui.Process(() => this.close({ action: action }));
      }
      return TCICalculatorTestDialog.super.prototype.getActionProcess.call(this, action);
    };

    function openTCICalculatorTestDialog() {
      const windowManager = new OO.ui.WindowManager();
      $(document.body).append(windowManager.$element);
      const dialog = new TCICalculatorTestDialog();
      windowManager.addWindows([dialog]);
      windowManager.openWindow(dialog);
    }

    mw.util.addPortletLink('p-cactions', '#', '🎼 TCI Test', 'ca-tcitest', 'Open TCI test');
    $(document).on('click', '#ca-tcitest', function (e) {
      e.preventDefault();
      openTCICalculatorTestDialog();
    });
  });
})(jQuery, mediaWiki, OO);
Cookies help us deliver our services. By using The Traditional Tune Archive services, you agree to our use of cookies.