mumu 0.11.0

Lava Mumu is a language for those in the now and that know
Documentation
// Combined Filter script for text input + component dropdown + doc-blocks
document.addEventListener("DOMContentLoaded", function() {
  const filterInput = document.getElementById("filter-input");
  const componentSelect = document.getElementById("component-filter-select");
  const groupSelect = document.getElementById("group-filter-select"); // If needed for group filtering

  let currentComponentFilter = componentSelect ? componentSelect.value : "";
  let currentGroupFilter = groupSelect ? groupSelect.value : "";
  
  function applyFilters() {
    const filterValue = filterInput ? filterInput.value.trim().toLowerCase() : "";

    // Filter the sidebar links
    const listItems = document.querySelectorAll(".function-list li");
    listItems.forEach(li => {
      const link = li.querySelector("a");
      if (!link) return;
      const text = link.textContent.toLowerCase();
      const matchesText = text.includes(filterValue);

      const componentAttr = li.getAttribute("data-component") || "";
      const matchesComponent =
        (currentComponentFilter === "" || componentAttr === currentComponentFilter);

      const groupAttr = li.getAttribute("data-groupUuid") || "";
      const matchesGroup =
        (currentGroupFilter === "" || groupAttr === currentGroupFilter);

      li.style.display = (matchesText && matchesComponent && matchesGroup) ? "" : "none";
    });

    // Hide or show doc-blocks in main content
    const docBlocks = document.querySelectorAll(".doc-block");
    docBlocks.forEach(block => {
      const blockComponent = block.getAttribute("data-component") || "";
      const blockGroup = block.getAttribute("data-groupUuid") || "";

      const componentMatches = (currentComponentFilter === "" || blockComponent === currentComponentFilter);
      const groupMatches = (currentGroupFilter === "" || blockGroup === currentGroupFilter);

      // We'll also check the doc-block's text content for the text filter, if desired
      const textContent = block.textContent.toLowerCase();
      const textMatches = textContent.includes(filterValue);

      if (componentMatches && groupMatches && textMatches) {
        block.style.display = "";
      } else {
        block.style.display = "none";
      }
    });
  }

  if (filterInput) {
    filterInput.addEventListener("input", applyFilters);
  }
  if (componentSelect) {
    componentSelect.addEventListener("change", function() {
      currentComponentFilter = this.value;
      applyFilters();
    });
  }
  if (groupSelect) {
    groupSelect.addEventListener("change", function() {
      currentGroupFilter = this.value;
      applyFilters();
    });
  }

  // ==========================
  //  Anchor / Hash Scrolling
  // ==========================
  //
  // 1) For each link in the sidebar, intercept clicks to scroll the .main-content
  //    container to the target doc-block instead of scrolling the entire page.
  //
  // 2) Also, if the page loads with a #hash, we do the same scroll logic on DOM load.

  // A helper to scroll .main-content to an element with a given ID
  function scrollMainContentToId(elementId) {
    const mainContent = document.querySelector(".main-content");
    if (!mainContent) return;

    const targetEl = document.getElementById(elementId);
    if (!targetEl) return;

    // Calculate how far the target is from the top of .main-content
    let offsetTop = targetEl.offsetTop - mainContent.offsetTop;

    mainContent.scrollTo({
      top: offsetTop,
      behavior: "smooth"
    });
  }

  // Intercept sidebar link clicks
  const sidebarLinks = document.querySelectorAll(".function-list li a[href^='#']");
  sidebarLinks.forEach(link => {
    link.addEventListener("click", function(e) {
      const hash = this.getAttribute("href").slice(1); // remove '#'
      if (hash) {
        e.preventDefault();
        scrollMainContentToId(hash);
      }
    });
  });

  // If there's an initial hash when page loads, scroll to it
  if (location.hash && location.hash.length > 1) {
    const initialHash = location.hash.substring(1); // remove '#'
    scrollMainContentToId(initialHash);
  }

  // Finally, run applyFilters once on DOMContentLoaded to ensure correct filtering
  applyFilters();
});


// Syntax highlighting function and auto-apply
window.highlightMuMuSyntax = function(code) {
  code = code
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');

  const lines = code.split('\n');
  function highlightNonComment(str) {
    let html = str.replace(/"([^"\\]|\\.)*"/g, function(m) {
      return `<span class="af-string">${m}</span>`;
    });
    html = html.replace(/\b-?\d+\b/g, function(m) {
      return `<span class="af-number">${m}</span>`;
    });
    html = html.replace(/\b_\b/g, '<span class="af-placeholder">_</span>');

    const builtins = [
      'fn','map','eq','plus','sput','slog','sum','inc','keys','tap',
      'upper','lower','multiply','prop','assoc','merge','select','cat',
      'comp','range','filter','reduce','pluck','head','tail','take',
      'some','none','flatten','push','fall','every','find','group',
      'intersect','sort','union','each','length'
    ];
    const builtinsPattern = new RegExp('\\b(' + builtins.join('|') + ')\\b', 'g');
    html = html.replace(builtinsPattern, function(match) {
      if (match === 'fn') {
        return `<span class="af-keyword">${match}</span>`;
      } else {
        return `<span class="af-builtin">${match}</span>`;
      }
    });
    html = html.replace(/=>/g, '<span class="af-operator">=&gt;</span>');
    return html;
  }

  const processedLines = lines.map(function(line) {
    const commentIndex = line.indexOf('//');
    if (commentIndex >= 0) {
      const nonComment = line.substring(0, commentIndex);
      const comment = line.substring(commentIndex);
      return highlightNonComment(nonComment) + '<span class="af-comment">' + comment + '</span>';
    } else {
      return highlightNonComment(line);
    }
  });
  return processedLines.join('\n');
};

