@mapwhit/tilerenderer 0.47.2 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/build/min/package.json +1 -1
  2. package/package.json +1 -1
  3. package/src/data/bucket/symbol_bucket.js +25 -11
  4. package/src/data/program_configuration.js +18 -10
  5. package/src/geo/transform.js +4 -2
  6. package/src/index.js +1 -1
  7. package/src/source/rtl_text_plugin.js +1 -0
  8. package/src/source/source_cache.js +1 -1
  9. package/src/source/tile.js +6 -5
  10. package/src/source/worker.js +1 -10
  11. package/src/style/style.js +4 -19
  12. package/src/style/style_layer/symbol_style_layer_properties.js +1 -1
  13. package/src/style-spec/expression/compound_expression.js +30 -16
  14. package/src/style-spec/expression/definitions/coercion.js +13 -0
  15. package/src/style-spec/expression/definitions/comparison.js +193 -0
  16. package/src/style-spec/expression/definitions/formatted.js +123 -0
  17. package/src/style-spec/expression/definitions/index.js +10 -60
  18. package/src/style-spec/expression/definitions/interpolate.js +17 -7
  19. package/src/style-spec/expression/definitions/literal.js +5 -0
  20. package/src/style-spec/expression/parsing_context.js +4 -0
  21. package/src/style-spec/expression/types.js +12 -1
  22. package/src/style-spec/feature_filter/index.js +1 -1
  23. package/src/style-spec/reference/v8.json +118 -47
  24. package/src/symbol/get_anchors.js +11 -22
  25. package/src/symbol/mergelines.js +4 -1
  26. package/src/symbol/placement.js +8 -2
  27. package/src/symbol/quads.js +7 -6
  28. package/src/symbol/shaping.js +185 -40
  29. package/src/symbol/symbol_layout.js +9 -6
  30. package/src/symbol/transform_text.js +12 -1
  31. package/src/ui/map.js +8 -25
  32. package/src/style-spec/expression/definitions/equals.js +0 -93
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.47.1"
2
+ "version": "0.47.2"
3
3
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapwhit/tilerenderer",
3
3
  "description": "A WebGL interactive maps library",
4
- "version": "0.47.2",
4
+ "version": "0.48.0",
5
5
  "main": "src/index.js",
6
6
  "license": "BSD-3-Clause",
