@mim/histui 0.1.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.
@@ -0,0 +1,270 @@
1
+ export function escapeHtml(value) {
2
+ return String(value ?? "")
3
+ .replaceAll("&", "&")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#039;");
8
+ }
9
+
10
+ export function textOf(value, language, fallback = "en") {
11
+ if (!value) return "";
12
+ if (typeof value === "string") return value;
13
+ if (value[language]) return value[language];
14
+ const baseLanguage = language.split("-")[0];
15
+ if (value[baseLanguage]) return value[baseLanguage];
16
+ if (value[fallback]) return value[fallback];
17
+ if (value.en) return value.en;
18
+ const first = Object.values(value).find((entry) => typeof entry === "string" && entry.trim());
19
+ return first || "";
20
+ }
21
+
22
+ export function collectLanguageText(value) {
23
+ if (!value) return "";
24
+ if (typeof value === "string") return value;
25
+ if (Array.isArray(value)) return value.map(collectLanguageText).join(" ");
26
+ if (typeof value === "object") {
27
+ return Object.values(value).map(collectLanguageText).join(" ");
28
+ }
29
+ return String(value);
30
+ }
31
+
32
+ export function parseEDTFDate(value) {
33
+ if (!value || typeof value !== "string") return null;
34
+ const trimmed = value.trim();
35
+ const normalized = trimmed.replace(/[?~%]$/, "");
36
+ const match = normalized.match(/^([+-]?)([0-9X]{4,6})(?:-([0-9X]{2})(?:-([0-9X]{2}))?)?$/);
37
+ if (!match) return null;
38
+
39
+ const sign = match[1] === "-" ? -1 : 1;
40
+ const yearDigits = match[2].replaceAll("X", "0");
41
+ let year = sign * Number.parseInt(yearDigits, 10);
42
+ if (!Number.isFinite(year)) return null;
43
+
44
+ const month = match[3] && !match[3].includes("X") ? Number.parseInt(match[3], 10) : 1;
45
+ const day = match[4] && !match[4].includes("X") ? Number.parseInt(match[4], 10) : 1;
46
+ const monthOffset = Number.isFinite(month) ? Math.max(0, Math.min(11, month - 1)) / 12 : 0;
47
+ const dayOffset = Number.isFinite(day) ? Math.max(0, Math.min(30, day - 1)) / 365 : 0;
48
+
49
+ return {
50
+ raw: value,
51
+ year,
52
+ month,
53
+ day,
54
+ value: year + monthOffset + dayOffset,
55
+ approximate: /[~%]$/.test(trimmed),
56
+ uncertain: /[?%]$/.test(trimmed),
57
+ precision: match[4] ? "day" : match[3] ? "month" : value.includes("X") ? "approximate" : "year"
58
+ };
59
+ }
60
+
61
+ export function formatYear(year, language = "en", t) {
62
+ if (!Number.isFinite(year)) return "";
63
+ const rounded = Math.trunc(year);
64
+ const numberFormatter = new Intl.NumberFormat(language, { useGrouping: false });
65
+ if (rounded <= 0) {
66
+ const bceYear = Math.abs(rounded) + 1;
67
+ return `${numberFormatter.format(bceYear)} ${t ? t("bce") : "BCE"}`;
68
+ }
69
+ return `${numberFormatter.format(rounded)} ${t ? t("ce") : "CE"}`;
70
+ }
71
+
72
+ export function formatEDTFToken(value, language = "en", t) {
73
+ const parsed = parseEDTFDate(value);
74
+ if (!parsed) return value || "";
75
+ const prefix = parsed.approximate ? `${t ? t("circa") : "c."} ` : "";
76
+ const clean = value.replace(/[?~%]$/, "");
77
+ const parts = clean.split("-");
78
+ const yearToken = parts[0] === "" ? `-${parts[1]}` : parts[0];
79
+ const year = parsed.year;
80
+
81
+ if (parsed.precision === "day" && parsed.year > 0 && !clean.includes("X")) {
82
+ const date = new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day));
83
+ return `${prefix}${new Intl.DateTimeFormat(language, {
84
+ year: "numeric",
85
+ month: "short",
86
+ day: "numeric",
87
+ timeZone: "UTC"
88
+ }).format(date)}`;
89
+ }
90
+
91
+ if (parsed.precision === "month" && parsed.year > 0 && !clean.includes("X")) {
92
+ const date = new Date(Date.UTC(parsed.year, parsed.month - 1, 1));
93
+ return `${prefix}${new Intl.DateTimeFormat(language, {
94
+ year: "numeric",
95
+ month: "short",
96
+ timeZone: "UTC"
97
+ }).format(date)}`;
98
+ }
99
+
100
+ if (yearToken.includes("X")) return `${prefix}${yearToken}`;
101
+ return `${prefix}${formatYear(year, language, t)}`;
102
+ }
103
+
104
+ export function primaryAttestation(record) {
105
+ const dates = record.temporal?.dates || [];
106
+ return [...dates].sort((a, b) => (a.rank || 99) - (b.rank || 99))[0] || null;
107
+ }
108
+
109
+ export function attestationRange(attestation) {
110
+ const date = attestation?.date || {};
111
+ const start = parseEDTFDate(date.from || date.earliestFrom || date.latestFrom);
112
+ const explicitEnd = date.to || date.latestTo || date.earliestTo;
113
+ const currentYear = new Date().getUTCFullYear();
114
+ const end = date.ongoing
115
+ ? { raw: "present", year: currentYear, value: currentYear }
116
+ : parseEDTFDate(explicitEnd || date.from || date.latestFrom || date.earliestFrom);
117
+
118
+ if (!start && !end) return { start: 0, end: 0 };
119
+ const startValue = start?.value ?? end.value;
120
+ const endValue = Math.max(end?.value ?? startValue, startValue);
121
+ return { start: startValue, end: endValue };
122
+ }
123
+
124
+ export function formatExtent(attestation, language = "en", fallback = "en", t) {
125
+ if (!attestation?.date) return "";
126
+ const date = attestation.date;
127
+ const start = formatEDTFToken(date.from || date.earliestFrom || date.latestFrom, language, t);
128
+ const endToken = date.ongoing ? (t ? t("ongoing") : "present") : date.to || date.latestTo || date.earliestTo;
129
+ const end = endToken && endToken !== date.from ? formatEDTFToken(endToken, language, t) : "";
130
+ const circa = date.circa && !String(date.from || "").endsWith("~") ? `${t ? t("circa") : "c."} ` : "";
131
+ const original = attestation.original?.text ? textOf(attestation.original.text, language, fallback) : attestation.original?.value || "";
132
+ const range = end ? `${circa}${start} - ${end}` : `${circa}${start}`;
133
+ return original ? `${range} (${original})` : range;
134
+ }
135
+
136
+ export function normalizePastStruct(document, datasetConfig = {}) {
137
+ const isDataset = Boolean(document.records);
138
+ const dataset = isDataset
139
+ ? document.dataset
140
+ : {
141
+ id: document.id,
142
+ title: document.label,
143
+ defaultLanguage: datasetConfig.defaultLanguage || "en",
144
+ languages: datasetConfig.languages || ["en"]
145
+ };
146
+ const records = isDataset ? document.records : [document];
147
+ const fallbackLanguage = dataset.defaultLanguage || datasetConfig.defaultLanguage || "en";
148
+
149
+ const normalized = records
150
+ .map((record) => normalizeRecord(record, fallbackLanguage, dataset.id))
151
+ .sort((a, b) => a.__meta.start - b.__meta.start || a.__meta.importance - b.__meta.importance);
152
+
153
+ return {
154
+ paststructVersion: document.paststructVersion || "1.0",
155
+ dataset,
156
+ records: normalized,
157
+ fallbackLanguage
158
+ };
159
+ }
160
+
161
+ export function normalizeRecord(record, fallbackLanguage = "en", datasetId = "") {
162
+ const attestations = (record.temporal?.dates || []).map((attestation) => {
163
+ const range = attestationRange(attestation);
164
+ return { ...attestation, __range: range };
165
+ });
166
+ const preferred = [...attestations].sort((a, b) => (a.rank || 99) - (b.rank || 99))[0] || null;
167
+ const range = preferred?.__range || { start: 0, end: 0 };
168
+ const scale = record.significance?.scale || 10;
169
+ const value = record.significance?.value || Math.max(4, record.recordType === "event" ? 5 : 6);
170
+ const importance = Math.max(1, Math.min(10, Math.round((value / scale) * 10)));
171
+ const categories = (record.categories || []).map((category) => [category.main, category.sub].filter(Boolean).join(" / "));
172
+ const countries = [...new Set((record.places || []).map((place) => place.modernCountry).filter(Boolean))];
173
+ const confidence = preferred?.confidence || "unknown";
174
+ const temporalUncertainty = confidence !== "certain" || Boolean(preferred?.date?.circa) || /[?~%]/.test(preferred?.date?.from || "");
175
+
176
+ const searchText = [
177
+ collectLanguageText(record.label),
178
+ collectLanguageText(record.description),
179
+ collectLanguageText(record.notes),
180
+ collectLanguageText(record.keywords),
181
+ collectLanguageText(record.funFacts),
182
+ collectLanguageText(record.entities),
183
+ collectLanguageText(record.places),
184
+ record.id,
185
+ record.recordType,
186
+ record.type,
187
+ record.factuality,
188
+ categories.join(" "),
189
+ countries.join(" ")
190
+ ].join(" ").toLocaleLowerCase();
191
+
192
+ return {
193
+ ...record,
194
+ temporal: {
195
+ ...record.temporal,
196
+ dates: attestations
197
+ },
198
+ __meta: {
199
+ datasetId,
200
+ fallbackLanguage,
201
+ preferred,
202
+ start: range.start,
203
+ end: Math.max(range.end, range.start),
204
+ duration: Math.max(0, range.end - range.start),
205
+ importance,
206
+ confidence,
207
+ scope: record.significance?.scope || "local",
208
+ categories,
209
+ countries,
210
+ hasMedia: Boolean(record.media?.length),
211
+ temporalUncertainty,
212
+ searchText
213
+ }
214
+ };
215
+ }
216
+
217
+ export function collectFacets(records, language = "en", fallback = "en") {
218
+ const facets = {
219
+ recordTypes: new Map(),
220
+ types: new Map(),
221
+ factuality: new Map(),
222
+ confidence: new Map(),
223
+ scopes: new Map(),
224
+ categories: new Map(),
225
+ countries: new Map()
226
+ };
227
+
228
+ function add(map, key, label = key) {
229
+ if (!key) return;
230
+ const entry = map.get(key) || { key, label, count: 0 };
231
+ entry.count += 1;
232
+ map.set(key, entry);
233
+ }
234
+
235
+ for (const record of records) {
236
+ add(facets.recordTypes, record.recordType);
237
+ add(facets.types, record.type);
238
+ add(facets.factuality, record.factuality || "unknown");
239
+ add(facets.confidence, record.__meta.confidence || "unknown");
240
+ add(facets.scopes, record.__meta.scope || "local");
241
+ for (const category of record.__meta.categories) add(facets.categories, category);
242
+ for (const country of record.__meta.countries) add(facets.countries, country);
243
+ }
244
+
245
+ return Object.fromEntries(
246
+ Object.entries(facets).map(([key, value]) => {
247
+ return [
248
+ key,
249
+ [...value.values()].sort((a, b) => {
250
+ if (b.count !== a.count) return b.count - a.count;
251
+ return a.label.localeCompare(b.label, language);
252
+ })
253
+ ];
254
+ })
255
+ );
256
+ }
257
+
258
+ export function clamp(value, min, max) {
259
+ return Math.max(min, Math.min(max, value));
260
+ }
261
+
262
+ export function compactLabel(value) {
263
+ return String(value || "")
264
+ .replaceAll("-", " ")
265
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
266
+ }
267
+
268
+ export function unique(values) {
269
+ return [...new Set(values.filter(Boolean))];
270
+ }