@internetarchive/elements 0.4.1 → 0.4.2-webdev-9107.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.
Files changed (45) hide show
  1. package/dist/src/elements/ia-reviews/assets/delete-icon.svg +8 -0
  2. package/dist/src/elements/ia-reviews/assets/star-basic.svg +6 -0
  3. package/dist/src/elements/ia-reviews/assets/star-selected.svg +8 -0
  4. package/dist/src/elements/ia-reviews/assets/star-unselected.svg +8 -0
  5. package/dist/src/elements/ia-reviews/ia-review-form.d.ts +104 -0
  6. package/dist/src/elements/ia-reviews/ia-review-form.d.ts.map +1 -0
  7. package/dist/src/elements/ia-reviews/ia-review-form.js +678 -0
  8. package/dist/src/elements/ia-reviews/ia-review-form.js.map +1 -0
  9. package/dist/src/elements/ia-reviews/ia-review.d.ts +49 -0
  10. package/dist/src/elements/ia-reviews/ia-review.d.ts.map +1 -0
  11. package/dist/src/elements/ia-reviews/ia-review.js +349 -0
  12. package/dist/src/elements/ia-reviews/ia-review.js.map +1 -0
  13. package/dist/src/elements/ia-reviews/ia-reviews.d.ts +63 -0
  14. package/dist/src/elements/ia-reviews/ia-reviews.d.ts.map +1 -0
  15. package/dist/src/elements/ia-reviews/ia-reviews.js +357 -0
  16. package/dist/src/elements/ia-reviews/ia-reviews.js.map +1 -0
  17. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.d.ts +8 -0
  18. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.d.ts.map +1 -0
  19. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.js +15 -0
  20. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.js.map +1 -0
  21. package/dist/src/elements/ia-reviews/utils/collapse-space.d.ts +13 -0
  22. package/dist/src/elements/ia-reviews/utils/collapse-space.d.ts.map +1 -0
  23. package/dist/src/elements/ia-reviews/utils/collapse-space.js +19 -0
  24. package/dist/src/elements/ia-reviews/utils/collapse-space.js.map +1 -0
  25. package/dist/src/elements/ia-reviews/utils/friendly-truncate.d.ts +14 -0
  26. package/dist/src/elements/ia-reviews/utils/friendly-truncate.d.ts.map +1 -0
  27. package/dist/src/elements/ia-reviews/utils/friendly-truncate.js +65 -0
  28. package/dist/src/elements/ia-reviews/utils/friendly-truncate.js.map +1 -0
  29. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.d.ts +9 -0
  30. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.d.ts.map +1 -0
  31. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.js +40 -0
  32. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.js.map +1 -0
  33. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.d.ts +9 -0
  34. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.d.ts.map +1 -0
  35. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.js +29 -0
  36. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.js.map +1 -0
  37. package/dist/src/elements/ia-reviews/utils/truncate-screenname.d.ts +13 -0
  38. package/dist/src/elements/ia-reviews/utils/truncate-screenname.d.ts.map +1 -0
  39. package/dist/src/elements/ia-reviews/utils/truncate-screenname.js +19 -0
  40. package/dist/src/elements/ia-reviews/utils/truncate-screenname.js.map +1 -0
  41. package/dist/src/elements/index.d.ts +3 -0
  42. package/dist/src/elements/index.d.ts.map +1 -1
  43. package/dist/src/elements/index.js +3 -0
  44. package/dist/src/elements/index.js.map +1 -1
  45. package/package.json +17 -1
