Skip to content

Commit bd6e84d

Browse files
authored
Make tag values that are external links clickable using tag2link (#813)
1 parent fb6df4e commit bd6e84d

File tree

5 files changed

+110
-5
lines changed

5 files changed

+110
-5
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"showdown": "^1.8.6",
5353
"stream": "^0.0.3",
5454
"superagent": "^3.5.2",
55+
"tag2link": "^2025.5.21",
5556
"terra-draw": "^1.1.0",
5657
"terra-draw-maplibre-gl-adapter": "^1.0.1"
5758
},

src/components/element_info.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Dropdown } from './dropdown';
88
import { Button } from './button';
99
import thumbsDown from '../assets/thumbs-down.svg';
1010
import type { RootStateType } from '../store';
11+
import { TagValue } from './tag_value.js';
1112

1213
/*
1314
* Displays info about an element that was created/modified/deleted.
@@ -303,7 +304,9 @@ function TagsTable({ action }) {
303304
<span dir="auto">{key}</span>
304305
</td>
305306
<td>
306-
<span dir="auto">{newval}</span>
307+
<span dir="auto">
308+
<TagValue k={key} v={newval} />
309+
</span>
307310
</td>
308311
</tr>
309312
);
@@ -314,7 +317,9 @@ function TagsTable({ action }) {
314317
<span dir="auto">{key}</span>
315318
</td>
316319
<td>
317-
<span dir="auto">{newval}</span>
320+
<span dir="auto">
321+
<TagValue k={key} v={newval} />
322+
</span>
318323
</td>
319324
</tr>
320325
);
@@ -325,7 +330,9 @@ function TagsTable({ action }) {
325330
<span dir="auto">{key}</span>
326331
</td>
327332
<td>
328-
<span dir="auto">{oldval}</span>
333+
<span dir="auto">
334+
<TagValue k={key} v={oldval} />
335+
</span>
329336
</td>
330337
</tr>
331338
);
@@ -336,9 +343,13 @@ function TagsTable({ action }) {
336343
<span dir="auto">{key}</span>
337344
</td>
338345
<td>
339-
<del dir="auto">{oldval}</del>
346+
<del dir="auto">
347+
<TagValue k={key} v={oldval} />
348+
</del>
340349
{' → '}
341-
<ins dir="auto">{newval}</ins>
350+
<ins dir="auto">
351+
<TagValue k={key} v={newval} />
352+
</ins>
342353
</td>
343354
</tr>
344355
);

src/components/tag_value.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// @ts-check
2+
import { Fragment } from 'react';
3+
import tag2linkRaw from 'tag2link';
4+
5+
const RANKS = ['deprecated', 'normal', 'preferred'];
6+
7+
/**
8+
* @typedef {{
9+
* key: `Key:${string}`;
10+
* url: string;
11+
* source: string;
12+
* rank: "normal" | "preferred";
13+
* }} Tag2LinkItem
14+
*/
15+
16+
/** @param {Tag2LinkItem[]} input */
17+
function convertSourceData(input) {
18+
/** @type {Record<string, string>} */
19+
const output = {};
20+
21+
const allKeys = new Set(input.map(item => item.key));
22+
23+
for (const key of allKeys) {
24+
// find the item with the best rank
25+
const bestDefinition = input
26+
.filter(item => item.key === key)
27+
.sort((a, b) => RANKS.indexOf(b.rank) - RANKS.indexOf(a.rank))[0];
28+
29+
output[key.replace('Key:', '')] = bestDefinition.url;
30+
}
31+
32+
return output;
33+
}
34+
35+
export const TAG2LINK = convertSourceData(tag2linkRaw);
36+
37+
/** @type {React.FC<{ k: string; v: string }>} */
38+
export const TagValue = ({ k, v }) => {
39+
const placeholderUrl = TAG2LINK[k];
40+
41+
// simple key, not clickable
42+
if (!placeholderUrl) return v;
43+
44+
// clickable values
45+
return v.split(';').map((chunk, index) => ((
46+
<Fragment key={index}>
47+
{!!index && ';'}
48+
<a
49+
href={
50+
/^https?:\/\//i.test(chunk)
51+
? chunk
52+
: placeholderUrl.replaceAll('$1', chunk)
53+
}
54+
target="_blank"
55+
rel="noreferrer"
56+
>
57+
{chunk}
58+
</a>
59+
</Fragment>
60+
)));
61+
};

src/components/tag_value.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createElement } from 'react';
2+
import renderer from 'react-test-renderer';
3+
import { TagValue } from './tag_value';
4+
5+
describe('TagValue', () => {
6+
it.each`
7+
tag | expected
8+
${'highway=primary'} | ${[]}
9+
${'wikidata=Q123'} | ${['https://www.wikidata.org/entity/Q123']}
10+
${'wikidata=Q123;Q456'} | ${['https://www.wikidata.org/entity/Q123', 'https://www.wikidata.org/entity/Q456']}
11+
${'contact:instagram=bob'} | ${['https://www.instagram.com/bob']}
12+
${'contact:instagram=https://instagr.am/bob'} | ${['https://instagr.am/bob']}
13+
${'contact:instagram=alice;https://instagr.am/bob'} | ${['https://www.instagram.com/alice', 'https://instagr.am/bob']}
14+
${'website=http://example.com'} | ${['http://example.com']}
15+
${'ref:FR:CEF=1234'} | ${['https://messes.info/lieu/1234']}
16+
`('$tag', ({ tag, expected }) => {
17+
const [k, v] = tag.split('=');
18+
const container = renderer.create(createElement(TagValue, { k, v }));
19+
20+
const actual = container.root.findAllByType('a');
21+
expect(actual).toHaveLength(expected.length);
22+
23+
for (let i = 0; i < expected.length; i++) {
24+
expect(actual[i].props.href).toStrictEqual(expected[i]);
25+
}
26+
});
27+
});

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14381,6 +14381,11 @@ table@4.0.2:
1438114381
slice-ansi "1.0.0"
1438214382
string-width "^2.1.1"
1438314383

14384+
tag2link@^2025.5.21:
14385+
version "2025.5.21"
14386+
resolved "https://registry.yarnpkg.com/tag2link/-/tag2link-2025.5.21.tgz#5f02fd412e854744a141b4d6217e6acf33e23d93"
14387+
integrity sha512-vcz6/6U5V3QYPA7geLrtzLMqhCwg6OvoGP665DbGgPRSWASjFn0J4jA/NyifD3Tmp2yf8RnEuI6QBvhXGBVSHg==
14388+
1438414389
tailwindcss@^3.0.2:
1438514390
version "3.4.15"
1438614391
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.15.tgz#04808bf4bf1424b105047d19e7d4bfab368044a9"

0 commit comments

Comments
 (0)