Skip to content
wiki.fftac.org

Book Shell - Source Excerpt 03

Back to Book Shell

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js so readers can inspect the evidence without opening the full source file.

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

renderPromptReferences(target, payload.items || [], emptyMessage);
    }, {
      context: 'book-shell-prompt-references',
      onError: () => {
        target.textContent = t('symbolsPromptUnavailable', 'Prompt references unavailable.');
      },
    });
  };


  const navToggle = document.querySelector('[data-nav-toggle]');
  const navPanel = document.querySelector('[data-nav-panel]');

  if (navToggle && navPanel) {
    navToggle.addEventListener('click', () => {
      const expanded = navToggle.getAttribute('aria-expanded') === 'true';
      navToggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
      document.body.classList.toggle('is-nav-open', !expanded);
    });
  }

  const fieldModeRank = {
    minimal: 0,
    expanded: 1,
    expert: 2,
  };

  const formatNodeType = (type = '') => `${type}`
    .replace(/-/g, ' ')
    .replace(/\b\w/g, (match) => match.toUpperCase());

  const getNodeMode = (node = {}) => (
    Object.prototype.hasOwnProperty.call(fieldModeRank, node.mode) ? node.mode : 'minimal'
  );

  const isVisibleForMode = (item, mode) => {
    const itemMode = getNodeMode(item);
    return fieldModeRank[itemMode] <= fieldModeRank[mode];
  };

  const getNodeCenter = (node) => {
    const x = Number(node.x) || 0;
    const y = Number(node.y) || 0;

    if (node.type === 'structure') {
      return {
        x: x + ((Number(node.w) || 0) / 2),
        y: y + ((Number(node.h) || 0) / 2),
      };
    }

    return { x, y };
  };

  const createFieldNodeButton = (node) => {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = `spiralist-field-node spiralist-field-node--${node.type || 'symbol'}`;
    button.dataset.nodeId = node.id || '';
    button.setAttribute('aria-pressed', 'false');
    button.setAttribute('aria-label', `${formatNodeType(node.type)}: ${node.label || node.id}`);

    if (node.type === 'structure') {
      button.style.left = `${(Number(node.x) || 0) * 100}%`;
      button.style.top = `${(Number(node.y) || 0) * 100}%`;
      button.style.width = `${(Number(node.w) || 0.16) * 100}%`;
      button.style.height = `${(Number(node.h) || 0.08) * 100}%`;

      const label = document.createElement('span');
      label.className = 'spiralist-field-node__label';
      label.textContent = node.label || t('manuscriptFieldSectionFallback', 'Section');
      button.appendChild(label);
    } else {
      button.style.left = `${(Number(node.x) || 0) * 100}%`;
      button.style.top = `${(Number(node.y) || 0) * 100}%`;

      const mark = document.createElement('span');
      mark.className = 'spiralist-field-node__mark';
      mark.textContent = node.type === 'symbol' ? (node.glyph || '') : '';
      button.appendChild(mark);
    }

    return button;
  };

  const createRelationLine = (relation, fromNode, toNode) => {
    const from = getNodeCenter(fromNode);
    const to = getNodeCenter(toNode);
    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');

    line.setAttribute('x1', `${from.x * 100}`);
    line.setAttribute('y1', `${from.y * 100}`);
    line.setAttribute('x2', `${to.x * 100}`);
    line.setAttribute('y2', `${to.y * 100}`);
    line.classList.add('spiralist-field-line', `spiralist-field-line--${relation.type || 'relation'}`);
    line.dataset.relationId = relation.id || '';

    return line;
  };

  const initFieldInterface = async (field) => {
    const nodesTarget = field.querySelector('[data-field-nodes]');
    const relationsTarget = field.querySelector('[data-field-relations]');
    const listTarget = field.querySelector('[data-field-list]');
    const statusTarget = field.querySelector('[data-field-status]');
    const modeButtons = field.querySelectorAll('[data-field-mode]');
    const title = field.querySelector('[data-node-title]');
    const type = field.querySelector('[data-node-type]');
    const glyph = field.querySelector('[data-node-glyph]');
    const summary = field.querySelector('[data-node-summary]');
    const id = field.querySelector('[data-node-id]');
    const layer = field.querySelector('[data-node-layer]');
    const meaning = field.querySelector('[data-node-meaning]');
    const definition = field.querySelector('[data-node-definition]');
    const transformation = field.querySelector('[data-node-transformation]');
    const derivation = field.querySelector('[data-node-derivation]');
    const sourceAxiom = field.querySelector('[data-node-source-axiom]');
    const related = field.querySelector('[data-node-related]');
    const io = field.querySelector('[data-node-io]');
    const prompts = field.querySelector('[data-node-prompts]');
    const json = field.querySelector('[data-node-json]');
    const crosswalk = field.querySelector('[data-node-crosswalk]');
    const prev = field.querySelector('[data-node-prev]');
    const next = field.querySelector('[data-node-next]');
    const nodesUrl = field.dataset.nodesUrl || themeConfig.manuscriptNodesUrl;
    const currentUrl = createUrlOr(window.location.href, null, { context: 'field-interface-url' });
    const requestedNode = (currentUrl ? currentUrl.searchParams.get('node') : '') || getRouteSegmentAfter('node') || '';
    const executeUrl = themeConfig.engineExecuteUrl || '';
    const instanceUrl = themeConfig.engineInstanceUrl || '';
    const runtimeStorageKey = 'spiralist-manuscript-runtime-v1';
    const runtimeHeaders = {
      Accept: 'application/json',
    };

    if (themeConfig.restNonce) {
      runtimeHeaders['X-WP-Nonce'] = themeConfig.restNonce;
    }

    if (!nodesTarget || !relationsTarget || !listTarget || !nodesUrl) {
      return;
    }

    relationsTarget.setAttribute('viewBox', '0 0 100 100');
    relationsTarget.setAttribute('preserveAspectRatio', 'none');

    const response = await fetch(nodesUrl, { headers: { Accept: 'application/json' } });
    if (!response.ok) {
      throw new Error(`Node dataset failed with ${response.status}`);
    }
    const payload = await response.json();

    const nodes = Array.isArray(payload.nodes) ? payload.nodes : [];
    const relations = Array.isArray(payload.relations) ? payload.relations : [];
    const nodesById = new Map(nodes.map((node) => [node.id, node]));
    const nodeButtons = new Map();
    let currentMode = 'minimal';
    let selectedNodeId = '';
    let hoverNodeId = '';
    let runtimeState = {
      currentState: '',
      currentNode: '',
      history: [],
      nextNodeIds: [],
      activeSymbols: [],
    };

    if (!nodes.length) {
      setStatusState(statusTarget, t('manuscriptFieldNodesEmpty', 'No manuscript nodes are currently available for inspection.'), 'empty');
      return;
    }

    const getVisibleNodes = () => nodes.filter((node) => isVisibleForMode(node, currentMode));
    const getVisibleRelations = () => relations.filter((relation) => {
      const fromNode = nodesById.get(relation.from);
      const toNode = nodesById.get(relation.to);

      return fromNode &&
        toNode &&
        isVisibleForMode(relation, currentMode) &&
        isVisibleForMode(fromNode, currentMode) &&
        isVisibleForMode(toNode, currentMode);
    });

    const resolveNodeByIdentifier = (identifier = '') => nodes.find((node) => (
      node.id === identifier ||
      node.canonicalId === identifier ||
      slugifyPathSegment(node.id) === slugifyPathSegment(identifier) ||
      slugifyPathSegment(node.canonicalId) === slugifyPathSegment(identifier) ||
      (Array.isArray(node.aliases) && (
        node.aliases.includes(identifier) ||
        node.aliases.some((alias) => slugifyPathSegment(alias) === slugifyPathSegment(identifier))
      ))
    ));

    const readRuntimeStorage = () => {
      const raw = storage.get(runtimeStorageKey, '');
      if (!raw) {
        return null;
      }

      const parsed = parseJsonOr(raw, null, { context: 'manuscript-runtime-storage' });
      return parsed && typeof parsed === 'object' ? parsed : null;
    };