@hhkaos/webmentions-widget 0.3.0 → 0.4.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 (3) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/src/core.js +81 -1
package/README.md CHANGED
@@ -210,6 +210,10 @@ assigns remote HTML.
210
210
  folds them together, so only ever branch on `mention-of`.
211
211
  - Bridgy mangles emoji into runs of `?` and `U+FFFD` when extracting plain text.
212
212
  `stripMojibake` removes the debris — the emoji is not recoverable.
213
+ - Responses are sometimes **invalid JSON**: source content is copied into string
214
+ literals unescaped, so a backslash or a raw newline in a mention's content
215
+ makes the whole payload unparseable. `parseWebmentionJson` repairs those two
216
+ cases; valid payloads pass through untouched.
213
217
 
214
218
  ## Development
215
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hhkaos/webmentions-widget",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Framework-agnostic, dependency-free widget to fetch and render webmention.io mentions for the current page.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/core.js CHANGED
@@ -461,6 +461,86 @@ export function mergeSnapshot(existing, incoming) {
461
461
  };
462
462
  }
463
463
 
464
+ const VALID_JSON_ESCAPES = new Set(['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u']);
465
+
466
+ /**
467
+ * Repair the malformed JSON webmention.io sometimes serves.
468
+ *
469
+ * Its serializer copies source-page content into string literals without
470
+ * escaping it, so a mention whose content contains a backslash (a shell example
471
+ * ending in `\`, say) or a raw newline produces a payload that `JSON.parse`
472
+ * rejects outright. The mention is then invisible — not because the API was
473
+ * down, but because its response could not be read at all.
474
+ *
475
+ * Walks the text tracking whether it is inside a string literal, escaping only
476
+ * the offending characters and leaving valid escapes untouched.
477
+ */
478
+ export function repairJson(text) {
479
+ let out = '';
480
+ let inString = false;
481
+
482
+ for (let index = 0; index < text.length; index += 1) {
483
+ const char = text[index];
484
+
485
+ if (!inString) {
486
+ if (char === '"') {
487
+ inString = true;
488
+ }
489
+
490
+ out += char;
491
+ continue;
492
+ }
493
+
494
+ if (char === '"') {
495
+ inString = false;
496
+ out += char;
497
+ continue;
498
+ }
499
+
500
+ if (char === '\\') {
501
+ const next = text[index + 1];
502
+
503
+ if (VALID_JSON_ESCAPES.has(next)) {
504
+ out += char + next;
505
+ index += 1;
506
+ } else {
507
+ out += '\\\\';
508
+ }
509
+
510
+ continue;
511
+ }
512
+
513
+ const code = char.charCodeAt(0);
514
+
515
+ if (code < 0x20) {
516
+ if (code === 0x0a) {
517
+ out += '\\n';
518
+ } else if (code === 0x0d) {
519
+ out += '\\r';
520
+ } else if (code === 0x09) {
521
+ out += '\\t';
522
+ } else {
523
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
524
+ }
525
+
526
+ continue;
527
+ }
528
+
529
+ out += char;
530
+ }
531
+
532
+ return out;
533
+ }
534
+
535
+ /** Parse a webmention.io response, repairing it only if it will not parse. */
536
+ export function parseWebmentionJson(text) {
537
+ try {
538
+ return JSON.parse(text);
539
+ } catch {
540
+ return JSON.parse(repairJson(text));
541
+ }
542
+ }
543
+
464
544
  export class WebmentionFetchError extends Error {
465
545
  constructor(message, {status, attempts, cause} = {}) {
466
546
  super(message, {cause});
@@ -612,7 +692,7 @@ export async function fetchWebmentions({
612
692
  continue;
613
693
  }
614
694
 
615
- const data = await response.json();
695
+ const data = parseWebmentionJson(await response.text());
616
696
 
617
697
  return endpoint.json
618
698
  ? normalizeJsonFeed(data)