@nstarkman/data-resolver 20261023.0.1 → 20261028.0.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/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  Versions are dates, `YYYYMMDD.0.N`; the four research-kit packages are released together at one
4
4
  version. A breaking change says how to migrate.
5
5
 
6
+ ## 20261028.0.0
7
+
8
+ ### Changed
9
+
10
+ - Released with `@nstarkman/collaborator-map` 20261028.0.0. `collect()` is unchanged: a paper's
11
+ snapshot keeps the people among its authors, which now includes the site's author.
12
+
6
13
  ## 20261023.0.1
7
14
 
8
15
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nstarkman/data-resolver",
3
- "version": "20261023.0.1",
3
+ "version": "20261028.0.0",
4
4
  "description": "The nstarkman.space data layer as a store, and the collector a paper site snapshots from.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/inline.js CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { dateLabel } from '@nstarkman/research-store';
12
12
  import { sortAchievements } from './mentees.js';
13
+ import { money } from './money.js';
13
14
 
14
15
  // `[text](https://…)` for the web, and `[text](item:some-id)` to point at
15
16
  // another entry on the same CV — the CV cross-references itself (a position
@@ -75,16 +76,66 @@ export function lines(long, base = '') {
75
76
  * The extra lines go last, after the thesis and supervisors, so a line index
76
77
  * means the same thing whether or not they are included — the builder ticks
77
78
  * lines by index, and inserting in the middle would silently move its ticks.
79
+ * A talk's title (`talk`) is the exception that proves it: it comes first, in
80
+ * quotation marks, because it is what a presentation's first line always was.
78
81
  */
79
- export function detailLines(item, { complete = false } = {}) {
82
+ export function detailLines(item, { complete = false, currency, forms = false } = {}) {
83
+ const prose = (long) => amountLines(long, item, { currency, forms }).map(noFinalPeriod);
80
84
  return [
81
- ...lines(item.details),
82
- ...(item.thesis ? [spans(`Thesis: ${item.thesis}`)] : []),
83
- ...(item.supervisors?.length ? [spans(`Supervisors: ${item.supervisors.join(', ')}`)] : []),
84
- ...(complete ? lines(item.detailsComplete) : []),
85
+ ...(item.talk ? [quoted(item.talk)] : []),
86
+ ...prose(item.details),
87
+ ...(item.thesis ? [noFinalPeriod(spans(`Thesis: ${item.thesis}`))] : []),
88
+ ...(item.supervisors?.length ? [noFinalPeriod(spans(`Supervisors: ${item.supervisors.join(', ')}`))] : []),
89
+ ...(complete ? prose(item.detailsComplete) : []),
85
90
  ];
86
91
  }
87
92
 
93
+ /**
94
+ * A title as a line in quotation marks: “…”. The marks belong to the rendering, not the record,
95
+ * and a double quote inside the title becomes a single one. The closing mark follows the title
96
+ * without its final period, as any line under a heading has none.
97
+ */
98
+ export function quoted(title) {
99
+ const inner = title.replace(/["“]([^"”]*)["”]/g, '‘$1’');
100
+ return [{ t: '“' }, ...noFinalPeriod(spans(inner)), { t: '”' }];
101
+ }
102
+
103
+ /**
104
+ * A CV line is a label or a phrase under a heading, so it does not end in a period: the
105
+ * last span loses one. A line ending in an abbreviation ("et al.", "etc.") or an ellipsis
106
+ * keeps it. The record is not changed, only what is shown.
107
+ */
108
+ export const KEEPS_PERIOD = /(\.\.|\b(?:al|etc|Jr|Sr|Inc|vs|St|Co|Ltd)\.)$/;
109
+ export function noFinalPeriod(line) {
110
+ const last = line[line.length - 1];
111
+ if (!last || !last.t) return line;
112
+ const t = last.t.trimEnd();
113
+ if (!t.endsWith('.') || KEEPS_PERIOD.test(t)) return line;
114
+ const rest = t.slice(0, -1);
115
+ return rest ? [...line.slice(0, -1), { ...last, t: rest }] : line.slice(0, -1);
116
+ }
117
+
118
+ /**
119
+ * `lines`, with `{amount}` standing for the item's `amount` written as `money` writes
120
+ * it in `currency`: so a figure in prose is stored once, in the data, and follows
121
+ * the CV's currency style. With `forms` the span carries all three forms, for the
122
+ * builder's page to swap; a token on an item with no `amount` is left as it is,
123
+ * which the data tests refuse.
124
+ */
125
+ function amountLines(long, item, { currency, forms }) {
126
+ const token = '{amount}';
127
+ return (Array.isArray(long) ? long : [long]).filter(Boolean).map((line) => {
128
+ if (!item.amount || !line.includes(token)) return spans(line);
129
+ return line.split(token).flatMap((piece, k) => [
130
+ ...(k ? [{
131
+ t: money(item.amount, currency),
132
+ ...(forms && { forms: { code: money(item.amount), us: money(item.amount, 'us'), dollar: money(item.amount, 'dollar') } }),
133
+ }] : []),
134
+ ...spans(piece),
135
+ ]);
136
+ });
137
+ }
138
+
88
139
  /**
89
140
  * A mentee's marked achievements, one span array per line, oldest first so the
90
141
  * entry reads as a progression. They follow everything `detailLines` returns,
package/src/money.js ADDED
@@ -0,0 +1,16 @@
1
+ // How an `amount` is written. Pure, and its own module so the store and the prose
2
+ // expansion in inline.js can both use it without importing each other.
3
+
4
+ /** By style: `us` replaces the D with a sign, `dollar` gives US dollars the bare
5
+ * sign. A currency with no sign keeps its code. */
6
+ const SIGNS = { us: { USD: 'US$', CAD: 'CA$' }, dollar: { USD: '$', CAD: 'CA$' } };
7
+
8
+ /** "USD 10,000", "CAD 20,000–30,000 p.a.", or null; in a `style`, "US$10,000",
9
+ * "$10,000", "CA$20,000–30,000 p.a.". The code is the default. */
10
+ export function money(a, style) {
11
+ if (!a) return null;
12
+ const n = (v) => v.toLocaleString('en-US');
13
+ const span = a.valueMax ? `${n(a.value)}–${n(a.valueMax)}` : n(a.value);
14
+ const sign = SIGNS[style]?.[a.currency];
15
+ return `${sign ?? `${a.currency} `}${span}${a.perAnnum ? ' p.a.' : ''}`;
16
+ }
package/src/store.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // given — the website's whole data/ (its src/lib/data.js), or the slice of it a paper
4
4
  // site keeps (collect.js). Moved here from src/lib/data.js unchanged but for that.
5
5
 
6
+ import { money } from './money.js';
7
+
6
8
  /**
7
9
  * Partial dates (YYYY, YYYY-MM, YYYY-MM-DD) compare correctly as strings, so a
8
10
  * month orders items inside a year even though only the year is ever shown.
@@ -291,14 +293,6 @@ export function createStore({ items: given, lists = new Map(), config }) {
291
293
  return stops;
292
294
  }
293
295
 
294
- /** "USD 10,000", "CAD 20,000\u201330,000 p.a.", or null. */
295
- function money(a) {
296
- if (!a) return null;
297
- const n = (v) => v.toLocaleString('en-US');
298
- const span = a.valueMax ? `${n(a.value)}\u2013${n(a.valueMax)}` : n(a.value);
299
- return `${a.currency} ${span}${a.perAnnum ? ' p.a.' : ''}`;
300
- }
301
-
302
296
  /** "The Astrophysical Journal 979, 155"; short, "ApJ 979, 155", where
303
297
  * config/journals.json has the journal's short name. */
304
298
  function venueLine(item, { short = false } = {}) {
@@ -355,6 +349,9 @@ export function createStore({ items: given, lists = new Map(), config }) {
355
349
  event: 'link', homepage: 'link',
356
350
  };
357
351
 
352
+ /** A link's mark: its rel's, but a school's homepage is a graduation cap. */
353
+ const linkIcon = (item, l) => (item.type === 'education' && l.rel === 'homepage' ? 'cap' : REL_ICON[relKey(l)] ?? 'link');
354
+
358
355
  /**
359
356
  * The one link a title points at: the article, else the preprint, else its
360
357
  * ADS record, else null. The ADS link is synthesised as a `paper` rel, so it
@@ -465,5 +462,5 @@ export function createStore({ items: given, lists = new Map(), config }) {
465
462
  return { start, end };
466
463
  }
467
464
 
468
- return { items, lists, config, byType, featured, resolve, titleOf, splitTitle, HIGHLIGHT_TOPICS, SOFTWARE_GROUPS, displayName, authorPosition, venueUrl, softwarePapers, softwareUsed, affiliationAt, authors, positionStops, money, venueLine, year, dateLabel, relKey, REL_ICON, primaryLink, links, assists, assistItem, groupedByDefault, institutionGroups, groupTitle, groupDate };
465
+ return { items, lists, config, byType, featured, resolve, titleOf, splitTitle, HIGHLIGHT_TOPICS, SOFTWARE_GROUPS, displayName, authorPosition, venueUrl, softwarePapers, softwareUsed, affiliationAt, authors, positionStops, money, venueLine, year, dateLabel, relKey, REL_ICON, linkIcon, primaryLink, links, assists, assistItem, groupedByDefault, institutionGroups, groupTitle, groupDate };
469
466
  }