mumu 0.11.0

Lava Mumu is a language for those in the now and that know
Documentation
/***************************************************************
 * docs-dynamic.js
 * ... unchanged file header ...
 **************************************************************/

document.addEventListener('DOMContentLoaded', () => {
  fetch('functions-list.json')
    .then(r => r.json())
    .then(async (fileList) => {
      console.log('List of function JSON files:', fileList);

      const allDocs = [];
      for (const fname of fileList) {
        const url = 'functions/' + fname;
        try {
          const resp = await fetch(url);
          if (!resp.ok) {
            console.warn(`Failed to fetch ${url}: HTTP ${resp.status}`);
            continue;
          }
          const docObj = await resp.json();
          allDocs.push(docObj);
        } catch (err) {
          console.error(`Error fetching doc ${url}:`, err);
        }
      }

      buildDocsPage(allDocs);
    })
    .catch(err => {
      console.error('Error loading functions-list.json:', err);
    });
});

function buildDocsPage(docs) {
  const articleDocs = [];
  const functionDocs = [];
  for (const d of docs) {
    const badges = Array.isArray(d.heading?.badges) ? d.heading.badges : [];
    if (badges.includes('ARTICLE')) {
      articleDocs.push(d);
    } else {
      functionDocs.push(d);
    }
  }

  articleDocs.sort((a, b) => {
    const at = a.heading?.title || '';
    const bt = b.heading?.title || '';
    return at.localeCompare(bt);
  });
  functionDocs.sort((a, b) => {
    const at = a.heading?.title || '';
    const bt = b.heading?.title || '';
    return at.localeCompare(bt);
  });

  buildSidebarNav(functionDocs);

  let htmlOutput = '';
  for (const art of articleDocs) {
    htmlOutput += buildDocBlockHtml(art);
  }
  for (const fnDoc of functionDocs) {
    htmlOutput += buildDocBlockHtml(fnDoc);
  }

  const container = document.getElementById('docs-container');
  if (!container) {
    console.warn('No #docs-container to insert docs!');
    return;
  }
  container.innerHTML = htmlOutput;

  enableEditabilityAllDocs();
  attachJsonButtons();
  highlightAllCodeBlocks();
  setupFiltersAndScrolling();
  applyFilters();
}

function buildSidebarNav(functionDocs) {
  const compSet = new Set();
  const groupSet = new Set();

  for (const doc of functionDocs) {
    if (doc.dataComponent) compSet.add(doc.dataComponent);
    if (doc.group) groupSet.add(doc.group);
  }

  const listEl = document.querySelector('.function-list');
  if (!listEl) {
    console.warn('No .function-list element found.');
    return;
  }
  listEl.innerHTML = '';

  for (const doc of functionDocs) {
    const comp = doc.dataComponent || '';
    const title = doc.heading?.title || '';
    const finalId = comp && title ? `${comp}:${title}` : 'unknown';
    const li = document.createElement('li');
    li.setAttribute('data-component', comp);
    li.setAttribute('data-groupUuid', doc.group || '');

    const a = document.createElement('a');
    a.href = '#' + finalId;
    a.setAttribute('data-component', comp);
    a.textContent = finalId;

    li.appendChild(a);
    listEl.appendChild(li);
  }

  const compSelect = document.getElementById('component-filter-select');
  if (compSelect) {
    for (let i = compSelect.options.length - 1; i >= 1; i--) {
      compSelect.remove(i);
    }
    [...compSet].sort().forEach(c => {
      const opt = document.createElement('option');
      opt.value = c;
      opt.textContent = c;
      compSelect.appendChild(opt);
    });
  }

  const groupSelect = document.getElementById('group-filter-select');
  if (groupSelect) {
    for (let i = groupSelect.options.length - 1; i >= 1; i--) {
      groupSelect.remove(i);
    }
    [...groupSet].sort().forEach(g => {
      const opt = document.createElement('option');
      opt.value = g;
      opt.textContent = g;
      groupSelect.appendChild(opt);
    });
  }
}

/**
 * NEW: buildSectionHtml(section)
 */
