Source: permalink.js

/** @module permalink */

import * as consts from './consts.js';
import * as graph_components from './graph_components.js';

/**
 * makes a character url compliant and makes sure it does not clash with the delimiters
 * @param {string} c - the character to be made url compliant
 * @returns {string} the url compliant character
 */
function char_url_compliance(c) {
  if (c === consts.FIELD_DELIM  ||
      c === consts.VERTEX_DELIM ||
      c === consts.EDGE_DELIM) {
    return escape(c);  // these absolutely need to encoded to avoid clashes
  } else if (!consts.LEGAL_CHARS.includes(c)) {
    return encodeURI(c);  // encode iff not legal in url
  } else {
    return c;  // otherwise, return the character as is
  }
}

/**
 * turn a field into a string
 * @param {string | float} field
 * @returns {string} the string repr of the field
 */
function to_string_field(field) {
  // backward compatibility for users who have saved graphs on localstore without mealy or moore outputs
  if (field === undefined) {
    return consts.EMPTY_SYMBOL;
  }
  const field_str = field.toString();
  return Array.from(field_str).reduce((acc, c) => acc + char_url_compliance(c), '');
}

/**
 * basically the 'toString' method for the graph
 * @param {string} type - the type of the graph in {'NFA', 'PDA', 'Turing'}
 * @param {Object} graph - check 'graph_components.js' for the structure of the graph
 * @returns {string} the graph as a string
 */
export function serialize(type, graph) {
  let result = type;

  for (const vertex of Object.values(graph)) {
    result += to_string_field(vertex.name)          + consts.FIELD_DELIM;
    result += to_string_field(Math.round(vertex.y)) + consts.FIELD_DELIM;
    result += to_string_field(Math.round(vertex.x)) + consts.FIELD_DELIM;
    result += to_string_field(Math.round(vertex.r)) + consts.FIELD_DELIM;
    result += to_string_field(vertex.moore_output)  + consts.FIELD_DELIM;
    const composite_bit = (vertex.is_start ? 1 : 0) + (vertex.is_final ? 2 : 0);
    result += to_string_field(composite_bit)        + consts.VERTEX_DELIM;
  }

  const vertex_name_to_id = {};
  for (const [id, vertex] of Object.values(graph).entries()) {
    vertex_name_to_id[vertex.name] = id;
  }

  for (const vertex of Object.values(graph)) {
    for (const edge of vertex.out) {
      result += to_string_field(vertex_name_to_id[edge.from]) + consts.FIELD_DELIM;
      result += to_string_field(vertex_name_to_id[edge.to])   + consts.FIELD_DELIM;
      result += to_string_field(edge.transition)              +
                to_string_field(edge.pop_symbol)              +
                to_string_field(edge.push_symbol)             +
                to_string_field(edge.mealy_output)            +
                to_string_field(edge.move)                    + consts.FIELD_DELIM;
      result += to_string_field(Math.round(edge.a1*10))       + consts.FIELD_DELIM;
      result += to_string_field(Math.round(edge.a2*10))       + consts.FIELD_DELIM;
      result += to_string_field(Math.round(edge.angle1*10))   + consts.FIELD_DELIM;
      result += to_string_field(Math.round(edge.angle2*10))   + consts.EDGE_DELIM;
    }
  }

  return result;
}

/**
 * find the type of the graph and return it along with the rest of the unparsed graph string
 * @param {string} graph_str - the string representation of the graph
 * @returns {Array<string>} type of the graph in {'NFA', 'PDA', 'Turing'} and the rest of the unparsed graph string
 */
function parse_type(graph_str) {
  for (const type of Object.values(consts.MACHINE_TYPES)) {
    if (graph_str.startsWith(type)) {
      return [type, graph_str.slice(type.length)];
    }
  }
  throw new Error('Invalid graph type');
}

/**
 * basically the 'fromString' method for the graph
 * @param {string} graph_str - the string representation of the graph
 * @returns {Array<string|Object>} type of the graph in {'NFA', 'PDA', 'Turing'} and the graph
 */
export function deserialize(graph_str) {
  const graph = {};
  const [type, typeless_graph_str] = parse_type(graph_str);

  if (typeless_graph_str.length === 0) {  // the degenerate case when there is no vertex
    return [type, graph];
  }

  const split_by_vertex = typeless_graph_str.split(consts.VERTEX_DELIM);
  const vertices = split_by_vertex.slice(0, -1);
  const rest = split_by_vertex[split_by_vertex.length - 1];

  const vertex_id_to_name = [];

  for (const vertex of vertices) {
    const fields = vertex.split(consts.FIELD_DELIM).map(decodeURIComponent);
    const name          = fields[0];
    const y             = parseFloat(fields[1]);
    const x             = parseFloat(fields[2]);
    const r             = parseFloat(fields[3]);
    // backwards compatibility (in case an older permalink doesn't contain this field)
    const moore_output  = (fields.length === 6) ? fields[4] : consts.DEFAULT_MOORE_OUTPUT;
    const composite_bit = (fields.length === 6) ? parseInt(fields[5]) : parseInt(fields[4]);
    graph[name] = graph_components.make_vertex(name, x, y, r, composite_bit&1, composite_bit&2, undefined, moore_output);
    vertex_id_to_name.push(name);  // construct the mapping from id to name
  }

  if (rest.length === 0) {  // the degenerate case when there is no edge
    return [type, graph];
  }

  const edges = rest.split(consts.EDGE_DELIM).slice(0, -1);
  for (const edge of edges) {
    const fields = edge.split(consts.FIELD_DELIM).map(decodeURIComponent);
    const from          = vertex_id_to_name[parseInt(fields[0])];
    const to            = vertex_id_to_name[parseInt(fields[1])];
    const composite_str = fields[2];
    const transition    = composite_str.charAt(0);
    const pop_symbol    = composite_str.charAt(1);
    const push_symbol   = composite_str.charAt(2);
    // backwards compatibility (in case an older permalink doesn't contain this field)
    const mealy_output  = (composite_str.length === 5) ? composite_str.charAt(3) : consts.DEFAULT_MEALY_OUTPUT;
    const move          = (composite_str.length === 5) ? composite_str.charAt(4) : composite_str.charAt(3);
    const a1            = parseFloat(fields[3])/10.0;
    const a2            = parseFloat(fields[4])/10.0;
    const angle1        = parseFloat(fields[5])/10.0;
    const angle2        = parseFloat(fields[6])/10.0;
    graph[from].out.push(graph_components.make_edge(
      from, to, transition, a1, a2, angle1, angle2, pop_symbol, push_symbol, move, mealy_output
    ));
  }

  return [type, graph];
}