UNPKG

@sutton-signwriting/core

Version:

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

1,020 lines (986 loc) 32.2 kB
/** * Sutton SignWriting Core Module v2.0.0 (https://github.com/sutton-signwriting/core) * Author: Steve Slevinski (https://SteveSlevinski.me) * fsw.mjs is released under the MIT License. */ /** * 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$1 = { '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$1.nullorsymbol = `(?:${re$1.null}|${re$1.symbol})`; re$1.prefix = `(?:${re$1.sort}${re$1.nullorsymbol}+)`; re$1.spatial = `${re$1.symbol}${re$1.coord}`; re$1.signbox = `${re$1.box}${re$1.coord}(?:${re$1.spatial})*`; re$1.sign = `${re$1.prefix}?${re$1.signbox}`; re$1.sortable = `${re$1.prefix}${re$1.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 = { '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.colorbase = `(?:${re.colorhex}|${re.colorname})`; re.color = `_${re.colorbase}_`; re.colors = `_${re.colorbase}(?:,${re.colorbase})?_`; re.background = `G${re.color}`; re.detail = `D${re.colors}`; re.detailsym = `D[0-9]{2}${re.colors}`; re.classes = `${re.classbase}(?: ${re.classbase})*`; re.full = `-(${re.colorize})?(${re.padding})?(${re.background})?(${re.detail})?(${re.zoom})?(?:-((?:${re.detailsym})*))?(?:-(${re.classes})?!(?:(${re.id})!)?)?`; const prefixColor = color => { const regex = new RegExp(`^${re.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$1 = styleString => { const regex = `^${re.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.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] }); }; /** 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 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)); const parse = { /** * 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$1.symbol})(${re$1.coord})?(${re.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$1.prefix})?(${re$1.signbox})(${re.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$1.sign}(${re.full})?|${re$1.spatial}(${re.full})?)`; const matches = fswText.match(new RegExp(regex, 'g')); return matches ? [...matches] : []; } }; const compose = { /** * 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$1.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$1.coord) || [''])[0] || ''; const styleStr = typeof fswSymObject.style === 'string' && (fswSymObject.style.match(re.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$1.box); const x = (fswSignObject.max && fswSignObject.max[0] || '').toString(); const y = (fswSignObject.max && fswSignObject.max[1] || '').toString(); const max = ((x + 'x' + y).match(re$1.coord) || [''])[0] || ''; if (!max) return undefined; let prefix = ''; if (fswSignObject.sequence && Array.isArray(fswSignObject.sequence)) { prefix = fswSignObject.sequence.map(key => (key.match(re$1.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$1.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$1.coord) || [''])[0] || ''; if (coord) { return symbol + coord; } } } return ''; }).join(''); } const styleStr = typeof fswSignObject.style === 'string' && (fswSignObject.style.match(re.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 = fsw => { let lanes = { "B": 0, "L": -1, "M": 0, "R": 1 }; let parsed = parse.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.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$1(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.text(fsw).map(fswSegment => { if (/[BLMR]/.test(fswSegment)) { const sign = parse.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.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 = { '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 = options => { if (typeof options !== 'object') options = {}; return { ...columnDefaults, ...options, punctuation: { ...columnDefaults.punctuation, ...options.punctuation }, style: { ...columnDefaults.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 = (fswText, options) => { if (typeof fswText !== 'string') return {}; const values = columnDefaultsMerge(options); let input = parse.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(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 = [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 = [0x100, 0x205, 0x2f7, 0x2ff, 0x36d, 0x37f, 0x387]; /** * Array of numbers for the 30 symbol groups. * @alias fsw.group * @type {number[]} */ const group = [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 = { '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 = (key, type) => { const parsed = parse.symbol(key); if (parsed.symbol) { const dec = parseInt(parsed.symbol.slice(1, 4), 16); const range = ranges[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 = ['#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 = key => { const parsed = parse.symbol(key); let color = '#000000'; if (parsed.symbol) { const dec = parseInt(parsed.symbol.slice(1, 4), 16); const index = category.findIndex(val => val > dec); color = colors[index < 0 ? 6 : index - 1]; } return color; }; export { category, colorize, colors, columnDefaults, columnDefaultsMerge, columns, compose, createTokenizer, detokenize, group, info, isType, kind, parse, ranges, re$1 as re, tokenize }; /* support ongoing development */ /* https://patreon.com/signwriting */ /* https://donate.sutton-signwriting.io */