UNPKG

@sutton-signwriting/core

Version:

a javascript package for node and browsers that supports general processing of the Sutton SignWriting script

3,963 lines • 132 kB
/**
* Sutton SignWriting Core Module v2.0.0 (https://github.com/sutton-signwriting/core)
* Author: Steve Slevinski  (https://SteveSlevinski.me)
* core.js is released under the MIT License.
*/

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.ssw = global.ssw || {}, global.ssw.core = {})));
})(this, (function (exports) { 'use strict';

  /**
   * Object of regular expressions for FSW strings
   * 
   * @alias fsw.re
   * @property {string} null - the null symbol
   * @property {string} symbol - a symbol
   * @property {string} nullorsymbol - null or a symbol
   * @property {string} sort - the sorting marker
   * @property {string} prefix - a sorting marker followed by one or more symbols with nulls
   * @property {string} box - a signbox marker
   * @property {string} coord - a coordinate
   * @property {string} spatial - a symbol followed by a coordinate
   * @property {string} signbox - a signbox marker, max coordinate and zero or more spatial symbols
   * @property {string} sign - an optional prefix followed by a signbox
   * @property {string} sortable - a mandatory prefix followed by a signbox
   */
  let re$4 = {
    'null': 'S00000',
    'symbol': 'S[123][0-9a-f]{2}[0-5][0-9a-f]',
    'coord': '[0-9]{3}x[0-9]{3}',
    'sort': 'A',
    'box': '[BLMR]'
  };
  re$4.nullorsymbol = `(?:${re$4.null}|${re$4.symbol})`;
  re$4.prefix = `(?:${re$4.sort}${re$4.nullorsymbol}+)`;
  re$4.spatial = `${re$4.symbol}${re$4.coord}`;
  re$4.signbox = `${re$4.box}${re$4.coord}(?:${re$4.spatial})*`;
  re$4.sign = `${re$4.prefix}?${re$4.signbox}`;
  re$4.sortable = `${re$4.prefix}${re$4.signbox}`;

  /**
   * Object of regular expressions for style strings
   * 
   * @alias style.re
   * @type {object}
   * @property {string} colorize - regular expression for colorize section
   * @property {string} colorhex - regular expression for color hex values with 3 or 6 characters
   * @property {string} colorname - regular expression for css color name
   * @property {string} padding - regular expression for padding section
   * @property {string} zoom - regular expression for zoom section
   * @property {string} classbase - regular expression for class name definition
   * @property {string} id - regular expression for id definition
   * @property {string} colorbase - regular expression for color hex or color name
   * @property {string} color - regular expression for single color entry
   * @property {string} colors - regular expression for double color entry
   * @property {string} background - regular expression for background section
   * @property {string} detail - regular expression for color details for line and optional fill
   * @property {string} detailsym - regular expression for color details for individual symbols
   * @property {string} classes - regular expression for one or more class names
   * @property {string} full - full regular expression for style string
   */
  let re$3 = {
    'colorize': 'C',
    'colorhex': '(?:[0-9a-fA-F]{3}){1,2}',
    'colorname': '[a-zA-Z]+',
    'padding': 'P[0-9]{2}',
    'zoom': 'Z(?:[0-9]+(?:\\.[0-9]+)?|x)',
    'classbase': '-?[_a-zA-Z][_a-zA-Z0-9-]{0,100}',
    'id': '[a-zA-Z][_a-zA-Z0-9-]{0,100}'
  };
  re$3.colorbase = `(?:${re$3.colorhex}|${re$3.colorname})`;
  re$3.color = `_${re$3.colorbase}_`;
  re$3.colors = `_${re$3.colorbase}(?:,${re$3.colorbase})?_`;
  re$3.background = `G${re$3.color}`;
  re$3.detail = `D${re$3.colors}`;
  re$3.detailsym = `D[0-9]{2}${re$3.colors}`;
  re$3.classes = `${re$3.classbase}(?: ${re$3.classbase})*`;
  re$3.full = `-(${re$3.colorize})?(${re$3.padding})?(${re$3.background})?(${re$3.detail})?(${re$3.zoom})?(?:-((?:${re$3.detailsym})*))?(?:-(${re$3.classes})?!(?:(${re$3.id})!)?)?`;

  const prefixColor = color => {
    const regex = new RegExp(`^${re$3.colorhex}$`);
    return (regex.test(color) ? '#' : '') + color;
  };
  const definedProps = obj => Object.fromEntries(Object.entries(obj).filter(([k, v]) => v !== undefined));

  /**
   * Function to parse style string to object
   * @function style.parse
   * @param {string} styleString - a style string
   * @returns {StyleObject} elements of style string
   * @example
   * style.parse('-CP10G_blue_D_red,Cyan_')
   * 
   * return {
   *  'colorize': true,
   *  'padding': 10,
   *  'background': 'blue',
   *  'detail': ['red', 'Cyan']
   * }
   */
  const parse$4 = styleString => {
    const regex = `^${re$3.full}`;
    const m = (typeof styleString === 'string' ? styleString.match(new RegExp(regex)) : []) || [];
    return definedProps({
      'colorize': !m[1] ? undefined : !!m[1],
      'padding': !m[2] ? undefined : parseInt(m[2].slice(1)),
      'background': !m[3] ? undefined : prefixColor(m[3].slice(2, -1)),
      'detail': !m[4] ? undefined : m[4].slice(2, -1).split(',').map(prefixColor),
      'zoom': !m[5] ? undefined : m[5] === 'Zx' ? 'x' : parseFloat(m[5].slice(1)),
      'detailsym': !m[6] ? undefined : m[6].match(new RegExp(re$3.detailsym, 'g')).map(val => {
        const parts = val.split('_');
        const detail = parts[1].split(',').map(prefixColor);
        return {
          'index': parseInt(parts[0].slice(1)),
          'detail': detail
        };
      }),
      'classes': !m[7] ? undefined : m[7],
      'id': !m[8] ? undefined : m[8]
    });
  };

  /**
   * Function to compose style string from object
   * @function style.compose
   * @param {StyleObject} styleObject - an object of style options
   * @returns {string} style string 
   * @example 
   * style.compose({
   *  'colorize': true,
   *  'padding': 10,
   *  'background': 'blue',
   *  'detail': ['red', 'Cyan'],
   *  'zoom': 1.1,
   *  'detailsym': [
   *    {
   *      'index': 1,
   *      'detail': ['#ff00ff']
   *    },
   *    {
   *      'index': 2,
   *      'detail': ['yellow', 'green']
   *    }
   *  ],
   *  'classes': 'primary blinking',
   *  'id': 'cursor'
   * })
   *
   * return '-CP10G_blue_D_red,Cyan_Z1.1-D01_ff00ff_D02_yellow,green_-primary blinking!cursor!'
   */
  const compose$4 = styleObject => {
    if (typeof styleObject !== 'object' || styleObject === null) return undefined;

    // three sections
    let style1 = '-';
    style1 += !styleObject.colorize ? '' : 'C';
    const padding = parseInt(styleObject.padding);
    style1 += !padding || padding <= 0 || padding > 99 ? '' : 'P' + (padding > 9 ? padding : '0' + padding);
    const background = !styleObject.background || !(typeof styleObject.background === 'string') ? undefined : styleObject.background.match(re$3.colorbase)[0];
    style1 += !background ? '' : 'G_' + background + '_';
    const detail1 = !styleObject.detail || !styleObject.detail[0] || !(typeof styleObject.detail[0] === 'string') ? undefined : styleObject.detail[0].match(re$3.colorbase)[0];
    const detail2 = !styleObject.detail || !styleObject.detail[1] || !(typeof styleObject.detail[1] === 'string') ? undefined : styleObject.detail[1].match(re$3.colorbase)[0];
    if (detail1) {
      style1 += 'D_' + detail1;
      if (detail2) {
        style1 += ',' + detail2;
      }
      style1 += '_';
    }
    const zoom = styleObject.zoom === 'x' ? 'x' : parseFloat(styleObject.zoom);
    style1 += !zoom || zoom <= 0 ? '' : 'Z' + zoom;
    let style2 = '';
    const detailsym = !styleObject.detailsym || !Array.isArray(styleObject.detailsym) ? [] : styleObject.detailsym.map(styleObject => {
      const index = parseInt(styleObject.index);
      if (!index || index <= 0 || index > 99) return '';
      let style = 'D' + (index > 9 ? index : '0' + index);
      const detail1 = !styleObject.detail || !styleObject.detail[0] ? undefined : styleObject.detail[0].match(re$3.colorbase)[0];
      const detail2 = !styleObject.detail || !styleObject.detail[1] ? undefined : styleObject.detail[1].match(re$3.colorbase)[0];
      if (detail1) {
        style += '_' + detail1;
        if (detail2) {
          style += ',' + detail2;
        }
        style += '_';
      }
      return style;
    });
    style2 += detailsym.join('');
    let style3 = '';
    const classes = !styleObject.classes || !(typeof styleObject.classes === 'string') ? undefined : styleObject.classes.match(re$3.classes)[0];
    style3 += !classes ? '' : classes;
    const id = !styleObject.id || !(typeof styleObject.id === 'string') ? undefined : styleObject.id.match(re$3.id)[0];
    style3 += classes || id ? '!' : '';
    style3 += !id ? '' : id + '!';
    return style1 + (style2 || style3 ? '-' + style2 : '') + (style3 ? '-' + style3 : '');
  };

  /**
   * Function to merge style objects
   * @function style.merge
   * @param {StyleObject} style1 - a style object
   * @param {StyleObject} style2 - a style object
   * @returns {StyleObject} a style object
   * @example
   * style.merge({'colorize': true},{zoom:2})
   * 
   * return {
   *  'colorize': true,
   *  'zoom': 2
   * }
   */
  const merge = (style1, style2) => {
    if (typeof style1 !== 'object') style1 = {};
    if (typeof style2 !== 'object') style2 = {};
    const zoom1 = 'zoom' in style1 ? style1['zoom'] : 1;
    const zoom2 = 'zoom' in style2 ? style2['zoom'] : 1;
    return {
      ...style1,
      ...style2,
      ...{
        zoom: zoom1 * zoom2
      }
    };
  };

  const rgb2arr = rgb => {
    if (typeof rgb !== 'string') return [0, 0, 0];
    return rgb.replace(/rgba?\((.+?)\)/ig, (_, values) => {
      return values;
    }).split(',').map(Number);
  };
  const arr2hex = arr => {
    return arr.slice(0, 3).map(num => num.toString(16).padStart(2, '0')).join('');
  };

  /**
   * Function to convert rgb color to hex or "transparent" if below tolerance
   * @function style.rgb2hex
   * @param {string} rgb - an rgb color
   * @param {number} [tolerance=0] - max alpha for full transparency
   * @returns {string} a hex color or "transparent"
   * @example
   * style.rgb2hex("rgb(255,255,255)")
   * return "ffffff"
   * 
   * style.rgb2hex("rgba(255,255,255,0.5)",0.5)
   * return "transparent"
   */
  const rgb2hex = (rgb, tolerance = 0) => {
    const arr = rgb2arr(rgb);
    if (arr.length == 4 && arr[3] <= tolerance) {
      return 'transparent';
    } else {
      return arr2hex(arr);
    }
  };

  /**
   * Function to merge color with background based on alpha transparency
   * @function style.rgba2hex
   * @param {string} color - an rgba color
   * @param {string} background - an rgba background color
   * @returns {string} a hex color or "transparent"
   * @example
   * style.rgba2hex("rgba(255,255,255,0.5)","rgb(0,0,0)")
   * 
   * return "7f7f7f"
   */
  const rgba2hex = (color, background) => {
    const bArr = rgb2arr(background);
    const cArr = rgb2arr(color);
    const alpha = cArr.length == 4 ? cArr[3] : 1;
    if (alpha == 0) {
      return 'transparent';
    } else {
      return arr2hex(cArr.map((v, i) => parseInt((1 - alpha) * bArr[i] + alpha * v)));
    }
  };

  /** The style module contains regular expressions and functions for parsing and composing style strings.
   * [Style string definition](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-09.html#name-styling-string)
   * @module style
   */

  var index$5 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    re: re$3,
    parse: parse$4,
    compose: compose$4,
    merge: merge,
    rgb2hex: rgb2hex,
    rgba2hex: rgba2hex
  });

  /**
   * Object of regular expressions for SWU strings in UTF-16
   * 
   * @alias swu.re
   * @property {string} null - the null symbol
   * @property {string} symbol - a symbol
   * @property {string} nullorsymbol - null or a symbol
   * @property {string} sort - the sorting marker
   * @property {string} prefix - a sorting marker followed by one or more symbols with nulls
   * @property {string} box - a signbox marker
   * @property {string} coord - a coordinate
   * @property {string} spatial - a symbol followed by a coordinate
   * @property {string} signbox - a signbox marker, max coordinate and zero or more spatial symbols
   * @property {string} sign - an optional prefix followed by a signbox
   * @property {string} sortable - a mandatory prefix followed by a signbox
   */
  let re$2 = {
    'null': '\uD8C0\uDC00',
    'symbol': '(?:(?:\uD8C0[\uDC01-\uDFFF])|(?:[\uD8C1-\uD8FC][\uDC00-\uDFFF])|(?:\uD8FD[\uDC00-\uDC80]))',
    'coord': '(?:\uD836[\uDC0C-\uDDFF]){2}',
    'sort': '\uD836\uDC00',
    'box': '\uD836[\uDC01-\uDC04]'
  };
  re$2.nullorsymbol = `(?:${re$2.null}|${re$2.symbol})`;
  re$2.prefix = `(?:${re$2.sort}(?:${re$2.nullorsymbol})+)`;
  re$2.spatial = `${re$2.symbol}${re$2.coord}`;
  re$2.signbox = `${re$2.box}${re$2.coord}(?:${re$2.spatial})*`;
  re$2.sign = `${re$2.prefix}?${re$2.signbox}`;
  re$2.sortable = `${re$2.prefix}${re$2.signbox}`;

  /**
   * An array of symbol IDs in minimized format such as "101011"
   * 
   * @alias convert.symidArr
   * @type {string[]}
   */
  const symidArr = ["101011", "101021", "101031", "101041", "101051", "101061", "101071", "101081", "101091", "101101", "101111", "101121", "101131", "101141", "102011", "102021", "102031", "102041", "102051", "102061", "102071", "102081", "102091", "102101", "102111", "102121", "102131", "102141", "102151", "102161", "103011", "103021", "103031", "103041", "103051", "103061", "103071", "103081", "103091", "103101", "103111", "103121", "103131", "103141", "103151", "103161", "103171", "103181", "103191", "103201", "103211", "103221", "103231", "103241", "103251", "103261", "103271", "103281", "103291", "103301", "103311", "103321", "103331", "103341", "103351", "103361", "103371", "103381", "104011", "104021", "104031", "104041", "104051", "104061", "104071", "104081", "105011", "105021", "105031", "105041", "105051", "105061", "105071", "105081", "105091", "105101", "105111", "105121", "105131", "105141", "105151", "105161", "105171", "105181", "105191", "105201", "105211", "105221", "105231", "105241", "105251", "105261", "105271", "105281", "105291", "105301", "105311", "105321", "105331", "105341", "105351", "105361", "105371", "105381", "105391", "105401", "105411", "105421", "105431", "105441", "105451", "105461", "105471", "105481", "105491", "105501", "105511", "105521", "105531", "105541", "105551", "105561", "105571", "105581", "106011", "106021", "106031", "106041", "106051", "106061", "106071", "106081", "106091", "106101", "106111", "106121", "106131", "106141", "106151", "106161", "106171", "106181", "106191", "106201", "106211", "106221", "106231", "106241", "106251", "106261", "106271", "106281", "106291", "106301", "107011", "107021", "107031", "107041", "107051", "107061", "107071", "107081", "107091", "107101", "107111", "107121", "107131", "107141", "107151", "107161", "107171", "107181", "107191", "107201", "107211", "107221", "108011", "108021", "108031", "108041", "108051", "108061", "108071", "108081", "108091", "108101", "108111", "108121", "108131", "108141", "108151", "108161", "108171", "108181", "108191", "109011", "109021", "109031", "109041", "109051", "109061", "109071", "109081", "109091", "109101", "109111", "109121", "109131", "109141", "109151", "109161", "109171", "109181", "109191", "109201", "109211", "109221", "109231", "109241", "109251", "109261", "109271", "109281", "109291", "109301", "109311", "109321", "109331", "109341", "109351", "109361", "109371", "109381", "109391", "109401", "110011", "110021", "110031", "110041", "110051", "110061", "110071", "110081", "110091", "110101", "110111", "110121", "110131", "110141", "110151", "110161", "201011", "201021", "201031", "201041", "201051", "201061", "201071", "201081", "201091", "201101", "201111", "201121", "201131", "201141", "201151", "201161", "201171", "202011", "202012", "202021", "202022", "202031", "202041", "202042", "202051", "202052", "202061", "202071", "202081", "202082", "202091", "202092", "202101", "202102", "202111", "202121", "202131", "203011", "203012", "203013", "203014", "203015", "203021", "203022", "203031", "203032", "203041", "203051", "203052", "203061", "203062", "203071", "203072", "203073", "203081", "203082", "203083", "203084", "203091", "203092", "203093", "203101", "203102", "203103", "203111", "203112", "203113", "203121", "203122", "203123", "203131", "203141", "203151", "203161", "203171", "203181", "203191", "203201", "203202", "203203", "204011", "204012", "204013", "204014", "204021", "204022", "204023", "204024", "204031", "204032", "204033", "204034", "204041", "204042", "204043", "204044", "205011", "205012", "205013", "205014", "205015", "205021", "205022", "205031", "205032", "205041", "205051", "205052", "205061", "205062", "205071", "205081", "205082", "205083", "205091", "205101", "205102", "205103", "205111", "205112", "205113", "205121", "205122", "205123", "205131", "205141", "205151", "205161", "205171", "205181", "205191", "206011", "206012", "206013", "206014", "206021", "206022", "206023", "206024", "206031", "206032", "206041", "206042", "206043", "206051", "206052", "206053", "206054", "206061", "206062", "206063", "206064", "206065", "206066", "206071", "206072", "206073", "206081", "206091", "206101", "206111", "207011", "207021", "207031", "207041", "207051", "207061", "207071", "207091", "207101", "207111", "207121", "207131", "207141", "207151", "207161", "207162", "207163", "208011", "208012", "208021", "208022", "208023", "208024", "208031", "208032", "208033", "208034", "208041", "208042", "208051", "208061", "208071", "208081", "208082", "208091", "208092", "208093", "208094", "208101", "208102", "208103", "208104", "208111", "208112", "208121", "208131", "208141", "209011", "209012", "209013", "209014", "209015", "209021", "209031", "209041", "209042", "209043", "209051", "209061", "209071", "209081", "210011", "210012", "210021", "210022", "210031", "210032", "210033", "210041", "210042", "210043", "210051", "210052", "210061", "210062", "210071", "210072", "210073", "210074", "210081", "210082", "301011", "301021", "301031", "301032", "301041", "301042", "301043", "301044", "401011", "401021", "401031", "401041", "401051", "401061", "401071", "401081", "401091", "401101", "401102", "402011", "402012", "402013", "402021", "402022", "402023", "402024", "402031", "402032", "402033", "402041", "402042", "402043", "402044", "402045", "402051", "402052", "402053", "402054", "402055", "402061", "402062", "402063", "402071", "402072", "402073", "402081", "402082", "402083", "402091", "402101", "402111", "403011", "403012", "403013", "403021", "403022", "403023", "403031", "403041", "403042", "403043", "403044", "403051", "403052", "403061", "403062", "403071", "403072", "404011", "404012", "404013", "404021", "404022", "404023", "404031", "404032", "404033", "404041", "404042", "404043", "404051", "404052", "404053", "404061", "404062", "404063", "404071", "404072", "404073", "404081", "404082", "404083", "404091", "404092", "404093", "404101", "404111", "404112", "405011", "405012", "405013", "405014", "405015", "405021", "405031", "405041", "405051", "405052", "405061", "405062", "405071", "405072", "405081", "405091", "405101", "405111", "405121", "405131", "501011", "501021", "501031", "501041", "501051", "501061", "501071", "501081", "501091", "502011", "502021", "502022", "502023", "502024", "502031", "502032", "502033", "502041", "601011", "601012", "601021", "601031", "601041", "601051", "601061", "601071", "701011", "701012", "701021", "701022", "701031"];

  /** The convert module contains functions to convert between Formal SignWriitng in ASCII (FSW) and SignWriting in Unicode (SWU) characters, along with other types of data.
   * [Characters set definitions](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-09.html#name-characters)
   * @module convert
   */

  /**
   * Function to convert an SWU structural marker to FSW equivalent
   * @function convert.swu2mark
   * @param {string} swuMark - character for SWU structural marker
   * @returns {string} FSW structural marker
   * @example
   * convert.swu2mark('𝠀')
   * 
   * return 'A'
   */
  const swu2mark = swuMark => {
    return {
      '𝠀': 'A',
      '𝠁': 'B',
      '𝠂': 'L',
      '𝠃': 'M',
      '𝠄': 'R'
    }[swuMark];
  };

  /**
   * Function to convert an FSW structural marker to SWU equivalent
   * @function convert.mark2swu
   * @param {string} fswMark - character for FSW structural marker
   * @returns {string} SWU structural marker
   * @example
   * convert.mark2swu('A')
   * 
   * return '𝠀'
   */
  const mark2swu = fswMark => {
    return {
      'A': '𝠀',
      'B': '𝠁',
      'L': '𝠂',
      'M': '𝠃',
      'R': '𝠄'
    }[fswMark];
  };

  /**
   * Function to convert an SWU number character to an integer
   * @function convert.swu2num
   * @param {string} swuNum - SWU number character
   * @returns {number} Integer value for number
   * @example
   * convert.swu2num('𝤆')
   * 
   * return 500
   */
  const swu2num = swuNum => parseInt(swuNum.codePointAt(0)) - 0x1D80C + 250;

  /**
   * Function to convert a number to an SWU number character
   * @function convert.num2swu
   * @param {number} num - Integer value for number
   * @returns {string} SWU number character
   * @example
   * convert.num2swu(500)
   * 
   * return '𝤆'
   */
  const num2swu = num => String.fromCodePoint(0x1D80C + parseInt(num) - 250);

  /**
   * Function to convert two SWU number characters to an array of x,y integers
   * @function convert.swu2coord
   * @param {string} swuCoord - Two SWU number character
   * @returns {number[]} Array of x,y integers
   * @example
   * convert.swu2coord('𝤆𝤆')
   * 
   * return [500, 500]
   */
  const swu2coord = swuCoord => [swu2num(swuCoord.slice(0, 2)), swu2num(swuCoord.slice(2, 4))];

  /**
   * Function to convert an array of x,y integers to two SWU number characters
   * @function convert.coord2swu
   * @param {number[]} coord - Array of x,y integers
   * @returns {string} Two SWU number character
   * @example
   * convert.coord2swu([500, 500])
   * 
   * return '𝤆𝤆'
   */
  const coord2swu = coord => coord.map(num => num2swu(num)).join('');

  /**
   * Function to convert an FSW coordinate string to an array of x,y integers
   * @function convert.fsw2coord
   * @param {string} fswCoord - An FSW coordinate string
   * @returns {number[]} Array of x,y integers
   * @example
   * convert.fsw2coord('500x500')
   * 
   * return [500, 500]
   */
  const fsw2coord = fswCoord => fswCoord.split('x').map(num => parseInt(num));

  /**
   * Function to convert an array of x,y integers to an FSW coordinate string
   * @function convert.coord2fsw
   * @param {number[]} coord - Array of x,y integers
   * @returns {string} An FSW coordinate string
   * @example
   * convert.coord2fsw([500, 500])
   * 
   * return '500x500'
   */
  const coord2fsw = coord => coord.join('x');

  /**
   * Function to convert an SWU symbol character to a code point on plane 4
   * @function convert.swu2code
   * @param {string} swuSym - SWU symbol character
   * @returns {number} Code point on plane 4
   * @example
   * convert.swu2code('񀀁')
   * 
   * return 0x40001
   */
  const swu2code = swuSym => parseInt(swuSym.codePointAt(0));

  /**
   * Function to convert a code point on plane 4 to an SWU symbol character
   * @function convert.code2swu
   * @param {number} code - Code point on plane 4
   * @returns {string} SWU symbol character
   * @example
   * convert.code2swu(0x40001)
   * 
   * return '񀀁'
   */
  const code2swu = code => String.fromCodePoint(code);

  /**
   * Function to convert an SWU symbol character to a 16-bit ID
   * @function convert.swu2id
   * @param {string} swuSym - SWU symbol character
   * @returns {number} 16-bit ID
   * @example
   * convert.swu2id('񀀁')
   * 
   * return 1
   */
  const swu2id = swuSym => swu2code(swuSym) - 0x40000;

  /**
   * Function to convert a 16-bit ID to an SWU symbol character
   * @function convert.id2swu
   * @param {number} id - 16-bit ID
   * @returns {string} SWU symbol character
   * @example
   * convert.id2swu(1)
   * 
   * return '񀀁'
   */
  const id2swu = id => code2swu(id + 0x40000);

  /**
   * Function to convert an FSW symbol key to a 16-bit ID
   * @function convert.key2id
   * @param {string} key - FSW symbol key
   * @returns {number} 16-bit ID
   * @example
   * convert.key2id('S10000')
   * 
   * return 1
   */
  const key2id = key => key === "S00000" ? 0 : 1 + (parseInt(key.slice(1, 4), 16) - 256) * 96 + parseInt(key.slice(4, 5), 16) * 16 + parseInt(key.slice(5, 6), 16);

  /**
   * Function to convert a 16-bit ID to an FSW symbol key
   * @function convert.id2key
   * @param {number} id - 16-bit ID
   * @returns {string} FSW symbol key
   * @example
   * convert.id2key(1)
   * 
   * return 'S10000'
   */
  const id2key = id => {
    if (id === 0) {
      return "S00000";
    }
    const symcode = id - 1;
    const base = parseInt(symcode / 96);
    const fill = parseInt((symcode - base * 96) / 16);
    const rotation = parseInt(symcode - base * 96 - fill * 16);
    return 'S' + (base + 0x100).toString(16) + fill.toString(16) + rotation.toString(16);
  };

  /**
   * Function to convert an SWU symbol character to an FSW symbol key
   * @function convert.swu2key
   * @param {string} swuSym - SWU symbol character
   * @returns {string} FSW symbol key
   * @example
   * convert.swu2key('񀀁')
   * 
   * return 'S10000'
   */
  const swu2key = swuSym => {
    if (swuSym === "񀀀") {
      return "S00000";
    }
    const symcode = swu2code(swuSym) - 0x40001;
    const base = parseInt(symcode / 96);
    const fill = parseInt((symcode - base * 96) / 16);
    const rotation = parseInt(symcode - base * 96 - fill * 16);
    return 'S' + (base + 0x100).toString(16) + fill.toString(16) + rotation.toString(16);
  };

  /**
   * Function to convert an FSW symbol key to an SWU symbol character
   * @function convert.key2swu
   * @param {string} key - FSW symbol key
   * @returns {string} SWU symbol character
   * @example
   * convert.key2swu('S10000')
   * 
   * return '񀀁'
   */
  const key2swu = key => {
    if (key === "S00000") {
      return code2swu(0x40000);
    }
    return code2swu(0x40001 + (parseInt(key.slice(1, 4), 16) - 256) * 96 + parseInt(key.slice(4, 5), 16) * 16 + parseInt(key.slice(5, 6), 16));
  };

  /**
   * Function to convert SWU text to FSW text
   * @function convert.swu2fsw
   * @param {string} swuText - SWU text
   * @returns {string} FSW text
   * @example
   * convert.swu2fsw('𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭')
   * 
   * return 'AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475'
   */
  const swu2fsw = swuText => {
    if (!swuText) return '';
    let fsw = swuText.replace(/𝠀/g, "A").replace(/𝠁/g, "B").replace(/𝠂/g, "L").replace(/𝠃/g, "M").replace(/𝠄/g, "R");
    const syms = fsw.match(new RegExp(re$2.nullorsymbol, 'g'));
    if (syms) {
      syms.forEach(function (sym) {
        fsw = fsw.replace(sym, swu2key(sym));
      });
    }
    const coords = fsw.match(new RegExp(re$2.coord, 'g'));
    if (coords) {
      coords.forEach(function (coord) {
        fsw = fsw.replace(coord, swu2coord(coord).join('x'));
      });
    }
    return fsw;
  };

  /**
   * Function to convert FSW text to SWU text
   * @function convert.fsw2swu
   * @param {string} fswText - FSW text
   * @returns {string} SWU text
   * @example
   * convert.fsw2swu('AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475')
   * 
   * return '𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭'
   */
  const fsw2swu = fswText => {
    if (!fswText) return '';
    const prefixes = fswText.match(new RegExp(re$4.prefix, 'g'));
    if (prefixes) {
      prefixes.forEach(function (prefix) {
        fswText = fswText.replace(prefix, '𝠀' + prefix.slice(1).match(/.{6}/g).map(key => key2swu(key)).join(''));
      });
    }
    const boxes = fswText.match(new RegExp(re$4.box + re$4.coord, 'g'));
    if (boxes) {
      boxes.forEach(function (boxes) {
        fswText = fswText.replace(boxes, mark2swu(boxes.slice(0, 1)) + coord2swu(fsw2coord(boxes.slice(1, 8))));
      });
    }
    const spatials = fswText.match(new RegExp(re$4.spatial, 'g'));
    if (spatials) {
      spatials.forEach(function (spatial) {
        fswText = fswText.replace(spatial, key2swu(spatial.slice(0, 6)) + coord2swu(fsw2coord(spatial.slice(6, 13))));
      });
    }
    return fswText;
  };

  /**
   * Function to convert base or full symid Min to symid Max
   * @function convert.symidMax
   * @param {string} symidMin - Symbol ID minimized
   * @returns {string} Symbol ID maximized
   * @example
   * convert.symidMax('101011')
   * 
   * return '01-01-001-01'
   * @example
   * convert.symidMax('101011616')
   * 
   * return '01-01-001-01-06-16'
   */
  const symidMax = symidMin => {
    if (!/^\d{6}(?:\d{3})?$/.test(symidMin)) {
      return '';
    }
    let max = `0${symidMin.charAt(0)}-${symidMin.charAt(1)}${symidMin.charAt(2)}-0${symidMin.charAt(3)}${symidMin.charAt(4)}-0${symidMin.charAt(5)}`;
    if (symidMin.length > 6) {
      max += `-0${symidMin.charAt(6)}-${symidMin.charAt(7)}${symidMin.charAt(8)}`;
    }
    return max;
  };

  /**
   * Function to convert base or full symid Max to symid Min
   * @function convert.symidMin
   * @param {string} symidMax - Symbol ID maximized
   * @returns {string} Symbol ID minimized
   * @example
   * convert.symidMin('01-01-001-01')
   * 
   * return '101011'
   * @example
   * convert.symidMin('01-01-001-01-06-16')
   * 
   * return '101011616'
   */
  const symidMin = symidMax => {
    const matches = symidMax.match(/^0(\d)-(\d{2})-0(\d{2})-0(\d)(?:-0(\d)-(\d{2}))?$/);
    if (!matches) {
      return '';
    }
    if (matches[5]) {
      return matches[1] + matches[2] + matches[3] + matches[4] + matches[5] + matches[6];
    } else {
      return matches[1] + matches[2] + matches[3] + matches[4];
    }
  };

  /**
   * Function to convert base or full symid to key
   * @function convert.symid2key
   * @param {string} symid - Symbol ID
   * @returns {string} Symbol key
   * @example
   * convert.symid2key('01-01-001-01')
   * 
   * return 'S100'
   * @example
   * convert.symid2key('01-01-001-01-06-16')
   * 
   * return 'S1005f'
   */
  const symid2key = symid => {
    const matches = symid.match(/^0(\d)-(\d{2})-0(\d{2})-0(\d)(?:-0(\d)-(\d{2}))?$/);
    if (!matches) {
      return '';
    }
    const symidMin = matches[1] + matches[2] + matches[3] + matches[4];
    const i = symidArr.indexOf(symidMin);
    if (i === -1) {
      return '';
    }
    if (matches[5]) {
      return 'S' + (256 + i).toString(16) + (parseInt(matches[5], 10) - 1) + (parseInt(matches[6], 10) - 1).toString(16);
    } else {
      return 'S' + (256 + i).toString(16);
    }
  };

  /**
   * Function to convert base or full key to symid
   * @function convert.key2symid
   * @param {string} key - Symbol key
   * @returns {string} Symbol ID
   * @example
   * convert.key2symid('S100')
   * 
   * return '01-01-001-01'
   * @example
   * convert.key2symid('S1005f')
   * 
   * return '01-01-001-01-06-16'
   */
  const key2symid = key => {
    const matches = key.match(/^S([1-3][0-9a-f]{2})(?:([0-5])([0-9a-f]))?$/);
    if (!matches) {
      return '';
    }
    const i = parseInt(matches[1], 16) - 256;
    if (i >= symidArr.length) {
      return '';
    }
    if (matches[3]) {
      return symidMax(symidArr[i]) + '-0' + (1 + parseInt(matches[2])) + '-' + (parseInt(matches[3], 16) + 1).toString().padStart(2, '0');
    } else {
      return symidMax(symidArr[i]);
    }
  };

  var index$4 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    swu2mark: swu2mark,
    mark2swu: mark2swu,
    swu2num: swu2num,
    num2swu: num2swu,
    swu2coord: swu2coord,
    coord2swu: coord2swu,
    fsw2coord: fsw2coord,
    coord2fsw: coord2fsw,
    swu2code: swu2code,
    code2swu: code2swu,
    swu2id: swu2id,
    id2swu: id2swu,
    key2id: key2id,
    id2key: id2key,
    swu2key: swu2key,
    key2swu: key2swu,
    swu2fsw: swu2fsw,
    fsw2swu: fsw2swu,
    symidArr: symidArr,
    symidMax: symidMax,
    symidMin: symidMin,
    symid2key: symid2key,
    key2symid: key2symid
  });

  const parse$3 = {
    /**
     * Function to parse an fsw symbol with optional coordinate and style string
     * @function fsw.parse.symbol
     * @param {string} fswSym - an fsw symbol
     * @returns {SymbolObject} elements of fsw symbol
     * @example
     * fsw.parse.symbol('S10000500x500-C')
     * 
     * return {
     *  'symbol': 'S10000',
     *  'coord': [500, 500],
     *  'style': '-C'
     * }
     */
    symbol: fswSym => {
      const regex = `^(${re$4.symbol})(${re$4.coord})?(${re$3.full})?`;
      const symbol = typeof fswSym === 'string' ? fswSym.match(new RegExp(regex)) : undefined;
      return {
        'symbol': symbol ? symbol[1] : undefined,
        'coord': symbol && symbol[2] ? fsw2coord(symbol[2]) : undefined,
        'style': symbol ? symbol[3] : undefined
      };
    },
    /**
     * Function to parse an fsw sign with style string
     * @function fsw.parse.sign
     * @param {string} fswSign - an fsw sign
     * @returns { SignObject } elements of fsw sign
     * @example
     * fsw.parse.sign('AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475-C')
     * 
     * return {
     *  sequence: ['S10011', 'S10019', 'S2e704', 'S2e748'],
     *  box: 'M',
     *  max: [525, 535],
     *  spatials: [
     *    {
     *      symbol: 'S2e748',
     *      coord: [483, 510]
     *    },
     *    {
     *      symbol: 'S10011',
     *      coord: [501, 466]
     *    },
     *    {
     *      symbol: 'S2e704',
     *      coord: [510, 500]
     *    },
     *    {
     *      symbol: 'S10019',
     *      coord: [476, 475]
     *    }
     *  ],
     *  style: '-C'
     * }
     */
    sign: fswSign => {
      const regex = `^(${re$4.prefix})?(${re$4.signbox})(${re$3.full})?`;
      const sign = typeof fswSign === 'string' ? fswSign.match(new RegExp(regex)) : undefined;
      if (sign) {
        return {
          'sequence': sign[1] ? sign[1].slice(1).match(/.{6}/g) : undefined,
          'box': sign[2][0],
          'max': fsw2coord(sign[2].slice(1, 8)),
          'spatials': sign[2].length < 9 ? undefined : sign[2].slice(8).match(/(.{13})/g).map(m => {
            return {
              symbol: m.slice(0, 6),
              coord: [parseInt(m.slice(6, 9)), parseInt(m.slice(10, 13))]
            };
          }),
          'style': sign[3]
        };
      } else {
        return {};
      }
    },
    /**
     * Function to parse an fsw text
     * @function fsw.parse.text
     * @param {string} fswText - an fsw text
     * @returns {string[]} fsw signs and punctuations
     * @example
     * fsw.parse.text('AS14c20S27106M518x529S14c20481x471S27106503x489 AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468 S38800464x496')
     * 
     * return [
     *  'AS14c20S27106M518x529S14c20481x471S27106503x489',
     *  'AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468',
     *  'S38800464x496'
     * ]
     */
    text: fswText => {
      if (typeof fswText !== 'string') return [];
      const regex = `(${re$4.sign}(${re$3.full})?|${re$4.spatial}(${re$3.full})?)`;
      const matches = fswText.match(new RegExp(regex, 'g'));
      return matches ? [...matches] : [];
    }
  };

  const compose$3 = {
    /**
     * Function to compose an fsw symbol with optional coordinate and style string
     * @function fsw.compose.symbol
     * @param {SymbolObject} fswSymObject - an fsw symbol object
     * @returns {string} an fsw symbol string
     * @example
     * fsw.compose.symbol({
     *  'symbol': 'S10000',
     *  'coord': [480, 480],
     *  'style': '-C'
     * })
     * 
     * return 'S10000480x480-C'
     */
    symbol: fswSymObject => {
      if (typeof fswSymObject.symbol === 'string') {
        const symbol = (fswSymObject.symbol.match(re$4.symbol) || [''])[0];
        if (symbol) {
          const x = (fswSymObject.coord && fswSymObject.coord[0] || '').toString();
          const y = (fswSymObject.coord && fswSymObject.coord[1] || '').toString();
          const coord = ((x + 'x' + y).match(re$4.coord) || [''])[0] || '';
          const styleStr = typeof fswSymObject.style === 'string' && (fswSymObject.style.match(re$3.full) || [''])[0] || '';
          return symbol + coord + styleStr;
        }
      }
      return undefined;
    },
    /**
     * Function to compose an fsw sign with style string
     * @function fsw.compose.sign
     * @param {SignObject} fswSignObject - an fsw symbol object
     * @returns {string} an fsw sign string
     * @example
     * fsw.compose.sign({
     *  sequence: ['S10011', 'S10019', 'S2e704', 'S2e748'],
     *  box: 'M',
     *  max: [525, 535],
     *  spatials: [
     *    {
     *      symbol: 'S2e748',
     *      coord: [483, 510]
     *    },
     *    {
     *      symbol: 'S10011',
     *      coord: [501, 466]
     *    },
     *    {
     *      symbol: 'S2e704',
     *      coord: [510, 500]
     *    },
     *    {
     *      symbol: 'S10019',
     *      coord: [476, 475]
     *    }
     *  ],
     *  style: '-C'
     * })
     * 
     * return 'AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475-C'
     */
    sign: fswSignObject => {
      let box = typeof fswSignObject.box !== 'string' ? 'M' : (fswSignObject.box + 'M').match(re$4.box);
      const x = (fswSignObject.max && fswSignObject.max[0] || '').toString();
      const y = (fswSignObject.max && fswSignObject.max[1] || '').toString();
      const max = ((x + 'x' + y).match(re$4.coord) || [''])[0] || '';
      if (!max) return undefined;
      let prefix = '';
      if (fswSignObject.sequence && Array.isArray(fswSignObject.sequence)) {
        prefix = fswSignObject.sequence.map(key => (key.match(re$4.nullorsymbol) || [''])[0]).join('');
        prefix = prefix ? 'A' + prefix : '';
      }
      let signbox = '';
      if (fswSignObject.spatials && Array.isArray(fswSignObject.spatials)) {
        signbox = fswSignObject.spatials.map(spatial => {
          if (typeof spatial.symbol === 'string') {
            const symbol = (spatial.symbol.match(re$4.symbol) || [''])[0];
            if (symbol) {
              const x = (spatial.coord && spatial.coord[0] || '').toString();
              const y = (spatial.coord && spatial.coord[1] || '').toString();
              const coord = ((x + 'x' + y).match(re$4.coord) || [''])[0] || '';
              if (coord) {
                return symbol + coord;
              }
            }
          }
          return '';
        }).join('');
      }
      const styleStr = typeof fswSignObject.style === 'string' && (fswSignObject.style.match(re$3.full) || [''])[0] || '';
      return prefix + box + max + signbox + styleStr;
    }
  };

  /**
   * Function to gather sizing information about an fsw sign or symbol
   * @function fsw.info
   * @param {string} fsw - an fsw sign or symbol
   * @returns {SegmentInfo} information about the fsw string
   * @example
   * fsw.info('AS14c20S27106L518x529S14c20481x471S27106503x489-P10Z2')
   * 
   * return {
   *   minX: 481,
   *   minY: 471,
   *   width: 37,
   *   height: 58,
   *   lane: -1,
   *   padding: 10,
   *   segment: 'sign',
   *   zoom: 2
   * }
   */
  const info$1 = fsw => {
    let lanes = {
      "B": 0,
      "L": -1,
      "M": 0,
      "R": 1
    };
    let parsed = parse$3.sign(fsw);
    let width, height, segment, x1, x2, y1, y2, lane;
    if (parsed.spatials) {
      x1 = Math.min(...parsed.spatials.map(spatial => spatial.coord[0]));
      x2 = parsed.max[0];
      width = x2 - x1;
      y1 = Math.min(...parsed.spatials.map(spatial => spatial.coord[1]));
      y2 = parsed.max[1];
      height = y2 - y1;
      segment = 'sign';
      lane = parsed.box;
    } else {
      parsed = parse$3.symbol(fsw);
      lane = "M";
      if (parsed.coord) {
        x1 = parsed.coord[0];
        width = (500 - x1) * 2;
        y1 = parsed.coord[1];
        height = (500 - y1) * 2;
        segment = 'symbol';
      } else {
        x1 = 490;
        width = 20;
        y1 = 490;
        height = 20;
        segment = 'none';
      }
    }
    let style = parse$4(parsed.style);
    let zoom = style.zoom || 1;
    let padding = style.padding || 0;
    return {
      minX: x1,
      minY: y1,
      width: width,
      height: height,
      segment: segment,
      lane: lanes[lane],
      padding: padding,
      zoom: zoom
    };
  };

  /**
   * Default special tokens configuration
   * ```
   * DEFAULT_SPECIAL_TOKENS = [
   *   { index: 0, name: 'UNK', value: '[UNK]' },
   *   { index: 1, name: 'PAD', value: '[PAD]' },
   *   { index: 2, name: 'CLS', value: '[CLS]' },
   *   { index: 3, name: 'SEP', value: '[SEP]' }
   * ];
   * ```
   */
  const DEFAULT_SPECIAL_TOKENS = [{
    index: 0,
    name: 'UNK',
    value: '[UNK]'
  }, {
    index: 1,
    name: 'PAD',
    value: '[PAD]'
  }, {
    index: 2,
    name: 'CLS',
    value: '[CLS]'
  }, {
    index: 3,
    name: 'SEP',
    value: '[SEP]'
  }];

  /**
   * Generates an array of all possible tokens for the FSW tokenizer
   * @private
   * @function generateTokens
   * @returns {string[]} Array of all possible tokens
   */
  const generateTokens = () => {
    const range = (start, end) => Array.from({
      length: end - start
    }, (_, i) => start + i);
    const hexRange = (start, end) => range(start, end + 1).map(i => i.toString(16));
    const sequence = ["A"];
    const signbox = ["B", "L", "M", "R"];
    const nullToken = ["S000"];
    const baseSymbols = range(0x100, 0x38b + 1).map(i => `S${i.toString(16)}`);
    const rows = hexRange(0, 15).map(i => `r${i}`);
    const cols = hexRange(0, 5).map(i => `c${i}`);
    const positions = range(250, 750).map(i => `p${i}`);
    return [...sequence, ...signbox, ...nullToken, ...baseSymbols, ...rows, ...cols, ...positions];
  };

  /**
   * Creates mappings for special tokens
   * @private
   * @function createSpecialTokenMappings
   * @param {Array} specialTokens - Array of special token objects
   * @returns {Object} Special token mappings
   */
  const createSpecialTokenMappings = specialTokens => {
    const byIndex = {};
    const byName = {};
    const byValue = {};
    const indices = new Set();
    specialTokens.forEach(token => {
      if (indices.has(token.index)) {
        throw new Error(`Duplicate token index: ${token.index}`);
      }
      indices.add(token.index);
      byIndex[token.index] = token;
      byName[token.name] = token;
      byValue[token.value] = token;
    });
    return {
      byIndex,
      byName,
      byValue,
      getByIndex: index => byIndex[index] || byIndex[specialTokens.find(t => t.name === 'UNK').index],
      getByName: name => byName[name] || byName['UNK'],
      getByValue: value => byValue[value] || byName['UNK'],
      getAllValues: () => specialTokens.map(t => t.value),
      getAllIndices: () => specialTokens.map(t => t.index)
    };
  };

  /**
   * Creates index-to-string and string-to-index mappings for tokens
   * @private
   * @function createTokenMappings
   * @param {string[]} tokens - Array of tokens to map
   * @param {Object} specialTokenMappings - Special tokens mapping object
   * @param {number} startingIndex - Starting index for regular tokens
   * @returns {Object} Object containing i2s and s2i mappings
   */
  const createTokenMappings = (tokens, specialTokenMappings, startingIndex) => {
    const i2s = {};
    const s2i = {};

    // Add special tokens first
    Object.values(specialTokenMappings.byIndex).forEach(token => {
      i2s[token.index] = token.value;
      s2i[token.value] = token.index;
    });

    // Add regular tokens
    tokens.forEach((token, i) => {
      const index = startingIndex + i;
      i2s[index] = token;
      s2i[token] = index;
    });
    return {
      i2s,
      s2i
    };
  };

  /**
   * Tokenizes an FSW string into an array of tokens
   * @function fsw.tokenize
   * @param {string} fsw - FSW string to tokenize
   * @param {Object} options - Tokenization options
   * @param {boolean} [options.sequence=true] - Whether to include sequence tokens
   * @param {boolean} [options.signbox=true] - Whether to include signbox tokens
   * @param {string} [options.sep="[SEP]"] - Separator token
   * @returns {string[]} Array of tokens
   * @example
   * fsw.tokenize("AS10e00M507x515S10e00492x485",{sequence:false,sep:null})
   * 
   * return [
   *   'M', 'p507', 'p515','S10e', 'c0', 'r0', 'p492', 'p485'
   * ]
   */
  const tokenize = (fsw, {
    sequence = true,
    signbox = true,
    sep = "[SEP]"
  } = {}) => {
    const tokenizeSymbol = symbol => [symbol.slice(0, 4), `c${symbol.charAt(4)}`, `r${symbol.charAt(5)}`];
    const tokenizeCoord = coord => coord.map(p => `p${p}`);
    const segments = parse$3.text(fsw).map(fswSegment => {
      if (/[BLMR]/.test(fswSegment)) {
        const sign = parse$3.sign(fswSegment);
        const tokens = [];
        if (sign.sequence && sequence) {
          tokens.push("A", ...sign.sequence.map(seqItem => tokenizeSymbol(seqItem)).flat());
        }
        if (signbox) {
          tokens.push(sign.box, ...tokenizeCoord(sign.max), ...sign.spatials.flatMap(symbol => [...tokenizeSymbol(symbol.symbol), ...tokenizeCoord(symbol.coord)]));
        }
        return sep ? [...tokens, sep] : tokens;
      } else {
        const parsed = parse$3.symbol(fswSegment);
        if (!signbox && !sequence) {
          return [];
        }
        let tokens = [];
        if (!signbox && sequence) {
          tokens = ["A", ...tokenizeSymbol(parsed.symbol)];
        } else {
          tokens = ["M", ...tokenizeCoord(parsed.coord.map(c => 1000 - c)), ...tokenizeSymbol(parsed.symbol), ...tokenizeCoord(parsed.coord)];
        }
        return tokens.length > 0 && sep ? [...tokens, sep] : tokens;
      }
    });
    return segments.flatMap(segment => segment);
  };

  /**
   * Converts an array of tokens back into an FSW string
   * @function fsw.detokenize
   * @param {string[]} tokens - Array of tokens to convert
   * @param {Array} specialTokens - Array of special token objects to filter out
   * @returns {string} FSW string
   * @example
   * fsw.detokenize(['M', 'p507', 'p515','S10e', 'c0', 'r0', 'p492', 'p485'])
   * 
   * return "M507x515S10e00492x485"
   */
  const detokenize = (tokens, specialTokens = DEFAULT_SPECIAL_TOKENS) => {
    const specialValues = new Set(specialTokens.map(t => t.value));
    return tokens.filter(t => !specialValues.has(t)).join(' ').replace(/\bp(\d{3})\s+p(\d{3})/g, '$1x$2').replace(/ c(\d)\d? r(.)/g, '$1$2').replace(/ c(\d)\d?/g, '$10').replace(/ r(.)/g, '0$1').replace(/ /g, '').replace(/(\d)([BLMR])/g, '$1 $2').replace(/(\d)(AS)/g, '$1 $2').replace(/(A(?:S00000|S[123][0-9a-f]{2}[0-5][0-9a-f])+)( )([BLMR])/g, '$1$3');
  };

  /**
   * Splits tokens into chunks of specified size while preserving sign boundaries
   * @function fsw.chunkTokens
   * @param {string[]} tokens - Array of tokens to chunk
   * @param {number} chunkSize - Maximum size of each chunk
   * @param {Object} options - Chunking options
   * @param {string} [options.cls="[CLS]"] - CLS token
   * @param {string} [options.sep="[SEP]"] - SEP token
   * @param {string} [options.pad="[PAD]"] - PAD token
   * @returns {string[][]} Array of token chunks
   */
  const chunkTokens = (tokens, chunkSize, {
    cls = "[CLS]",
    sep = "[SEP]",
    pad = "[PAD]"
  } = {}) => {
    if (chunkSize < 60) {
      throw new Error('Chunk size must be at least 60 tokens to accommodate a typical sign');
    }
    const chunks = [];
    let currentChunk = [];
    let tokenIndex = 0;
    while (tokenIndex < tokens.length) {
      currentChunk = [cls];
      while (tokenIndex < tokens.length) {
        tokens[tokenIndex];
        let lookAhead = tokenIndex;
        while (lookAhead < tokens.length && tokens[lookAhead] !== sep) {
          lookAhead++;
        }
        const signSize = lookAhead - tokenIndex + 1;
        if (currentChunk.length + signSize > chunkSize - 1) {
          break;
        }
        while (tokenIndex <= lookAhead) {
          currentChunk.push(tokens[tokenIndex]);
          tokenIndex++;
        }
      }
      while (currentChunk.length < chunkSize) {
        currentChunk.push(pad);
      }
      chunks.push(currentChunk);
    }
    return chunks;
  };

  /**
   * Creates a tokenizer object with encoding and decoding capabilities
   * @function fsw.createTokenizer
   * @param {Object} [specialTokens] - Special tokens mapping object
   * @param {number} [startingIndex] - Starting index for regular tokens
   * @returns {TokenizerObject} Tokenizer object
   * @example
   * const t = fsw.createTokenizer()
   * 
   * t.encode('M507x515S10e00492x485')
   * 
   * return [7, 941, 949,  24, 678, 662, 926, 919, 3]
   */
  const createTokenizer = (specialTokens = DEFAULT_SPECIAL_TOKENS, startingIndex = null) => {
    const specialTokenMappings = createSpecialTokenMappings(specialTokens);
    const calculatedStartingIndex = startingIndex ?? (specialTokenMappings.getAllIndices().length > 0 ? Math.max(...specialTokenMappings.getAllIndices()) + 1 : 0);
    const tokens = generateTokens();
    const {
      i2s,
      s2i
    } = createTokenMappings(tokens, specialTokenMappings, calculatedStartingIndex);
    const tokenizer = {
      i2s,
      s2i,
      specialTokens: specialTokenMappings,
      length: Object.keys(i2s).length,
      vocab: () => Object.values(i2s),
      encodeTokens: tokens => tokens.map(t => {
        return s2i[t] !== undefined ? s2i[t] : specialTokenMappings.getByValue(t).index;
      }),
      decodeTokens: indices => indices.map(i => i2s[i] || specialTokenMappings.getByName('UNK').value),
      encode: (text, options = {}) => {
        const tokens = tokenize(text, {
          ...options,
          sep: specialTokenMappings.getByName('SEP').value
        });
        return tokenizer.encodeTokens(tokens);
      },
      decode: tokens => {
        if (tokens.length === 0) return "";
        if (Array.isArray(tokens[0])) {
          const decodedChunks = tokens.map(chunk => tokenizer.decodeTokens(chunk));
          return detokenize(decodedChunks.flat(), specialTokens);
        }
        const decodedTokens = tokenizer.decodeTokens(tokens);
        return detokenize(decodedTokens, specialTokens);
      },
      chunk: (tokens, chunkSize) => chunkTokens(tokens, chunkSize, {
        cls: specialTokenMappings.getByName('CLS').value,
        sep: specialTokenMappings.getByName('SEP').value,
        pad: specialTokenMappings.getByName('PAD').value
      })
    };
    return tokenizer;
  };

  const columnDefaults$1 = {
    'height': 500,
    'width': 150,
    'offset': 50,
    'pad': 20,
    'margin': 5,
    'dynamic': false,
    'background': undefined,
    'punctuation': {
      'spacing': true,
      'pad': 30,
      'pull': true
    },
    'style': {
      'detail': ['black', 'white'],
      'zoom': 1
    }
  };

  /**
   * Function to an object of column options with default values
   * 
   * @function fsw.columnDefaultsMerge
   * @param {ColumnOptions} options - object of column options
   * @returns {ColumnOptions} object of column options merged with column defaults
   * @example
   * fsw.columnDefaultsMerge({height: 500,width:150})
   * 
   * return {
   *   "height": 500,
   *   "width": 150,
   *   "offset": 50,
   *   "pad": 20,
   *   "margin": 5,
   *   "dynamic": false,
   *   "punctuation": {
   *     "spacing": true,
   *     "pad": 30,
   *     "pull": true
   *   },
   *   "style": {
   *     "detail": [
   *       "black",
   *       "white"
   *     ],
   *     "zoom": 1
   *   }
   * }
   */
  const columnDefaultsMerge$1 = options => {
    if (typeof options !== 'object') options = {};
    return {
      ...columnDefaults$1,
      ...options,
      punctuation: {
        ...columnDefaults$1.punctuation,
        ...options.punctuation
      },
      style: {
        ...columnDefaults$1.style,
        ...options.style
      }
    };
  };

  /**
   * Function to transform an FSW text to an array of columns
   * 
   * @function fsw.columns
   * @param {string} fswText - FSW text of signs and punctuation
   * @param {ColumnOptions} options - object of column options
   * @returns {{options:ColumnOptions,widths:number[],columns:ColumnData}} object of column options, widths array, and column data
   * @example
   * fsw.columns('AS14c20S27106M518x529S14c20481x471S27106503x489 AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468 S38800464x496', {height: 500,width:150})
   * 
   * return {
   *   "options": {
   *     "height": 500,
   *     "width": 150,
   *     "offset": 50,
   *     "pad": 20,
   *     "margin": 5,
   *     "dynamic": false,
   *     "punctuation": {
   *       "spacing": true,
   *       "pad": 30,
   *       "pull": true
   *     },
   *     "style": {
   *       "detail": [
   *         "black",
   *         "white"
   *       ],
   *       "zoom": 1
   *     }
   *   },
   *   "widths": [
   *     150
   *   ],
   *   "columns": [
   *     [
   *       {
   *         "x": 56,
   *         "y": 20,
   *         "minX": 481,
   *         "minY": 471,
   *         "width": 37,
   *         "height": 58,
   *         "lane": 0,
   *         "padding": 0,
   *         "segment": "sign",
   *         "text": "AS14c20S27106M518x529S14c20481x471S27106503x489",
   *         "zoom": 1
   *       },
   *       {
   *         "x": 57,
   *         "y": 118,
   *         "minX": 482,
   *         "minY": 468,
   *         "width": 36,
   *         "height": 65,
   *         "lane": 0,
   *         "padding": 0,
   *         "segment": "sign",
   *         "text": "AS18701S1870aS2e734S20500M518x533S1870a489x515S18701482x490S20500508x496S2e734500x468",
   *         "zoom": 1
   *       },
   *       {
   *         "x": 39,
   *         "y": 203,
   *         "minX": 464,
   *         "minY": 496,
   *         "width": 72,
   *         "height": 8,
   *         "lane": 0,
   *         "padding": 0,
   *         "segment": "symbol",
   *         "text": "S38800464x496",
   *         "zoom": 1
   *       }
   *     ]
   *   ]
   * }
   */
  const columns$1 = (fswText, options) => {
    if (typeof fswText !== 'string') return {};
    const values = columnDefaultsMerge$1(options);
    let input = parse$3.text(fswText);
    let cursor = 0;
    let cols = [];
    let col = [];
    let plus = 0;
    let center = parseInt(values.width / 2);
    let maxHeight = values.height - values.margin;
    let pullable = true;
    let finalize = false;
    for (let val of input) {
      let informed = info$1(val);
      cursor += plus;
      if (values.punctuation.spacing) {
        cursor += informed.segment == 'sign' ? values.pad : 0;
      } else {
        cursor += values.pad;
      }
      finalize = cursor + informed.height > maxHeight;
      if (finalize && informed.segment == 'symbol' && values.punctuation.pull && pullable) {
        finalize = false;
        pullable = false;
      }
      if (col.length == 0) {
        finalize = false;
      }
      if (finalize) {
        cursor = values.pad;
        cols.push(col);
        col = [];
        pullable = true;
      }
      col.push(Object.assign(informed, {
        x: center + values.offset * informed.lane - (500 - informed.minX) * informed.zoom * values.style.zoom,
        y: cursor,
        text: val
      }));
      cursor += informed.height * informed.zoom * values.style.zoom;
      if (values.punctuation.spacing) {
        plus = informed.segment == 'sign' ? values.pad : values.punctuation.pad;
      } else {
        plus = values.pad;
      }
    }
    if (col.length) {
      cols.push(col);
    }

    // over height issue when pulling punctuation
    if (values.punctuation.pull) {
      for (let col of cols) {
        let last = col[col.length - 1];
        let diff = last.y + last.height - (values.height - values.margin);
        if (diff > 0) {
          let adj = parseInt(diff / col.length) + 1;
          for (let i in col) {
            col[i].y -= adj * i + adj;
          }
        }
      }
    }

    // contract, expand, adjust
    let widths = [];
    for (let col of cols) {
      let min = [center - values.offset - values.pad];
      let max = [center + values.offset + values.pad];
      for (let item of col) {
        min.push(item.x - values.pad);
        max.push(item.x + item.width + values.pad);
      }
      min = Math.min(...min);
      max = Math.max(...max);
      let width = values.width;
      let adj = 0;
      if (!values.dynamic) {
        adj = center - parseInt((min + max) / 2);
      } else {
        width = max - min;
        adj = -min;
      }
      for (let item of col) {
        item.x += adj;
      }
      widths.push(width);
    }
    return {
      'options': values,
      'widths': widths,
      'columns': cols
    };
  };

  /**
   * Array of numbers for kinds of symbols: writing, location, and punctuation.
   * @alias fsw.kind
   * @type {number[]}
   */
  const kind$1 = [0x100, 0x37f, 0x387];

  /**
   * Array of numbers for categories of symbols: hand, movement, dynamics, head, trunk & limb, location, and punctuation.
   * @alias fsw.category
   * @type {number[]}
   */
  const category$1 = [0x100, 0x205, 0x2f7, 0x2ff, 0x36d, 0x37f, 0x387];

  /**
   * Array of numbers for the 30 symbol groups.
   * @alias fsw.group
   * @type {number[]}
   */
  const group$1 = [0x100, 0x10e, 0x11e, 0x144, 0x14c, 0x186, 0x1a4, 0x1ba, 0x1cd, 0x1f5, 0x205, 0x216, 0x22a, 0x255, 0x265, 0x288, 0x2a6, 0x2b7, 0x2d5, 0x2e3, 0x2f7, 0x2ff, 0x30a, 0x32a, 0x33b, 0x359, 0x36d, 0x376, 0x37f, 0x387];

  /**
   * Object of symbol ranges with starting and ending numbers.
   * 
   *   { all, writing, hand, movement, dynamic, head, hcenter, vcenter, trunk, limb, location, punctuation }
   * @alias fsw.ranges
   * @type {object}
   */
  const ranges$1 = {
    'all': [0x100, 0x38b],
    'writing': [0x100, 0x37e],
    'hand': [0x100, 0x204],
    'movement': [0x205, 0x2f6],
    'dynamic': [0x2f7, 0x2fe],
    'head': [0x2ff, 0x36c],
    'hcenter': [0x2ff, 0x36c],
    'vcenter': [0x2ff, 0x375],
    'trunk': [0x36d, 0x375],
    'limb': [0x376, 0x37e],
    'location': [0x37f, 0x386],
    'punctuation': [0x387, 0x38b]
  };

  /**
   * Function to test if symbol is of a certain type.
   * @function fsw.isType
   * @param {string} key - an FSW symbol key
   * @param {string} type - the name of a symbol range
   * @returns {boolean} is symbol of specified type
   * @example
   * fsw.isType('S10000', 'hand')
   * 
   * return true
   */
  const isType$1 = (key, type) => {
    const parsed = parse$3.symbol(key);
    if (parsed.symbol) {
      const dec = parseInt(parsed.symbol.slice(1, 4), 16);
      const range = ranges$1[type];
      if (range) {
        return range[0] <= dec && range[1] >= dec;
      }
    }
    return false;
  };

  /**
   * Array of colors associated with the seven symbol categories.
   * @alias fsw.colors
   * @type {string[]}
   */
  const colors$1 = ['#0000CC', '#CC0000', '#FF0099', '#006600', '#000000', '#884411', '#FF9900'];

  /**
   * Function that returns the standardized color for a symbol.
   * @function fsw.colorize
   * @param {string} key - an FSW symbol key
   * @returns {string} name of standardized color for symbol
   * @example
   * fsw.colorize('S10000')
   * 
   * return '#0000CC'
   */
  const colorize$1 = key => {
    const parsed = parse$3.symbol(key);
    let color = '#000000';
    if (parsed.symbol) {
      const dec = parseInt(parsed.symbol.slice(1, 4), 16);
      const index = category$1.findIndex(val => val > dec);
      color = colors$1[index < 0 ? 6 : index - 1];
    }
    return color;
  };

  /** The fsw module contains functions for handling Formal SignWriting in ASCII (FSW) characters.
   * [FSW characters definition](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-09.html#name-formal-signwriting-in-ascii)
   * @module fsw
   */

  var index$3 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    re: re$4,
    parse: parse$3,
    compose: compose$3,
    info: info$1,
    tokenize: tokenize,
    detokenize: detokenize,
    createTokenizer: createTokenizer,
    columnDefaults: columnDefaults$1,
    columnDefaultsMerge: columnDefaultsMerge$1,
    columns: columns$1,
    kind: kind$1,
    category: category$1,
    group: group$1,
    ranges: ranges$1,
    isType: isType$1,
    colors: colors$1,
    colorize: colorize$1
  });

  /**
   * Object of regular expressions for FSW query strings
   * 
   * @alias fswquery.re
   * @type {object}
   * @property {string} null - the null symbol
   * @property {string} base - FSW symbol base with neither fill or rotation
   * @property {string} coord - FSW coordinate of X and Y values separated by 'x'
   * @property {string} var - variance string for searching sign box
   * @property {string} symbol - FSW symbol key starting with 'S'
   * @property {string} nullorsymbol - null or a symbol
   * @property {string} range - FSW range starting with 'R'
   * @property {string} item - FSW symbol or range query string
   * @property {string} list - several FSW symbols and FSW ranges as a logical OR for searching 
   * @property {string} prefix - a sequential list of FSW symbol keys with nulls starting with 'A'
   * @property {string} signbox - several groups of FSW lists, each group having a coordinate
   * @property {string} full - a query string to search prefix in order and the signbox with variance
   */
  let re$1 = {
    'null': 'S00000',
    'base': '[123][0-9a-f]{2}',
    'coord': '(?:[0-9]{3}x[0-9]{3})?',
    'var': 'V[0-9]+'
  };
  re$1.symbol = `S${re$1.base}[0-5u][0-9a-fu]`;
  re$1.nullorsymbol = `(?:${re$1.null}|${re$1.symbol})`;
  re$1.range = `R${re$1.base}t${re$1.base}`;
  re$1.item = `(?:${re$1.null}|${re$1.symbol}|${re$1.range})`;
  re$1.list = `${re$1.item}(?:o${re$1.item})*`;
  re$1.prefix = `(?:A(?:${re$1.list})+)?T`;
  re$1.signbox = `(?:${re$1.list}${re$1.coord})*`;
  re$1.full = `Q(${re$1.prefix})?(${re$1.signbox})?(${re$1.var})?(-?)`;

  const parsePrefix$1 = text => {
    return {
      required: true,
      parts: text == 'T' ? undefined : text.match(new RegExp(`${re$1.list}`, 'g')).map(part => {
        if (part.includes('o')) {
          return ['or'].concat(part.match(new RegExp(`(${re$1.item})`, 'g')).map(part => part[0] == 'S' ? part : part.slice(1).split('t')));
        } else {
          return part[0] == 'S' ? part : part.slice(1).split('t');
        }
      })
    };
  };
  const parseSignbox$1 = text => {
    return text.match(new RegExp(`(${re$1.list}${re$1.coord})`, 'g')).map(part => {
      let coord, front;
      if (part.includes('x')) {
        coord = fsw2coord(part.slice(-7));
        front = part.slice(0, -7);
      } else {
        front = part;
      }
      if (front.includes('o')) {
        return {
          or: front.split('o').map(part => {
            if (part.includes('S')) {
              return part;
            } else {
              return part.slice(1).split('t');
            }
          }),
          coord,
          coord
        };
      } else if (front.includes('S')) {
        return {
          symbol: front,
          coord: coord
        };
      } else {
        return {
          range: front.slice(1).split('t'),
          coord: coord
        };
      }
    });
  };

  /**
   * Function to parse FSW query string to object
   * @function fswquery.parse
   * @param {string} fswQueryString - an FSW query string
   * @returns {QueryObject} elements of a of query string identified by regular expression
   * @example
   * fswquery.parse('QAS10000S10500oS20500oR2fft304TS100uuR205t206oS207uu510x510V5-')
   * 
   * return {
   *   "query": true,
   *   "prefix": {
   *     "required": true,
   *     "parts": [
   *       "S10000",
   *       [
   *         "or",
   *         "S10500",
   *         "S20500",
   *         [
   *           "2ff",
   *           "304"
   *         ]
   *       ]
   *     ]
   *   },
   *   "signbox": [
   *     {
   *       "symbol": "S100uu"
   *     },
   *     {
   *       "or": [
   *         [
   *           "205",
   *           "206"
   *         ],
   *         "S207uu"
   *       ],
   *       "coord": [
   *         510,
   *         510
   *       ]
   *     }
   *   ],
   *   "variance": 5,
   *   "style": true
   * }
   */
  const parse$2 = fswQueryString => {
    const query = typeof fswQueryString === 'string' ? fswQueryString.match(new RegExp(`^${re$1.full}`)) : undefined;
    return {
      'query': query ? true : undefined,
      'prefix': query && query[1] ? parsePrefix$1(query[1]) : undefined,
      'signbox': query && query[2] ? parseSignbox$1(query[2]) : undefined,
      'variance': query && query[3] ? parseInt(query[3].slice(1)) : undefined,
      'style': query && query[4] ? true : undefined
    };
  };

  /**
   * Function to compose FSW query string from object
   * @function fswquery.compose
   * @param {QueryObject} fswQueryObject - an object of query options
   * @returns {string} FSW query string 
   * @example 
   * fswquery.compose({
   *  query: true,
   *  prefix: {
   *    required: true,
   *    parts: [
   *      'S10000',
   *      ['100', '204'],
   *      'S20500'
   *    ]
   *  },
   *  signbox: [
   *    { symbol: 'S20000' },
   *    {
   *      range: ['100', '105'],
   *      coord: [500, 500]
   *    }
   *  ],
   *  variance: 5,
   *  style: true
   * })
   *
   * return 'QAS10000R100t204S20500TS20000R100t105500x500V5-'
   */
  const compose$2 = fswQueryObject => {
    if (!fswQueryObject || !fswQueryObject.query) {
      return undefined;
    }
    let query = 'Q';
    if (fswQueryObject.prefix && fswQueryObject.prefix.required) {
      if (Array.isArray(fswQueryObject.prefix.parts)) {
        query += 'A';
        query += fswQueryObject.prefix.parts.map(part => {
          if (typeof part === 'string') {
            return part;
          } else {
            if (Array.isArray(part) && part.length == 2) {
              return `R${part[0]}t${part[1]}`;
            } else if (Array.isArray(part) && part.length > 2 && part[0] == 'or') {
              part.shift();
              return part.map(part => {
                if (typeof part === 'string') {
                  return part;
                } else {
                  if (Array.isArray(part) && part.length == 2) {
                    return `R${part[0]}t${part[1]}`;
                  }
                }
              }).join('o');
            }
          }
        }).join('');
      }
      query += 'T';
    }
    if (Array.isArray(fswQueryObject.signbox)) {
      query += fswQueryObject.signbox.map(part => {
        let out;
        if (part.or) {
          out = part.or.map(item => {
            if (typeof item === 'string') {
              return item;
            } else {
              if (Array.isArray(item) && item.length == 2) {
                return `R${item[0]}t${item[1]}`;
              }
            }
          }).join('o');
        } else if (part.symbol) {
          out = part.symbol;
        } else {
          if (part.range && Array.isArray(part.range) && part.range.length == 2) {
            out = `R${part.range[0]}t${part.range[1]}`;
          }
        }
        return out + (Array.isArray(part.coord) && part.coord.length == 2 ? part.coord.join('x') : '');
      }).join('');
    }
    query += fswQueryObject.style ? '-' : '';
    query = query.match(new RegExp(`^${re$1.full}`))[0];
    return query;
  };

  /**
   * Function to convert an FSW sign to a query string
   * 
   * For the flags parameter, use one or more of the following.
   * - A: exact symbol in temporal prefix
   * - a: general symbol in temporal prefix
   * - S: exact symbol in spatial signbox
   * - s: general symbol in spatial signbox
   * - L: spatial signbox symbol at location
   * @function fswquery.fsw2query
   * @param {string} fswSign - FSW sign
   * @param {string} flags - flags for query string creation
   * @returns {string} FSW query string
   * @example
   * fswquery.fsw2query('AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475', 'ASL')
   * 
   * return 'QAS10011S10019S2e704S2e748TS2e748483x510S10011501x466S2e704510x500S10019476x475'
   */
  const fsw2query = (fswSign, flags) => {
    let query = '';
    const parsed = parse$3.sign(fswSign);
    if (parsed.box) {
      const A_flag = flags.indexOf('A') > -1;
      const a_flag = flags.indexOf('a') > -1;
      const S_flag = flags.indexOf('S') > -1;
      const s_flag = flags.indexOf('s') > -1;
      const L_flag = flags.indexOf('L') > -1;
      if (A_flag || a_flag || S_flag || s_flag) {
        if ((A_flag || a_flag) && parsed.sequence) {
          query += 'A';
          query += parsed.sequence.map(sym => sym.slice(0, 4) + (a_flag ? 'uu' : sym.slice(4, 6))).join('');
          query += 'T';
        }
        if ((S_flag || s_flag) && parsed.spatials) {
          query += parsed.spatials.map(spatial => spatial.symbol.slice(0, 4) + (s_flag ? 'uu' : spatial.symbol.slice(4, 6)) + (L_flag ? spatial.coord.join('x') : '')).join('');
        }
      }
      return query ? "Q" + query : undefined;
    } else {
      return undefined;
    }
  };

  //needs rewritten, but it works
  /**
   * Function to transform a range to a regular expression
   * @function fswquery.range
   * @param {(number|string)} min - either a decimal number or hexidecimal string
   * @param {(number|string)} max - either a decimal number or hexidecimal string
   * @param {boolean?} hex - if true, the regular expression will match a hexidecimal range
   * @returns {string} a regular expression that matches a range
   * @example
   * fswquery.range(500,750)
   * 
   * return '(([56][0-9][0-9])|(7[0-4][0-9])|(750))'
   * @example
   * fswquery.range('100','10e',true)
   * 
   * return '10[0-9a-e]'
   */
  const range$1 = (min, max, hex) => {
    let pattern;
    let re;
    let diff;
    let tmax;
    let cnt;
    let minV;
    let maxV;
    if (!hex) {
      hex = '';
    }
    min = ("000" + min).slice(-3);
    max = '' + max;
    pattern = '';
    if (min === max) {
      return min;
    }

    //ending pattern will be series of connected OR ranges
    re = [];

    //first pattern+  10's don't match and the min 1's are not zero
    //odd number to 9
    if (!(min[0] == max[0] && min[1] == max[1])) {
      if (min[2] != '0') {
        pattern = min[0] + min[1];
        if (hex) {
          //switch for dex
          switch (min[2]) {
            case "f":
              pattern += 'f';
              break;
            case "e":
              pattern += '[ef]';
              break;
            case "d":
            case "c":
            case "b":
            case "a":
              pattern += '[' + min[2] + '-f]';
              break;
            default:
              switch (min[2]) {
                case "9":
                  pattern += '[9a-f]';
                  break;
                case "8":
                  pattern += '[89a-f]';
                  break;
                default:
                  pattern += '[' + min[2] + '-9a-f]';
                  break;
              }
              break;
          }
          diff = 15 - parseInt(min[2], 16) + 1;
          min = '' + (parseInt(min, 16) + diff).toString(16);
          re.push(pattern);
        } else {
          //switch for dex
          switch (min[2]) {
            case "9":
              pattern += '9';
              break;
            case "8":
              pattern += '[89]';
              break;
            default:
              pattern += '[' + min[2] + '-9]';
              break;
          }
          diff = 9 - min[2] + 1;
          min = '' + (min * 1 + diff);
          re.push(pattern);
        }
      }
    }
    pattern = '';

    //if hundreds are different, get odd to 99 or ff
    if (min[0] != max[0]) {
      if (min[1] != '0') {
        if (hex) {
          //scrape to ff
          pattern = min[0];
          switch (min[1]) {
            case "f":
              pattern += 'f';
              break;
            case "e":
              pattern += '[ef]';
              break;
            case "d":
            case "c":
            case "b":
            case "a":
              pattern += '[' + min[1] + '-f]';
              break;
            case "9":
              pattern += '[9a-f]';
              break;
            case "8":
              pattern += '[89a-f]';
              break;
            default:
              pattern += '[' + min[1] + '-9a-f]';
              break;
          }
          pattern += '[0-9a-f]';
          diff = 15 - parseInt(min[1], 16) + 1;
          min = '' + (parseInt(min, 16) + diff * 16).toString(16);
          re.push(pattern);
        } else {
          //scrape to 99
          pattern = min[0];
          diff = 9 - min[1] + 1;
          switch (min[1]) {
            case "9":
              pattern += '9';
              break;
            case "8":
              pattern += '[89]';
              break;
            default:
              pattern += '[' + min[1] + '-9]';
              break;
          }
          pattern += '[0-9]';
          diff = 9 - min[1] + 1;
          min = '' + (min * 1 + diff * 10);
          re.push(pattern);
        }
      }
    }
    pattern = '';

    //if hundreds are different, get to same
    if (min[0] != max[0]) {
      if (hex) {
        diff = parseInt(max[0], 16) - parseInt(min[0], 16);
        tmax = (parseInt(min[0], 16) + diff - 1).toString(16);
        switch (diff) {
          case 1:
            pattern = min[0];
            break;
          case 2:
            pattern = '[' + min[0] + tmax + ']';
            break;
          default:
            if (parseInt(min[0], 16) > 9) {
              minV = 'h';
            } else {
              minV = 'd';
            }
            if (parseInt(tmax, 16) > 9) {
              maxV = 'h';
            } else {
              maxV = 'd';
            }
            switch (minV + maxV) {
              case "dd":
                pattern += '[' + min[0] + '-' + tmax + ']';
                break;
              case "dh":
                diff = 9 - min[0];
                //firs get up to 9
                switch (diff) {
                  case 0:
                    pattern += '[9';
                    break;
                  case 1:
                    pattern += '[89';
                    break;
                  default:
                    pattern += '[' + min[0] + '-9';
                    break;
                }
                switch (tmax[0]) {
                  case 'a':
                    pattern += 'a]';
                    break;
                  case 'b':
                    pattern += 'ab]';
                    break;
                  default:
                    pattern += 'a-' + tmax + ']';
                    break;
                }
                break;
              case "hh":
                pattern += '[' + min[0] + '-' + tmax + ']';
                break;
            }
        }
        pattern += '[0-9a-f][0-9a-f]';
        diff = parseInt(max[0], 16) - parseInt(min[0], 16);
        min = '' + (parseInt(min, 16) + diff * 256).toString(16);
        re.push(pattern);
      } else {
        diff = max[0] - min[0];
        tmax = min[0] * 1 + diff - 1;
        switch (diff) {
          case 1:
            pattern = min[0];
            break;
          case 2:
            pattern = '[' + min[0] + tmax + ']';
            break;
          default:
            pattern = '[' + min[0] + '-' + tmax + ']';
            break;
        }
        pattern += '[0-9][0-9]';
        min = '' + (min * 1 + diff * 100);
        re.push(pattern);
      }
    }
    pattern = '';

    //if tens are different, get to same
    if (min[1] != max[1]) {
      if (hex) {
        diff = parseInt(max[1], 16) - parseInt(min[1], 16);
        tmax = (parseInt(min[1], 16) + diff - 1).toString(16);
        pattern = min[0];
        switch (diff) {
          case 1:
            pattern += min[1];
            break;
          case 2:
            pattern += '[' + min[1] + tmax + ']';
            break;
          default:
            if (parseInt(min[1], 16) > 9) {
              minV = 'h';
            } else {
              minV = 'd';
            }
            if (parseInt(tmax, 16) > 9) {
              maxV = 'h';
            } else {
              maxV = 'd';
            }
            switch (minV + maxV) {
              case "dd":
                pattern += '[' + min[1];
                if (diff > 1) {
                  pattern += '-';
                }
                pattern += tmax + ']';
                break;
              case "dh":
                diff = 9 - min[1];
                //firs get up to 9
                switch (diff) {
                  case 0:
                    pattern += '[9';
                    break;
                  case 1:
                    pattern += '[89';
                    break;
                  default:
                    pattern += '[' + min[1] + '-9';
                    break;
                }
                switch (max[1]) {
                  case 'a':
                    pattern += ']';
                    break;
                  case 'b':
                    pattern += 'a]';
                    break;
                  default:
                    pattern += 'a-' + (parseInt(max[1], 16) - 1).toString(16) + ']';
                    break;
                }
                break;
              case "hh":
                pattern += '[' + min[1];
                if (diff > 1) {
                  pattern += '-';
                }
                pattern += (parseInt(max[1], 16) - 1).toString(16) + ']';
                break;
            }
            break;
        }
        pattern += '[0-9a-f]';
        diff = parseInt(max[1], 16) - parseInt(min[1], 16);
        min = '' + (parseInt(min, 16) + diff * 16).toString(16);
        re.push(pattern);
      } else {
        diff = max[1] - min[1];
        tmax = min[1] * 1 + diff - 1;
        pattern = min[0];
        switch (diff) {
          case 1:
            pattern += min[1];
            break;
          case 2:
            pattern += '[' + min[1] + tmax + ']';
            break;
          default:
            pattern += '[' + min[1] + '-' + tmax + ']';
            break;
        }
        pattern += '[0-9]';
        min = '' + (min * 1 + diff * 10);
        re.push(pattern);
      }
    }
    pattern = '';

    //if digits are different, get to same
    if (min[2] != max[2]) {
      if (hex) {
        pattern = min[0] + min[1];
        diff = parseInt(max[2], 16) - parseInt(min[2], 16);
        if (parseInt(min[2], 16) > 9) {
          minV = 'h';
        } else {
          minV = 'd';
        }
        if (parseInt(max[2], 16) > 9) {
          maxV = 'h';
        } else {
          maxV = 'd';
        }
        switch (minV + maxV) {
          case "dd":
            pattern += '[' + min[2];
            if (diff > 1) {
              pattern += '-';
            }
            pattern += max[2] + ']';
            break;
          case "dh":
            diff = 9 - min[2];
            //firs get up to 9
            switch (diff) {
              case 0:
                pattern += '[9';
                break;
              case 1:
                pattern += '[89';
                break;
              default:
                pattern += '[' + min[2] + '-9';
                break;
            }
            switch (max[2]) {
              case 'a':
                pattern += 'a]';
                break;
              case 'b':
                pattern += 'ab]';
                break;
              default:
                pattern += 'a-' + max[2] + ']';
                break;
            }
            break;
          case "hh":
            pattern += '[' + min[2];
            if (diff > 1) {
              pattern += '-';
            }
            pattern += max[2] + ']';
            break;
        }
        diff = parseInt(max[2], 16) - parseInt(min[2], 16);
        min = '' + (parseInt(min, 16) + diff).toString(16);
        re.push(pattern);
      } else {
        diff = max[2] - min[2];
        pattern = min[0] + min[1];
        switch (diff) {
          case 0:
            pattern += min[2];
            break;
          case 1:
            pattern += '[' + min[2] + max[2] + ']';
            break;
          default:
            pattern += '[' + min[2] + '-' + max[2] + ']';
            break;
        }
        min = '' + (min * 1 + diff);
        re.push(pattern);
      }
    }
    pattern = '';

    //last place is whole hundred
    if (min[2] == '0' && max[2] == '0') {
      pattern = max;
      re.push(pattern);
    }
    pattern = '';
    cnt = re.length;
    if (cnt == 1) {
      pattern = re[0];
    } else {
      pattern = re.join(')|(');
      pattern = '((' + pattern + '))';
    }
    return pattern;
  };

  const regexSymbol = sym => {
    let segment = sym.slice(0, 4);
    let fill = sym.slice(4, 5);
    if (fill == 'u') {
      segment += '[0-5]';
    } else {
      segment += fill;
    }
    let rotate = sym.slice(5, 6);
    if (rotate == 'u') {
      segment += '[0-9a-f]';
    } else {
      segment += rotate;
    }
    return segment;
  };
  const regexRange$1 = symRange => {
    let from = symRange.slice(1, 4);
    let to = symRange.slice(5, 8);
    return 'S' + range$1(from, to, 'hex') + '[0-5][0-9a-f]';
  };

  //needs rewritten, but it works
  /**
   * Function to transform an FSW query string to one or more regular expressions
   * @function fswquery.regex
   * @param {string} query - an FSW query string
   * @returns {string[]} an array of one or more regular expressions
   * @example
   * fswquery.regex('QS100uuS20500480x520')
   * 
   * return [
   *   '(?:A(?:S[123][0-9a-f]{2}[0-5][0-9a-f])+)?[BLMR]([0-9]{3}x[0-9]{3})(S[123][0-9a-f]{2}[0-5][0-9a-f][0-9]{3}x[0-9]{3})*S100[0-5][0-9a-f][0-9]{3}x[0-9]{3}(S[123][0-9a-f]{2}[0-5][0-9a-f][0-9]{3}x[0-9]{3})*',
   *   '(?:A(?:S[123][0-9a-f]{2}[0-5][0-9a-f])+)?[BLMR]([0-9]{3}x[0-9]{3})(S[123][0-9a-f]{2}[0-5][0-9a-f][0-9]{3}x[0-9]{3})*S20500((4[6-9][0-9])|(500))x((5[0-3][0-9])|(540))(S[123][0-9a-f]{2}[0-5][0-9a-f][0-9]{3}x[0-9]{3})*'
   * ]
   */
  const regex$1 = query => {
    query = query.match(new RegExp(`^${re$1.full}`))[0];
    if (!query) {
      return '';
    }
    var matches;
    var matchesOr;
    var matched;
    var orList;
    var i;
    var j;
    var segment;
    var coord;
    var x;
    var y;
    var fuzz = 20;
    var q_style = '(?:' + re$3.full + ')?';
    var q_sortable;
    if (query == 'Q') {
      return [re$4.prefix + "?" + re$4.signbox];
    }
    if (query == 'Q-') {
      return [re$4.prefix + "?" + re$4.signbox + q_style];
    }
    if (query == 'QT') {
      return [re$4.prefix + re$4.signbox];
    }
    if (query == 'QT-') {
      return [re$4.prefix + re$4.signbox + q_style];
    }
    var segments = [];
    var sortable = query.indexOf('T') + 1;
    if (sortable) {
      q_sortable = '(?:A';
      var qat = query.slice(0, sortable);
      query = query.replace(qat, '');
      if (qat == 'QT') {
        q_sortable += re$4.nullorsymbol + '+)';
      } else {
        matches = qat.match(new RegExp(re$1.list, 'g'));
        if (matches) {
          for (i = 0; i < matches.length; i += 1) {
            orList = [];
            matchesOr = matches[i].match(new RegExp(re$1.item, 'g'));
            if (matchesOr) {
              for (j = 0; j < matchesOr.length; j += 1) {
                matched = matchesOr[j].match(new RegExp(re$1.nullorsymbol));
                if (matched) {
                  orList.push(regexSymbol(matched[0]));
                } else {
                  orList.push(regexRange$1(matchesOr[j]));
                }
              }
              if (orList.length == 1) {
                q_sortable += orList[0];
              } else {
                q_sortable += '(?:' + orList.join('|') + ')';
              }
            }
          }
          q_sortable += re$4.nullorsymbol + '*)';
        }
      }
    }
    //get the variance
    matches = query.match(new RegExp(re$1.var, 'g'));
    if (matches) {
      fuzz = matches.toString().slice(1) * 1;
    }
    //this gets all symbols and ranges with or without location
    matches = query.match(new RegExp(re$1.list + re$1.coord, 'g'));
    if (matches) {
      for (i = 0; i < matches.length; i += 1) {
        orList = [];
        matchesOr = matches[i].match(new RegExp('(' + re$1.symbol + '|' + re$1.range + ')', 'g'));
        if (matchesOr) {
          for (j = 0; j < matchesOr.length; j += 1) {
            matched = matchesOr[j].match(new RegExp(re$1.symbol));
            if (matched) {
              orList.push(regexSymbol(matched[0]));
            } else {
              orList.push(regexRange$1(matchesOr[j]));
            }
          }
          if (orList.length == 1) {
            segment = orList[0];
          } else {
            segment = '(?:' + orList.join('|') + ')';
          }
        }
        if (matches[i].includes('x')) {
          coord = fsw2coord(matches[i].slice(-7));
          x = coord[0];
          y = coord[1];
          segment += range$1(x - fuzz, x + fuzz);
          segment += 'x';
          segment += range$1(y - fuzz, y + fuzz);
        } else {
          segment += re$4.coord;
        }

        // add to general fsw word
        segment = re$4.signbox + segment + '(?:' + re$4.symbol + re$4.coord + ')*';
        if (sortable) {
          segment = q_sortable + segment;
        } else {
          segment = re$4.prefix + "?" + segment;
        }
        if (query.indexOf('-') > 0) {
          segment += q_style;
        }
        segments.push(segment);
      }
    }
    if (!segments.length) {
      if (query.indexOf('-') > 0) {
        segment += q_style;
      }
      segments.push(q_sortable + re$4.signbox);
    }
    return segments;
  };

  //needs rewritten, but it works
  /**
   * Function that uses a query string to match signs from a string of text.
   * @function fswquery.results
   * @param {string} query - an FSW query string
   * @param {string} text - a string of text containing multiple signs
   * @returns {string[]} an array of FSW signs
   * @example
   * fswquery.results('QAS10011T','AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475 AS15a21S15a07S21100S2df04S2df14M521x538S15a07494x488S15a21498x489S2df04498x517S2df14497x461S21100479x486 AS1f010S10018S20600M519x524S10018485x494S1f010490x494S20600481x476')
   * 
   * return [
   *   'AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475'
   * ]
   */
  const results$1 = (query, text) => {
    if (!text) {
      return [];
    }
    let pattern;
    let matches;
    let parts;
    let words;
    let re = regex$1(query);
    if (!re) {
      return [];
    }
    let i;
    for (i = 0; i < re.length; i += 1) {
      pattern = re[i];
      matches = text.match(new RegExp(pattern, 'g'));
      if (matches) {
        text = matches.join(' ');
      } else {
        text = '';
      }
    }
    if (text) {
      parts = text.split(' ');
      words = parts.filter(function (element) {
        return element in parts ? false : parts[element] = true;
      }, {});
    } else {
      words = [];
    }
    return words;
  };

  //needs rewritten, but it works
  /**
   * Function that uses an FSW query string to match signs from multiple lines of text.
   * @function fswquery.lines
   * @param {string} query - an FSW query string
   * @param {string} text - multiple lines of text, each starting with an FSW sign
   * @returns {string[]} an array of lines of text, each starting with an FSW sign
   * @example
   * fswquery.lines('QAS10011T',`AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475 line one
   * AS15a21S15a07S21100S2df04S2df14M521x538S15a07494x488S15a21498x489S2df04498x517S2df14497x461S21100479x486 line two
   * AS1f010S10018S20600M519x524S10018485x494S1f010490x494S20600481x476 line three`)
   * 
   * return [
   *   'AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475 line one'
   * ]
   */
  const lines$1 = (query, text) => {
    if (!text) {
      return [];
    }
    let pattern;
    let matches;
    let parts;
    let words;
    let re = regex$1(query);
    if (!re) {
      return [];
    }
    let i;
    for (i = 0; i < re.length; i += 1) {
      pattern = re[i];
      pattern = '^' + pattern + '.*';
      matches = text.match(new RegExp(pattern, 'mg'));
      if (matches) {
        text = matches.join("\n");
      } else {
        text = '';
      }
    }
    if (text) {
      parts = text.split("\n");
      words = parts.filter(function (element) {
        return element in parts ? false : parts[element] = true;
      }, {});
    } else {
      words = [];
    }
    return words;
  };

  /** The fswquery module contains functions for handling the FSW query language.
   * [Query Language definition](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-09.html#name-query-language)
   * @module fswquery
   */

  var index$2 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    re: re$1,
    parse: parse$2,
    compose: compose$2,
    fsw2query: fsw2query,
    range: range$1,
    regex: regex$1,
    results: results$1,
    lines: lines$1
  });

  const parse$1 = {
    /**
     * Function to parse an swu symbol with optional coordinate and style string
     * @function swu.parse.symbol
     * @param {string} swuSym - an swu symbol
     * @returns {SymbolObject} elements of swu symbol
     * @example
     * swu.parse.symbol('񀀁𝤆𝤆-C')
     * 
     * return {
     *  'symbol': '񀀁',
     *  'coord': [500, 500],
     *  'style': '-C'
     * }
     */
    symbol: swuSym => {
      const regex = `^(${re$2.symbol})(${re$2.coord})?(${re$3.full})?`;
      const symbol = typeof swuSym === 'string' ? swuSym.match(new RegExp(regex)) : undefined;
      return {
        'symbol': symbol ? symbol[1] : undefined,
        'coord': symbol && symbol[2] ? swu2coord(symbol[2]) : undefined,
        'style': symbol ? symbol[3] : undefined
      };
    },
    /**
      * Function to parse an swu sign with style string
      * @function swu.parse.sign
      * @param {string} swuSign - an swu sign
      * @returns {SignObject} elements of swu sign
      * @example
      * swu.parse.sign('𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭-C')
      * 
      * return {
      *  sequence: ['񀀒','񀀚','񋚥','񋛩'],
      *  box: '𝠃',
      *  max: [525, 535],
      *  spatials: [
      *    {
      *      symbol: '񋛩',
      *      coord: [483, 510]
      *    },
      *    {
      *      symbol: '񀀒',
      *      coord: [501, 466]
      *    },
      *    {
      *      symbol: '񋚥',
      *      coord: [510, 500]
      *    },
      *    {
      *      symbol: '񀀚',
      *      coord: [476, 475]
      *    }
      *  ],
      *  style: '-C'
      * }
      */
    sign: swuSign => {
      const regex = `^(${re$2.prefix})?(${re$2.signbox})(${re$3.full})?`;
      const sign = typeof swuSign === 'string' ? swuSign.match(new RegExp(regex)) : undefined;
      if (sign) {
        return {
          'sequence': sign[1] ? sign[1].slice(2).match(/.{2}/g) : undefined,
          'box': sign[2].slice(0, 2),
          'max': swu2coord(sign[2].slice(2, 6)),
          'spatials': sign[2].length < 7 ? undefined : sign[2].slice(6).match(/(.{6})/g).map(m => {
            return {
              symbol: m.slice(0, 2),
              coord: swu2coord(m.slice(2))
            };
          }),
          'style': sign[3]
        };
      } else {
        return {};
      }
    },
    /**
     * Function to parse an swu text
     * @function swu.parse.text
     * @param {string} swuText - an swu text
     * @returns {string[]} swu signs and punctuations
     * @example
     * swu.parse.text('𝠀񁲡񈩧𝠃𝤘𝤣񁲡𝣳𝣩񈩧𝤉𝣻 𝠀񃊢񃊫񋛕񆇡𝠃𝤘𝤧񃊫𝣻𝤕񃊢𝣴𝣼񆇡𝤎𝤂񋛕𝤆𝣦 񏌁𝣢𝤂')
     * 
     * return [
     *  '𝠀񁲡񈩧𝠃𝤘𝤣񁲡𝣳𝣩񈩧𝤉𝣻',
     *  '𝠀񃊢񃊫񋛕񆇡𝠃𝤘𝤧񃊫𝣻𝤕񃊢𝣴𝣼񆇡𝤎𝤂񋛕𝤆𝣦',
     *  '񏌁𝣢𝤂'
     * ]
     */
    text: swuText => {
      if (typeof swuText !== 'string') return [];
      const regex = `(${re$2.sign}(${re$3.full})?|${re$2.spatial}(${re$3.full})?)`;
      const matches = swuText.match(new RegExp(regex, 'g'));
      return matches ? [...matches] : [];
    }
  };

  /**
   * Function to encode SWU characters using the UTF-16 escape format.
   * @function swu.encode
   * @param {string} swu - SWU characters
   * @returns {string} UTF-16 escape format
   * @example
   * swu.encode('񀀁𝤆𝤆')
   * 
   * return '\\uD8C0\\uDC01\\uD836\\uDD06\\uD836\\uDD06'
   */
  const encode = swu => swu.replace(/[\u007F-\uFFFF]/g, function (chr) {
    return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4).toUpperCase();
  });

  /**
   * Function to decode UTF-16 escape format to SWU characters.
   * @function swu.decode
   * @param {string} encoded - UTF-16 escape format
   * @returns {string} SWU characters
   * @example
   * swu.decode('\\uD8C0\\uDC01\\uD836\\uDD06\\uD836\\uDD06')
   * 
   * return '񀀁𝤆𝤆'
   */
  const decode = encoded => encoded.replace(/\\u([0-9A-F]{4})/g, function (match, chr) {
    return String.fromCharCode(parseInt(chr, 16));
  });

  /**
   * Function to decompose an SWU character into UTF-16 surrogate pairs.
   * @function swu.pair
   * @param {string} swuChar - an SWU character
   * @returns {string[]} an array of UTF-16 surrogate pairs
   * @example
   * swu.pair('񀀁')
   * 
   * return ['D8C0', 'DC01']
   */
  const pair = swuChar => [swuChar.charCodeAt(0).toString(16).toUpperCase(), swuChar.charCodeAt(1).toString(16).toUpperCase()];

  const compose$1 = {
    /**
     * Function to compose an swu symbol with optional coordinate and style string
     * @function swu.compose.symbol
     * @param {SymbolObject} swuSymObject - an swu symbol object
     * @returns {string} an swu symbol string
     * @example
     * swu.compose.symbol({
     *  'symbol': '񀀁',
     *  'coord': [500, 500],
     *  'style': '-C'
     * })
     * 
     * return '񀀁𝤆𝤆-C'
     */
    symbol: swuSymObject => {
      if (typeof swuSymObject !== 'object' || swuSymObject === null) return undefined;
      if (typeof swuSymObject.symbol === 'string') {
        const symbol = (swuSymObject.symbol.match(re$2.symbol) || [''])[0];
        if (symbol) {
          const x = swuSymObject.coord && swuSymObject.coord[0] || '';
          const y = swuSymObject.coord && swuSymObject.coord[1] || '';
          const coord = x && y ? coord2swu([x, y]) : '';
          const styleStr = typeof swuSymObject.style === 'string' && (swuSymObject.style.match(re$3.full) || [''])[0] || '';
          return symbol + coord + styleStr;
        }
      }
      return undefined;
    },
    /**
     * Function to compose an swu sign with style string
     * @function swu.compose.sign
     * @param {SignObject} swuSignObject - an swu sign object
     * @returns {string} an swu sign string
     * @example
     * swu.compose.sign({
     *  sequence: ['񀀒','񀀚','񋚥','񋛩'],
     *  box: '𝠃',
     *  max: [525, 535],
     *  spatials: [
     *    {
     *      symbol: '񋛩',
     *      coord: [483, 510]
     *    },
     *    {
     *      symbol: '񀀒',
     *      coord: [501, 466]
     *    },
     *    {
     *      symbol: '񋚥',
     *      coord: [510, 500]
     *    },
     *    {
     *      symbol: '񀀚',
     *      coord: [476, 475]
     *    }
     *  ],
     *  style: '-C'
     * })
     * 
     * return '𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭-C'
     */
    sign: swuSignObject => {
      if (typeof swuSignObject !== 'object' || swuSignObject === null) return undefined;
      let box = typeof swuSignObject.box !== 'string' ? '𝠃' : (swuSignObject.box + '𝠃').match(re$2.box);
      const x = swuSignObject.max && swuSignObject.max[0] || '';
      const y = swuSignObject.max && swuSignObject.max[1] || '';
      const max = x && y ? coord2swu([x, y]) : undefined;
      if (!max) return undefined;
      let prefix = '';
      if (swuSignObject.sequence && Array.isArray(swuSignObject.sequence)) {
        prefix = swuSignObject.sequence.map(key => (key.match(re$2.nullorsymbol) || [''])[0]).join('');
        prefix = prefix ? '𝠀' + prefix : '';
      }
      let signbox = '';
      if (swuSignObject.spatials && Array.isArray(swuSignObject.spatials)) {
        signbox = swuSignObject.spatials.map(spatial => {
          if (typeof spatial.symbol === 'string') {
            const symbol = (spatial.symbol.match(re$2.symbol) || [''])[0];
            if (symbol) {
              const x = spatial.coord && spatial.coord[0] || '';
              const y = spatial.coord && spatial.coord[1] || '';
              const coord = x && y ? coord2swu([x, y]) : '';
              if (coord) {
                return symbol + coord;
              }
            }
          }
          return '';
        }).join('');
      }
      const styleStr = typeof swuSignObject.style === 'string' && (swuSignObject.style.match(re$3.full) || [''])[0] || '';
      return prefix + box + max + signbox + styleStr;
    }
  };

  /**
   * Function to gather sizing information about an swu sign or symbol
   * @function swu.info
   * @param {string} swu - an swu sign or symbol
   * @returns {SegmentInfo} information about the swu string
   * @example
   * swu.info('𝠀񁲡񈩧𝠂𝤘𝤣񁲡𝣳𝣩񈩧𝤉𝣻-P10Z2')
   * 
   * return {
   *   minX: 481,
   *   minY: 471,
   *   width: 37,
   *   height: 58,
   *   lane: -1,
   *   padding: 10,
   *   segment: 'sign',
   *   zoom: 2
   * }
   */
  const info = swu => {
    let lanes = {
      '𝠁': 0,
      '𝠂': -1,
      '𝠃': 0,
      '𝠄': 1
    };
    let parsed = parse$1.sign(swu);
    let width, height, segment, x1, x2, y1, y2, lane;
    if (parsed.spatials) {
      x1 = Math.min(...parsed.spatials.map(spatial => spatial.coord[0]));
      x2 = parsed.max[0];
      width = x2 - x1;
      y1 = Math.min(...parsed.spatials.map(spatial => spatial.coord[1]));
      y2 = parsed.max[1];
      height = y2 - y1;
      segment = 'sign';
      lane = parsed.box;
    } else {
      parsed = parse$1.symbol(swu);
      lane = "𝠃";
      if (parsed.coord) {
        x1 = parsed.coord[0];
        width = (500 - x1) * 2;
        y1 = parsed.coord[1];
        height = (500 - y1) * 2;
        segment = 'symbol';
      } else {
        x1 = 490;
        width = 20;
        y1 = 490;
        height = 20;
        segment = 'none';
      }
    }
    let style = parse$4(parsed.style);
    let zoom = style.zoom || 1;
    let padding = style.padding || 0;
    return {
      minX: x1,
      minY: y1,
      width: width,
      height: height,
      segment: segment,
      lane: lanes[lane],
      padding: padding,
      zoom: zoom
    };
  };

  const columnDefaults = {
    'height': 500,
    'width': 150,
    'offset': 50,
    'pad': 20,
    'margin': 5,
    'dynamic': false,
    'background': undefined,
    'punctuation': {
      'spacing': true,
      'pad': 30,
      'pull': true
    },
    'style': {
      'detail': ['black', 'white'],
      'zoom': 1
    }
  };

  /**
   * Function to an object of column options with default values
   * 
   * @function swu.columnDefaultsMerge
   * @param {ColumnOptions} options - object of column options
   * @returns {ColumnOptions} object of column options merged with column defaults
   * @example
   * swu.columnDefaultsMerge({height: 500,width:150})
   * 
   * return {
   *   "height": 500,
   *   "width": 150,
   *   "offset": 50,
   *   "pad": 20,
   *   "margin": 5,
   *   "dynamic": false,
   *   "punctuation": {
   *     "spacing": true,
   *     "pad": 30,
   *     "pull": true
   *   },
   *   "style": {
   *     "detail": [
   *       "black",
   *       "white"
   *     ],
   *     "zoom": 1
   *   }
   * }
   */
  const columnDefaultsMerge = options => {
    if (typeof options !== 'object') options = {};
    return {
      ...columnDefaults,
      ...options,
      punctuation: {
        ...columnDefaults.punctuation,
        ...options.punctuation
      },
      style: {
        ...columnDefaults.style,
        ...options.style
      }
    };
  };

  /**
   * Function to transform an SWU text to an array of columns
   * 
   * @function swu.columns
   * @param {string} swuText - SWU text of signs and punctuation
   * @param {ColumnOptions} options - object of column options
   * @returns {{options:ColumnOptions,widths:number[],columns:ColumnData}} object of column options, widths array, and column data
   * @example
   * swu.columns('𝠀񁲡񈩧𝠃𝤘𝤣񁲡𝣳𝣩񈩧𝤉𝣻 𝠀񃊢񃊫񋛕񆇡𝠃𝤘𝤧񃊫𝣻𝤕񃊢𝣴𝣼񆇡𝤎𝤂񋛕𝤆𝣦 񏌁𝣢𝤂', {height: 500,width:150})
   * 
   * return {
   *   "options": {
   *     "height": 500,
   *     "width": 150,
   *     "offset": 50,
   *     "pad": 20,
   *     "margin": 5,
   *     "dynamic": false,
   *     "punctuation": {
   *       "spacing": true,
   *       "pad": 30,
   *       "pull": true
   *     },
   *     "style": {
   *       "detail": [
   *         "black",
   *         "white"
   *       ],
   *       "zoom": 1
   *     }
   *   },
   *   "widths": [
   *     150
   *   ],
   *   "columns": [
   *     [
   *       {
   *         "x": 56,
   *         "y": 20,
   *         "minX": 481,
   *         "minY": 471,
   *         "width": 37,
   *         "height": 58,
   *         "lane": 0,
   *         "padding": 0,
   *         "segment": "sign",
   *         "text": "𝠀񁲡񈩧𝠃𝤘𝤣񁲡𝣳𝣩񈩧𝤉𝣻",
   *         "zoom": 1
   *       },
   *       {
   *         "x": 57,
   *         "y": 118,
   *         "minX": 482,
   *         "minY": 468,
   *         "width": 36,
   *         "height": 65,
   *         "lane": 0,
   *         "padding": 0,
   *         "segment": "sign",
   *         "text": "𝠀񃊢񃊫񋛕񆇡𝠃𝤘𝤧񃊫𝣻𝤕񃊢𝣴𝣼񆇡𝤎𝤂񋛕𝤆𝣦",
   *         "zoom": 1
   *       },
   *       {
   *         "x": 39,
   *         "y": 203,
   *         "minX": 464,
   *         "minY": 496,
   *         "width": 72,
   *         "height": 8,
   *         "lane": 0,
   *         "padding": 0,
   *         "segment": "symbol",
   *         "text": "񏌁𝣢𝤂",
   *         "zoom": 1
   *       }
   *     ]
   *   ]
   * }
   */
  const columns = (swuText, options) => {
    if (typeof swuText !== 'string') return {};
    const values = columnDefaultsMerge(options);
    let input = parse$1.text(swuText);
    let cursor = 0;
    let cols = [];
    let col = [];
    let plus = 0;
    let center = parseInt(values.width / 2);
    let maxHeight = values.height - values.margin;
    let pullable = true;
    let finalize = false;
    for (let val of input) {
      let informed = info(val);
      cursor += plus;
      if (values.punctuation.spacing) {
        cursor += informed.segment == 'sign' ? values.pad : 0;
      } else {
        cursor += values.pad;
      }
      finalize = cursor + informed.height > maxHeight;
      if (finalize && informed.segment == 'symbol' && values.punctuation.pull && pullable) {
        finalize = false;
        pullable = false;
      }
      if (col.length == 0) {
        finalize = false;
      }
      if (finalize) {
        cursor = values.pad;
        cols.push(col);
        col = [];
        pullable = true;
      }
      col.push(Object.assign(informed, {
        x: center + values.offset * informed.lane - (500 - informed.minX) * informed.zoom * values.style.zoom,
        y: cursor,
        text: val
      }));
      cursor += informed.height * informed.zoom * values.style.zoom;
      if (values.punctuation.spacing) {
        plus = informed.segment == 'sign' ? values.pad : values.punctuation.pad;
      } else {
        plus = values.pad;
      }
    }
    if (col.length) {
      cols.push(col);
    }

    // over height issue when pulling punctuation
    if (values.punctuation.pull) {
      for (let col of cols) {
        let last = col[col.length - 1];
        let diff = last.y + last.height - (values.height - values.margin);
        if (diff > 0) {
          let adj = parseInt(diff / col.length) + 1;
          for (let i in col) {
            col[i].y -= adj * i + adj;
          }
        }
      }
    }

    // contract, expand, adjust
    let widths = [];
    for (let col of cols) {
      let min = [center - values.offset - values.pad];
      let max = [center + values.offset + values.pad];
      for (let item of col) {
        min.push(item.x - values.pad);
        max.push(item.x + item.width + values.pad);
      }
      min = Math.min(...min);
      max = Math.max(...max);
      let width = values.width;
      let adj = 0;
      if (!values.dynamic) {
        adj = center - parseInt((min + max) / 2);
      } else {
        width = max - min;
        adj = -min;
      }
      for (let item of col) {
        item.x += adj;
      }
      widths.push(width);
    }
    return {
      'options': values,
      'widths': widths,
      'columns': cols
    };
  };

  /**
   * Array of plane 4 code points for kinds of symbols: writing, location, and punctuation.
   * @alias swu.kind
   * @type {array}
   */
  const kind = [0x40001, 0x4efa1, 0x4f2a1];

  /**
   * Array of plane 4 code points for categories of symbols: hand, movement, dynamics, head, trunk & limb, location, and punctuation.
   * @alias swu.category
   * @type {array}
   */
  const category = [0x40001, 0x461e1, 0x4bca1, 0x4bfa1, 0x4e8e1, 0x4efa1, 0x4f2a1];

  /**
   * Array of plane 4 code points for the 30 symbol groups.
   * @alias swu.group
   * @type {array}
   */
  const group = [0x40001, 0x40541, 0x40b41, 0x41981, 0x41c81, 0x43241, 0x43d81, 0x445c1, 0x44ce1, 0x45be1, 0x461e1, 0x46841, 0x46fc1, 0x47fe1, 0x485e1, 0x49301, 0x49e41, 0x4a4a1, 0x4afe1, 0x4b521, 0x4bca1, 0x4bfa1, 0x4c3c1, 0x4cfc1, 0x4d621, 0x4e161, 0x4e8e1, 0x4ec41, 0x4efa1, 0x4f2a1];

  /**
   * Object of symbol ranges with starting and ending code points on plane 4.
   * 
   *   { all, writing, hand, movement, dynamic, head, hcenter, vcenter, trunk, limb, location, punctuation }
   * @alias swu.ranges
   * @type {object}
   */
  const ranges = {
    'all': [0x40001, 0x4f480],
    'writing': [0x40001, 0x4efa0],
    'hand': [0x40001, 0x461e0],
    'movement': [0x461e1, 0x4bca0],
    'dynamic': [0x4bca1, 0x4bfa0],
    'head': [0x4bfa1, 0x4e8e0],
    'hcenter': [0x4bfa1, 0x4e8e0],
    'vcenter': [0x4bfa1, 0x4ec40],
    'trunk': [0x4e8e1, 0x4ec40],
    'limb': [0x4ec41, 0x4efa0],
    'location': [0x4efa1, 0x4f2a0],
    'punctuation': [0x4f2a1, 0x4f480]
  };

  /**
   * Function to test if symbol is of a certain type.
   * @function swu.isType
   * @param {string} swuSym - an SWU symbol character
   * @param {string} type - the name of a symbol range
   * @returns {boolean} is symbol of specified type
   * @example
   * swu.isType('񀀁', 'hand')
   * 
   * return true
   */
  const isType = (swuSym, type) => {
    const parsed = parse$1.symbol(swuSym);
    if (parsed.symbol) {
      const code = swu2code(parsed.symbol);
      const range = ranges[type];
      if (range) {
        return range[0] <= code && range[1] >= code;
      }
    }
    return false;
  };

  /**
   * Array of colors associated with the seven symbol categories.
   * @alias swu.colors
   * @type {array}
   */
  const colors = ['#0000CC', '#CC0000', '#FF0099', '#006600', '#000000', '#884411', '#FF9900'];

  /**
   * Function that returns the standardized color for a symbol.
   * @function swu.colorize
   * @param {string} swuSym - an SWU symbol character
   * @returns {string} name of standardized color for symbol
   * @example
   * swu.colorize('񀀁')
   * 
   * return '#0000CC'
   */
  const colorize = swuSym => {
    const parsed = parse$1.symbol(swuSym);
    let color = '#000000';
    if (parsed.symbol) {
      const code = swu2code(parsed.symbol);
      const index = category.findIndex(val => val > code);
      color = colors[index < 0 ? 6 : index - 1];
    }
    return color;
  };

  /** The swu module contains functions for handling SignWriting in Unicode (SWU) characters.
   * [SWU characters definition](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-09.html#name-signwriting-in-unicode-swu)
   * @module swu
   */

  var index$1 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    re: re$2,
    parse: parse$1,
    encode: encode,
    decode: decode,
    pair: pair,
    compose: compose$1,
    info: info,
    columnDefaults: columnDefaults,
    columnDefaultsMerge: columnDefaultsMerge,
    columns: columns,
    kind: kind,
    category: category,
    group: group,
    ranges: ranges,
    isType: isType,
    colors: colors,
    colorize: colorize
  });

  /**
   * Object of regular expressions for SWU query strings
   * 
   * @alias swuquery.re
   * @type {object}
   * @property {string} null - the null symbol
   * @property {string} base - SWU symbol
   * @property {string} coord - SWU coordinate of X and Y number characters
   * @property {string} var - variance string for searching sign box
   * @property {string} symbol - SWU symbol character with ignore fill and rotation flags
   * @property {string} nullorsymbol - null or a symbol
   * @property {string} range - SWU range starting with 'R'
   * @property {string} item - SWU symbol or range query string
   * @property {string} list - several SWU symbols and SWU ranges as a logical OR for searching 
   * @property {string} prefix - a sequential list of SWU symbol characters with nulls starting with SWU 'A' character
   * @property {string} signbox - several groups of SWU lists, each group having a coordinate
   * @property {string} full - a query string to search prefix in order and the signbox with variance
   */
  let re = {
    'null': '\uD8C0\uDC00',
    'base': '(?:(?:\uD8C0[\uDC01-\uDFFF])|(?:[\uD8C1-\uD8FC][\uDC00-\uDFFF])|(?:\uD8FD[\uDC00-\uDC80]))',
    'coord': '(?:(?:\uD836[\uDC0C-\uDDFF]){2})?',
    'var': 'V[0-9]+'
  };
  re.symbol = `${re.base}f?r?`;
  re.nullorsymbol = `(?:${re.null}|${re.symbol})`;
  re.range = `R${re.base}${re.base}`;
  re.item = `(?:${re.null}|${re.symbol}|${re.range})`;
  re.list = `${re.item}(?:o${re.item})*`;
  re.prefix = `(?:A(?:${re.list})+)?T`;
  re.signbox = `(?:${re.list}${re.coord})*`;
  re.full = `Q(${re.prefix})?(${re.signbox})?(${re.var})?(-?)`;

  const parsePrefix = text => {
    return {
      required: true,
      parts: text == 'T' ? undefined : text.match(new RegExp(`(${re.list})`, 'g')).map(part => {
        if (part.includes('o')) {
          return ['or'].concat(part.match(new RegExp(`(${re.item})`, 'g')).map(part => part[0] != 'R' ? part : [part.slice(1, 3), part.slice(3, 5)]));
        } else {
          return part[0] != 'R' ? part : [part.slice(1, 3), part.slice(3, 5)];
        }
      })
    };
  };
  const parseSignbox = text => {
    return text.match(new RegExp(`(${re.list}${re.coord})`, 'g')).map(part => {
      let coord, front;
      coord = part.match(new RegExp(`${re$2.coord}`));
      if (coord) {
        coord = swu2coord(coord[0]);
        front = part.slice(0, -4);
      } else {
        coord = undefined;
        front = part;
      }
      if (front.includes('o')) {
        return {
          or: front.split('o').map(part => {
            if (!part.includes('R')) {
              return part;
            } else {
              return [part.slice(1, 3), part.slice(3, 5)];
            }
          }),
          coord,
          coord
        };
      } else if (!front.includes('R')) {
        return {
          symbol: front,
          coord: coord
        };
      } else {
        return {
          range: [front.slice(1, 3), front.slice(3, 5)],
          coord: coord
        };
      }
    });
  };

  /**
   * Function to parse SWU query string to object
   * @function swuquery.parse
   * @param {string} swuQueryString - an SWU query string
   * @returns {QueryObject} elements of an SWU query string
   * @example
   * swuquery.parse('QA񀀁R񀀁񆆑񆇡T񆀁R񀀁񀇱𝤆𝤆V5-')
   * 
   * return { 
   *   query: true,
   *   prefix: {
   *     required: true,
   *     parts: [
   *       '񀀁',
   *       ['񀀁', '񆆑'],
   *       '񆇡'
   *     ]
   *   },
   *   signbox: [
   *     { symbol: '񆀁' },
   *     {
   *       range: ['񀀁', '񀇱'],
   *       coord: [500, 500]
   *     }
   *   ],
   *   variance: 5,
   *   style: true 
   * }
   */
  const parse = swuQueryString => {
    const query = typeof swuQueryString === 'string' ? swuQueryString.match(new RegExp(`^${re.full}`)) : undefined;
    return {
      'query': query ? true : undefined,
      'prefix': query && query[1] ? parsePrefix(query[1]) : undefined,
      'signbox': query && query[2] ? parseSignbox(query[2]) : undefined,
      'variance': query && query[3] ? parseInt(query[3].slice(1)) : undefined,
      'style': query && query[4] ? true : undefined
    };
  };

  /**
   * Function to compose SWU query string from object
   * @function swuquery.compose
   * @param {QueryObject} swuQueryObject - an object of query options
   * @returns {string} SWU query string 
   * @example 
   * swuquery.compose({ 
    *   query: true,
    *   prefix: {
    *     required: true,
    *     parts: [
    *       '񀀁',
    *       ['񀀁', '񆆑'],
    *       '񆇡'
    *     ]
    *   },
    *   signbox: [
    *     { symbol: '񆀁' },
    *     {
    *       range: ['񀀁', '񀇱'],
    *       coord: [500, 500]
    *     }
    *   ],
    *   variance: 5,
    *   style: true 
    * })
    *
    * return 'QA񀀁R񀀁񆆑񆇡T񆀁R񀀁񀇱𝤆𝤆V5-'
    */
  const compose = swuQueryObject => {
    if (!swuQueryObject || !swuQueryObject.query) {
      return undefined;
    }
    let query = 'Q';
    if (swuQueryObject.prefix && swuQueryObject.prefix.required) {
      if (Array.isArray(swuQueryObject.prefix.parts)) {
        query += 'A';
        query += swuQueryObject.prefix.parts.map(part => {
          if (typeof part === 'string') {
            return part;
          } else {
            if (Array.isArray(part) && part.length == 2) {
              return `R${part[0]}${part[1]}`;
            } else if (Array.isArray(part) && part.length > 2 && part[0] == 'or') {
              part.shift();
              return part.map(part => {
                if (typeof part === 'string') {
                  return part;
                } else {
                  if (Array.isArray(part) && part.length == 2) {
                    return `R${part[0]}${part[1]}`;
                  }
                }
              }).join('o');
            }
          }
        }).join('');
      }
      query += 'T';
    }
    if (Array.isArray(swuQueryObject.signbox)) {
      query += swuQueryObject.signbox.map(part => {
        let out;
        if (part.or) {
          out = part.or.map(item => {
            if (typeof item === 'string') {
              return item;
            } else {
              if (Array.isArray(item) && item.length == 2) {
                return `R${item[0]}${item[1]}`;
              }
            }
          }).join('o');
        } else if (part.symbol) {
          out = part.symbol;
        } else {
          if (part.range && Array.isArray(part.range) && part.range.length == 2) {
            out = `R${part.range[0]}${part.range[1]}`;
          }
        }
        return out + (Array.isArray(part.coord) && part.coord.length == 2 ? coord2swu(part.coord) : '');
      }).join('');
    }
    query += swuQueryObject.style ? '-' : '';
    query = query.match(new RegExp(`^${re.full}`))[0];
    return query;
  };

  /**
   * Function to convert an SWU sign to a query string
   * 
   * For the flags parameter, use one or more of the following.
   * - A: exact symbol in temporal prefix
   * - a: general symbol in temporal prefix
   * - S: exact symbol in spatial signbox
   * - s: general symbol in spatial signbox
   * - L: spatial signbox symbol at location
   * @function swuquery.swu2query
   * @param {string} swuSign - SWU sign
   * @param {string} flags - flags for query string creation
   * @returns {string} SWU query string
   * @example
   * swuquery.swu2query('𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭', 'ASL')
   * 
   * return 'QA񀀒񀀚񋚥񋛩T񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭'
   */
  const swu2query = (swuSign, flags) => {
    let query = '';
    const parsed = parse$1.sign(swuSign);
    if (parsed.box) {
      const A_flag = flags.indexOf('A') > -1;
      const a_flag = flags.indexOf('a') > -1;
      const S_flag = flags.indexOf('S') > -1;
      const s_flag = flags.indexOf('s') > -1;
      const L_flag = flags.indexOf('L') > -1;
      if (A_flag || a_flag || S_flag || s_flag) {
        if ((A_flag || a_flag) && parsed.sequence) {
          query += 'A';
          query += parsed.sequence.map(sym => sym + (a_flag ? 'fr' : '')).join('');
          query += 'T';
        }
        if ((S_flag || s_flag) && parsed.spatials) {
          query += parsed.spatials.map(spatial => spatial.symbol + (s_flag ? 'fr' : '') + (L_flag ? coord2swu(spatial.coord) : '')).join('');
        }
      }
      return query ? "Q" + query : undefined;
    } else {
      return undefined;
    }
  };

  //needs rewritten, but it works
  /**
   * Function to transform a range of SWU characters to a regular expression
   * @function swuquery.range
   * @param {string} min - an SWU character
   * @param {string} max - an SWU character
   * @returns {string} a regular expression that matches a range of SWU characters
   * @example
   * swuquery.range('񀀁', '񀇡')
   * 
   * return '\uD8C0[\uDC01-\uDDE1]'
   * @example
   * swuquery.range('𝣔', '𝤸')
   * 
   * return '\uD836[\uDCD4-\uDD38]'
   */
  const range = (min, max) => {
    if (min > max) return '';
    let pattern = '';
    let cnt;
    let re = [];
    min = pair(min);
    max = pair(max);
    if (min.length != 2 && max.length != 2) return '';
    // HEAD // min[0] with range of min[1] to (DFFF or max[1])
    if (min[0] == max[0]) {
      if (min[1] == max[1]) {
        pattern = '\\u' + min[0] + '\\u' + min[1];
        re.push(pattern);
      } else {
        pattern = '\\u' + min[0] + '[\\u' + min[1] + '-\\u' + max[1] + ']';
        re.push(pattern);
      }
    } else {
      if (min[1] == "DFFF") {
        pattern = '\\u' + min[0] + '\\uDFFF';
      } else {
        pattern = '\\u' + min[0] + '[\\u' + min[1] + '-\\uDFFF]';
      }
      re.push(pattern);

      // BODY // range of (min[0] +1) to (max[0] -1) with all DC00-DFFF
      let diff = parseInt(max[0], 16) - parseInt(min[0], 16);
      if (diff == 2) {
        pattern = '\\u' + (parseInt(min[0], 16) + 1).toString(16).toUpperCase();
        pattern += '[\\uDC00-\\uDFFF]';
        re.push(pattern);
      }
      if (diff > 2) {
        pattern = '[';
        pattern += '\\u' + (parseInt(min[0], 16) + 1).toString(16).toUpperCase();
        pattern += '-\\u' + (parseInt(max[0], 16) - 1).toString(16).toUpperCase();
        pattern += '][\\uDC00-\\uDFFF]';
        re.push(pattern);
      }

      // TAIL // max[0] with range of DC00 to max[1]
      if (max[1] == "DC00") {
        pattern = '\\u' + max[0] + '\\uDC00';
      } else {
        pattern = '\\u' + max[0] + '[\\uDC00-\\u' + max[1] + ']';
      }
      re.push(pattern);
    }
    cnt = re.length;
    if (cnt == 1) {
      pattern = re[0];
    } else {
      pattern = re.join(')|(');
      pattern = '((' + pattern + '))';
    }
    return decode(pattern);
  };

  //needs rewritten, but it works
  /**
   * Function to transform an SWU symbol with fill and rotation flags to a regular expression
   * @function swuquery.symbolRanges
   * @param {string} symbolFR - an SWU character with optional flags of 'f' for any fill and 'r' for any rotation
   * @returns {string} a regular expression that matches one or more ranges of SWU symbols
   * @example <caption>Match an exact symbol</caption>
   * swuquery.symbolRanges('񀀁')
   * 
   * return '\uD8C0\uDC01');
   * @example <caption>Match a symbol with any fill</caption>
   * swuquery.symbolRanges('񀀁f')
   * 
   * return '(\uD8C0\uDC01|\uD8C0\uDC11|\uD8C0\uDC21|\uD8C0\uDC31|\uD8C0\uDC41|\uD8C0\uDC51)'
   * @example <caption>Match a symbol with any rotation</caption>
   * swuquery.symbolRanges('񀀁r')
   * 
   * return '\uD8C0[\uDC01-\uDC10]'
   * @example <caption>Match a symbol with any fill or rotation</caption>
   * swuquery.symbolRanges('񀀁fr')
   * 
   * return '\uD8C0[\uDC01-\uDC60]'
   */
  const symbolRanges = symbolFR => {
    let match = symbolFR.match(new RegExp(re.symbol));
    if (match) {
      let sym = match[0].slice(0, 2);
      let key = swu2key(sym);
      let base = key.slice(0, 4);
      let start, end;
      if (match[0].slice(-2) == 'fr') {
        start = key2swu(base + "00");
        end = key2swu(base + "5f");
        return range(start, end);
      } else if (match[0].slice(-1) == 'r') {
        start = key2swu(key.slice(0, 5) + '0');
        end = key2swu(key.slice(0, 5) + 'f');
        return range(start, end);
      } else if (match[0].slice(-1) == 'f') {
        let list = [0, 1, 2, 3, 4, 5].map(function (f) {
          return key2swu(base + f + key.slice(-1));
        });
        return "(" + list.join("|") + ")";
      } else {
        return sym;
      }
    } else {
      return '';
    }
  };

  const regexRange = symRange => {
    let from = swu2key(symRange.slice(1, 3));
    let to = swu2key(symRange.slice(-2));
    from = key2swu(from.slice(0, 4) + '00');
    to = key2swu(to.slice(0, 4) + '5f');
    return range(from, to);
  };

  //needs rewritten, but it works
  /**
   * Function to transform an SWU query string to one or more regular expressions
   * @function swuquery.regex
   * @param {string} query - an SWU query string
   * @returns {string[]} an array of one or more regular expressions
   * @example
   * swuquery.regex('QA񀀒T')
   * 
   * return [
   *   '(\uD836\uDC00\uD8C0\uDC12((?:(?:\uD8C0[\uDC01-\uDFFF])|(?:[\uD8C1-\uD8FC][\uDC00-\uDFFF])|(?:\uD8FD[\uDC00-\uDC80])))*)\uD836[\uDC01-\uDC04](?:\uD836[\uDC0C-\uDDFF]){2}((?:(?:\uD8C0[\uDC01-\uDFFF])|(?:[\uD8C1-\uD8FC][\uDC00-\uDFFF])|(?:\uD8FD[\uDC00-\uDC80]))(?:\uD836[\uDC0C-\uDDFF]){2})*'
   * ]
   */
  const regex = query => {
    query = query.match(new RegExp(`^${re.full}`))[0];
    if (!query) {
      return '';
    }
    let matches;
    let matchesOr;
    let matched;
    let orList;
    let i;
    let j;
    let coord;
    let segment;
    let x;
    let y;
    let fuzz = 20;
    let re_word = re$2.box + re$2.coord + '(?:' + re$2.symbol + re$2.coord + ')*';
    let re_sortable = '(?:' + re$2.sort + re$2.nullorsymbol + '+)';
    let q_var = '(?:V[0-9]+)';
    let q_style = '(?:' + re$3.full + ')?';
    let q_sortable;
    if (query == 'Q') {
      return [re$2.sign];
    }
    if (query == 'Q-') {
      return [re$2.sign + "(" + re$3.full + ")?"];
    }
    if (query == 'QT') {
      return [re$2.sortable];
    }
    if (query == 'QT-') {
      return [re$2.sortable + "(" + re$3.full + ")?"];
    }
    let segments = [];
    let sortable = query.indexOf('T') + 1;
    if (sortable) {
      q_sortable = '(?:' + re$2.sort;
      let qat = query.slice(0, sortable);
      query = query.replace(qat, '');
      if (qat == 'QT') {
        q_sortable += re$2.nullorsymbol + '+)';
      } else {
        matches = qat.match(new RegExp(re.list, 'g'));
        if (matches) {
          for (i = 0; i < matches.length; i += 1) {
            orList = [];
            matchesOr = matches[i].match(new RegExp(re.item, 'g'));
            if (matchesOr) {
              for (j = 0; j < matchesOr.length; j += 1) {
                matched = matchesOr[j].match(new RegExp(re.range));
                if (matched) {
                  orList.push(regexRange(matchesOr[j]));
                } else {
                  orList.push(symbolRanges(matchesOr[j]));
                }
              }
              if (orList.length == 1) {
                q_sortable += orList[0];
              } else {
                q_sortable += '(?:' + orList.join('|') + ')';
              }
            }
          }
          q_sortable += re$2.nullorsymbol + '*)';
        }
      }
    }

    //get the variance
    matches = query.match(new RegExp(q_var, 'g'));
    if (matches) {
      fuzz = matches.toString().slice(1) * 1;
    }

    //this gets all symbols and ranges with or without location
    matches = query.match(new RegExp(re.list + re.coord, 'g'));
    if (matches) {
      for (i = 0; i < matches.length; i += 1) {
        orList = [];
        matchesOr = matches[i].match(new RegExp('(' + re.range + '|' + re.symbol + ')', 'g'));
        if (matchesOr) {
          for (j = 0; j < matchesOr.length; j += 1) {
            matched = matchesOr[j].match(new RegExp(re.range));
            if (matched) {
              orList.push(regexRange(matchesOr[j]));
            } else {
              orList.push(symbolRanges(matchesOr[j]));
            }
          }
          if (orList.length == 1) {
            segment = orList[0];
          } else {
            segment = '(?:' + orList.join('|') + ')';
          }
        }
        coord = matches[i].match(new RegExp(`${re$2.coord}`));
        if (coord) {
          coord = swu2coord(coord[0]);
          x = coord[0];
          y = coord[1];
          segment += range(num2swu(x - fuzz), num2swu(x + fuzz));
          segment += range(num2swu(y - fuzz), num2swu(y + fuzz));
        } else {
          segment += re$2.coord;
        }

        // add to general swu word
        segment = re_word + segment + '(?:' + re$2.symbol + re$2.coord + ')*';
        if (sortable) {
          segment = q_sortable + segment;
        } else {
          segment = re_sortable + "?" + segment;
        }
        if (query.indexOf('-') > 0) {
          segment += q_style;
        }
        segments.push(segment);
      }
    }
    if (!segments.length) {
      if (query.indexOf('-') > 0) {
        segment += q_style;
      }
      segments.push(q_sortable + re_word);
    }
    return segments;
  };

  //needs rewritten, but it works
  /**
   * Function that uses a query string to match signs from a string of text.
   * @function swuquery.results
   * @param {string} query - an SWU query string
   * @param {string} text - a string of text containing multiple signs
   * @returns {string[]} an array of SWU signs
   * @example
   * swuquery.results('QA񀀒T','𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭 𝠀񂇢񂇈񆙡񋎥񋎵𝠃𝤛𝤬񂇈𝤀𝣺񂇢𝤄𝣻񋎥𝤄𝤗񋎵𝤃𝣟񆙡𝣱𝣸 𝠀񅨑񀀙񆉁𝠃𝤙𝤞񀀙𝣷𝤀񅨑𝣼𝤀񆉁𝣳𝣮')
   * 
   * return [
   *   '𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭'
   * ]
   */
  const results = (query, text) => {
    if (!text) {
      return [];
    }
    let pattern;
    let matches;
    let parts;
    let words;
    let res = regex(query);
    if (!res) {
      return [];
    }
    let i;
    for (i = 0; i < res.length; i += 1) {
      pattern = res[i];
      matches = text.match(new RegExp(pattern, 'g'));
      if (matches) {
        text = matches.join(' ');
      } else {
        text = '';
      }
    }
    if (text) {
      parts = text.split(' ');
      words = parts.filter(function (element) {
        return element in parts ? false : parts[element] = true;
      }, {});
    } else {
      words = [];
    }
    return words;
  };

  //needs rewritten, but it works
  /**
   * Function that uses an SWU query string to match signs from multiple lines of text.
   * @function swuquery.lines
   * @param {string} query - an SWU query string
   * @param {string} text - multiple lines of text, each starting with an SWU sign
   * @returns {string[]} an array of lines of text, each starting with an SWU sign
   * @example
   * swuquery.lines('QA񀀒T',`𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭 line one
   * 𝠀񂇢񂇈񆙡񋎥񋎵𝠃𝤛𝤬񂇈𝤀𝣺񂇢𝤄𝣻񋎥𝤄𝤗񋎵𝤃𝣟񆙡𝣱𝣸 line two
   * 𝠀񅨑񀀙񆉁𝠃𝤙𝤞񀀙𝣷𝤀񅨑𝣼𝤀񆉁𝣳𝣮 line three`)
   * 
   * return [
   *   '𝠀񀀒񀀚񋚥񋛩𝠃𝤟𝤩񋛩𝣵𝤐񀀒𝤇𝣤񋚥𝤐𝤆񀀚𝣮𝣭 line one'
   * ]
   */
  const lines = (query, text) => {
    if (!text) {
      return [];
    }
    let pattern;
    let matches;
    let parts;
    let words;
    let res = regex(query);
    if (!res) {
      return [];
    }
    let i;
    for (i = 0; i < res.length; i += 1) {
      pattern = res[i];
      pattern = '^' + pattern + '.*';
      matches = text.match(new RegExp(pattern, 'mg'));
      if (matches) {
        text = matches.join("\n");
      } else {
        text = '';
      }
    }
    if (text) {
      parts = text.split("\n");
      words = parts.filter(function (element) {
        return element in parts ? false : parts[element] = true;
      }, {});
    } else {
      words = [];
    }
    return words;
  };

  /** The swuquery module contains functions for handling the SWU query language.
   * [Query Language definition](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-09.html#name-query-language)
   * @module swuquery
   */

  var index = /*#__PURE__*/Object.freeze({
    __proto__: null,
    re: re,
    parse: parse,
    compose: compose,
    swu2query: swu2query,
    range: range,
    symbolRanges: symbolRanges,
    regex: regex,
    results: results,
    lines: lines
  });

  exports.convert = index$4;
  exports.fsw = index$3;
  exports.fswquery = index$2;
  exports.style = index$5;
  exports.swu = index$1;
  exports.swuquery = index;

  Object.defineProperty(exports, '__esModule', { value: true });

}));

/* support ongoing development */
/*   https://patreon.com/signwriting */
/*   https://donate.sutton-signwriting.io */