From c1ca888b72218132bc81d0e1fd52769237a16a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ky=E2=84=93e=20Hensel?= Date: Thu, 13 Feb 2025 01:42:29 +1100 Subject: [PATCH] linkify keys & tags in the preset docs from the wiki (#10763) --- CHANGELOG.md | 2 + modules/services/osm_wikibase.js | 34 +++++++++++++- modules/ui/tag_reference.js | 4 +- test/spec/services/osm_wikibase.js | 71 ++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) 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(''); + }); + }); });