document.addEventListener("DOMContentLoaded", function() {
  var codeBlocks = document.querySelectorAll('.code-block');
  codeBlocks.forEach(function(block) {
    var originalCode = block.textContent;
    block.innerHTML = window.highlightMuMuSyntax(originalCode);
  });
});


// ====================== LAVA MARQUEE ANIMATION SCRIPT ======================
const marqueeText = "LAVA => ";
const marqueeContainer = document.getElementById("marquee-content");

const halfRepeatCount = 40;
let marqueeSpans = [];
let marqueeWidth = 0;
let containerWidth = 0;
let totalLetters = 0;

function createMarqueeContent() {
  marqueeContainer.innerHTML = "";
  marqueeSpans = [];

  for (let b = 0; b < halfRepeatCount; b++) {
    addBlock(b);
  }
  const firstHalf = [...marqueeSpans];
  for (let b = 0; b < halfRepeatCount; b++) {
    const offset = b * marqueeText.length;
    for (let i = 0; i < marqueeText.length; i++) {
      const original = firstHalf[offset + i];
      const clone = original.cloneNode(true);
      marqueeContainer.appendChild(clone);
      marqueeSpans.push(clone);
    }
  }
}

function addBlock(blockIndex) {
  for (let i = 0; i < marqueeText.length; i++) {
    const letter = marqueeText[i];
    const span = document.createElement("span");
    span.className = "marquee-letter";
    span.textContent = letter;
    span.dataset.baseScale = "1.0";
    span.dataset.blockIndex = blockIndex.toString();
    span.dataset.letterIndex = i.toString();
    marqueeContainer.appendChild(span);
    marqueeSpans.push(span);
  }
}

function measureDimensions() {
  const container = document.getElementById("marquee-container");
  containerWidth = container.offsetWidth;
  marqueeWidth = marqueeContainer.scrollWidth;
  totalLetters = marqueeSpans.length - 1;
}

createMarqueeContent();
measureDimensions();
window.addEventListener('resize', measureDimensions);

// CHANGED: cycleDuration is 10× faster => changed from 240.0 to 24.0
let cycleDuration = 24.0;
let timeInCycle = 0;
let direction = 1;
let lastTime = performance.now();

// We'll track how many times we've returned to the left => 3 cycles total
let cycleCount = 0;

function easeInOutSine(t) {
  return 0.5 - 0.5 * Math.cos(Math.PI * t);
}

let wavePos = 0;
let waveDir = 1;
let waveSpeed = 0.125;
const waveRadius = 6;
const waveAmp = 0.4;
const waveSpacing = 0.7;

function computeMaxShift() {
  return containerWidth - marqueeWidth;
}
let maxShift = 0;

function animateMarquee(now) {
  const dt = (now - lastTime) / 1000;
  lastTime = now;

  measureDimensions();
  maxShift = computeMaxShift();

  // Horizontal marquee motion
  let step = dt / cycleDuration;
  timeInCycle += direction * step;

  // If we've gone below 0 => we finished a pass from right to left
  if (timeInCycle < 0) {
    timeInCycle = 0;
    direction = 1;
    cycleCount++;
    if (cycleCount >= 3) {
      // Stop after 3 cycles
      // Do not request next frame => marquee remains at left
      return;
    }
  } else if (timeInCycle > 1) {
    timeInCycle = 1;
    direction = -1;
  }

  const frac = easeInOutSine(timeInCycle);
  const xPos = frac * maxShift;
  marqueeContainer.style.transform = `translateX(${xPos}px)`;

  // Traveling wave across letters
  wavePos += waveSpeed * waveDir * dt;
  if (wavePos < 0) {
    wavePos = 0;
    waveDir = 1;
  } else if (wavePos > totalLetters) {
    wavePos = totalLetters;
    waveDir = -1;
  }

  const nowSec = now / 1000;
  for (let s of marqueeSpans) {
    const blockIndex = parseInt(s.dataset.blockIndex);
    const letterIndex = parseInt(s.dataset.letterIndex);
    const baseScale = parseFloat(s.dataset.baseScale);

    // Color wave (hue from red to yellow)
    const colorFlowFrequency = 0.02;
    const colorFlowBlockOffset = 0.5;
    const colorPhase =
      nowSec * colorFlowFrequency * 2.0 * Math.PI +
      blockIndex * colorFlowBlockOffset;

    const hueMin = 0;
    const hueMax = 60;
    const hueRange = hueMax - hueMin;
    const hueCenter = (hueMin + hueMax) / 2;
    const hue = hueCenter + (hueRange / 2) * Math.sin(colorPhase);
    s.style.color = `hsl(${hue}, 80%, 60%)`;

    // Base wave
    const baseWaveFrequency = 0.4;
    const baseWaveOffset = 0.04;
    const baseWaveAmplitude = 0.4;
    const globalIndex = blockIndex * marqueeText.length + letterIndex;
    const wavePhase =
      nowSec * baseWaveFrequency * 2.0 * Math.PI + globalIndex * baseWaveOffset;
    const waveVal = Math.sin(wavePhase);
    const waveScale = baseScale + baseWaveAmplitude * waveVal;

    // Ripple effect if near wavePos
    const dist = Math.abs(globalIndex - wavePos);
    let rippleFactor = 1;
    if (dist <= waveRadius) {
      const ripplePhase = dist * waveSpacing;
      const rippleVal = Math.cos(ripplePhase) * waveAmp;
      rippleFactor = 1 + rippleVal;
    }
    const finalScale = waveScale * rippleFactor;
    s.style.transform = `scale(${finalScale}, ${finalScale})`;
  }

  requestAnimationFrame(animateMarquee);
}
requestAnimationFrame(animateMarquee);