1 | /**
|
2 | * Sutton SignWriting Core Module v1.2.0 (https://github.com/sutton-signwriting/core)
|
3 | * Author: Steve Slevinski (https://SteveSlevinski.me)
|
4 | * fsw.mjs is released under the MIT License.
|
5 | */
|
6 |
|
7 | /**
|
8 | * Object of regular expressions for FSW strings
|
9 | *
|
10 | * { symbol, coord, sort, box, prefix, spatial, signbox, sign, sortable }
|
11 | * @alias fsw.re
|
12 | * @type {object}
|
13 | */
|
14 | let re = {
|
15 | 'symbol': 'S[123][0-9a-f]{2}[0-5][0-9a-f]',
|
16 | 'coord': '[0-9]{3}x[0-9]{3}',
|
17 | 'sort': 'A',
|
18 | 'box': '[BLMR]'
|
19 | };
|
20 | re.prefix = `(?:${re.sort}(?:${re.symbol})+)`;
|
21 | re.spatial = `${re.symbol}${re.coord}`;
|
22 | re.signbox = `${re.box}${re.coord}(?:${re.spatial})*`;
|
23 | re.sign = `${re.prefix}?${re.signbox}`;
|
24 | re.sortable = `${re.prefix}${re.signbox}`;
|
25 |
|
26 | /**
|
27 | * Object of regular expressions for style strings
|
28 | *
|
29 | * { colorize, colorhex, colorname, padding, zoom, zoomsym, classbase, id, colorbase, color, colors, background, detail, detailsym, classes, full }
|
30 | * @alias style.re
|
31 | * @type {object}
|
32 | */
|
33 | let re$1 = {
|
34 | 'colorize': 'C',
|
35 | 'colorhex': '(?:[0-9a-fA-F]{3}){1,2}',
|
36 | 'colorname': '[a-zA-Z]+',
|
37 | 'padding': 'P[0-9]{2}',
|
38 | 'zoom': 'Z(?:[0-9]+(?:\\.[0-9]+)?|x)',
|
39 | 'zoomsym': 'Z[0-9]{2},[0-9]+(?:\\.[0-9]+)?(?:,[0-9]{3}x[0-9]{3})?',
|
40 | 'classbase': '-?[_a-zA-Z][_a-zA-Z0-9-]{0,100}',
|
41 | 'id': '[a-zA-Z][_a-zA-Z0-9-]{0,100}'
|
42 | };
|
43 | re$1.colorbase = `(?:${re$1.colorhex}|${re$1.colorname})`;
|
44 | re$1.color = `_${re$1.colorbase}_`;
|
45 | re$1.colors = `_${re$1.colorbase}(?:,${re$1.colorbase})?_`;
|
46 | re$1.background = `G${re$1.color}`;
|
47 | re$1.detail = `D${re$1.colors}`;
|
48 | re$1.detailsym = `D[0-9]{2}${re$1.colors}`;
|
49 | re$1.classes = `${re$1.classbase}(?: ${re$1.classbase})*`;
|
50 | re$1.full = `-(${re$1.colorize})?(${re$1.padding})?(${re$1.background})?(${re$1.detail})?(${re$1.zoom})?(?:-((?:${re$1.detailsym})*)((?:${re$1.zoomsym})*))?(?:-(${re$1.classes})?!(?:(${re$1.id})!)?)?`;
|
51 |
|
52 | /** 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.
|
53 | * [Characters set definitions](https://tools.ietf.org/id/draft-slevinski-formal-signwriting-07.html#rfc.section.2.2)
|
54 | * @module convert
|
55 | */
|
56 | /**
|
57 | * Function to convert an FSW coordinate string to an array of x,y integers
|
58 | * @function convert.fsw2coord
|
59 | * @param {string} fswCoord - An FSW coordinate string
|
60 | * @returns {number[]} Array of x,y integers
|
61 | * @example
|
62 | * convert.fsw2coord('500x500')
|
63 | *
|
64 | * return [500, 500]
|
65 | */
|
66 |
|
67 |
|
68 | const fsw2coord = fswCoord => fswCoord.split('x').map(num => parseInt(num));
|
69 |
|
70 | const parse = {
|
71 | /**
|
72 | * Function to parse an fsw symbol with optional coordinate and style string
|
73 | * @function fsw.parse.symbol
|
74 | * @param {string} fswSym - an fsw symbol
|
75 | * @returns {object} elements of fsw symbol
|
76 | * @example
|
77 | * fsw.parse.symbol('S10000500x500-C')
|
78 | *
|
79 | * return {
|
80 | * 'symbol': 'S10000',
|
81 | * 'coord': [500, 500],
|
82 | * 'style': '-C'
|
83 | * }
|
84 | */
|
85 | symbol: fswSym => {
|
86 | const regex = `^(${re.symbol})(${re.coord})?(${re$1.full})?`;
|
87 | const symbol = typeof fswSym === 'string' ? fswSym.match(new RegExp(regex)) : undefined;
|
88 | return {
|
89 | 'symbol': symbol ? symbol[1] : undefined,
|
90 | 'coord': symbol && symbol[2] ? fsw2coord(symbol[2]) : undefined,
|
91 | 'style': symbol ? symbol[3] : undefined
|
92 | };
|
93 | },
|
94 |
|
95 | /**
|
96 | * Function to parse an fsw sign with style string
|
97 | * @function fsw.parse.sign
|
98 | * @param {string} fswSign - an fsw sign
|
99 | * @returns {object} elements of fsw sign
|
100 | * @example
|
101 | * fsw.parse.sign('AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475-C')
|
102 | *
|
103 | * return {
|
104 | * sequence: ['S10011', 'S10019', 'S2e704', 'S2e748'],
|
105 | * box: 'M',
|
106 | * max: [525, 535],
|
107 | * spatials: [
|
108 | * {
|
109 | * symbol: 'S2e748',
|
110 | * coord: [483, 510]
|
111 | * },
|
112 | * {
|
113 | * symbol: 'S10011',
|
114 | * coord: [501, 466]
|
115 | * },
|
116 | * {
|
117 | * symbol: 'S2e704',
|
118 | * coord: [510, 500]
|
119 | * },
|
120 | * {
|
121 | * symbol: 'S10019',
|
122 | * coord: [476, 475]
|
123 | * }
|
124 | * ],
|
125 | * style: '-C'
|
126 | * }
|
127 | */
|
128 | sign: fswSign => {
|
129 | const regex = `^(${re.prefix})?(${re.signbox})(${re$1.full})?`;
|
130 | const sign = typeof fswSign === 'string' ? fswSign.match(new RegExp(regex)) : undefined;
|
131 |
|
132 | if (sign) {
|
133 | return {
|
134 | 'sequence': sign[1] ? sign[1].slice(1).match(/.{6}/g) : undefined,
|
135 | 'box': sign[2][0],
|
136 | 'max': fsw2coord(sign[2].slice(1, 8)),
|
137 | 'spatials': sign[2].length < 9 ? undefined : sign[2].slice(8).match(/(.{13})/g).map(m => {
|
138 | return {
|
139 | symbol: m.slice(0, 6),
|
140 | coord: [parseInt(m.slice(6, 9)), parseInt(m.slice(10, 13))]
|
141 | };
|
142 | }),
|
143 | 'style': sign[3]
|
144 | };
|
145 | } else {
|
146 | return {};
|
147 | }
|
148 | }
|
149 | };
|
150 |
|
151 | const compose = {
|
152 | /**
|
153 | * Function to compose an fsw symbol with optional coordinate and style string
|
154 | * @function fsw.compose.symbol
|
155 | * @param {object} fswSymObject - an fsw symbol object
|
156 | * @param {string} fswSymObject.symbol - an fsw symbol key
|
157 | * @param {number[]} fswSymObject.coord - top-left coordinate of symbol with 500,500 center
|
158 | * @param {string} fswSymObject.style - a style string for custom appearance
|
159 | * @returns {string} an fsw symbol string
|
160 | * @example
|
161 | * fsw.compose.symbol({
|
162 | * 'symbol': 'S10000',
|
163 | * 'coord': [480, 480],
|
164 | * 'style': '-C'
|
165 | * })
|
166 | *
|
167 | * return 'S10000480x480-C'
|
168 | */
|
169 | symbol: fswSymObject => {
|
170 | if (typeof fswSymObject.symbol === 'string') {
|
171 | const symbol = (fswSymObject.symbol.match(re.symbol) || [''])[0];
|
172 |
|
173 | if (symbol) {
|
174 | const x = (fswSymObject.coord && fswSymObject.coord[0] || '').toString();
|
175 | const y = (fswSymObject.coord && fswSymObject.coord[1] || '').toString();
|
176 | const coord = ((x + 'x' + y).match(re.coord) || [''])[0] || '';
|
177 | const styleStr = typeof fswSymObject.style === 'string' && (fswSymObject.style.match(re$1.full) || [''])[0] || '';
|
178 | return symbol + coord + styleStr;
|
179 | }
|
180 | }
|
181 |
|
182 | return undefined;
|
183 | },
|
184 |
|
185 | /**
|
186 | * Function to compose an fsw sign with style string
|
187 | * @function fsw.compose.sign
|
188 | * @param {string[]} fswSignObject.sequence - an ordered array of symbols
|
189 | * @param {string} fswSignObject.box - a choice BLMR: horizontal Box, Left, Middle, and Right lane
|
190 | * @param {number[]} fswSymObject.max - max bottom left coordinate of the signbox space
|
191 | * @param {{symbol:string,coord:number[]}[]} fswSymObject.spatials - array of symbols with top-left coordinate placement
|
192 | * @param {string} fswSymObject.style - a style string for custom appearance
|
193 | * @returns {string} an fsw sign string
|
194 | * @example
|
195 | * fsw.compose.sign({
|
196 | * sequence: ['S10011', 'S10019', 'S2e704', 'S2e748'],
|
197 | * box: 'M',
|
198 | * max: [525, 535],
|
199 | * spatials: [
|
200 | * {
|
201 | * symbol: 'S2e748',
|
202 | * coord: [483, 510]
|
203 | * },
|
204 | * {
|
205 | * symbol: 'S10011',
|
206 | * coord: [501, 466]
|
207 | * },
|
208 | * {
|
209 | * symbol: 'S2e704',
|
210 | * coord: [510, 500]
|
211 | * },
|
212 | * {
|
213 | * symbol: 'S10019',
|
214 | * coord: [476, 475]
|
215 | * }
|
216 | * ],
|
217 | * style: '-C'
|
218 | * })
|
219 | *
|
220 | * return 'AS10011S10019S2e704S2e748M525x535S2e748483x510S10011501x466S2e704510x500S10019476x475-C'
|
221 | */
|
222 | sign: fswSignObject => {
|
223 | let box = typeof fswSignObject.box !== 'string' ? 'M' : (fswSignObject.box + 'M').match(re.box);
|
224 | const x = (fswSignObject.max && fswSignObject.max[0] || '').toString();
|
225 | const y = (fswSignObject.max && fswSignObject.max[1] || '').toString();
|
226 | const max = ((x + 'x' + y).match(re.coord) || [''])[0] || '';
|
227 | if (!max) return undefined;
|
228 | let prefix = '';
|
229 |
|
230 | if (fswSignObject.sequence && Array.isArray(fswSignObject.sequence)) {
|
231 | prefix = fswSignObject.sequence.map(key => (key.match(re.symbol) || [''])[0]).join('');
|
232 | prefix = prefix ? 'A' + prefix : '';
|
233 | }
|
234 |
|
235 | let signbox = '';
|
236 |
|
237 | if (fswSignObject.spatials && Array.isArray(fswSignObject.spatials)) {
|
238 | signbox = fswSignObject.spatials.map(spatial => {
|
239 | if (typeof spatial.symbol === 'string') {
|
240 | const symbol = (spatial.symbol.match(re.symbol) || [''])[0];
|
241 |
|
242 | if (symbol) {
|
243 | const x = (spatial.coord && spatial.coord[0] || '').toString();
|
244 | const y = (spatial.coord && spatial.coord[1] || '').toString();
|
245 | const coord = ((x + 'x' + y).match(re.coord) || [''])[0] || '';
|
246 |
|
247 | if (coord) {
|
248 | return symbol + coord;
|
249 | }
|
250 | }
|
251 | }
|
252 |
|
253 | return '';
|
254 | }).join('');
|
255 | }
|
256 |
|
257 | const styleStr = typeof fswSignObject.style === 'string' && (fswSignObject.style.match(re$1.full) || [''])[0] || '';
|
258 | return prefix + box + max + signbox + styleStr;
|
259 | }
|
260 | };
|
261 |
|
262 | /**
|
263 | * Array of numbers for kinds of symbols: writing, location, and punctuation.
|
264 | * @alias fsw.kind
|
265 | * @type {array}
|
266 | */
|
267 |
|
268 | const kind = [0x100, 0x37f, 0x387];
|
269 | /**
|
270 | * Array of numbers for categories of symbols: hand, movement, dynamics, head, trunk & limb, location, and punctuation.
|
271 | * @alias fsw.category
|
272 | * @type {array}
|
273 | */
|
274 |
|
275 | const category = [0x100, 0x205, 0x2f7, 0x2ff, 0x36d, 0x37f, 0x387];
|
276 | /**
|
277 | * Array of numbers for the 30 symbol groups.
|
278 | * @alias fsw.group
|
279 | * @type {array}
|
280 | */
|
281 |
|
282 | 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];
|
283 | /**
|
284 | * Object of symbol ranges with starting and ending numbers.
|
285 | *
|
286 | * { all, writing, hand, movement, dynamic, head, hcenter, vcenter, trunk, limb, location, punctuation }
|
287 | * @alias fsw.ranges
|
288 | * @type {object}
|
289 | */
|
290 |
|
291 | const ranges = {
|
292 | 'all': [0x100, 0x38b],
|
293 | 'writing': [0x100, 0x37e],
|
294 | 'hand': [0x100, 0x204],
|
295 | 'movement': [0x205, 0x2f6],
|
296 | 'dynamic': [0x2f7, 0x2fe],
|
297 | 'head': [0x2ff, 0x36c],
|
298 | 'hcenter': [0x2ff, 0x36c],
|
299 | 'vcenter': [0x2ff, 0x375],
|
300 | 'trunk': [0x36d, 0x375],
|
301 | 'limb': [0x376, 0x37e],
|
302 | 'location': [0x37f, 0x386],
|
303 | 'punctuation': [0x387, 0x38b]
|
304 | };
|
305 | /**
|
306 | * Function to test if symbol is of a certain type.
|
307 | * @function fsw.isType
|
308 | * @param {string} key - an FSW symbol key
|
309 | * @param {string} type - the name of a symbol range
|
310 | * @returns {boolean} is symbol of specified type
|
311 | * @example
|
312 | * fsw.isType('S10000', 'hand')
|
313 | *
|
314 | * return true
|
315 | */
|
316 |
|
317 | const isType = (key, type) => {
|
318 | const parsed = parse.symbol(key);
|
319 |
|
320 | if (parsed.symbol) {
|
321 | const dec = parseInt(parsed.symbol.slice(1, 4), 16);
|
322 | const range = ranges[type];
|
323 |
|
324 | if (range) {
|
325 | return range[0] <= dec && range[1] >= dec;
|
326 | }
|
327 | }
|
328 |
|
329 | return false;
|
330 | };
|
331 |
|
332 | /**
|
333 | * Array of colors associated with the seven symbol categories.
|
334 | * @alias fsw.colors
|
335 | * @type {array}
|
336 | */
|
337 |
|
338 | const colors = ['#0000CC', '#CC0000', '#FF0099', '#006600', '#000000', '#884411', '#FF9900'];
|
339 | /**
|
340 | * Function that returns the standardized color for a symbol.
|
341 | * @function fsw.colorize
|
342 | * @param {string} key - an FSW symbol key
|
343 | * @returns {string} name of standardized color for symbol
|
344 | * @example
|
345 | * fsw.colorize('S10000')
|
346 | *
|
347 | * return '#0000CC'
|
348 | */
|
349 |
|
350 | const colorize = key => {
|
351 | const parsed = parse.symbol(key);
|
352 | let color = '#000000';
|
353 |
|
354 | if (parsed.symbol) {
|
355 | const dec = parseInt(parsed.symbol.slice(1, 4), 16);
|
356 | const index = category.findIndex(val => val > dec);
|
357 | color = colors[index < 0 ? 6 : index - 1];
|
358 | }
|
359 |
|
360 | return color;
|
361 | };
|
362 |
|
363 | export { category, colorize, colors, compose, group, isType, kind, parse, ranges, re };
|
364 |
|
365 | /* support ongoing development on https://patreon.com/signwriting */
|