@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
JavaScript
/**
* 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 */