function buildSectionHtml(section) {
  if (!section || !section.type) return '';
  switch (section.type) {
    case 'heading':
      if (!section.level || !section.text) return '';
      return `<h${section.level} class="${section.level > 2 ? 'section-heading' : 'article-title'}">${escapeHTML(section.text)}</h${section.level}>`;
    case 'paragraph':
      return `<p>${escapeHTML(section.text || '')}</p>`;
    case 'html':
      return section.html || '';
    case 'list':
      if (!Array.isArray(section.items)) return '';
      return `<ul>${section.items.map(item => `<li>${escapeHTML(item)}</li>`).join('')}</ul>`;
    case 'codeBlock':
      return `<div class="code-block">${escapeHTML(section.code || '')}</div>`;
    case 'image':
      return `<img src="${escapeAttr(section.src)}" alt="${escapeAttr(section.alt || '')}" style="max-width:100%;display:block;margin:1em auto;" />`;
    default:
      return '';
  }
}

/**
 * buildDocBlockHtml(doc):
 * For articles, renders all sections.
 * For function docs, works as before.
 */
function buildDocBlockHtml(doc) {
  const comp = doc.dataComponent || '';
  const title = doc.heading?.title || '(No title)';
  const finalId = comp && title ? `${comp}:${title}` : 'unknown';

  const badges = Array.isArray(doc.heading?.badges) ? doc.heading.badges : [];
  const badgeHtml = badges.map(b => `<span class="category-badge">${escapeHTML(b)}</span>`).join(' ');
  const compBadge = comp ? `<span class="component-badge">:${escapeHTML(comp)}</span>` : '';
  const headingHtml = `<h1>${escapeHTML(title)} ${compBadge} ${badgeHtml}</h1>`;

  let synopsisHtml = '';
  if (doc.synopsis && doc.synopsis.trim()) {
    synopsisHtml = `
<div class="description-block bg-orange">
  ${escapeHTML(doc.synopsis)}
</div>`;
  }

  let mainHtml = '';
  // If article (has sections), render all
  if (doc.sections && Array.isArray(doc.sections)) {
    mainHtml = doc.sections.map(buildSectionHtml).join('');
  } else if (doc.htmlBody && doc.htmlBody.trim()) {
    mainHtml = doc.htmlBody;
  } else if (Array.isArray(doc.codeBlocks) && doc.codeBlocks.length > 0) {
    let blocks = '';
    doc.codeBlocks.forEach(block => {
      const replaced = block.replace(/\\n/g, '\n');
      blocks += `<div class="code-block">${escapeHTML(replaced)}</div>`;
    });
    mainHtml = blocks;
  }

  let notesHtml = '';
  if (Array.isArray(doc.notes) && doc.notes.length > 0) {
    const lines = doc.notes.map(n => `<p>${escapeHTML(n)}</p>`).join('');
    notesHtml = `<div class="notes-block">${lines}</div>`;
  }

  const docStr = JSON.stringify(doc);
  const dataAttr = ` data-doc='${escapeAttr(docStr)}'`;
  const jsonButton = `<button type="button" class="json-button">JSON</button>`;

  const groupVal = doc.group || '';

  return `
<div class="doc-block"
     id="${escapeHTML(finalId)}"
     data-component="${escapeHTML(comp)}"
     data-groupUuid="${escapeHTML(groupVal)}"
     ${dataAttr}>
  ${headingHtml}
  ${synopsisHtml}
  ${mainHtml}
  ${notesHtml}
  ${jsonButton}
</div>
`;
}

/* ...the rest of docs-dynamic.js is unchanged... */
function enableEditabilityAllDocs() { /* ... unchanged ... */ }
function attachJsonButtons() { /* ... unchanged ... */ }
function setupFiltersAndScrolling() { /* ... unchanged ... */ }
function applyFilters() { /* ... unchanged ... */ }
function scrollMainContentTo(idVal) { /* ... unchanged ... */ }
function highlightAllCodeBlocks() { /* ... unchanged ... */ }
function highlightMuMuSyntax(code) { /* ... unchanged ... */ }
function escapeHTML(str) { /* ... unchanged ... */ }
function escapeAttr(str) { /* ... unchanged ... */ }
function getCaretCharacterOffsetWithin(el) { /* ... unchanged ... */ }
function setCaretCharacterOffsetWithin(el, offset) { /* ... unchanged ... */ }
function isOrContainsNode(ancestor, descendant) { /* ... unchanged ... */ }