diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js
index 55638d86b..53891db21 100644
--- a/modules/ui/entity_editor.js
+++ b/modules/ui/entity_editor.js
@@ -20,7 +20,7 @@ import { uiRawMembershipEditor } from './raw_membership_editor';
import { uiRawTagEditor } from './raw_tag_editor';
import { uiTagReference } from './tag_reference';
import { uiPresetEditor } from './preset_editor';
-import { utilRebind } from '../util';
+import { utilCleanTags, utilRebind } from '../util';
export function uiEntityEditor(context) {
@@ -213,51 +213,6 @@ export function uiEntityEditor(context) {
}
- function clean(orig) {
-
- function cleanVal(k, v) {
- function keepSpaces(k) {
- return /_hours|_times|:conditional$/.test(k);
- }
-
- function skip(k) {
- return /^(description|note|fixme)$/.test(k);
- }
-
- if (skip(k)) return v;
-
- var cleaned = v
- .split(';')
- .map(function(s) { return s.trim(); })
- .join(keepSpaces(k) ? '; ' : ';');
-
- // The code below is not intended to validate websites and emails.
- // It is only intended to prevent obvious copy-paste errors. (#2323)
- // clean website- and email-like tags
- if (k.indexOf('website') !== -1 ||
- k.indexOf('email') !== -1 ||
- cleaned.indexOf('http') === 0) {
- cleaned = cleaned
- .replace(/[\u200B-\u200F\uFEFF]/g, ''); // strip LRM and other zero width chars
-
- }
-
- return cleaned;
- }
-
- var out = {};
- for (var k in orig) {
- if (!k) continue;
- var v = orig[k];
- if (v !== undefined) {
- out[k] = cleanVal(k, v);
- }
- }
-
- return out;
- }
-
-
// Tag changes that fire on input can all get coalesced into a single
// history operation when the user leaves the field. #2342
function changeTags(changed, onInput) {
@@ -274,7 +229,7 @@ export function uiEntityEditor(context) {
}
if (!onInput) {
- tags = clean(tags);
+ tags = utilCleanTags(tags);
}
if (!_isEqual(entity.tags, tags)) {
diff --git a/modules/util/clean_tags.js b/modules/util/clean_tags.js
new file mode 100644
index 000000000..200da3a66
--- /dev/null
+++ b/modules/util/clean_tags.js
@@ -0,0 +1,43 @@
+export function utilCleanTags(tags) {
+ var out = {};
+ for (var k in tags) {
+ if (!k) continue;
+ var v = tags[k];
+ if (v !== undefined) {
+ out[k] = cleanValue(k, v);
+ }
+ }
+
+ return out;
+
+
+ function cleanValue(k, v) {
+ function keepSpaces(k) {
+ return /_hours|_times|:conditional$/.test(k);
+ }
+
+ function skip(k) {
+ return /^(description|note|fixme)$/.test(k);
+ }
+
+ if (skip(k)) return v;
+
+ var cleaned = v
+ .split(';')
+ .map(function(s) { return s.trim(); })
+ .join(keepSpaces(k) ? '; ' : ';');
+
+ // The code below is not intended to validate websites and emails.
+ // It is only intended to prevent obvious copy-paste errors. (#2323)
+ // clean website- and email-like tags
+ if (k.indexOf('website') !== -1 ||
+ k.indexOf('email') !== -1 ||
+ cleaned.indexOf('http') === 0) {
+ cleaned = cleaned
+ .replace(/[\u200B-\u200F\uFEFF]/g, ''); // strip LRM and other zero width chars
+
+ }
+
+ return cleaned;
+ }
+}
diff --git a/modules/util/index.js b/modules/util/index.js
index 577c42db5..f64856de8 100644
--- a/modules/util/index.js
+++ b/modules/util/index.js
@@ -1,5 +1,6 @@
export { utilAsyncMap } from './util';
export { utilCallWhenIdle } from './call_when_idle';
+export { utilCleanTags } from './clean_tags';
export { utilDisplayName } from './util';
export { utilDisplayNameForPath } from './util';
export { utilDisplayType } from './util';
diff --git a/test/index.html b/test/index.html
index 9c0c50a29..42e7f145f 100644
--- a/test/index.html
+++ b/test/index.html
@@ -129,6 +129,7 @@
+
diff --git a/test/spec/util/clean_tags.js b/test/spec/util/clean_tags.js
new file mode 100644
index 000000000..44552c527
--- /dev/null
+++ b/test/spec/util/clean_tags.js
@@ -0,0 +1,75 @@
+describe('iD.utilCleanTags', function() {
+ it('handles empty tags object', function() {
+ var t = {};
+ var result = iD.utilCleanTags(t);
+ expect(result).to.eql({});
+ });
+
+ it('discards empty keys', function() {
+ var t = { '': 'bar' };
+ var result = iD.utilCleanTags(t);
+ expect(result).to.eql({});
+ });
+
+ it('discards undefined values', function() {
+ var t = { 'foo': undefined };
+ var result = iD.utilCleanTags(t);
+ expect(result).to.eql({});
+ });
+
+ it('trims whitespace', function() {
+ var t = {
+ 'leading': ' value',
+ 'trailing': 'value ',
+ 'both': ' value '
+ };
+ var result = iD.utilCleanTags(t);
+ expect(result).to.eql({
+ 'leading': 'value',
+ 'trailing': 'value',
+ 'both': 'value'
+ });
+ });
+
+ it('trims semicolon delimited whitespace', function() {
+ var t = {
+ 'leading': ' value1; value2',
+ 'trailing': 'value1 ;value2 ',
+ 'both': ' value1 ; value2 '
+ };
+ var result = iD.utilCleanTags(t);
+ expect(result).to.eql({
+ 'leading': 'value1;value2',
+ 'trailing': 'value1;value2',
+ 'both': 'value1;value2'
+ });
+ });
+
+ it('does not clean description, note, fixme', function() {
+ var t = {
+ 'description': ' value',
+ 'note': 'value ',
+ 'fixme': ' value '
+ };
+ var result = iD.utilCleanTags(t);
+ expect(result).to.eql(t);
+ });
+
+ it('uses semicolon-space delimiting for opening_hours, conditional: tags', function() {
+ var t = {
+ 'opening_hours': ' Mo-Su 08:00-18:00 ;Apr 10-15 off;Jun 08:00-14:00 ; Aug off; Dec 25 off ',
+ 'collection_times': ' Mo 10:00-12:00,12:30-15:00 ;Tu-Fr 08:00-12:00,12:30-15:00;Sa 08:00-12:00 ',
+ 'maxspeed:conditional': ' 120 @ (06:00-20:00) ;80 @ wet ',
+ 'restriction:conditional': ' no_u_turn @ (Mo-Fr 09:00-10:00,15:00-16:00;SH off) '
+ };
+ var result = iD.utilCleanTags(t);
+ expect(result).to.eql({
+ 'opening_hours': 'Mo-Su 08:00-18:00; Apr 10-15 off; Jun 08:00-14:00; Aug off; Dec 25 off',
+ 'collection_times': 'Mo 10:00-12:00,12:30-15:00; Tu-Fr 08:00-12:00,12:30-15:00; Sa 08:00-12:00',
+ 'maxspeed:conditional': '120 @ (06:00-20:00); 80 @ wet',
+ 'restriction:conditional': 'no_u_turn @ (Mo-Fr 09:00-10:00,15:00-16:00; SH off)'
+ });
+ });
+
+});
+