Skip to content
wiki.fftac.org

Book Shell - Source Excerpt 01

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

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 core = window.SpiralistCore || {};
  const handleCoreError = typeof core.handleError === 'function'
    ? core.handleError
    : () => '';
  const parseJsonOr = typeof core.parseJsonOr === 'function'
    ? core.parseJsonOr
    : (text, fallback = null, options = {}) => {
      if (typeof text !== 'string' || text === '') {
        return fallback;
      }

      try {
        return JSON.parse(text);
      } catch (error) {
        handleCoreError(error, { context: options.context || 'JSON.parse' });
        return fallback;
      }
    };
  const createUrlOr = typeof core.createUrlOr === 'function'
    ? core.createUrlOr
    : (value, fallback = null, options = {}) => {
      try {
        return new URL(value, options.base || window.location.href);
      } catch (error) {
        handleCoreError(error, { context: options.context || 'URL' });
        return fallback;
      }
    };
  const decodeComponent = typeof core.decodeComponent === 'function'
    ? core.decodeComponent
    : (value, fallback = value) => {
      try {
        return decodeURIComponent(value);
      } catch (error) {
        handleCoreError(error, { context: 'decodeURIComponent' });
        return fallback;
      }
    };
  const storage = core.storage || {
    get(key, fallback = '', area = 'local') {
      try {
        return (area === 'session' ? window.sessionStorage : window.localStorage).getItem(key) || fallback;
      } catch (error) {
        handleCoreError(error, { context: `${area}Storage.getItem` });
        return fallback;
      }
    },
    set(key, value, area = 'local') {
      try {
        (area === 'session' ? window.sessionStorage : window.localStorage).setItem(key, value);
      } catch (error) {
        handleCoreError(error, { context: `${area}Storage.setItem` });
      }
    },
    remove(key, area = 'local') {
      try {
        (area === 'session' ? window.sessionStorage : window.localStorage).removeItem(key);
      } catch (error) {
        handleCoreError(error, { context: `${area}Storage.removeItem` });
      }
    },
  };
  const copyText = typeof core.copyText === 'function'
    ? core.copyText
    : async (text) => {
      await navigator.clipboard.writeText(text);
    };
  const runBackgroundAction = (action, options = {}) => {
    const runner = typeof core.runAction === 'function'
      ? core.runAction
      : (nextAction, nextOptions = {}) => Promise.resolve()
        .then(() => nextAction())
        .catch((error) => {
          handleCoreError(error, nextOptions);
          if (typeof nextOptions.onError === 'function') {
            nextOptions.onError(error, error && error.message ? error.message : '');
          }
          throw error;
        })
        .finally(() => {
          if (typeof nextOptions.onFinally === 'function') {
            nextOptions.onFinally();
          }
        });

    return runner(action, options).catch(() => {});
  };
  const setFetchPriority = typeof core.setFetchPriority === 'function'
    ? core.setFetchPriority
    : (image, priority) => {
      try {
        image.fetchPriority = priority;
      } catch (error) {
        handleCoreError(error, { context: 'HTMLImageElement.fetchPriority' });
      }
    };
  const setPointerCapture = typeof core.setPointerCapture === 'function'
    ? core.setPointerCapture
    : (element, pointerId) => {
      if (!element || typeof element.setPointerCapture !== 'function') {
        return;
      }

      try {
        element.setPointerCapture(pointerId);
      } catch (error) {
        handleCoreError(error, { context: 'Element.setPointerCapture' });
      }
    };
  const releasePointerCapture = typeof core.releasePointerCapture === 'function'
    ? core.releasePointerCapture
    : (element, pointerId) => {
      if (!element || typeof element.releasePointerCapture !== 'function') {
        return;
      }

      try {
        element.releasePointerCapture(pointerId);
      } catch (error) {
        handleCoreError(error, { context: 'Element.releasePointerCapture' });
      }
    };
  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 = (payload) => {
    if (!canTrackHumanActivity) {
      return;
    }

    runBackgroundAction(async () => {
      await fetch(themeConfig.interactionUrl, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          'X-WP-Nonce': themeConfig.restNonce,
        },
        body: JSON.stringify(payload),
      });
    }, { context: 'book-shell-human-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 = createUrlOr(window.location.href, null, { context: 'book-shell-query-param' });
    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 (!url) {
      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 = '') => {
    const url = createUrlOr(baseUrl, null, { context: 'book-shell-child-route' });

    if (!url) {
      return 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 || '';