diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b52e354e..733bd2cd7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
#### :sparkles: Usability & Accessibility
* Autocomplete changeset `source` tag with sources of the previous 100 changesets of the user ([#10764], thanks [@k-yle])
* Also show search result for coordinates in `lon/lat` order in search results ([#10720], thanks [@Deeptanshu-sankhwar])
+* Linkify keys & tags in the preset docs from the wiki ([#10763], thanks [@k-yle])
* Allow broken (unclosed) areas to be continued ([#9635], thanks [@k-yle])
#### :scissors: Operations
* Fix splitting of closed ways (or areas) when two or more split-points are selected
@@ -69,6 +70,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
[#10747]: https://github.com/openstreetmap/iD/issues/10747
[#10748]: https://github.com/openstreetmap/iD/issues/10748
[#10755]: https://github.com/openstreetmap/iD/issues/10755
+[#10763]: https://github.com/openstreetmap/iD/pull/10763
[#10764]: https://github.com/openstreetmap/iD/issues/10764
[#10766]: https://github.com/openstreetmap/iD/pull/10766
[@hlfan]: https://github.com/hlfan
diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js
index d2bc29ed7..25670b345 100644
--- a/modules/services/osm_wikibase.js
+++ b/modules/services/osm_wikibase.js
@@ -102,6 +102,38 @@ export default {
return result.replace(/_/g, ' ').trim();
},
+ /**
+ * Converts text like `tag:...=...` into clickable links
+ *
+ * @param {string} unsafeText - unsanitized text
+ */
+ linkifyWikiText(unsafeText) {
+ /** @param {import('d3').Selection} selection */
+ return (selection) => {
+ const segments = unsafeText.split(/(key|tag):([\w-]+)(=([\w-]+))?/g);
+
+ for (let i = 0; i < segments.length; i += 5) {
+ const [plainText, , key, , value] = segments.slice(i);
+
+ if (plainText) {
+ selection
+ .append('span')
+ .text(plainText);
+ }
+
+ if (key) {
+ selection
+ .append('a')
+ .attr('href', `https://wiki.openstreetmap.org/wiki/${this.toSitelink(key, value)}`)
+ .attr('target', '_blank')
+ .attr('rel', 'noreferrer')
+ .append('code')
+ .text(`${key}=${value || '*'}`);
+ }
+ }
+ };
+ },
+
//
// Pass params object of the form:
@@ -269,7 +301,7 @@ export default {
// prepare result
var result = {
title: entity.title,
- description: description ? description.value : '',
+ description: that.linkifyWikiText(description?.value || ''),
descriptionLocaleCode: description ? description.language : '',
editURL: 'https://wiki.openstreetmap.org/wiki/' + entity.title
};
diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js
index b49f7540d..411cad415 100644
--- a/modules/ui/tag_reference.js
+++ b/modules/ui/tag_reference.js
@@ -53,7 +53,7 @@ export function uiTagReference(what) {
_body
.append('img')
.attr('class', 'tag-reference-wiki-image')
- .attr('alt', docs.description)
+ .attr('alt', docs.title)
.attr('src', docs.imageURL)
.on('load', function() { done(); })
.on('error', function() { d3_select(this).remove(); done(); });
@@ -69,7 +69,7 @@ export function uiTagReference(what) {
tagReferenceDescription = tagReferenceDescription
.attr('class', 'localized-text')
.attr('lang', docs.descriptionLocaleCode || 'und')
- .text(docs.description);
+ .call(docs.description);
} else {
tagReferenceDescription = tagReferenceDescription
.call(t.append('inspector.no_documentation_key'));
diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js
index 021ba46a5..ce736f477 100644
--- a/test/spec/services/osm_wikibase.js
+++ b/test/spec/services/osm_wikibase.js
@@ -331,4 +331,75 @@ describe('iD.serviceOsmWikibase', function () {
});
});
+ describe('linkifyWikiText', () => {
+ it('handles normal text', () => {
+ const main = document.createElement('main');
+ d3.select(main).call(iD.serviceOsmWikibase.linkifyWikiText('hello'));
+
+ expect(main.innerHTML).toBe('hello');
+ expect(main.textContent).toBe('hello');
+ });
+
+ it('prevents XSS attacks', () => {
+ const main = document.createElement('main');
+ d3.select(main).call(iD.serviceOsmWikibase.linkifyWikiText('123 456'));
+
+ expect(main.innerHTML).toBe('123 <script>bad</script> 456');
+ expect(main.textContent).toBe('123 456');
+ });
+
+ it('linkifies the tag: and key: syntax', () => {
+ const main = document.createElement('main');
+ d3.select(main).call(iD.serviceOsmWikibase.linkifyWikiText('use tag:natural=water with key:water instead'));
+
+ expect(main.innerHTML).toBe([
+ 'use ',
+ 'natural=water',
+ ' with ',
+ 'water=*',
+ ' instead'
+ ].join(''));
+ expect(main.textContent).toBe('use natural=water with water=* instead');
+ });
+
+ it('works if the string is 100% a link', () => {
+ const main = document.createElement('main');
+ d3.select(main).call(iD.serviceOsmWikibase.linkifyWikiText('tag:natural=water'));
+
+ expect(main.innerHTML).toBe([
+ 'natural=water',
+ ].join(''));
+ expect(main.textContent).toBe('natural=water');
+ });
+
+ it('works if the link is the first part of the string', () => {
+ const main = document.createElement('main');
+ d3.select(main).call(iD.serviceOsmWikibase.linkifyWikiText('tag:craft=sailmaker is better'));
+
+ expect(main.innerHTML).toBe([
+ 'craft=sailmaker',
+ ' is better'
+ ].join(''));
+ expect(main.textContent).toBe('craft=sailmaker is better');
+ });
+
+ it('works if the link is the last part of the string', () => {
+ const main = document.createElement('main');
+ d3.select(main).call(iD.serviceOsmWikibase.linkifyWikiText('prefer tag:craft=sailmaker'));
+
+ expect(main.innerHTML).toBe([
+ 'prefer ',
+ 'craft=sailmaker',
+ ].join(''));
+ expect(main.textContent).toBe('prefer craft=sailmaker');
+ });
+
+ it('handles empty strings', () => {
+ const main = document.createElement('main');
+ d3.select(main).call(iD.serviceOsmWikibase.linkifyWikiText(''));
+
+ expect(main.innerHTML).toBe('');
+ expect(main.textContent).toBe('');
+ });
+ });
});