@@ -0,0 +1,357 @@
1
+ import { __decorate } from "tslib";
2
+ import { css, html, LitElement, nothing, } from 'lit';
3
+ import { property, state } from 'lit/decorators.js';
4
+ import { customElement } from '../../util/custom-element.js';
5
+ import { msg } from '@lit/localize';
6
+ import { IaFetchHandler, } from '@internetarchive/fetch-handler';
7
+ import '../../elements/ia-button/ia-button.js';
8
+ import './ia-review.js';
9
+ import './ia-review-form.js';
10
+ /**
11
+ * Renders the full IA reviews piece
12
+ */
13
+ let IAReviews = class IAReviews extends LitElement {
14
+ constructor() {
15
+ super(...arguments);
16
+ /* The list of reviews to be rendered */
17
+ this.reviews = [];
18
+ /* Whether reviews are disabled for the item */
19
+ this.reviewsDisabled = false;
20
+ /* Whether reviews are frozen for the item */
21
+ this.reviewsFrozen = false;
22
+ /* Whether the person viewing the reviews section has the power to delete reviews */
23
+ this.canDelete = false;
24
+ /* Whether we should bypass the show/hide and more/less toggles */
25
+ this.displayReviewsByDefault = false;
26
+ /* Base for URLs */
27
+ this.baseHost = 'https://archive.org';
28
+ /** REVIEW FORM PROPERTIES */
29
+ /* The token for the review edit */
30
+ this.token = '';
31
+ /* The path for the endpoint we're submitting to */
32
+ this.endpointPath = '/write-review.php';
33
+ /** Form submitter's screenname, if applicable */
34
+ this.submitterScreenname = 'Anonymous';
35
+ /* Optionally avoid using recaptcha for the form */
36
+ this.bypassRecaptcha = false;
37
+ /* Whether the add/edit button has been clicked */
38
+ this.reviewAddEditRequested = false;
39
+ /* An optional handler for form submission to pass along to the form */
40
+ this.fetchHandler = new IaFetchHandler();
41
+ /* Whether to display the review form or the editable review */
42
+ this.displayReviewForm = false;
43
+ /* Whether to display the existing reviews */
44
+ this.displayReviews = false;
45
+ /* The sorted and filtered reviews to render */
46
+ this.filteredReviews = [];
47
+ /* The number of reviews */
48
+ this.reviewsCount = 0;
49
+ /* Whether recaptcha has been activated (by clicking the add/edit button) */
50
+ this.recaptchaActivated = false;
51
+ }
52
+ render() {
53
+ if (this.reviewsDisabled)
54
+ return this.reviewsDisabledTemplate;
55
+ if (this.reviewsCount === 0 && !this.displayReviewForm)
56
+ return this.noReviewsMsgTemplate;
57
+ if (!this.displayReviews)
58
+ return this.displayReviewsMsgTemplate;
59
+ return html `
60
+ <div class="reviews-list">
61
+ ${this.reviewsFrozen
62
+ ? html `<div class="message">
63
+ ${msg('Reviews can no longer be added to this item.')}
64
+ </div>`
65
+ : nothing}
66
+ ${this.editableCurrentReviewTemplate}
67
+ ${this.filteredReviews.map((review) => review.reviewer_itemname !== this.submitterItemname
68
+ ? this.renderReview(review)
69
+ : nothing)}
70
+ </div>
71
+ `;
72
+ }
73
+ willUpdate(changed) {
74
+ if (changed.has('reviews') || changed.has('submitterItemname')) {
75
+ this.reviewsCount = this.reviews.length;
76
+ this.sortFilterReviews();
77
+ }
78
+ if (changed.has('displayReviewForm') && this.displayReviewForm === true) {
79
+ if (!this.bypassRecaptcha && !this.recaptchaActivated)
80
+ this.recaptchaActivated = true;
81
+ this.displayReviews = true;
82
+ }
83
+ if (changed.has('displayReviewsByDefault') &&
84
+ this.displayReviewsByDefault) {
85
+ this.displayReviews = true;
86
+ }
87
+ }
88
+ /* Message to display if reviews are disabled */
89
+ get reviewsDisabledTemplate() {
90
+ return html `<div class="message">
91
+ ${msg('Reviews have been disabled for this item.')}
92
+ </div>`;
93
+ }
94
+ /* Message to display instead of the reviews list if there are no reviews yet */
95
+ get noReviewsMsgTemplate() {
96
+ if (this.reviewsFrozen)
97
+ return html `
98
+ <div class="message">
99
+ ${msg('Reviews cannot be added to this item.')}
100
+ </div>
101
+ `;
102
+ return html `
103
+ <div class="message">
104
+ ${msg('There are no reviews yet.')}
105
+ ${msg(html `
106
+ Be the first one to
107
+ <ia-button
108
+ mode="link"
109
+ class="no-reviews-btn"
110
+ @click=${this.addEditReview}
111
+ >
112
+ write a review</ia-button
113
+ >.
114
+ `)}
115
+ </div>
116
+ `;
117
+ }
118
+ /* Message to display instead of the reviews list if reviews are hidden */
119
+ get displayReviewsMsgTemplate() {
120
+ return html `
121
+ <div class="message">
122
+ ${this.reviewsCount === 1
123
+ ? msg('There is 1 review for this item.')
124
+ : msg(`There are ${this.reviewsCount} reviews for this item.`)}
125
+ <ia-button
126
+ mode="link"
127
+ class="display-reviews-btn"
128
+ @click=${() => (this.displayReviews = true)}
129
+ >
130
+ ${msg(`Display ${this.reviewsCount === 1 ? 'review' : 'reviews'}`)}</ia-button
131
+ >.
132
+ </div>
133
+ `;
134
+ }
135
+ /**
136
+ * Renders the review form, passing along all necessary properties; this will also render
137
+ * the patron's own review if applicable and/or following submission.
138
+ */
139
+ get editableCurrentReviewTemplate() {
140
+ if (!this.displayReviewForm && !this.currentReview)
141
+ return nothing;
142
+ return html `<div class="own-review-container">
143
+ ${!this.displayReviewForm
144
+ ? this.renderReview(this.currentReview)
145
+ : html `<ia-review-form
146
+ .identifier=${this.identifier}
147
+ .oldReview=${this.currentReview}
148
+ .baseHost=${this.baseHost}
149
+ .endpointPath=${this.endpointPath}
150
+ .submitterItemname=${this.submitterItemname}
151
+ .submitterScreenname=${this.submitterScreenname}
152
+ .maxSubjectLength=${this.maxSubjectLength}
153
+ .maxBodyLength=${this.maxBodyLength}
154
+ .token=${this.token}
155
+ .unrecoverableError=${this.reviewSubmissionError}
156
+ .fetchHandler=${this.fetchHandler}
157
+ .recaptchaManager=${this.recaptchaActivated
158
+ ? this.recaptchaManager
159
+ : undefined}
160
+ ?bypassRecaptcha=${this.bypassRecaptcha}
161
+ @reviewUpdated=${this.handleReviewUpdate}
162
+ @reviewEditCanceled=${this.handleEditCanceled}
163
+ ></ia-review-form>`}
164
+ </div>`;
165
+ }
166
+ /** Sorts the reviews and splits the patron's own review out of the reviews array */
167
+ sortFilterReviews() {
168
+ let patronsOwnReview;
169
+ const filteredReviews = [];
170
+ this.reviews.forEach((review) => {
171
+ if (!patronsOwnReview &&
172
+ review.reviewer_itemname === this.submitterItemname) {
173
+ patronsOwnReview = review;
174
+ }
175
+ else
176
+ filteredReviews.push(review);
177
+ });
178
+ this.currentReview = patronsOwnReview;
179
+ this.filteredReviews = this.sortReviews(filteredReviews);
180
+ }
181
+ /** Sorts reviews by create date for render */
182
+ sortReviews(reviews) {
183
+ const sortedReviews = [...reviews].sort((a, b) => a.createdate && b.createdate
184
+ ? new Date(b.createdate).getTime() - new Date(a.createdate).getTime()
185
+ : 0);
186
+ return sortedReviews;
187
+ }
188
+ /* Renders the given review, using the ia-review component */
189
+ renderReview(review) {
190
+ if (!review)
191
+ return nothing;
192
+ return html `<ia-review
193
+ .review=${review}
194
+ .identifier=${this.identifier}
195
+ .baseHost=${this.baseHost}
196
+ .csrfToken=${this.token}
197
+ ?canDelete=${this.canDelete}
198
+ ?bypassTruncation=${this.displayReviewsByDefault}
199
+ ></ia-review>`;
200
+ }
201
+ /** Prepare the review form for adding or editing */
202
+ addEditReview() {
203
+ if (!this.bypassRecaptcha)
204
+ this.recaptchaActivated = true;
205
+ this.displayReviewForm = true;
206
+ }
207
+ /** Handles successful review submission */
208
+ handleReviewUpdate(e) {
209
+ if (!this.currentReview && e.detail) {
210
+ this.dispatchEvent(new CustomEvent('newReviewAdded'));
211
+ this.reviewsCount += 1;
212
+ }
213
+ this.currentReview = e.detail;
214
+ this.displayReviewForm = false;
215
+ }
216
+ /** Closes the review form if requested */
217
+ handleEditCanceled() {
218
+ this.displayReviewForm = false;
219
+ // Display no-reviews message again if no new review was added
220
+ if (this.reviewsCount === 0) {
221
+ this.displayReviews = false;
222
+ }
223
+ }
224
+ static get styles() {
225
+ return [
226
+ css `
227
+ :host {
228
+ font-family: var(
229
+ --ia-font-stack,
230
+ 'Helvetica Neue',
231
+ Helvetica,
232
+ Arial,
233
+ sans-serif
234
+ );
235
+
236
+ color: var(--ia-text-color, #2c2c2c);
237
+ }
238
+
239
+ .reviews-list {
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: 2rem;
243
+ width: 100%;
244
+ }
245
+
246
+ .own-review-container {
247
+ --error-color: var(--container-error-color, #ea0202);
248
+ --link-color: var(--container-link-color, #4f65f5);
249
+ --container-error-color: #ea0202;
250
+ --container-link-color: #4f65f5;
251
+ --container-bg-color: #fbfbfd;
252
+ --container-border-color: #999999;
253
+
254
+ border: 2px solid var(--container-border-color, #999999);
255
+ border-radius: 5px;
256
+ background-color: var(--container-bg-color, #fbfbfd);
257
+ padding: 10px;
258
+ }
259
+
260
+ .message {
261
+ font-weight: 200;
262
+ }
263
+
264
+ .message ia-button {
265
+ display: inline;
266
+ vertical-align: baseline;
267
+ }
268
+
269
+ /* The button inside ia-button is a flex box, which would break these
270
+ out of the sentence they sit in. */
271
+ .message ia-button::part(button) {
272
+ display: inline;
273
+ min-height: 0;
274
+ padding: 0;
275
+ font-weight: 600;
276
+ }
277
+ `,
278
+ ];
279
+ }
280
+ };
281
+ __decorate([
282
+ property({ type: String })
283
+ ], IAReviews.prototype, "identifier", void 0);
284
+ __decorate([
285
+ property({ type: Array })
286
+ ], IAReviews.prototype, "reviews", void 0);
287
+ __decorate([
288
+ property({ type: Boolean })
289
+ ], IAReviews.prototype, "reviewsDisabled", void 0);
290
+ __decorate([
291
+ property({ type: Boolean })
292
+ ], IAReviews.prototype, "reviewsFrozen", void 0);
293
+ __decorate([
294
+ property({ type: Boolean })
295
+ ], IAReviews.prototype, "canDelete", void 0);
296
+ __decorate([
297
+ property({ type: Boolean })
298
+ ], IAReviews.prototype, "displayReviewsByDefault", void 0);
299
+ __decorate([
300
+ property({ type: Number })
301
+ ], IAReviews.prototype, "maxSubjectLength", void 0);
302
+ __decorate([
303
+ property({ type: Number })
304
+ ], IAReviews.prototype, "maxBodyLength", void 0);
305
+ __decorate([
306
+ property({ type: String })
307
+ ], IAReviews.prototype, "baseHost", void 0);
308
+ __decorate([
309
+ property({ type: String })
310
+ ], IAReviews.prototype, "token", void 0);
311
+ __decorate([
312
+ property({ type: String })
313
+ ], IAReviews.prototype, "endpointPath", void 0);
314
+ __decorate([
315
+ property({ type: String })
316
+ ], IAReviews.prototype, "submitterScreenname", void 0);
317
+ __decorate([
318
+ property({ type: String })
319
+ ], IAReviews.prototype, "submitterItemname", void 0);
320
+ __decorate([
321
+ property({ type: Object })
322
+ ], IAReviews.prototype, "recaptchaManager", void 0);
323
+ __decorate([
324
+ property({ type: Boolean })
325
+ ], IAReviews.prototype, "bypassRecaptcha", void 0);
326
+ __decorate([
327
+ property({ type: String })
328
+ ], IAReviews.prototype, "reviewSubmissionError", void 0);
329
+ __decorate([
330
+ property({ type: Boolean })
331
+ ], IAReviews.prototype, "reviewAddEditRequested", void 0);
332
+ __decorate([
333
+ property({ type: Object })
334
+ ], IAReviews.prototype, "fetchHandler", void 0);
335
+ __decorate([
336
+ state()
337
+ ], IAReviews.prototype, "displayReviewForm", void 0);
338
+ __decorate([
339
+ state()
340
+ ], IAReviews.prototype, "displayReviews", void 0);
341
+ __decorate([
342
+ state()
343
+ ], IAReviews.prototype, "filteredReviews", void 0);
344
+ __decorate([
345
+ state()
346
+ ], IAReviews.prototype, "currentReview", void 0);
347
+ __decorate([
348
+ state()
349
+ ], IAReviews.prototype, "reviewsCount", void 0);
350
+ __decorate([
351
+ state()
352
+ ], IAReviews.prototype, "recaptchaActivated", void 0);
353
+ IAReviews = __decorate([
354
+ customElement('ia-reviews')
355
+ ], IAReviews);
356
+ export { IAReviews };
357
+ //# sourceMappingURL=ia-reviews.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ia-reviews.js","sourceRoot":"","sources":["../../../../src/elements/ia-reviews/ia-reviews.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n css,\n CSSResultGroup,\n html,\n HTMLTemplateResult,\n LitElement,\n nothing,\n PropertyValues,\n} from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { customElement } from '@src/util/custom-element';\nimport { msg } from '@lit/localize';\n\nimport type { Review } from '@internetarchive/metadata-service';\nimport type { RecaptchaManagerInterface } from '@internetarchive/recaptcha-manager';\nimport {\n FetchHandlerInterface,\n IaFetchHandler,\n} from '@internetarchive/fetch-handler';\n\nimport '@src/elements/ia-button/ia-button';\n\nimport './ia-review';\nimport './ia-review-form';\n\n/**\n * Renders the full IA reviews piece\n */\n@customElement('ia-reviews')\nexport class IAReviews extends LitElement {\n /** GLOBAL/REVIEW-RENDERING PROPERTIES */\n /* The identifier for the item being reviewed */\n @property({ type: String }) identifier?: string;\n\n /* The list of reviews to be rendered */\n @property({ type: Array }) reviews: Review[] = [];\n\n /* Whether reviews are disabled for the item */\n @property({ type: Boolean }) reviewsDisabled = false;\n\n /* Whether reviews are frozen for the item */\n @property({ type: Boolean }) reviewsFrozen = false;\n\n /* Whether the person viewing the reviews section has the power to delete reviews */\n @property({ type: Boolean }) canDelete = false;\n\n /* Whether we should bypass the show/hide and more/less toggles */\n @property({ type: Boolean }) displayReviewsByDefault = false;\n\n /* Maximum allowable length for subject */\n @property({ type: Number }) maxSubjectLength?: number;\n\n /* Maximum allowable length for body */\n @property({ type: Number }) maxBodyLength?: number;\n\n /* Base for URLs */\n @property({ type: String }) baseHost = 'https://archive.org';\n\n /** REVIEW FORM PROPERTIES */\n /* The token for the review edit */\n @property({ type: String }) token: string = '';\n\n /* The path for the endpoint we're submitting to */\n @property({ type: String }) endpointPath: string = '/write-review.php';\n\n /** Form submitter's screenname, if applicable */\n @property({ type: String }) submitterScreenname: string = 'Anonymous';\n\n /** Form submitter's itemname, if applicable */\n @property({ type: String }) submitterItemname?: string;\n\n /* Service for activating the recaptcha challenge */\n @property({ type: Object }) recaptchaManager?: RecaptchaManagerInterface;\n\n /* Optionally avoid using recaptcha for the form */\n @property({ type: Boolean }) bypassRecaptcha: boolean = false;\n\n /* An optional error message to be displayed instead of the review form */\n @property({ type: String }) reviewSubmissionError?: string;\n\n /* Whether the add/edit button has been clicked */\n @property({ type: Boolean }) reviewAddEditRequested: boolean = false;\n\n /* An optional handler for form submission to pass along to the form */\n @property({ type: Object }) fetchHandler: FetchHandlerInterface =\n new IaFetchHandler();\n\n /* Whether to display the review form or the editable review */\n @state()\n displayReviewForm: boolean = false;\n\n /* Whether to display the existing reviews */\n @state()\n private displayReviews: boolean = false;\n\n /* The sorted and filtered reviews to render */\n @state()\n private filteredReviews: Review[] = [];\n\n /* The current version of the review */\n @state()\n private currentReview?: Review;\n\n /* The number of reviews */\n @state()\n private reviewsCount: number = 0;\n\n /* Whether recaptcha has been activated (by clicking the add/edit button) */\n @state()\n private recaptchaActivated: boolean = false;\n\n render() {\n if (this.reviewsDisabled) return this.reviewsDisabledTemplate;\n if (this.reviewsCount === 0 && !this.displayReviewForm)\n return this.noReviewsMsgTemplate;\n if (!this.displayReviews) return this.displayReviewsMsgTemplate;\n\n return html`\n <div class=\"reviews-list\">\n ${this.reviewsFrozen\n ? html`<div class=\"message\">\n ${msg('Reviews can no longer be added to this item.')}\n </div>`\n : nothing}\n ${this.editableCurrentReviewTemplate}\n ${this.filteredReviews.map((review) =>\n review.reviewer_itemname !== this.submitterItemname\n ? this.renderReview(review)\n : nothing,\n )}\n </div>\n `;\n }\n\n protected willUpdate(changed: PropertyValues): void {\n if (changed.has('reviews') || changed.has('submitterItemname')) {\n this.reviewsCount = this.reviews.length;\n this.sortFilterReviews();\n }\n\n if (changed.has('displayReviewForm') && this.displayReviewForm === true) {\n if (!this.bypassRecaptcha && !this.recaptchaActivated)\n this.recaptchaActivated = true;\n this.displayReviews = true;\n }\n\n if (\n changed.has('displayReviewsByDefault') &&\n this.displayReviewsByDefault\n ) {\n this.displayReviews = true;\n }\n }\n\n /* Message to display if reviews are disabled */\n private get reviewsDisabledTemplate(): HTMLTemplateResult {\n return html`<div class=\"message\">\n ${msg('Reviews have been disabled for this item.')}\n </div>`;\n }\n\n /* Message to display instead of the reviews list if there are no reviews yet */\n private get noReviewsMsgTemplate(): HTMLTemplateResult {\n if (this.reviewsFrozen)\n return html`\n <div class=\"message\">\n ${msg('Reviews cannot be added to this item.')}\n </div>\n `;\n\n return html`\n <div class=\"message\">\n ${msg('There are no reviews yet.')}\n ${msg(html`\n Be the first one to\n <ia-button\n mode=\"link\"\n class=\"no-reviews-btn\"\n @click=${this.addEditReview}\n >\n write a review</ia-button\n >.\n `)}\n </div>\n `;\n }\n\n /* Message to display instead of the reviews list if reviews are hidden */\n private get displayReviewsMsgTemplate(): HTMLTemplateResult {\n return html`\n <div class=\"message\">\n ${this.reviewsCount === 1\n ? msg('There is 1 review for this item.')\n : msg(`There are ${this.reviewsCount} reviews for this item.`)}\n <ia-button\n mode=\"link\"\n class=\"display-reviews-btn\"\n @click=${() => (this.displayReviews = true)}\n >\n ${msg(\n `Display ${this.reviewsCount === 1 ? 'review' : 'reviews'}`,\n )}</ia-button\n >.\n </div>\n `;\n }\n\n /**\n * Renders the review form, passing along all necessary properties; this will also render\n * the patron's own review if applicable and/or following submission.\n */\n private get editableCurrentReviewTemplate():\n | HTMLTemplateResult\n | typeof nothing {\n if (!this.displayReviewForm && !this.currentReview) return nothing;\n\n return html`<div class=\"own-review-container\">\n ${!this.displayReviewForm\n ? this.renderReview(this.currentReview)\n : html`<ia-review-form\n .identifier=${this.identifier}\n .oldReview=${this.currentReview}\n .baseHost=${this.baseHost}\n .endpointPath=${this.endpointPath}\n .submitterItemname=${this.submitterItemname}\n .submitterScreenname=${this.submitterScreenname}\n .maxSubjectLength=${this.maxSubjectLength}\n .maxBodyLength=${this.maxBodyLength}\n .token=${this.token}\n .unrecoverableError=${this.reviewSubmissionError}\n .fetchHandler=${this.fetchHandler}\n .recaptchaManager=${this.recaptchaActivated\n ? this.recaptchaManager\n : undefined}\n ?bypassRecaptcha=${this.bypassRecaptcha}\n @reviewUpdated=${this.handleReviewUpdate}\n @reviewEditCanceled=${this.handleEditCanceled}\n ></ia-review-form>`}\n </div>`;\n }\n\n /** Sorts the reviews and splits the patron's own review out of the reviews array */\n private sortFilterReviews(): void {\n let patronsOwnReview: Review | undefined;\n const filteredReviews: Review[] = [];\n\n this.reviews.forEach((review) => {\n if (\n !patronsOwnReview &&\n review.reviewer_itemname === this.submitterItemname\n ) {\n patronsOwnReview = review;\n } else filteredReviews.push(review);\n });\n\n this.currentReview = patronsOwnReview;\n this.filteredReviews = this.sortReviews(filteredReviews);\n }\n\n /** Sorts reviews by create date for render */\n private sortReviews(reviews: Review[]): Review[] {\n const sortedReviews = [...reviews].sort((a, b) =>\n a.createdate && b.createdate\n ? new Date(b.createdate).getTime() - new Date(a.createdate).getTime()\n : 0,\n );\n\n return sortedReviews;\n }\n\n /* Renders the given review, using the ia-review component */\n private renderReview(review?: Review): HTMLTemplateResult | typeof nothing {\n if (!review) return nothing;\n\n return html`<ia-review\n .review=${review}\n .identifier=${this.identifier}\n .baseHost=${this.baseHost}\n .csrfToken=${this.token}\n ?canDelete=${this.canDelete}\n ?bypassTruncation=${this.displayReviewsByDefault}\n ></ia-review>`;\n }\n\n /** Prepare the review form for adding or editing */\n private addEditReview(): void {\n if (!this.bypassRecaptcha) this.recaptchaActivated = true;\n this.displayReviewForm = true;\n }\n\n /** Handles successful review submission */\n private handleReviewUpdate(e: CustomEvent<Review>): void {\n if (!this.currentReview && e.detail) {\n this.dispatchEvent(new CustomEvent<void>('newReviewAdded'));\n this.reviewsCount += 1;\n }\n this.currentReview = e.detail;\n this.displayReviewForm = false;\n }\n\n /** Closes the review form if requested */\n private handleEditCanceled(): void {\n this.displayReviewForm = false;\n\n // Display no-reviews message again if no new review was added\n if (this.reviewsCount === 0) {\n this.displayReviews = false;\n }\n }\n\n static get styles(): CSSResultGroup {\n return [\n css`\n :host {\n font-family: var(\n --ia-font-stack,\n 'Helvetica Neue',\n Helvetica,\n Arial,\n sans-serif\n );\n\n color: var(--ia-text-color, #2c2c2c);\n }\n\n .reviews-list {\n display: flex;\n flex-direction: column;\n gap: 2rem;\n width: 100%;\n }\n\n .own-review-container {\n --error-color: var(--container-error-color, #ea0202);\n --link-color: var(--container-link-color, #4f65f5);\n --container-error-color: #ea0202;\n --container-link-color: #4f65f5;\n --container-bg-color: #fbfbfd;\n --container-border-color: #999999;\n\n border: 2px solid var(--container-border-color, #999999);\n border-radius: 5px;\n background-color: var(--container-bg-color, #fbfbfd);\n padding: 10px;\n }\n\n .message {\n font-weight: 200;\n }\n\n .message ia-button {\n display: inline;\n vertical-align: baseline;\n }\n\n /* The button inside ia-button is a flex box, which would break these\n out of the sentence they sit in. */\n .message ia-button::part(button) {\n display: inline;\n min-height: 0;\n padding: 0;\n font-weight: 600;\n }\n `,\n ];\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import type { FetchHandlerInterface } from '@internetarchive/fetch-handler';
2
+ export declare class MockFetchHandler implements FetchHandlerInterface {
3
+ fetchApiResponse<T>(): Promise<T>;
4
+ fetchApiPathResponse<T>(): Promise<T>;
5
+ fetchIAApiResponse<T>(): Promise<T>;
6
+ fetch(): Promise<Response>;
7
+ }
8
+ //# sourceMappingURL=mock-fetch-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-fetch-handler.d.ts","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/mocks/mock-fetch-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,qBAAa,gBAAiB,YAAW,qBAAqB;IACtD,gBAAgB,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC;IAIjC,oBAAoB,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC;IAIrC,kBAAkB,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC;IAInC,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC;CAGjC"}
@@ -0,0 +1,15 @@
1
+ export class MockFetchHandler {
2
+ async fetchApiResponse() {
3
+ return { success: true };
4
+ }
5
+ async fetchApiPathResponse() {
6
+ return {};
7
+ }
8
+ async fetchIAApiResponse() {
9
+ return {};
10
+ }
11
+ async fetch() {
12
+ return new Response();
13
+ }
14
+ }
15
+ //# sourceMappingURL=mock-fetch-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-fetch-handler.js","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/mocks/mock-fetch-handler.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,gBAAgB;IAC3B,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAO,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,oBAAoB;QACxB,OAAO,EAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,OAAO,EAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,QAAQ,EAAE,CAAC;IACxB,CAAC;CACF","sourcesContent":["import type { FetchHandlerInterface } from '@internetarchive/fetch-handler';\n\nexport class MockFetchHandler implements FetchHandlerInterface {\n async fetchApiResponse<T>(): Promise<T> {\n return { success: true } as T;\n }\n\n async fetchApiPathResponse<T>(): Promise<T> {\n return {} as T;\n }\n\n async fetchIAApiResponse<T>(): Promise<T> {\n return {} as T;\n }\n\n async fetch(): Promise<Response> {\n return new Response();\n }\n}\n"]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Trims the string, collapses runs of spaces and tabs down to one space, and
3
+ * turns runs of newlines and existing `<br>` tags into a single `<br />`.
4
+ *
5
+ * The whitespace patterns list only real whitespace. A pipe written into a
6
+ * character class here would be treated as whitespace too, and quietly
7
+ * disappear from any review body containing one.
8
+ *
9
+ * @param {string} str The string to transform
10
+ * @returns {string} The string with space collapsed
11
+ */
12
+ export default function collapseSpace(str: string): string;
13
+ //# sourceMappingURL=collapse-space.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collapse-space.d.ts","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/utils/collapse-space.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMzD"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Trims the string, collapses runs of spaces and tabs down to one space, and
3
+ * turns runs of newlines and existing `<br>` tags into a single `<br />`.
4
+ *
5
+ * The whitespace patterns list only real whitespace. A pipe written into a
6
+ * character class here would be treated as whitespace too, and quietly
7
+ * disappear from any review body containing one.
8
+ *
9
+ * @param {string} str The string to transform
10
+ * @returns {string} The string with space collapsed
11
+ */
12
+ export default function collapseSpace(str) {
13
+ return str
14
+ .trim()
15
+ .replace(/[ \t]+/g, ' ')
16
+ .replace(/(?:\r?\n)+/g, '<br />')
17
+ .replace(/(<br[^>]*>(<\/br>)?)+/g, '<br />');
18
+ }
19
+ //# sourceMappingURL=collapse-space.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collapse-space.js","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/utils/collapse-space.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,GAAW;IAC/C,OAAO,GAAG;SACP,IAAI,EAAE;SACN,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC;SAChC,OAAO,CAAC,wBAAwB,EAAE,QAAQ,CAAC,CAAC;AACjD,CAAC","sourcesContent":["/**\n * Trims the string, collapses runs of spaces and tabs down to one space, and\n * turns runs of newlines and existing `<br>` tags into a single `<br />`.\n *\n * The whitespace patterns list only real whitespace. A pipe written into a\n * character class here would be treated as whitespace too, and quietly\n * disappear from any review body containing one.\n *\n * @param {string} str The string to transform\n * @returns {string} The string with space collapsed\n */\nexport default function collapseSpace(str: string): string {\n return str\n .trim()\n .replace(/[ \\t]+/g, ' ')\n .replace(/(?:\\r?\\n)+/g, '<br />')\n .replace(/(<br[^>]*>(<\\/br>)?)+/g, '<br />');\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Truncates a string, given the original string and a max length.
3
+ * Continues up to 20 chars after the limit to find the last space,
4
+ * if possible.
5
+ *
6
+ * Avoids breaking up anchor tags.
7
+ *
8
+ * @param {string} str The original string
9
+ * @param {number} maxLength The max length to truncate to
10
+ * @param {boolean} useSpaceTruncation Whether to truncate at the last space
11
+ * @returns {string} The truncated field
12
+ */
13
+ export default function friendlyTruncate(str: string, maxLength?: number, useSpaceTruncation?: boolean): string;
14
+ //# sourceMappingURL=friendly-truncate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"friendly-truncate.d.ts","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/utils/friendly-truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CACtC,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,MAAY,EACvB,kBAAkB,GAAE,OAAc,GACjC,MAAM,CAoBR"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Truncates a string, given the original string and a max length.
3
+ * Continues up to 20 chars after the limit to find the last space,
4
+ * if possible.
5
+ *
6
+ * Avoids breaking up anchor tags.
7
+ *
8
+ * @param {string} str The original string
9
+ * @param {number} maxLength The max length to truncate to
10
+ * @param {boolean} useSpaceTruncation Whether to truncate at the last space
11
+ * @returns {string} The truncated field
12
+ */
13
+ export default function friendlyTruncate(str, maxLength = 100, useSpaceTruncation = true) {
14
+ if (str.length < maxLength)
15
+ return str;
16
+ let cutoffPoint = maxLength;
17
+ if (useSpaceTruncation) {
18
+ const lastSpace = str.indexOf(' ', maxLength);
19
+ const safeDistanceFromMax = lastSpace - maxLength <= 20;
20
+ // No need to truncate if the safe last space is at the end of the string
21
+ if (safeDistanceFromMax && lastSpace === str.length - 1) {
22
+ return str;
23
+ }
24
+ if (lastSpace !== -1 && safeDistanceFromMax) {
25
+ cutoffPoint = lastSpace;
26
+ }
27
+ }
28
+ return truncateTagSafe(str, cutoffPoint, maxLength);
29
+ }
30
+ /**
31
+ * Utility function to prevent truncation in the middle of
32
+ * an anchor tag.
33
+ *
34
+ * @param {string} str The string to truncate
35
+ * @param {number} cutoffPoint The index currently selsected for truncation
36
+ * @param {number} maxLength The max length for the string
37
+ * @returns {string} The string, truncated either earlier or later to preserve anchor tags
38
+ */
39
+ function truncateTagSafe(str, cutoffPoint, maxLength) {
40
+ let truncatedString = str.slice(0, cutoffPoint);
41
+ const openingAnchorTags = truncatedString.match(/<a/gi);
42
+ if (openingAnchorTags) {
43
+ const closingAnchorTags = truncatedString.match(/<\/a/gi);
44
+ if (!closingAnchorTags ||
45
+ closingAnchorTags.length < openingAnchorTags.length) {
46
+ const firstClosingTag = str.indexOf('</a>', cutoffPoint);
47
+ const safeDistanceFromMax = firstClosingTag - maxLength <= 20;
48
+ // No need to truncate if closing tag is at the end of the string
49
+ if (safeDistanceFromMax && str.length === firstClosingTag + 4) {
50
+ return str;
51
+ }
52
+ // Truncate at end of first closing tag if possible
53
+ if (firstClosingTag !== -1 && safeDistanceFromMax) {
54
+ truncatedString = str.slice(0, firstClosingTag + 4);
55
+ }
56
+ else {
57
+ // Otherwise, truncate before last opening tag
58
+ const lastOpeningTag = truncatedString.lastIndexOf('<a');
59
+ truncatedString = str.slice(0, lastOpeningTag);
60
+ }
61
+ }
62
+ }
63
+ return truncatedString.concat('...');
64
+ }
65
+ //# sourceMappingURL=friendly-truncate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"friendly-truncate.js","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/utils/friendly-truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CACtC,GAAW,EACX,YAAoB,GAAG,EACvB,qBAA8B,IAAI;IAElC,IAAI,GAAG,CAAC,MAAM,GAAG,SAAS;QAAE,OAAO,GAAG,CAAC;IAEvC,IAAI,WAAW,GAAG,SAAS,CAAC;IAE5B,IAAI,kBAAkB,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC9C,MAAM,mBAAmB,GAAG,SAAS,GAAG,SAAS,IAAI,EAAE,CAAC;QAExD,yEAAyE;QACzE,IAAI,mBAAmB,IAAI,SAAS,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxD,OAAO,GAAG,CAAC;QACb,CAAC;QAED,IAAI,SAAS,KAAK,CAAC,CAAC,IAAI,mBAAmB,EAAE,CAAC;YAC5C,WAAW,GAAG,SAAS,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,OAAO,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CACtB,GAAW,EACX,WAAmB,EACnB,SAAiB;IAEjB,IAAI,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAEhD,MAAM,iBAAiB,GAAG,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACxD,IAAI,iBAAiB,EAAE,CAAC;QACtB,MAAM,iBAAiB,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAE1D,IACE,CAAC,iBAAiB;YAClB,iBAAiB,CAAC,MAAM,GAAG,iBAAiB,CAAC,MAAM,EACnD,CAAC;YACD,MAAM,eAAe,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;YACzD,MAAM,mBAAmB,GAAG,eAAe,GAAG,SAAS,IAAI,EAAE,CAAC;YAE9D,iEAAiE;YACjE,IAAI,mBAAmB,IAAI,GAAG,CAAC,MAAM,KAAK,eAAe,GAAG,CAAC,EAAE,CAAC;gBAC9D,OAAO,GAAG,CAAC;YACb,CAAC;YAED,mDAAmD;YACnD,IAAI,eAAe,KAAK,CAAC,CAAC,IAAI,mBAAmB,EAAE,CAAC;gBAClD,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,CAAC,CAAC;YACtD,CAAC;iBAAM,CAAC;gBACN,8CAA8C;gBAC9C,MAAM,cAAc,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;gBACzD,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;YACjD,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC","sourcesContent":["/**\n * Truncates a string, given the original string and a max length.\n * Continues up to 20 chars after the limit to find the last space,\n * if possible.\n *\n * Avoids breaking up anchor tags.\n *\n * @param {string} str The original string\n * @param {number} maxLength The max length to truncate to\n * @param {boolean} useSpaceTruncation Whether to truncate at the last space\n * @returns {string} The truncated field\n */\nexport default function friendlyTruncate(\n str: string,\n maxLength: number = 100,\n useSpaceTruncation: boolean = true,\n): string {\n if (str.length < maxLength) return str;\n\n let cutoffPoint = maxLength;\n\n if (useSpaceTruncation) {\n const lastSpace = str.indexOf(' ', maxLength);\n const safeDistanceFromMax = lastSpace - maxLength <= 20;\n\n // No need to truncate if the safe last space is at the end of the string\n if (safeDistanceFromMax && lastSpace === str.length - 1) {\n return str;\n }\n\n if (lastSpace !== -1 && safeDistanceFromMax) {\n cutoffPoint = lastSpace;\n }\n }\n\n return truncateTagSafe(str, cutoffPoint, maxLength);\n}\n\n/**\n * Utility function to prevent truncation in the middle of\n * an anchor tag.\n *\n * @param {string} str The string to truncate\n * @param {number} cutoffPoint The index currently selsected for truncation\n * @param {number} maxLength The max length for the string\n * @returns {string} The string, truncated either earlier or later to preserve anchor tags\n */\nfunction truncateTagSafe(\n str: string,\n cutoffPoint: number,\n maxLength: number,\n): string {\n let truncatedString = str.slice(0, cutoffPoint);\n\n const openingAnchorTags = truncatedString.match(/<a/gi);\n if (openingAnchorTags) {\n const closingAnchorTags = truncatedString.match(/<\\/a/gi);\n\n if (\n !closingAnchorTags ||\n closingAnchorTags.length < openingAnchorTags.length\n ) {\n const firstClosingTag = str.indexOf('</a>', cutoffPoint);\n const safeDistanceFromMax = firstClosingTag - maxLength <= 20;\n\n // No need to truncate if closing tag is at the end of the string\n if (safeDistanceFromMax && str.length === firstClosingTag + 4) {\n return str;\n }\n\n // Truncate at end of first closing tag if possible\n if (firstClosingTag !== -1 && safeDistanceFromMax) {\n truncatedString = str.slice(0, firstClosingTag + 4);\n } else {\n // Otherwise, truncate before last opening tag\n const lastOpeningTag = truncatedString.lastIndexOf('<a');\n truncatedString = str.slice(0, lastOpeningTag);\n }\n }\n }\n\n return truncatedString.concat('...');\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Transforms any links in text into live clickable anchor tags.
3
+ * Adds rel="ugc nofollow" to each for spam mitigation.
4
+ *
5
+ * @param {string} text The text to be linked
6
+ * @returns {string} The text with live links added
7
+ */
8
+ export default function linkUrlsInText(text: string): string;
9
+ //# sourceMappingURL=link-urls-in-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-urls-in-text.d.ts","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/utils/link-urls-in-text.ts"],"names":[],"mappings":"AAkBA;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuB3D"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * This is not designed to capture all valid URLs but simply the most common ones.
3
+ * Highlights:
4
+ * - Optional "http://" or "https://"
5
+ * - Capital letter only captured if at beginning of hostname, to avoid false positives when
6
+ * people omit spaces e.g. "It's great.Right?"
7
+ * - Unlimited hostname parts, separated by periods
8
+ * - Any number of subdirectories/paths
9
+ * - Optional querystring parameters
10
+ * - Optional anchor
11
+ *
12
+ * Quotes and angle brackets terminate a match. The match is interpolated into
13
+ * an anchor tag that gets rendered as raw HTML, so a quote inside one would
14
+ * close the href and let the rest of the text become attributes.
15
+ */
16
+ const URL_REGEXP = /(http(s)?)?(:\/\/)?([a-zA-Z][-a-z0-9]*(\.[-a-z0-9]+)+(\/[^\s?#<>"']*)*(\?[^\s#<>"']*)?(#[^\s<>"']*)?)/;
17
+ /**
18
+ * Transforms any links in text into live clickable anchor tags.
19
+ * Adds rel="ugc nofollow" to each for spam mitigation.
20
+ *
21
+ * @param {string} text The text to be linked
22
+ * @returns {string} The text with live links added
23
+ */
24
+ export default function linkUrlsInText(text) {
25
+ // Protect existing links by ignoring them in search. Keep this pattern free of
26
+ // lookbehind assertions: Safari below 16.4 treats one as a syntax error and
27
+ // fails to load the module at all.
28
+ const textWithExistingLinksPreserved = text.replace(/href="([^"]+)"/, (_match, url) => `href="${url.replace('.', '__DOT__')}"`);
29
+ // Add live links
30
+ const textWithLiveLinks = textWithExistingLinksPreserved.replace(URL_REGEXP, (match) => {
31
+ const url = match.match(/^(https|http)/) ? match : `https://${match}`;
32
+ // The attribute value is escaped as well as the pattern being quote-free,
33
+ // so the href stays a single attribute whatever the match contains.
34
+ const href = url.replace(/"/g, '&quot;');
35
+ return `<a href="${href}" rel="ugc nofollow" target="_blank">${match}</a>`;
36
+ });
37
+ // Restore existing links
38
+ return textWithLiveLinks.replace('__DOT__', '.');
39
+ }
40
+ //# sourceMappingURL=link-urls-in-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-urls-in-text.js","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/utils/link-urls-in-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,GACd,uGAAuG,CAAC;AAE1G;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,IAAY;IACjD,+EAA+E;IAC/E,4EAA4E;IAC5E,mCAAmC;IACnC,MAAM,8BAA8B,GAAG,IAAI,CAAC,OAAO,CACjD,gBAAgB,EAChB,CAAC,MAAM,EAAE,GAAW,EAAE,EAAE,CAAC,SAAS,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,CACjE,CAAC;IAEF,iBAAiB;IACjB,MAAM,iBAAiB,GAAG,8BAA8B,CAAC,OAAO,CAC9D,UAAU,EACV,CAAC,KAAK,EAAE,EAAE;QACR,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,KAAK,EAAE,CAAC;QACtE,0EAA0E;QAC1E,oEAAoE;QACpE,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACzC,OAAO,YAAY,IAAI,wCAAwC,KAAK,MAAM,CAAC;IAC7E,CAAC,CACF,CAAC;IAEF,yBAAyB;IACzB,OAAO,iBAAiB,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;AACnD,CAAC","sourcesContent":["/**\n * This is not designed to capture all valid URLs but simply the most common ones.\n * Highlights:\n * - Optional \"http://\" or \"https://\"\n * - Capital letter only captured if at beginning of hostname, to avoid false positives when\n * people omit spaces e.g. \"It's great.Right?\"\n * - Unlimited hostname parts, separated by periods\n * - Any number of subdirectories/paths\n * - Optional querystring parameters\n * - Optional anchor\n *\n * Quotes and angle brackets terminate a match. The match is interpolated into\n * an anchor tag that gets rendered as raw HTML, so a quote inside one would\n * close the href and let the rest of the text become attributes.\n */\nconst URL_REGEXP =\n /(http(s)?)?(:\\/\\/)?([a-zA-Z][-a-z0-9]*(\\.[-a-z0-9]+)+(\\/[^\\s?#<>\"']*)*(\\?[^\\s#<>\"']*)?(#[^\\s<>\"']*)?)/;\n\n/**\n * Transforms any links in text into live clickable anchor tags.\n * Adds rel=\"ugc nofollow\" to each for spam mitigation.\n *\n * @param {string} text The text to be linked\n * @returns {string} The text with live links added\n */\nexport default function linkUrlsInText(text: string): string {\n // Protect existing links by ignoring them in search. Keep this pattern free of\n // lookbehind assertions: Safari below 16.4 treats one as a syntax error and\n // fails to load the module at all.\n const textWithExistingLinksPreserved = text.replace(\n /href=\"([^\"]+)\"/,\n (_match, url: string) => `href=\"${url.replace('.', '__DOT__')}\"`,\n );\n\n // Add live links\n const textWithLiveLinks = textWithExistingLinksPreserved.replace(\n URL_REGEXP,\n (match) => {\n const url = match.match(/^(https|http)/) ? match : `https://${match}`;\n // The attribute value is escaped as well as the pattern being quote-free,\n // so the href stays a single attribute whatever the match contains.\n const href = url.replace(/\"/g, '&quot;');\n return `<a href=\"${href}\" rel=\"ugc nofollow\" target=\"_blank\">${match}</a>`;\n },\n );\n\n // Restore existing links\n return textWithLiveLinks.replace('__DOT__', '.');\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Uses DOMpurify to sanitize a given review body.
3
+ * Currently only allows a tags.
4
+ *
5
+ * @param {string} reviewBody The review to be sanitized
6
+ * @returns {string} The sanitized review
7
+ */
8
+ export default function sanitizeReviewBody(reviewBody: string): string;
9
+ //# sourceMappingURL=sanitize-review-body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-review-body.d.ts","sourceRoot":"","sources":["../../../../../src/elements/ia-reviews/utils/sanitize-review-body.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAUrE"}