Skip to content
wiki.fftac.org

Book Shell - Part 01

Back to Book Shell

**Source path:** Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js

document.addEventListener('DOMContentLoaded', () => {
  const themeConfig = typeof spiralistTheme === 'object' && spiralistTheme !== null
    ? spiralistTheme
    : {};
  const uiText = typeof themeConfig.uiText === 'object' && themeConfig.uiText !== null
    ? themeConfig.uiText
    : {};
  const t = (key, fallback = '') => {
    const value = uiText[key];
    return typeof value === 'string' && value !== '' ? value : fallback;
  };
  const formatText = (key, fallback = '', replacements = {}) => {
    let value = t(key, fallback);

    Object.entries(replacements).forEach(([name, replacement]) => {
      value = value.split(`{${name}}`).join(`${replacement}`);
    });

    return value;
  };
  const canTrackHumanActivity = Boolean(
    themeConfig.isUserLoggedIn &&
    themeConfig.interactionUrl &&
    themeConfig.restNonce
  );
  const recordHumanInteraction = async (payload) => {
    if (!canTrackHumanActivity) {
      return;
    }

    try {
      await fetch(themeConfig.interactionUrl, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          'X-WP-Nonce': themeConfig.restNonce,
        },
        body: JSON.stringify(payload),
      });
    } catch (error) {
      // Activity tracking should never interrupt the visible symbol interaction.
    }
  };


  const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
  const setStatusState = (target, message, state = 'idle') => {
    if (!target) {
      return;
    }

    target.textContent = message;
    target.dataset.state = state;
  };

  const replaceQueryParam = (key, value) => {
    const url = new URL(window.location.href);
    const cleanValue = `${value ?? ''}`.trim();

    if (key === 'symbol' && themeConfig.symbolsPageUrl) {
      window.history.replaceState(window.history.state, '', buildChildRouteUrl(themeConfig.symbolsPageUrl, cleanValue ? [cleanValue] : [], window.location.hash));
      return;
    }

    if (key === 'node' && themeConfig.manuscriptPageUrl) {
      window.history.replaceState(window.history.state, '', buildChildRouteUrl(themeConfig.manuscriptPageUrl, cleanValue ? ['node', cleanValue] : [], window.location.hash));
      return;
    }

    if (cleanValue === '') {
      url.searchParams.delete(key);
    } else {
      url.searchParams.set(key, cleanValue);
    }

    window.history.replaceState(window.history.state, '', url);
  };

  const slugifyPathSegment = (value = '') => {
    const normalized = `${value ?? ''}`
      .trim()
      .normalize('NFKD')
      .replace(/[\u0300-\u036f]/g, '')
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '');

    return normalized || 'item';
  };

  const buildChildRouteUrl = (baseUrl, segments = [], hash = '') => {
    try {
      const url = new URL(baseUrl, window.location.href);
      const basePath = url.pathname.replace(/\/+$/, '');
      const childPath = segments
        .map((segment) => slugifyPathSegment(segment))
        .filter(Boolean)
        .map(encodeURIComponent)
        .join('/');

      url.pathname = `${basePath || ''}/${childPath ? `${childPath}/` : ''}`.replace(/\/{2,}/g, '/');
      url.search = '';
      url.hash = hash || '';

      return url.toString();
    } catch (error) {
      return baseUrl || window.location.href;
    }
  };

  const getRouteSegmentAfter = (marker) => {
    const wanted = slugifyPathSegment(marker);
    const segments = window.location.pathname
      .split('/')
      .map((segment) => {
        try {
          return decodeURIComponent(segment);
        } catch (error) {
          return segment;
        }
      })
      .filter(Boolean);

    for (let index = 0; index < segments.length - 1; index += 1) {
      if (slugifyPathSegment(segments[index]) === wanted) {
        return segments[index + 1] || '';
      }
    }

    return '';
  };

  const buildPromptSearchUrl = (query) => {
    if (!themeConfig.promptsPageUrl || !query) {
      return themeConfig.promptsPageUrl || '#';
    }

    return buildChildRouteUrl(themeConfig.promptsPageUrl, ['search', query]);
  };

  const parseIdentifierText = (value = '') => `${value}`
    .split(/[\n,]+/)
    .map((item) => item.trim())
    .filter(Boolean);
  const buildPromptReferenceTerms = (...sources) => {
    const seen = new Set();
    const values = [];
    const append = (candidate) => {
      if (Array.isArray(candidate)) {
        candidate.forEach(append);
        return;
      }

      const terms = parseIdentifierText(candidate || '');
      if (!terms.length) {
        const fallback = `${candidate ?? ''}`.trim();
        if (fallback !== '') {
          terms.push(fallback);
        }
      }

      terms.forEach((term) => {
        const normalized = `${term}`.trim();
        const key = normalized.toLowerCase();
        if (normalized === '' || seen.has(key)) {
          return;
        }

        seen.add(key);
        values.push(normalized);
      });
    };

    sources.forEach(append);
    return values;
  };
  const getPromptSearchQuery = (record = {}) =>
    buildPromptReferenceTerms(record.meaning, record.label, record.canonicalId, record.aliases || [])[0] || '';
  const buildPromptFilters = (record = {}) => {
    const type = `${record.type || ''}`.trim().toLowerCase();
    const axiomTerms = buildPromptReferenceTerms(
      record.axiomIds || [],
      Array.isArray(record.axioms) ? record.axioms.map((entry) => [entry.id || '', entry.label || '']) : [],
      type === 'axiom' ? [record.sourceAxiomId || '', record.canonicalId || '', record.label || '', record.meaning || ''] : []
    );

    return {
      relatedSymbol: type === 'symbol'
        ? buildPromptReferenceTerms(record.canonicalId || '', record.label || '', record.meaning || '', record.aliases || [])
        : [],
      relatedAxiom: axiomTerms,
      relatedTransformation: type === 'transformation'
        ? buildPromptReferenceTerms(record.canonicalId || '', record.label || '', record.meaning || '', record.aliases || [])
        : [],
      limit: 4,
    };
  };

  const promptReferenceCache = new Map();

  const renderPromptReferences = (target, items = [], emptyMessage = t('symbolsPromptEmpty', 'No related prompts yet.')) => {
    if (!target) {
      return;
    }

    target.replaceChildren();

    if (!Array.isArray(items) || !items.length) {
      target.textContent = emptyMessage;
      return;
    }

    const list = document.createElement('div');
    list.className = 'spiralist-reference-list';

    items.forEach((item) => {
      const card = document.createElement(item.url ? 'a' : 'div');
      card.className = 'spiralist-reference-card';

      if (item.url) {
        card.href = item.url;
      }

      const title = document.createElement('strong');
      title.textContent = item.title || item.id || t('symbolsPromptFallback', 'Prompt');

      const meta = document.createElement('small');
      meta.textContent = [item.systemLabel || item.system, item.roleLabel || item.role, item.version]
        .filter(Boolean)
        .join(' / ');

      const summary = document.createElement('span');
      summary.textContent = item.purpose || t('symbolsPromptReferenceFallback', 'Structured prompt reference.');

      card.append(title, meta, summary);
      list.appendChild(card);
    });

    target.appendChild(list);
  };

  const loadPromptReferences = async (target, filters = {}, emptyMessage = t('symbolsPromptEmpty', 'No related prompts yet.')) => {
    if (!target) {
      return;
    }

    if (!themeConfig.promptsUrl) {
      target.textContent = emptyMessage;
      return;
    }

    const params = new URLSearchParams();

    if (filters.system) {
      params.set('system', `${filters.system}`.trim());
    }
    if (filters.role) {
      params.set('role', `${filters.role}`.trim());
    }

    const relatedSymbol = Array.isArray(filters.relatedSymbol)
      ? filters.relatedSymbol
      : parseIdentifierText(filters.relatedSymbol || '');
    const relatedAxiom = Array.isArray(filters.relatedAxiom)
      ? filters.relatedAxiom
      : parseIdentifierText(filters.relatedAxiom || '');
    const relatedTransformation = Array.isArray(filters.relatedTransformation)
      ? filters.relatedTransformation
      : parseIdentifierText(filters.relatedTransformation || '');

    if (relatedSymbol.length) {
      params.set('related_symbol', relatedSymbol.join(','));
    }
    if (relatedAxiom.length) {
      params.set('related_axiom', relatedAxiom.join(','));
    }
    if (relatedTransformation.length) {
      params.set('related_transformation', relatedTransformation.join(','));
    }