terminalhire 0.42.0 → 0.42.2

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.
@@ -166,8 +166,24 @@ function nextPolledState(from, observed) {
166
166
  function nowISO() {
167
167
  return (/* @__PURE__ */ new Date()).toISOString();
168
168
  }
169
+ function defangText(s) {
170
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "") : s;
171
+ }
172
+ function finiteAmount(a) {
173
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
174
+ if (typeof a !== "string" || a.trim() === "") return null;
175
+ const n = Number(a);
176
+ return Number.isFinite(n) ? n : null;
177
+ }
169
178
  function normalizeClaim(c) {
170
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
179
+ return {
180
+ ...c,
181
+ kind: c.kind ?? "bounty",
182
+ policy: c.policy ?? null,
183
+ title: defangText(c.title),
184
+ repoFullName: defangText(c.repoFullName),
185
+ amountUSD: finiteAmount(c.amountUSD)
186
+ };
171
187
  }
172
188
  function readClaims() {
173
189
  try {
@@ -289,7 +305,7 @@ function acceptedPRRate(claims = readClaims()) {
289
305
  const merged = claims.filter((c) => c.state === "merged").length;
290
306
  return { merged, total, rate: total === 0 ? 0 : merged / total };
291
307
  }
292
- var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
308
+ var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS, CONTROL_CHARS;
293
309
  var init_claims = __esm({
294
310
  "src/claims.ts"() {
295
311
  "use strict";
@@ -345,6 +361,8 @@ var init_claims = __esm({
345
361
  ]),
346
362
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
347
363
  };
364
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
365
+ CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
348
366
  }
349
367
  });
350
368
 
@@ -11338,8 +11338,24 @@ function nextPolledState(from, observed) {
11338
11338
  function nowISO2() {
11339
11339
  return (/* @__PURE__ */ new Date()).toISOString();
11340
11340
  }
11341
+ function defangText(s) {
11342
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
11343
+ }
11344
+ function finiteAmount(a) {
11345
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
11346
+ if (typeof a !== "string" || a.trim() === "") return null;
11347
+ const n = Number(a);
11348
+ return Number.isFinite(n) ? n : null;
11349
+ }
11341
11350
  function normalizeClaim(c) {
11342
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
11351
+ return {
11352
+ ...c,
11353
+ kind: c.kind ?? "bounty",
11354
+ policy: c.policy ?? null,
11355
+ title: defangText(c.title),
11356
+ repoFullName: defangText(c.repoFullName),
11357
+ amountUSD: finiteAmount(c.amountUSD)
11358
+ };
11343
11359
  }
11344
11360
  function readClaims() {
11345
11361
  try {
@@ -11461,7 +11477,7 @@ function acceptedPRRate(claims = readClaims()) {
11461
11477
  const merged = claims.filter((c) => c.state === "merged").length;
11462
11478
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11463
11479
  }
11464
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
11480
+ var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
11465
11481
  var init_claims = __esm({
11466
11482
  "src/claims.ts"() {
11467
11483
  "use strict";
@@ -11517,6 +11533,8 @@ var init_claims = __esm({
11517
11533
  ]),
11518
11534
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
11519
11535
  };
11536
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
11537
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
11520
11538
  }
11521
11539
  });
11522
11540
 
@@ -11731,17 +11749,38 @@ var init_founder_paid_badge = __esm({
11731
11749
 
11732
11750
  // bin/jpi-bounties.js
11733
11751
  init_src();
11734
- init_cache_store();
11735
11752
  import { readFileSync as readFileSync7 } from "fs";
11736
11753
  import { join as join10 } from "path";
11737
11754
  import { homedir as homedir7 } from "os";
11738
11755
  import { createInterface } from "readline";
11739
11756
 
11757
+ // ../../packages/core/src/payout-split.ts
11758
+ var PLATFORM_TAKE_BPS = 1e3;
11759
+ function platformFeeCents(totalCents) {
11760
+ return Math.ceil(totalCents * PLATFORM_TAKE_BPS / 1e4);
11761
+ }
11762
+ function developerShareCents(totalCents) {
11763
+ return totalCents - platformFeeCents(totalCents);
11764
+ }
11765
+ function formatCents(cents) {
11766
+ return cents % 100 === 0 ? `$${cents / 100}` : `$${(cents / 100).toFixed(2)}`;
11767
+ }
11768
+
11769
+ // bin/jpi-bounties.js
11770
+ init_cache_store();
11771
+
11740
11772
  // bin/sanitize.js
11773
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
11741
11774
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
11742
11775
  function sanitizeText(s) {
11743
11776
  if (s == null) return "";
11744
- return String(s).replace(CONTROL_CHARS, "");
11777
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
11778
+ }
11779
+ function formatUsd(a) {
11780
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
11781
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
11782
+ const n = Number(a);
11783
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
11745
11784
  }
11746
11785
  function safeHttpUrl(url) {
11747
11786
  if (url == null) return null;
@@ -11814,13 +11853,33 @@ function prompt(question) {
11814
11853
  });
11815
11854
  }
11816
11855
  function formatAmount(b) {
11817
- return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
11856
+ return formatUsd(b.amountUSD);
11818
11857
  }
11819
11858
  var EFFORT_LABEL = {
11820
11859
  small: "small (~\xBD day)",
11821
11860
  medium: "medium (~1 day)",
11822
11861
  large: "large (multi-day)"
11823
11862
  };
11863
+ function founderClaimBlurb(amountUSD, paidWork) {
11864
+ if (paidWork === false) {
11865
+ return "Claim it from here \u2014 this posting records a credential for the work, not a payment. Take it for the credential, not the money.";
11866
+ }
11867
+ if (paidWork !== true) {
11868
+ return "Claim it from here \u2014 this listing is missing its payment status, so it is not safe to say whether the work pays. Try `terminalhire refresh`; if it is still missing, do not count on being paid for this one.";
11869
+ }
11870
+ let share = "";
11871
+ if (typeof amountUSD === "number" && Number.isFinite(amountUSD) && amountUSD > 0) {
11872
+ share = ` You'd receive ${formatCents(developerShareCents(Math.round(amountUSD * 100)))} of the ${formatCents(Math.round(amountUSD * 100))} posted; terminalhire keeps 10%.`;
11873
+ }
11874
+ return `Claim it from here \u2014 the founder pays through terminalhire rather than off-platform, and their acceptance is what triggers your payout.${share}`;
11875
+ }
11876
+ function founderClaimBlock(job) {
11877
+ const ref = opportunityShortToken(job.id);
11878
+ return `
11879
+ ${founderClaimBlurb(job.bounty?.amountUSD, job.bounty?.paidWork)}
11880
+ terminalhire claim ${ref}
11881
+ ${sanitizeText(job.bounty?.claimUrl ?? job.url)}`;
11882
+ }
11824
11883
  function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set(), continuityNote = null) {
11825
11884
  const b = job.bounty ?? {};
11826
11885
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
@@ -11963,7 +12022,9 @@ async function run() {
11963
12022
  const rows = founderRows2(surface, "bounties");
11964
12023
  console.log("\n\u26A1 Your TerminalHire postings\n");
11965
12024
  if (!rows.length) {
11966
- console.log(" No current posting data. Run `terminalhire refresh`, or lead with work:");
12025
+ console.log(
12026
+ " No current posting data. Run `terminalhire refresh`, or lead with work:"
12027
+ );
11967
12028
  console.log(" terminalhire config set lead dev\n");
11968
12029
  } else {
11969
12030
  rows.forEach((row, index) => {
@@ -11972,7 +12033,9 @@ async function run() {
11972
12033
  );
11973
12034
  });
11974
12035
  console.log("\n Review and act: https://terminalhire.com/dashboard?tab=postings");
11975
- console.log(" Lead with developer bounties instead: terminalhire config set lead dev\n");
12036
+ console.log(
12037
+ " Lead with developer bounties instead: terminalhire config set lead dev\n"
12038
+ );
11976
12039
  }
11977
12040
  return;
11978
12041
  }
@@ -12047,6 +12110,10 @@ Enter a number to open a bounty's claim page, or press Enter to exit: `
12047
12110
  const idx = parseInt(pick, 10) - 1;
12048
12111
  if (Number.isNaN(idx) || idx < 0 || idx >= shown.length) return;
12049
12112
  const chosen = shown[idx];
12113
+ if (chosen.bounty?.bountySource === "founder") {
12114
+ console.log(founderClaimBlock(chosen));
12115
+ return;
12116
+ }
12050
12117
  console.log(
12051
12118
  `
12052
12119
  Open this to claim/work the bounty (you go straight to the source \u2014 we never touch payment):
@@ -12062,6 +12129,8 @@ export {
12062
12129
  classifyEmptyStatus,
12063
12130
  continuityNoteForRow,
12064
12131
  filterPaidVisibility,
12132
+ founderClaimBlock,
12133
+ founderClaimBlurb,
12065
12134
  getBounties,
12066
12135
  isPinnedFounderBounty,
12067
12136
  printBounty,
@@ -10638,8 +10638,24 @@ function nextPolledState(from, observed) {
10638
10638
  function nowISO() {
10639
10639
  return (/* @__PURE__ */ new Date()).toISOString();
10640
10640
  }
10641
+ function defangText(s) {
10642
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
10643
+ }
10644
+ function finiteAmount(a) {
10645
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
10646
+ if (typeof a !== "string" || a.trim() === "") return null;
10647
+ const n = Number(a);
10648
+ return Number.isFinite(n) ? n : null;
10649
+ }
10641
10650
  function normalizeClaim(c) {
10642
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
10651
+ return {
10652
+ ...c,
10653
+ kind: c.kind ?? "bounty",
10654
+ policy: c.policy ?? null,
10655
+ title: defangText(c.title),
10656
+ repoFullName: defangText(c.repoFullName),
10657
+ amountUSD: finiteAmount(c.amountUSD)
10658
+ };
10643
10659
  }
10644
10660
  function readClaims() {
10645
10661
  try {
@@ -10761,7 +10777,7 @@ function acceptedPRRate(claims = readClaims()) {
10761
10777
  const merged = claims.filter((c) => c.state === "merged").length;
10762
10778
  return { merged, total, rate: total === 0 ? 0 : merged / total };
10763
10779
  }
10764
- var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
10780
+ var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
10765
10781
  var init_claims = __esm({
10766
10782
  "src/claims.ts"() {
10767
10783
  "use strict";
@@ -10817,6 +10833,8 @@ var init_claims = __esm({
10817
10833
  ]),
10818
10834
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
10819
10835
  };
10836
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
10837
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
10820
10838
  }
10821
10839
  });
10822
10840
 
@@ -26134,10 +26152,17 @@ function openInBrowser(url) {
26134
26152
  }
26135
26153
 
26136
26154
  // bin/sanitize.js
26155
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
26137
26156
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
26138
26157
  function sanitizeText(s) {
26139
26158
  if (s == null) return "";
26140
- return String(s).replace(CONTROL_CHARS, "");
26159
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
26160
+ }
26161
+ function formatUsd(a) {
26162
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
26163
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
26164
+ const n = Number(a);
26165
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
26141
26166
  }
26142
26167
 
26143
26168
  // src/policy-acks.ts
@@ -27064,7 +27089,7 @@ async function pollPR(prUrl) {
27064
27089
  }
27065
27090
  }
27066
27091
  function fmtAmount(a) {
27067
- return a != null ? "$" + a.toLocaleString() : "$\u2014";
27092
+ return formatUsd(a);
27068
27093
  }
27069
27094
  function fmtClaimAmount(c) {
27070
27095
  return c.kind === "contribution" ? "contribution" : fmtAmount(c.amountUSD);
@@ -27205,8 +27230,15 @@ async function resolveBounty(arg) {
27205
27230
  return {
27206
27231
  bountyId,
27207
27232
  indexNativeId,
27208
- title,
27209
- repoFullName,
27233
+ // DEFANGED HERE, at the one place every tier of this resolver converges.
27234
+ // `claims.ts` cleans what comes OUT of the local store, which covers every later
27235
+ // read — but this object is printed (the preview card, the record card) and
27236
+ // handed to `recordClaim` BEFORE any store exists for it, so on a claim's first
27237
+ // use the store boundary has not run yet. `repoFullName` also goes on to be
27238
+ // interpolated into GitHub API URLs from here. Found by a cross-model review of
27239
+ // TERM-380; the store fix alone left this path open.
27240
+ title: sanitizeText(title),
27241
+ repoFullName: repoFullName == null ? repoFullName : sanitizeText(repoFullName),
27210
27242
  issueUrl,
27211
27243
  amountUSD,
27212
27244
  source,
@@ -28402,7 +28434,7 @@ function startableRow(c) {
28402
28434
  }
28403
28435
  async function pickStartableClaim(claims, { prompt = ask, isTTY = process.stdin.isTTY } = {}) {
28404
28436
  const active = claims.listClaims({ active: true });
28405
- await syncFounderApprovals(claims, active);
28437
+ const { approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, active);
28406
28438
  const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
28407
28439
  if (startable.length === 0) {
28408
28440
  console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
@@ -28430,23 +28462,24 @@ Which one? (1-${startable.length}, or q to quit) `);
28430
28462
  Nothing started \u2014 '${answer}' is not one of 1-${startable.length}.`);
28431
28463
  return null;
28432
28464
  }
28433
- return startable[n - 1].id;
28465
+ return { id: startable[n - 1].id, approvalsChecked, approvalsUnavailable };
28434
28466
  }
28435
28467
  async function cmdStart(id, flags = {}) {
28436
28468
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
28469
+ let approvalsChecked = false;
28470
+ let approvalsUnavailable = false;
28437
28471
  if (!id) {
28438
28472
  const picked = await pickStartableClaim(claims);
28439
28473
  if (!picked) return;
28440
- id = picked;
28474
+ ({ id, approvalsChecked, approvalsUnavailable } = picked);
28441
28475
  }
28442
28476
  let claim = claims.findClaim(id);
28443
28477
  if (!claim) {
28444
28478
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28445
28479
  process.exit(1);
28446
28480
  }
28447
- let approvalsChecked = false;
28448
28481
  if (claim.approval?.state === "pending") {
28449
- ({ approvalsChecked } = await syncFounderApprovals(claims, [claim]));
28482
+ ({ approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, [claim]));
28450
28483
  claim = claims.findClaim(id);
28451
28484
  }
28452
28485
  if (claim.worktreePath) {
@@ -28473,6 +28506,10 @@ ${sanitizeText(claim.title)}`);
28473
28506
  console.log(" same way.");
28474
28507
  if (shouldStatePending(claim, approvalsChecked)) {
28475
28508
  console.log("\n Access is pending \u2014 the founder has not approved your claim yet.");
28509
+ } else if (approvalsUnavailable) {
28510
+ console.log(
28511
+ "\n Terminalhire could not check approvals right now; local state was preserved.\n The next step below still works \u2014 it asks the server directly."
28512
+ );
28476
28513
  }
28477
28514
  printNextSteps([claim]);
28478
28515
  return;
@@ -28724,10 +28761,10 @@ var CLAIM_EVENT_LABEL = {
28724
28761
  rejected: "the founder rejected"
28725
28762
  };
28726
28763
  var LINE_BREAKS = /\r\n|[\r\n\v\f\u0085\u2028\u2029]/;
28727
- var CONTROL_CHARS2 = /[\u0000-\u001F\u007F-\u009F]/g;
28764
+ var CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
28728
28765
  function terminalSafeLines(raw) {
28729
28766
  if (typeof raw !== "string" || raw === "") return [];
28730
- return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS2, ""));
28767
+ return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS3, ""));
28731
28768
  }
