@hhkaos/webmentions-widget 0.5.4 → 0.6.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.
- package/README.md +35 -0
- package/package.json +1 -1
- package/src/core.js +41 -0
- package/src/react.js +47 -24
- package/src/render.js +29 -27
package/README.md
CHANGED
|
@@ -197,6 +197,12 @@ Finds the anchor in the source page that points back at you and quotes the
|
|
|
197
197
|
sentence around it, rather than excerpting from the top of the post. Falls back to
|
|
198
198
|
`content.text`. Pass `parseHTML` to run outside a browser.
|
|
199
199
|
|
|
200
|
+
### `getMentionAuthor(mention)`
|
|
201
|
+
|
|
202
|
+
Returns `{name, url, photo, initial, hue}`. `name` falls back to the source host,
|
|
203
|
+
and `initial`/`hue` are there so a mention whose h-card has no `photo` — a plain
|
|
204
|
+
blog, or your own site — still renders an avatar instead of a gap.
|
|
205
|
+
|
|
200
206
|
### `getMentionSourceUrl(mention, content)`
|
|
201
207
|
|
|
202
208
|
Appends a `#:~:text=` fragment so the source link lands on the quoted sentence.
|
|
@@ -209,6 +215,35 @@ can hydrate from a build-time snapshot without touching the network.
|
|
|
209
215
|
Every remote string is written with `textContent`. Nothing in this package ever
|
|
210
216
|
assigns remote HTML.
|
|
211
217
|
|
|
218
|
+
## Avatars
|
|
219
|
+
|
|
220
|
+
Both renderers always emit an avatar element per mention. With `author.photo`
|
|
221
|
+
it is an `<img>` carrying the photo class (`webmention-photo` in React,
|
|
222
|
+
`webmentions__photo` in the DOM renderer); without one it is a `<span>` holding
|
|
223
|
+
the author's initial, carrying that same class plus `is-initial` and a
|
|
224
|
+
`--webmention-avatar-hue` custom property (0–359, stable per author name).
|
|
225
|
+
|
|
226
|
+
The package ships no CSS. Style the fallback yourself — the hue is offered, not
|
|
227
|
+
imposed:
|
|
228
|
+
|
|
229
|
+
```css
|
|
230
|
+
.webmention-photo {
|
|
231
|
+
border-radius: 50%;
|
|
232
|
+
flex: 0 0 40px;
|
|
233
|
+
height: 40px;
|
|
234
|
+
width: 40px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.webmention-photo.is-initial {
|
|
238
|
+
align-items: center;
|
|
239
|
+
background: hsl(var(--webmention-avatar-hue, 220) 45% 92%);
|
|
240
|
+
color: hsl(var(--webmention-avatar-hue, 220) 45% 30%);
|
|
241
|
+
display: flex;
|
|
242
|
+
font-weight: 700;
|
|
243
|
+
justify-content: center;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
212
247
|
## Notes on webmention.io quirks
|
|
213
248
|
|
|
214
249
|
- The jf2 feed says `mention-of`; some payloads say `mention`. `normalizeProperty`
|
package/package.json
CHANGED
package/src/core.js
CHANGED
|
@@ -46,6 +46,47 @@ export function cleanText(value) {
|
|
|
46
46
|
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The author as a renderer needs it: never an empty name, never an undefined
|
|
51
|
+
* photo. An h-card without `photo` is common — a mention from a plain blog, or
|
|
52
|
+
* from your own site — so `initial` is always there to fall back on and every
|
|
53
|
+
* mention can carry an avatar-shaped element.
|
|
54
|
+
*/
|
|
55
|
+
export function getMentionAuthor(mention) {
|
|
56
|
+
const author = mention?.author || {};
|
|
57
|
+
const source = mention?.url || mention?.['wm-source'] || '';
|
|
58
|
+
let host = '';
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
host = new URL(source).hostname.replace(/^www\./, '');
|
|
62
|
+
} catch {
|
|
63
|
+
host = '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const name = cleanText(author.name) || host || 'Someone';
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
url: author.url || source,
|
|
71
|
+
photo: author.photo || '',
|
|
72
|
+
initial: (name.charAt(0) || '?').toUpperCase(),
|
|
73
|
+
// A stable hue per author, so a wall of initials reads as distinct people
|
|
74
|
+
// rather than as one repeated placeholder. Only useful to a stylesheet, so
|
|
75
|
+
// it travels as a custom property rather than as an inline colour.
|
|
76
|
+
hue: getAuthorHue(name),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getAuthorHue(name) {
|
|
81
|
+
let hash = 0;
|
|
82
|
+
|
|
83
|
+
for (let index = 0; index < name.length; index += 1) {
|
|
84
|
+
hash = (hash * 31 + name.charCodeAt(index)) % 360;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return hash;
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
/**
|
|
50
91
|
* webmention.io and Bridgy mangle emoji into runs of "?" (and sometimes U+FFFD)
|
|
51
92
|
* when extracting plain text from a toot or skeet. The emoji is unrecoverable,
|
package/src/react.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
fetchWebmentions,
|
|
13
13
|
filterMentionsByTargets,
|
|
14
14
|
formatMentionDate,
|
|
15
|
+
getMentionAuthor,
|
|
15
16
|
getMentionContent,
|
|
16
17
|
getMentionSourceUrl,
|
|
17
18
|
getMentionType,
|
|
@@ -147,8 +148,36 @@ function renderLabel(label, keyPrefix) {
|
|
|
147
148
|
));
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Every mention gets an avatar-shaped element, photo or not: a row that
|
|
153
|
+
* sometimes starts with a 40px image and sometimes with nothing has two
|
|
154
|
+
* different layouts, and the one without reads as a rendering failure.
|
|
155
|
+
*/
|
|
156
|
+
function Avatar({author, className, fallbackClassName, size}) {
|
|
157
|
+
if (author.photo) {
|
|
158
|
+
return h('img', {
|
|
159
|
+
className,
|
|
160
|
+
src: author.photo,
|
|
161
|
+
alt: '',
|
|
162
|
+
loading: 'lazy',
|
|
163
|
+
width: size,
|
|
164
|
+
height: size,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return h(
|
|
169
|
+
'span',
|
|
170
|
+
{
|
|
171
|
+
className: fallbackClassName,
|
|
172
|
+
'aria-hidden': 'true',
|
|
173
|
+
style: {'--webmention-avatar-hue': author.hue},
|
|
174
|
+
},
|
|
175
|
+
author.initial,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
function Face({mention, classNames}) {
|
|
151
|
-
const author = mention
|
|
180
|
+
const author = getMentionAuthor(mention);
|
|
152
181
|
const sourceUrl = mention.url || mention['wm-source'] || author.url;
|
|
153
182
|
|
|
154
183
|
return h(
|
|
@@ -161,24 +190,20 @@ function Face({mention, classNames}) {
|
|
|
161
190
|
href: sourceUrl,
|
|
162
191
|
rel: 'nofollow noopener',
|
|
163
192
|
target: '_blank',
|
|
164
|
-
title: `${author.name
|
|
193
|
+
title: `${author.name} — ${getMentionType(mention)}`,
|
|
165
194
|
},
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
width: 32,
|
|
173
|
-
height: 32,
|
|
174
|
-
})
|
|
175
|
-
: h('span', {'aria-hidden': 'true'}, (author.name || '?').slice(0, 1).toUpperCase()),
|
|
195
|
+
h(Avatar, {
|
|
196
|
+
author,
|
|
197
|
+
className: classNames.facepilePhoto,
|
|
198
|
+
fallbackClassName: classNames.facepileInitial,
|
|
199
|
+
size: 32,
|
|
200
|
+
}),
|
|
176
201
|
),
|
|
177
202
|
);
|
|
178
203
|
}
|
|
179
204
|
|
|
180
205
|
function Thread({mention, classNames, labels, locale, maxLength}) {
|
|
181
|
-
const author = mention
|
|
206
|
+
const author = getMentionAuthor(mention);
|
|
182
207
|
const published = mention.published || mention['wm-received'];
|
|
183
208
|
const formattedDate = formatMentionDate(published, locale);
|
|
184
209
|
const content = useMemo(
|
|
@@ -190,16 +215,12 @@ function Thread({mention, classNames, labels, locale, maxLength}) {
|
|
|
190
215
|
return h(
|
|
191
216
|
'li',
|
|
192
217
|
{className: classNames.threadItem},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
width: 40,
|
|
200
|
-
height: 40,
|
|
201
|
-
})
|
|
202
|
-
: null,
|
|
218
|
+
h(Avatar, {
|
|
219
|
+
author,
|
|
220
|
+
className: classNames.threadPhoto,
|
|
221
|
+
fallbackClassName: classNames.threadInitial,
|
|
222
|
+
size: 40,
|
|
223
|
+
}),
|
|
203
224
|
h(
|
|
204
225
|
'div',
|
|
205
226
|
{className: classNames.threadBody},
|
|
@@ -214,7 +235,7 @@ function Thread({mention, classNames, labels, locale, maxLength}) {
|
|
|
214
235
|
rel: 'nofollow noopener',
|
|
215
236
|
target: '_blank',
|
|
216
237
|
},
|
|
217
|
-
h('span', {className: 'p-name'}, author.name
|
|
238
|
+
h('span', {className: 'p-name'}, author.name),
|
|
218
239
|
),
|
|
219
240
|
' ',
|
|
220
241
|
h('span', null, getMentionType(mention, labels)),
|
|
@@ -280,9 +301,11 @@ const REACT_CLASS_NAMES = {
|
|
|
280
301
|
facepileItem: 'webmention-facepile-item',
|
|
281
302
|
facepileLink: 'webmention-facepile-link',
|
|
282
303
|
facepilePhoto: 'webmention-facepile-photo',
|
|
304
|
+
facepileInitial: 'webmention-facepile-photo is-initial',
|
|
283
305
|
threadList: 'webmentions-list',
|
|
284
306
|
threadItem: 'h-cite webmention',
|
|
285
307
|
threadPhoto: 'webmention-photo',
|
|
308
|
+
threadInitial: 'webmention-photo is-initial',
|
|
286
309
|
threadBody: 'webmention-body',
|
|
287
310
|
threadMeta: 'webmention-meta',
|
|
288
311
|
threadAuthor: 'p-author h-card u-url',
|
package/src/render.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
describeTimestamp,
|
|
11
11
|
fetchWebmentions,
|
|
12
12
|
formatMentionDate,
|
|
13
|
+
getMentionAuthor,
|
|
13
14
|
getMentionContent,
|
|
14
15
|
getMentionSourceUrl,
|
|
15
16
|
getMentionType,
|
|
@@ -26,6 +27,7 @@ export const DEFAULT_CLASS_NAMES = {
|
|
|
26
27
|
facepileItem: 'webmentions__facepile-item',
|
|
27
28
|
facepileLink: 'webmentions__facepile-link',
|
|
28
29
|
facepilePhoto: 'webmentions__facepile-photo',
|
|
30
|
+
facepileInitial: 'webmentions__facepile-photo is-initial',
|
|
29
31
|
threadList: 'webmentions__list',
|
|
30
32
|
threadItem: 'webmentions__item h-cite',
|
|
31
33
|
threadBody: 'webmentions__body',
|
|
@@ -33,7 +35,8 @@ export const DEFAULT_CLASS_NAMES = {
|
|
|
33
35
|
threadAuthor: 'webmentions__author p-author h-card u-url',
|
|
34
36
|
threadContent: 'webmentions__content p-content',
|
|
35
37
|
threadSource: 'webmentions__source u-url',
|
|
36
|
-
threadPhoto: '',
|
|
38
|
+
threadPhoto: 'webmentions__photo',
|
|
39
|
+
threadInitial: 'webmentions__photo is-initial',
|
|
37
40
|
updated: 'webmentions__updated',
|
|
38
41
|
};
|
|
39
42
|
|
|
@@ -78,26 +81,14 @@ function resolveElement(value, root, doc) {
|
|
|
78
81
|
return value;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
host = new URL(source).hostname.replace(/^www\./, '');
|
|
88
|
-
} catch {
|
|
89
|
-
host = '';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
name: String(author.name || '').trim() || host || 'Someone',
|
|
94
|
-
url: author.url || source,
|
|
95
|
-
photo: author.photo || '',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function createAvatar(author, className, doc, size) {
|
|
84
|
+
/**
|
|
85
|
+
* Every mention gets an avatar-shaped element, photo or not: a row that
|
|
86
|
+
* sometimes starts with an image and sometimes with nothing has two different
|
|
87
|
+
* layouts, and the one without reads as a rendering failure.
|
|
88
|
+
*/
|
|
89
|
+
function createAvatar(author, classNames, doc, size) {
|
|
100
90
|
let element;
|
|
91
|
+
let className;
|
|
101
92
|
|
|
102
93
|
if (author.photo) {
|
|
103
94
|
element = doc.createElement('img');
|
|
@@ -106,10 +97,13 @@ function createAvatar(author, className, doc, size) {
|
|
|
106
97
|
element.loading = 'lazy';
|
|
107
98
|
element.width = size;
|
|
108
99
|
element.height = size;
|
|
100
|
+
className = classNames.photo;
|
|
109
101
|
} else {
|
|
110
102
|
element = doc.createElement('span');
|
|
111
103
|
element.setAttribute('aria-hidden', 'true');
|
|
112
|
-
element.textContent =
|
|
104
|
+
element.textContent = author.initial;
|
|
105
|
+
element.style.setProperty('--webmention-avatar-hue', author.hue);
|
|
106
|
+
className = classNames.initial ?? classNames.photo;
|
|
113
107
|
}
|
|
114
108
|
|
|
115
109
|
if (className) {
|
|
@@ -120,7 +114,7 @@ function createAvatar(author, className, doc, size) {
|
|
|
120
114
|
}
|
|
121
115
|
|
|
122
116
|
function createFace(mention, classNames, doc) {
|
|
123
|
-
const author =
|
|
117
|
+
const author = getMentionAuthor(mention);
|
|
124
118
|
const item = doc.createElement('li');
|
|
125
119
|
item.className = classNames.facepileItem;
|
|
126
120
|
|
|
@@ -130,7 +124,12 @@ function createFace(mention, classNames, doc) {
|
|
|
130
124
|
link.rel = 'nofollow noopener';
|
|
131
125
|
link.target = '_blank';
|
|
132
126
|
link.title = `${author.name} — ${getMentionType(mention)}`;
|
|
133
|
-
link.appendChild(createAvatar(
|
|
127
|
+
link.appendChild(createAvatar(
|
|
128
|
+
author,
|
|
129
|
+
{photo: classNames.facepilePhoto, initial: classNames.facepileInitial},
|
|
130
|
+
doc,
|
|
131
|
+
32,
|
|
132
|
+
));
|
|
134
133
|
|
|
135
134
|
item.appendChild(link);
|
|
136
135
|
|
|
@@ -171,7 +170,7 @@ function createFacepileGroup(property, mentions, {classNames, labels, doc}) {
|
|
|
171
170
|
}
|
|
172
171
|
|
|
173
172
|
function createThreadItem(mention, {classNames, labels, locale, maxLength, doc}) {
|
|
174
|
-
const author =
|
|
173
|
+
const author = getMentionAuthor(mention);
|
|
175
174
|
const published = mention.published || mention['wm-received'];
|
|
176
175
|
const formattedDate = formatMentionDate(published, locale);
|
|
177
176
|
const content = getMentionContent(mention, {maxLength});
|
|
@@ -180,9 +179,12 @@ function createThreadItem(mention, {classNames, labels, locale, maxLength, doc})
|
|
|
180
179
|
const item = doc.createElement('li');
|
|
181
180
|
item.className = classNames.threadItem;
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
item.appendChild(createAvatar(
|
|
183
|
+
author,
|
|
184
|
+
{photo: classNames.threadPhoto, initial: classNames.threadInitial},
|
|
185
|
+
doc,
|
|
186
|
+
40,
|
|
187
|
+
));
|
|
186
188
|
|
|
187
189
|
const body = doc.createElement('div');
|
|
188
190
|
body.className = classNames.threadBody;
|