7
7
  "repository": {
@@ -31,6 +31,7 @@ const { verticalizedCharacterMap } = require('../../util/verticalize_punctuation
31
31
  const { getSizeData } = require('../../symbol/symbol_size');
32
32
  const { register } = require('../../util/transfer_registry');
33
33
  const EvaluationParameters = require('../../style/evaluation_parameters');
34
+ const { Formatted } = require('../../style-spec/expression/definitions/formatted');
34
35
 
35
36
  // Opacity arrays are frequently updated but don't contain a lot of information, so we pack them
36
37
  // tight. Each Uint32 is actually four duplicate Uint8s for the four corners of a glyph
@@ -221,6 +222,18 @@ class SymbolBucket {
221
222
  this.lineVertexArray = new SymbolLineVertexArray();
222
223
  }
223
224
 
225
+ calculateGlyphDependencies(text, stack, textAlongLine, doesAllowVerticalWritingMode) {
226
+ for (let i = 0; i < text.length; i++) {
227
+ stack[text.charCodeAt(i)] = true;
228
+ if (textAlongLine && doesAllowVerticalWritingMode) {
229
+ const verticalChar = verticalizedCharacterMap[text.charAt(i)];
230
+ if (verticalChar) {
231
+ stack[verticalChar.charCodeAt(0)] = true;
232
+ }
233
+ }
234
+ }
235
+ }
236
+
224
237
  populate(features, options) {
225
238
  const layer = this.layers[0];
226
239
  const layout = layer.layout;
@@ -229,7 +242,7 @@ class SymbolBucket {
229
242
  const textField = layout.get('text-field');
230
243
  const iconImage = layout.get('icon-image');
231
244
  const hasText =
232
- (textField.value.kind !== 'constant' || textField.value.value.length > 0) &&
245
+ (textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) &&
233
246
  (textFont.value.kind !== 'constant' || textFont.value.value.length > 0);
234
247
  const hasIcon = iconImage.value.kind !== 'constant' || (iconImage.value.value && iconImage.value.value.length > 0);
235
248
 
@@ -286,15 +299,16 @@ class SymbolBucket {
286
299
  const stack = (stacks[fontStack] = stacks[fontStack] || {});
287
300
  const textAlongLine =
288
301
  layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point';
289
- const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text);
290
- for (let i = 0; i < text.length; i++) {
291
- stack[text.charCodeAt(i)] = true;
292
- if (textAlongLine && doesAllowVerticalWritingMode) {
293
- const verticalChar = verticalizedCharacterMap[text.charAt(i)];
294
- if (verticalChar) {
295
- stack[verticalChar.charCodeAt(0)] = true;
296
- }
302
+ if (text instanceof Formatted) {
303
+ for (const section of text.sections) {
304
+ const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString());
305
+ const sectionFont = section.fontStack || fontStack;
306
+ const sectionStack = (stacks[sectionFont] = stacks[sectionFont] || {});
307
+ this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, doesAllowVerticalWritingMode);
297
308
  }
309
+ } else {
310
+ const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text);
311
+ this.calculateGlyphDependencies(text, stack, textAlongLine, doesAllowVerticalWritingMode);
298
312
  }
299
313
  }
300
314
  }
@@ -642,8 +656,8 @@ class SymbolBucket {
642
656
  symbolInstanceIndexes.sort((aIndex, bIndex) => {
643
657
  const a = this.symbolInstances[aIndex];
644
658
  const b = this.symbolInstances[bIndex];
645
- const aRotated = (sin * a.anchor.x + cos * a.anchor.y) | 0;
646
- const bRotated = (sin * b.anchor.x + cos * b.anchor.y) | 0;
659
+ const aRotated = Math.round(sin * a.anchor.x + cos * a.anchor.y) | 0;
660
+ const bRotated = Math.round(sin * b.anchor.x + cos * b.anchor.y) | 0;
647
661
  return aRotated - bRotated || b.featureIndex - a.featureIndex;
648
662
  });
649
663
 
@@ -170,11 +170,15 @@ class SourceExpressionBinder {
170
170
 
171
171
  upload(context) {
172
172
  if (this.paintVertexArray?.arrayBuffer) {
173
- this.paintVertexBuffer = context.createVertexBuffer(
174
- this.paintVertexArray,
175
- this.paintVertexAttributes,
176
- this.expression.isStateDependent
177
- );
173
+ if (this.paintVertexBuffer?.buffer) {
174
+ this.paintVertexBuffer.updateData(this.paintVertexArray);
175
+ } else {
176
+ this.paintVertexBuffer = context.createVertexBuffer(
177
+ this.paintVertexArray,
178
+ this.paintVertexAttributes,
179
+ this.expression.isStateDependent
180
+ );
181
+ }
178
182
  }
179
183
  }
180
184
 
@@ -265,11 +269,15 @@ class CompositeExpressionBinder {
265
269
 
266
270
  upload(context) {
267
271
  if (this.paintVertexArray?.arrayBuffer) {
268
- this.paintVertexBuffer = context.createVertexBuffer(
269
- this.paintVertexArray,
270
- this.paintVertexAttributes,
271
- this.expression.isStateDependent
272
- );
272
+ if (this.paintVertexBuffer?.buffer) {
273
+ this.paintVertexBuffer.updateData(this.paintVertexArray);
274
+ } else {
275
+ this.paintVertexBuffer = context.createVertexBuffer(
276
+ this.paintVertexArray,
277
+ this.paintVertexAttributes,
278
+ this.expression.isStateDependent
279
+ );
280
+ }
273
281
  }
274
282
  }
275
283
 
@@ -17,12 +17,13 @@ const { vec4, mat4, mat2 } = require('@mapbox/gl-matrix');
17
17
  class Transform {
18
18
  constructor(minZoom, maxZoom, renderWorldCopies) {
19
19
  this.tileSize = 512; // constant
20
+ this.maxValidLatitude = 85.051129; // constant
20
21
 
21
22
  this._renderWorldCopies = renderWorldCopies === undefined ? true : renderWorldCopies;
22
23
  this._minZoom = minZoom || 0;
23
24
  this._maxZoom = maxZoom || 22;
24
25
 
25
- this.latRange = [-85.05113, 85.05113];
26
+ this.latRange = [-this.maxValidLatitude, this.maxValidLatitude];
26
27
 
27
28
  this.width = 0;
28
29
  this.height = 0;
@@ -262,7 +263,7 @@ class Transform {
262
263
  }
263
264
 
264
265
  /**
265
- * latitude to absolute x coord
266
+ * longitude to absolute x coord
266
267
  * @returns {number} pixel coordinate
267
268
  */
268
269
  lngX(lng) {
@@ -273,6 +274,7 @@ class Transform {
273
274
  * @returns {number} pixel coordinate
274
275
  */
275
276
  latY(lat) {
277
+ lat = clamp(lat, -this.maxValidLatitude, this.maxValidLatitude);
276
278
  const y = (180 / Math.PI) * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360));
277
279
  return ((180 - y) * this.worldSize) / 360;
278
280
  }
package/src/index.js CHANGED
@@ -43,7 +43,7 @@ module.exports = {
43
43
  * @param {string} pluginURL URL pointing to the Mapbox RTL text plugin source.
44
44
  * @param {Function} callback Called with an error argument if there is an error.
45
45
  * @example
46
- * mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.1.2/mapbox-gl-rtl-text.js');
46
+ * mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');
47
47
  * @see [Add support for right-to-left scripts](https://www.mapbox.com/mapbox-gl-js/example/mapbox-gl-rtl-text/)
48
48
  */
49
49
 
@@ -47,6 +47,7 @@ function setRTLTextPlugin(url, callback) {
47
47
  const plugin = {
48
48
  applyArabicShaping: null,
49
49
  processBidirectionalText: null,
50
+ processStyledBidirectionalText: null,
50
51
  isLoaded: function () {
51
52
  return (
52
53
  foregroundLoadComplete || // Foreground: loaded if the completion callback returned successfully
@@ -181,7 +181,7 @@ class SourceCache extends Evented {
181
181
  this._cache.reset();
182
182
 
183
183
  for (const i in this._tiles) {
184
- this._reloadTile(i, 'reloading');
184
+ if (this._tiles[i].state !== 'errored') this._reloadTile(i, 'reloading');
185
185
  }
186
186
  }
187
187
 
@@ -101,8 +101,9 @@ class Tile {
101
101
  }
102
102
 
103
103
  this.queryPadding = 0;
104
- for (const bucket of buckets) {
105
- this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(bucket.layerIds[0]).queryRadius(bucket));
104
+ for (const id in this.buckets) {
105
+ const bucket = this.buckets[id];
106
+ this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(id).queryRadius(bucket));
106
107
  }
107
108
 
108
109
  if (data.imageAtlas) {
@@ -297,8 +298,8 @@ class Tile {
297
298
 
298
299
  const vtLayers = this.latestFeatureIndex.loadVTLayers();
299
300
 
300
- for (const i in this.buckets) {
301
- const bucket = this.buckets[i];
301
+ for (const id in this.buckets) {
302
+ const bucket = this.buckets[id];
302
303
  // Buckets are grouped by common source-layer
303
304
  const sourceLayerId = bucket.layers[0]['sourceLayer'] || '_geojsonTileLayer';
304
305
  const sourceLayer = vtLayers[sourceLayerId];
@@ -307,7 +308,7 @@ class Tile {
307
308
 
308
309
  bucket.update(sourceLayerStates, sourceLayer, this.imageAtlas?.patternPositions || {});
309
310
  if (painter?.style) {
310
- this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(bucket.layerIds[0]).queryRadius(bucket));
311
+ this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(id).queryRadius(bucket));
311
312
  }
312
313
  }
313
314
  }
@@ -38,6 +38,7 @@ class Worker {
38
38
  }
39
39
  globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping;
40
40
  globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText;
41
+ globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText;
41
42
  };
42
43
  }
43
44
 
@@ -71,16 +72,6 @@ class Worker {
71
72
  }
72
73
  }
73
74
 
74
- /**
75
- * Load a {@link WorkerSource} script at params.url. The script is run
76
- * (using importScripts) with `registerWorkerSource` in scope, which is a
77
- * function taking `(name, workerSourceObject)`.
78
- * @private
79
- */
80
- loadWorkerSource(map, params) {
81
- this.self.importScripts(params.url);
82
- }
83
-
84
75
  loadRTLTextPlugin(map, pluginURL) {
85
76
  if (!globalRTLTextPlugin.isLoaded()) {
86
77
  this.self.importScripts(pluginURL);
@@ -633,6 +633,10 @@ class Style extends Evented {
633
633
  this.fire(new ErrorEvent(new Error('The sourceLayer parameter must be provided for vector source types.')));
634
634
  return;
635
635
  }
636
+ if (feature.id == null || feature.id === '') {
637
+ this.fire(new ErrorEvent(new Error('The feature id parameter must be provided.')));
638
+ return;
639
+ }
636
640
 
637
641
  sourceCache.setFeatureState(sourceLayer, feature.id, state);
638
642
  }
@@ -767,25 +771,6 @@ class Style extends Evented {
767
771
  return sourceCache ? querySourceFeatures(sourceCache, params) : [];
768
772
  }
769
773
 
770
- addSourceType(name, SourceType, callback) {
771
- if (Style.getSourceType(name)) {
772
- return callback(new Error(`A source type called "${name}" already exists.`));
773
- }
774
-
775
- Style.setSourceType(name, SourceType);
776
-
777
- if (!SourceType.workerSourceURL) {
778
- return callback(null, null);
779
- }
780
-
781
- this.dispatcher
782
- .broadcast('loadWorkerSource', {
783
- name: name,
784
- url: SourceType.workerSourceURL
785
- })
786
- .then(data => callback(null, data), callback);
787
- }
788
-
789
774
  getLight() {
790
775
  return this.light.getLight();
791
776
  }
@@ -106,7 +106,7 @@ const layout = new Properties({
106
106
  expression: { parameters: ['zoom'] }
107
107
  }),
108
108
  'text-field': new DataDrivenProperty({
109
- type: 'string',
109
+ type: 'formatted',
110
110
  default: '',
111
111
  tokens: true,
112
112
  expression: { parameters: ['zoom', 'feature'] }
@@ -46,20 +46,6 @@ class CompoundExpression {
46
46
  signature.length === args.length - 1 // correct param count
47
47
  );
48
48
 
49
- // First parse all the args
50
- const parsedArgs = [];
51
- for (let i = 1; i < args.length; i++) {
52
- const arg = args[i];
53
- let expected;
54
- if (overloads.length === 1) {
55
- const params = overloads[0][0];
56
- expected = Array.isArray(params) ? params[i - 1] : params.type;
57
- }
58
- const parsed = context.parse(arg, 1 + parsedArgs.length, expected);
59
- if (!parsed) return null;
60
- parsedArgs.push(parsed);
61
- }
62
-
63
49
  let signatureContext = null;
64
50
 
65
51
  for (const [params, evaluate] of overloads) {
@@ -67,6 +53,27 @@ class CompoundExpression {
67
53
  // we eventually succeed, we haven't polluted `context.errors`.
68
54
  signatureContext = new ParsingContext(context.registry, context.path, null, context.scope);
69
55
 
56
+ // First parse all the args, potentially coercing to the
57
+ // types expected by this overload.
58
+ const parsedArgs = [];
59
+ let argParseFailed = false;
60
+ for (let i = 1; i < args.length; i++) {
61
+ const arg = args[i];
62
+ const expectedType = Array.isArray(params) ? params[i - 1] : params.type;
63
+
64
+ const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType);
65
+ if (!parsed) {
66
+ argParseFailed = true;
67
+ break;
68
+ }
69
+ parsedArgs.push(parsed);
70
+ }
71
+ if (argParseFailed) {
72
+ // Couldn't coerce args of this overload to expected type, move
73
+ // on to next one.
74
+ continue;
75
+ }
76
+
70
77
  if (Array.isArray(params)) {
71
78
  if (params.length !== parsedArgs.length) {
72
79
  signatureContext.error(`Expected ${params.length} arguments, but found ${parsedArgs.length} instead.`);
@@ -92,8 +99,15 @@ class CompoundExpression {
92
99
  } else {
93
100
  const expected = overloads.length ? overloads : availableOverloads;
94
101
  const signatures = expected.map(([params]) => stringifySignature(params)).join(' | ');
95
- const actualTypes = parsedArgs.map(arg => toString(arg.type)).join(', ');
96
- context.error(`Expected arguments of type ${signatures}, but found (${actualTypes}) instead.`);
102
+ const actualTypes = [];
103
+ // For error message, re-parse arguments without trying to
104
+ // apply any coercions
105
+ for (let i = 1; i < args.length; i++) {
106
+ const parsed = context.parse(args[i], 1 + actualTypes.length);
107
+ if (!parsed) return null;
108
+ actualTypes.push(toString(parsed.type));
109
+ }
110
+ context.error(`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`);
97
111
  }
98
112
 
99
113
  return null;
@@ -3,6 +3,7 @@ const assert = require('assert');
3
3
  const { ColorType, ValueType, NumberType } = require('../types');
4
4
  const { Color, validateRGBA } = require('../values');
5
5
  const RuntimeError = require('../runtime_error');
6
+ const { Formatted, FormattedSection } = require('./formatted');
6
7
 
7
8
  const types = {
8
9
  'to-number': NumberType,
@@ -65,6 +66,18 @@ class Coercion {
65
66
  error || `Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
66
67
  );
67
68
  }
69
+ if (this.type.kind === 'formatted') {
70
+ let input;
71
+ for (const arg of this.args) {
72
+ input = arg.evaluate(ctx);
73
+ if (typeof input === 'string') {
74
+ return new Formatted([new FormattedSection(input, null, null)]);
75
+ }
76
+ }
77
+ throw new RuntimeError(
78
+ `Could not parse formatted text from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
79
+ );
80
+ }
68
81
  let value = null;
69
82
  for (const arg of this.args) {
70
83
  value = arg.evaluate(ctx);
@@ -0,0 +1,193 @@
1
+ const { toString, ValueType, BooleanType, CollatorType } = require('../types');
2
+ const Assertion = require('./assertion');
3
+ const { typeOf } = require('../values');
4
+ const RuntimeError = require('../runtime_error');
5
+
6
+ function isComparableType(op, type) {
7
+ if (op === '==' || op === '!=') {
8
+ // equality operator
9
+ return (
10
+ type.kind === 'boolean' ||
11
+ type.kind === 'string' ||
12
+ type.kind === 'number' ||
13
+ type.kind === 'null' ||
14
+ type.kind === 'value'
15
+ );
16
+ }
17
+ // ordering operator
18
+ return type.kind === 'string' || type.kind === 'number' || type.kind === 'value';
19
+ }
20
+
21
+ function eq(ctx, a, b) {
22
+ return a === b;
23
+ }
24
+ function neq(ctx, a, b) {
25
+ return a !== b;
26
+ }
27
+ function lt(ctx, a, b) {
28
+ return a < b;
29
+ }
30
+ function gt(ctx, a, b) {
31
+ return a > b;
32
+ }
33
+ function lteq(ctx, a, b) {
34
+ return a <= b;
35
+ }
36
+ function gteq(ctx, a, b) {
37
+ return a >= b;
38
+ }
39
+
40
+ function eqCollate(ctx, a, b, c) {
41
+ return c.compare(a, b) === 0;
42
+ }
43
+ function neqCollate(ctx, a, b, c) {
44
+ return !eqCollate(ctx, a, b, c);
45
+ }
46
+ function ltCollate(ctx, a, b, c) {
47
+ return c.compare(a, b) < 0;
48
+ }
49
+ function gtCollate(ctx, a, b, c) {
50
+ return c.compare(a, b) > 0;
51
+ }
52
+ function lteqCollate(ctx, a, b, c) {
53
+ return c.compare(a, b) <= 0;
54
+ }
55
+ function gteqCollate(ctx, a, b, c) {
56
+ return c.compare(a, b) >= 0;
57
+ }
58
+
59
+ /**
60
+ * Special form for comparison operators, implementing the signatures:
61
+ * - (T, T, ?Collator) => boolean
62
+ * - (T, value, ?Collator) => boolean
63
+ * - (value, T, ?Collator) => boolean
64
+ *
65
+ * For inequalities, T must be either value, string, or number. For ==/!=, it
66
+ * can also be boolean or null.
67
+ *
68
+ * Equality semantics are equivalent to Javascript's strict equality (===/!==)
69
+ * -- i.e., when the arguments' types don't match, == evaluates to false, != to
70
+ * true.
71
+ *
72
+ * When types don't match in an ordering comparison, a runtime error is thrown.
73
+ *
74
+ * @private
75
+ */
76
+ function makeComparison(op, compareBasic, compareWithCollator) {
77
+ const isOrderComparison = op !== '==' && op !== '!=';
78
+
79
+ return class Comparison {
80
+ constructor(lhs, rhs, collator) {
81
+ this.type = BooleanType;
82
+ this.lhs = lhs;
83
+ this.rhs = rhs;
84
+ this.collator = collator;
85
+ this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value';
86
+ }
87
+
88
+ static parse(args, context) {
89
+ if (args.length !== 3 && args.length !== 4) return context.error('Expected two or three arguments.');
90
+
91
+ const op = args[0];
92
+
93
+ let lhs = context.parse(args[1], 1, ValueType);
94
+ if (!lhs) return null;
95
+ if (!isComparableType(op, lhs.type)) {
96
+ return context.concat(1).error(`"${op}" comparisons are not supported for type '${toString(lhs.type)}'.`);
97
+ }
98
+ let rhs = context.parse(args[2], 2, ValueType);
99
+ if (!rhs) return null;
100
+ if (!isComparableType(op, rhs.type)) {
101
+ return context.concat(2).error(`"${op}" comparisons are not supported for type '${toString(rhs.type)}'.`);
102
+ }
103
+
104
+ if (lhs.type.kind !== rhs.type.kind && lhs.type.kind !== 'value' && rhs.type.kind !== 'value') {
105
+ return context.error(`Cannot compare types '${toString(lhs.type)}' and '${toString(rhs.type)}'.`);
106
+ }
107
+
108
+ if (isOrderComparison) {
109
+ // typing rules specific to less/greater than operators
110
+ if (lhs.type.kind === 'value' && rhs.type.kind !== 'value') {
111
+ // (value, T)
112
+ lhs = new Assertion(rhs.type, [lhs]);
113
+ } else if (lhs.type.kind !== 'value' && rhs.type.kind === 'value') {
114
+ // (T, value)
115
+ rhs = new Assertion(lhs.type, [rhs]);
116
+ }
117
+ }
118
+
119
+ let collator = null;
120
+ if (args.length === 4) {
121
+ if (
122
+ lhs.type.kind !== 'string' &&
123
+ rhs.type.kind !== 'string' &&
124
+ lhs.type.kind !== 'value' &&
125
+ rhs.type.kind !== 'value'
126
+ ) {
127
+ return context.error('Cannot use collator to compare non-string types.');
128
+ }
129
+ collator = context.parse(args[3], 3, CollatorType);
130
+ if (!collator) return null;
131
+ }
132
+
133
+ return new Comparison(lhs, rhs, collator);
134
+ }
135
+
136
+ evaluate(ctx) {
137
+ const lhs = this.lhs.evaluate(ctx);
138
+ const rhs = this.rhs.evaluate(ctx);
139
+
140
+ if (isOrderComparison && this.hasUntypedArgument) {
141
+ const lt = typeOf(lhs);
142
+ const rt = typeOf(rhs);
143
+ // check that type is string or number, and equal
144
+ if (lt.kind !== rt.kind || !(lt.kind === 'string' || lt.kind === 'number')) {
145
+ throw new RuntimeError(
146
+ `Expected arguments for "${op}" to be (string, string) or (number, number), but found (${lt.kind}, ${rt.kind}) instead.`
147
+ );
148
+ }
149
+ }
150
+
151
+ if (this.collator && !isOrderComparison && this.hasUntypedArgument) {
152
+ const lt = typeOf(lhs);
153
+ const rt = typeOf(rhs);
154
+ if (lt.kind !== 'string' || rt.kind !== 'string') {
155
+ return compareBasic(ctx, lhs, rhs);
156
+ }
157
+ }
158
+
159
+ return this.collator
160
+ ? compareWithCollator(ctx, lhs, rhs, this.collator.evaluate(ctx))
161
+ : compareBasic(ctx, lhs, rhs);
162
+ }
163
+
164
+ eachChild(fn) {
165
+ fn(this.lhs);
166
+ fn(this.rhs);
167
+ if (this.collator) {
168
+ fn(this.collator);
169
+ }
170
+ }
171
+
172
+ possibleOutputs() {
173
+ return [true, false];
174
+ }
175
+
176
+ serialize() {
177
+ const serialized = [op];
178
+ this.eachChild(child => {
179
+ serialized.push(child.serialize());
180
+ });
181
+ return serialized;
182
+ }
183
+ };
184
+ }
185
+
186
+ module.exports = {
187
+ Equals: makeComparison('==', eq, eqCollate),
188
+ NotEquals: makeComparison('!=', neq, neqCollate),
189
+ LessThan: makeComparison('<', lt, ltCollate),
190
+ GreaterThan: makeComparison('>', gt, gtCollate),
191
+ LessThanOrEqual: makeComparison('<=', lteq, lteqCollate),
192
+ GreaterThanOrEqual: makeComparison('>=', gteq, gteqCollate)
193
+ };