28732
28769
  function terminalSafeInline(raw) {
28733
28770
  return terminalSafeLines(raw).join(" ");
@@ -3617,8 +3617,24 @@ function nextPolledState(from, observed) {
3617
3617
  function nowISO2() {
3618
3618
  return (/* @__PURE__ */ new Date()).toISOString();
3619
3619
  }
3620
+ function defangText(s) {
3621
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
3622
+ }
3623
+ function finiteAmount(a) {
3624
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
3625
+ if (typeof a !== "string" || a.trim() === "") return null;
3626
+ const n = Number(a);
3627
+ return Number.isFinite(n) ? n : null;
3628
+ }
3620
3629
  function normalizeClaim(c) {
3621
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
3630
+ return {
3631
+ ...c,
3632
+ kind: c.kind ?? "bounty",
3633
+ policy: c.policy ?? null,
3634
+ title: defangText(c.title),
3635
+ repoFullName: defangText(c.repoFullName),
3636
+ amountUSD: finiteAmount(c.amountUSD)
3637
+ };
3622
3638
  }
3623
3639
  function readClaims() {
3624
3640
  try {
@@ -3740,7 +3756,7 @@ function acceptedPRRate(claims = readClaims()) {
3740
3756
  const merged = claims.filter((c) => c.state === "merged").length;
3741
3757
  return { merged, total, rate: total === 0 ? 0 : merged / total };
3742
3758
  }
3743
- var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
3759
+ var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
3744
3760
  var init_claims = __esm({
3745
3761
  "src/claims.ts"() {
3746
3762
  "use strict";
@@ -3796,6 +3812,8 @@ var init_claims = __esm({
3796
3812
  ]),
3797
3813
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
3798
3814
  };
3815
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
3816
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
3799
3817
  }
3800
3818
  });
3801
3819
 
@@ -3879,10 +3897,11 @@ function isContributeEnabled() {
3879
3897
  init_state_dir();
3880
3898
 
3881
3899
  // bin/sanitize.js
3900
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
3882
3901
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
3883
3902
  function sanitizeText(s) {
3884
3903
  if (s == null) return "";
3885
- return String(s).replace(CONTROL_CHARS, "");
3904
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
3886
3905
  }
3887
3906
  function safeHttpUrl(url) {
3888
3907
  if (url == null) return null;
@@ -11053,10 +11053,11 @@ function excludeOwnCard(results, ownLogin) {
11053
11053
  }
11054
11054
 
11055
11055
  // bin/sanitize.js
11056
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
11056
11057
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
11057
11058
  function sanitizeText(s) {
11058
11059
  if (s == null) return "";
11059
- return String(s).replace(CONTROL_CHARS, "");
11060
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
11060
11061
  }
11061
11062
  function safeHttpUrl(url) {
11062
11063
  if (url == null) return null;