@rr0/cms 0.3.80 → 0.3.82

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 (30) hide show
  1. package/dist/DataContentVisitor.js +4 -2
  2. package/dist/anchor/CaseAnchorHandler.js +3 -1
  3. package/dist/book/BookDirectoryStep.js +2 -1
  4. package/dist/people/PeopleHtmlRenderer.js +3 -3
  5. package/dist/science/crypto/ufo/enquete/dossier/CaseFactory.js +2 -3
  6. package/dist/science/crypto/ufo/enquete/dossier/CaseFactory.test.d.ts +1 -0
  7. package/dist/science/crypto/ufo/enquete/dossier/CaseFactory.test.js +38 -0
  8. package/dist/science/crypto/ufo/enquete/dossier/CaseService.js +2 -2
  9. package/dist/time/EventRenderer.js +2 -1
  10. package/dist/time/TimeContext.d.mts +7 -0
  11. package/dist/time/TimeContext.mjs +12 -0
  12. package/dist/time/TimeContext.test.mjs +30 -1
  13. package/dist/time/TimeDirectoryStep.js +1 -1
  14. package/dist/time/TimeEventRenderer.test.js +42 -1
  15. package/dist/time/datasource/ChronologyReplacer.js +2 -1
  16. package/dist/time/datasource/DatasourceTestCase.js +1 -1
  17. package/dist/time/datasource/rr0/RR0Datasource.test.js +6 -3
  18. package/dist/time/datasource/sceau/SceauDatasource.test.js +1 -1
  19. package/dist/time/html/HtmlDatetime.d.ts +36 -0
  20. package/dist/time/html/HtmlDatetime.js +102 -0
  21. package/dist/time/html/HtmlDatetime.test.d.ts +1 -0
  22. package/dist/time/html/HtmlDatetime.test.js +80 -0
  23. package/dist/time/html/TimeElementFactory.js +1 -2
  24. package/dist/time/html/TimeRenderer.js +1 -1
  25. package/dist/time/html/TimeReplacer.d.ts +6 -1
  26. package/dist/time/html/TimeReplacer.js +8 -3
  27. package/dist/time/html/TimeReplacer.test.js +32 -4
  28. package/dist/time/html/TimeStringCompleter.d.ts +6 -0
  29. package/dist/time/html/TimeStringCompleter.js +21 -4
  30. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import assert from "assert";
3
+ import { EventTime } from "@rr0/data";
3
4
  /**
4
5
  * Insert content in context file, according to data found in .json files aside of it.
5
6
  */
@@ -26,7 +27,7 @@ export class DataContentVisitor {
26
27
  async process(context, data) {
27
28
  this.processTitle(context, data);
28
29
  this.processURL(context, data);
29
- const events = data.events.sort((event1, event2) => event1.time ? event2.time ? event1.time.isBefore(event2.time) ? -1 : 1 : -1 : 1);
30
+ const events = data.events.sort((event1, event2) => event1.time ? event2.time ? EventTime.start(event1.time).isBefore(EventTime.start(event2.time)) ? -1 : 1 : -1 : 1);
30
31
  for (const event of events) {
31
32
  await this.processEvent(context, event, data);
32
33
  }
@@ -85,8 +86,9 @@ export class DataContentVisitor {
85
86
  timeParagraph(context, event) {
86
87
  const container = context.file.document.createElement("p");
87
88
  const eventContext = context.clone();
88
- const eventTime = eventContext.time.date = event.time;
89
+ const eventTime = event.time;
89
90
  assert.ok(eventTime, `Event of type "${event.type}" has no time for paragraph`);
91
+ eventContext.time.set(eventTime);
90
92
  container.dataset.time = eventTime.toString();
91
93
  const timeEl = this.timeElementFactory.create(eventContext, context, { url: true, contentOnly: false });
92
94
  return { eventP: container, timeEl };
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import { TimeContext } from "../time/TimeContext.mjs";
3
+ import { EventTime } from "@rr0/data";
3
4
  export class CaseAnchorHandler {
4
5
  constructor(caseService, timeTextBuilder) {
5
6
  this.caseService = caseService;
@@ -32,7 +33,8 @@ export class CaseAnchorHandler {
32
33
  }
33
34
  }
34
35
  else {
35
- caseContext.time = new TimeContext((_a = caseTime.year) === null || _a === void 0 ? void 0 : _a.value, (_b = caseTime.month) === null || _b === void 0 ? void 0 : _b.value, (_c = caseTime.day) === null || _c === void 0 ? void 0 : _c.value, (_d = caseTime.hour) === null || _d === void 0 ? void 0 : _d.value, (_e = caseTime.minute) === null || _e === void 0 ? void 0 : _e.value, (_f = caseTime.timeshift) === null || _f === void 0 ? void 0 : _f.toString());
36
+ const startTime = EventTime.start(caseTime);
37
+ caseContext.time = new TimeContext((_a = startTime.year) === null || _a === void 0 ? void 0 : _a.value, (_b = startTime.month) === null || _b === void 0 ? void 0 : _b.value, (_c = startTime.day) === null || _c === void 0 ? void 0 : _c.value, (_d = startTime.hour) === null || _d === void 0 ? void 0 : _d.value, (_e = startTime.minute) === null || _e === void 0 ? void 0 : _e.value, (_f = startTime.timeshift) === null || _f === void 0 ? void 0 : _f.toString());
36
38
  }
37
39
  titles.push(this.timeTextBuilder.build(caseContext));
38
40
  }
@@ -6,6 +6,7 @@ import { RR0FileUtil } from "../util/file/RR0FileUtil.js";
6
6
  import { StringUtil } from "../util/string/StringUtil.js";
7
7
  import { HtmlTag } from "../util/html/HtmlTag.js";
8
8
  import { Chapter } from "./Chapters.js";
9
+ import { EventTime } from "@rr0/data";
9
10
  /**
10
11
  * Scan directories for book information, then populates a template with collected data.
11
12
  */
@@ -77,7 +78,7 @@ export class BookDirectoryStep extends DirectoryStep {
77
78
  const details = [];
78
79
  const authors = dirBook.authors;
79
80
  const authorStr = authors ? authors.join(" & ") + ": " : "";
80
- const time = dirBook.publication.time;
81
+ const time = EventTime.start(dirBook.publication.time);
81
82
  if (time) {
82
83
  const timeDetail = time.year.value;
83
84
  details.push(HtmlTag.toString("time", timeDetail.toString()));
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import { Gender } from "@rr0/common";
3
- import { AbstractDataFactory } from "@rr0/data";
3
+ import { EventTime, AbstractDataFactory } from "@rr0/data";
4
4
  export class PeopleHtmlRenderer {
5
5
  renderLink(context, people, pseudoPeopleList, allCountries, occupations, filterOccupations = [], content) {
6
6
  var _a, _b, _c;
@@ -15,8 +15,8 @@ export class PeopleHtmlRenderer {
15
15
  if (people.hoax) {
16
16
  classList.push("canular");
17
17
  }
18
- const birthTimeStr = (_a = people.birthTime) === null || _a === void 0 ? void 0 : _a.year.toString();
19
- const deathTimeStr = (_b = people.deathTime) === null || _b === void 0 ? void 0 : _b.year.toString();
18
+ const birthTimeStr = (_a = EventTime.start(people.birthTime)) === null || _a === void 0 ? void 0 : _a.year.toString();
19
+ const deathTimeStr = (_b = EventTime.start(people.deathTime)) === null || _b === void 0 ? void 0 : _b.year.toString();
20
20
  if (people.isDeceased()) {
21
21
  classList.push("deceased");
22
22
  }
@@ -1,12 +1,11 @@
1
- import { TypedDataFactory } from "@rr0/data";
1
+ import { EventTime, TypedDataFactory } from "@rr0/data";
2
2
  import { NamedPlace } from "@rr0/place";
3
- import { Level2Date as EdtfDate } from "@rr0/time";
4
3
  export class CaseFactory extends TypedDataFactory {
5
4
  parse(dataJson) {
6
5
  const parsedCase = super.parse(dataJson);
7
6
  const time = dataJson.time;
8
7
  if (time) {
9
- Object.assign(parsedCase, { time: EdtfDate.fromString(time) });
8
+ Object.assign(parsedCase, { time: EventTime.parse(time) });
10
9
  }
11
10
  const placeName = dataJson.place;
12
11
  if (placeName) {
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { FileContents, FileContentsLang } from "@javarome/fileutil";
3
+ import { EventTime, RR0EventFactory } from "@rr0/data";
4
+ import { Level2Date as EdtfDate, Level2Interval as EdtfInterval } from "@rr0/time";
5
+ import { CaseFactory } from "./CaseFactory.js";
6
+ describe("CaseFactory", () => {
7
+ function read(time) {
8
+ const lang = new FileContentsLang();
9
+ lang.lang = "";
10
+ lang.variants = [];
11
+ const file = new FileContents("test/science/crypto/ufo/enquete/dossier/SomeCase/case.json", "utf-8", `{
12
+ "type": "case",
13
+ "title": "Some case",
14
+ "time": "${time}",
15
+ "place": "Washington",
16
+ "events": []
17
+ }
18
+ `, new Date("2025-02-22T22:22:07.720Z"), lang);
19
+ return new CaseFactory(new RR0EventFactory()).createFromFile(file);
20
+ }
21
+ test("a date", () => {
22
+ const aCase = read("1952-07-19");
23
+ expect(aCase.time).toBeInstanceOf(EdtfDate);
24
+ expect(aCase.time.toString()).toBe("1952-07-19");
25
+ });
26
+ test("an approximate date", () => {
27
+ expect(read("~1952").time.toString()).toBe("~1952");
28
+ });
29
+ test("between x and y", () => {
30
+ const aCase = read("1952-07-19/1952-07-26");
31
+ expect(aCase.time).toBeInstanceOf(EdtfInterval);
32
+ expect(aCase.time.toString()).toBe("1952-07-19/1952-07-26");
33
+ expect(EventTime.start(aCase.time).day.value).toBe(19);
34
+ });
35
+ test("years", () => {
36
+ expect(read("1966/1993").time.toString()).toBe("1966/1993");
37
+ });
38
+ });
@@ -1,5 +1,5 @@
1
1
  import path from "path";
2
- import { AbstractDataService } from "@rr0/data";
2
+ import { EventTime, AbstractDataService } from "@rr0/data";
3
3
  import { TimeContext } from "../../../../../time/TimeContext.mjs";
4
4
  export class CaseService extends AbstractDataService {
5
5
  constructor(dataService, factory, timeElementFactory, files) {
@@ -16,7 +16,7 @@ export class CaseService extends AbstractDataService {
16
16
  const classificationLabels = context.messages.case.classification.hynek[hynek];
17
17
  details.push(classificationLabels.short);
18
18
  }
19
- const time = aCase.time;
19
+ const time = EventTime.start(aCase.time); // The year of an interval is the one it starts at
20
20
  const caseContext = context.clone();
21
21
  if (time) {
22
22
  caseContext.time = new TimeContext((_a = time.year) === null || _a === void 0 ? void 0 : _a.value, (_b = time.month) === null || _b === void 0 ? void 0 : _b.value, (_c = time.day) === null || _c === void 0 ? void 0 : _c.value, (_d = time.hour) === null || _d === void 0 ? void 0 : _d.value, (_e = time.minute) === null || _e === void 0 ? void 0 : _e.value, (_f = time.timeshift) === null || _f === void 0 ? void 0 : _f.toString());
@@ -31,8 +31,9 @@ export class EventRenderer {
31
31
  }
32
32
  async render(context, event, container, options = { url: true, contentOnly: false }) {
33
33
  const eventContext = context.clone();
34
- const eventTime = eventContext.time.date = event.time;
34
+ const eventTime = event.time;
35
35
  assert.ok(eventTime, `Event of type "${event.type}" has no time`);
36
+ eventContext.time.set(eventTime);
36
37
  container.dataset.time = eventTime.toString();
37
38
  const timeEl = this.timeElementFactory.create(eventContext, context, options);
38
39
  container.append(timeEl);
@@ -161,6 +161,13 @@ export class TimeContext {
161
161
  * @return {this}
162
162
  */
163
163
  reset(): this;
164
+ /**
165
+ * Sets what this context is about: a date, or an interval when the time is only known to be within bounds.
166
+ *
167
+ * @param {EdtfDate | EdtfInterval | undefined} time
168
+ * @return {this}
169
+ */
170
+ set(time: EdtfDate | EdtfInterval | undefined): this;
164
171
  /**
165
172
  * @param {TimeContext} other
166
173
  * @return {boolean}
@@ -307,6 +307,18 @@ export class TimeContext {
307
307
  this.duration = undefined;
308
308
  return this;
309
309
  }
310
+ /**
311
+ * Sets what this context is about: a date, or an interval when the time is only known to be within bounds.
312
+ *
313
+ * @param {EdtfDate | EdtfInterval | undefined} time
314
+ * @return {this}
315
+ */
316
+ set(time) {
317
+ this.date = time instanceof EdtfInterval ? undefined : time;
318
+ this.interval = time instanceof EdtfInterval ? time : undefined;
319
+ this.duration = undefined;
320
+ return this;
321
+ }
310
322
  /**
311
323
  * @param {TimeContext} other
312
324
  * @return {boolean}
@@ -1,5 +1,6 @@
1
1
  import { TimeContext } from "./TimeContext.mjs";
2
2
  import { describe, expect, test } from "vitest";
3
+ import { Level2Date, Level2Interval } from "@rr0/time";
3
4
  describe("TimeContext", () => {
4
5
  const timeContext = new TimeContext();
5
6
  const SECOND = 1000;
@@ -60,7 +61,7 @@ describe("TimeContext", () => {
60
61
  expect(timeContext.getMinutes()).toBe(55);
61
62
  });
62
63
  test("updateFromStr with a time zone containing a P is a date, not a duration", () => {
63
- for (const str of ["1957-02-27 21:45PST", "1991-09-18 00:38:42PDT", "1957-02-27 21:45 (PST)"]) {
64
+ for (const str of ["1957-02-27 21:45PST", "1991-09-18 00:38:42PDT"]) {
64
65
  const context = new TimeContext();
65
66
  expect(context.updateFromStr(str)).toBe(true);
66
67
  expect(context.duration).toBeUndefined();
@@ -95,4 +96,32 @@ describe("TimeContext", () => {
95
96
  expect(clone.getHour()).toBe(17);
96
97
  expect(clone.getMinutes()).toBe(56);
97
98
  });
99
+ describe("set", () => {
100
+ test("a date", () => {
101
+ const context = new TimeContext().set(Level2Date.fromString("1952-07-19"));
102
+ expect(context.date.toString()).toBe("1952-07-19");
103
+ expect(context.interval).toBeUndefined();
104
+ });
105
+ test("an interval", () => {
106
+ const context = new TimeContext().set(Level2Interval.fromString("1952-07-19/1952-07-26"));
107
+ expect(context.date).toBeUndefined();
108
+ expect(context.interval.toString()).toBe("1952-07-19/1952-07-26");
109
+ expect(context.from.day.value).toBe(19);
110
+ expect(context.to.day.value).toBe(26);
111
+ });
112
+ test("replaces what was set before", () => {
113
+ const context = new TimeContext();
114
+ context.updateFromStr("1952-07-19");
115
+ context.set(Level2Interval.fromString("1966/1993"));
116
+ expect(context.date).toBeUndefined();
117
+ context.set(Level2Date.fromString("1970"));
118
+ expect(context.interval).toBeUndefined();
119
+ expect(context.date.toString()).toBe("1970");
120
+ });
121
+ test("nothing", () => {
122
+ const context = new TimeContext().set(undefined);
123
+ expect(context.date).toBeUndefined();
124
+ expect(context.interval).toBeUndefined();
125
+ });
126
+ });
98
127
  });
@@ -51,7 +51,7 @@ export class TimeDirectoryStep extends DirectoryStep {
51
51
  const eventContext = context.clone();
52
52
  this.service.setContextFromFile(eventContext, event.dirName);
53
53
  if (event.time) {
54
- eventContext.time.date = event.time;
54
+ eventContext.time.set(event.time);
55
55
  }
56
56
  const ref = this.elementFactory.create(eventContext, context, { url: true, contentOnly: true });
57
57
  item.appendChild(ref);
@@ -8,7 +8,7 @@ import { SourceRenderer } from "../source/SourceRenderer.js";
8
8
  import { NoteFileCounter } from "../note/NoteFileCounter.js";
9
9
  import { NoteRenderer } from "../note/NoteRenderer.js";
10
10
  import { HautsDeSeineCityCode } from "../org/eu/fr/region/idf/92/HautsDeSeineCityCode.js";
11
- import { AllDataService } from "@rr0/data";
11
+ import { AllDataService, EventTime } from "@rr0/data";
12
12
  import { Place } from "@rr0/place";
13
13
  import { OrganizationPlace } from "../place/OrganizationPlace.js";
14
14
  import { hautsDeSeine } from "../org/eu/fr/region/idf/92/HautsDeSeine.js";
@@ -49,4 +49,45 @@ describe("TimeEventRenderer", () => {
49
49
  await renderer.render(context, c, elem);
50
50
  expect(elem.innerHTML).toBe(`<span class="time-resolved">en <time datetime="1970-03">mars 1970</time></span> À <span class="place">Nanterre (Hauts-de-Seine (Île-de-France (France)))</span>, some sighting <span class="source"><span class="peopl">Some Author</span>&nbsp;: <a href="https://somesite.com/case1">Case 1</a>, <i>Some site</i>, jeudi 13 décembre 2001</span>.`);
51
51
  });
52
+ describe("time", () => {
53
+ async function renderTime(time) {
54
+ const context = cmsTestUtil.time.newHtmlContext("1/9/7/0/03/index.html");
55
+ const c = {
56
+ events: [], type: "event", eventType: "sighting", time: EventTime.parse(time), description: "some sighting",
57
+ sources: []
58
+ };
59
+ const elem = context.file.document.createElement("li");
60
+ await renderer.render(context, c, elem);
61
+ return { html: elem.innerHTML, dataTime: elem.dataset.time };
62
+ }
63
+ test("a date", async () => {
64
+ const { html, dataTime } = await renderTime("1970-03-05");
65
+ expect(dataTime).toBe("1970-03-05");
66
+ expect(html).toContain(`<time datetime="1970-03-05"`);
67
+ expect(html).not.toContain("time-interval");
68
+ });
69
+ test("between x and y", async () => {
70
+ const { html, dataTime } = await renderTime("1970-03-05/1970-03-09");
71
+ expect(dataTime).toBe("1970-03-05/1970-03-09");
72
+ expect(html).toContain(`<span class="time-interval">`);
73
+ expect(html).toContain(`<time datetime="1970-03-05"`);
74
+ expect(html).toContain(`<time datetime="1970-03-09"`);
75
+ });
76
+ test("years", async () => {
77
+ const { html } = await renderTime("1966/1993");
78
+ expect(html).toContain(`<time datetime="1966"`);
79
+ expect(html).toContain(`<time datetime="1993"`);
80
+ });
81
+ test("from x", async () => {
82
+ const { html, dataTime } = await renderTime("1970-03/..");
83
+ expect(dataTime).toBe("1970-03/");
84
+ expect(html).toContain(`<time datetime="1970-03"`);
85
+ expect(html).toContain(`<span class="time-interval">`);
86
+ });
87
+ test("around x", async () => {
88
+ const { html, dataTime } = await renderTime("~1970-03");
89
+ expect(dataTime).toBe("~1970-03");
90
+ expect(html).toContain(`<time datetime="1970-03"`);
91
+ });
92
+ });
52
93
  });
@@ -1,5 +1,6 @@
1
1
  import { RR0HttpDatasource } from "./rr0/RR0HttpDatasource.js";
2
2
  import { HttpSource } from "./HttpSource.js";
3
+ import { EventTime } from "@rr0/data";
3
4
  /**
4
5
  * Replaces a (ul) tag from (chronology) files with case summaries from external datasources.
5
6
  */
@@ -101,7 +102,7 @@ export class ChronologyReplacer {
101
102
  const casesToMerge = sourceCases.map(sourceCase => mapping.mapper.map(context, sourceCase, fetchTime));
102
103
  const casesToAdd = [];
103
104
  for (const caseToMerge of casesToMerge) {
104
- const foundExisting = existingCases.find(existingCase => existingCase.time.isEqual(caseToMerge.time)
105
+ const foundExisting = existingCases.find(existingCase => EventTime.equals(existingCase.time, caseToMerge.time)
105
106
  && existingCase.place === caseToMerge.place);
106
107
  if (foundExisting) {
107
108
  context.logger.debug("Merging ", caseToMerge, " into ", foundExisting);
@@ -72,7 +72,7 @@ export class DatasourceTestCase {
72
72
  const datasource = this.mapping.datasource;
73
73
  const source = expectedSources[0];
74
74
  const sourceContext = context.clone();
75
- sourceContext.time.date = source.publication.time;
75
+ sourceContext.time.set(source.publication.time);
76
76
  const publicationStr = source.publication ? `, ${this.timeTextBuilder.build(sourceContext)}` : "";
77
77
  const indexStr = source.index ? `, ${source.index}` : "";
78
78
  const authorStr = datasource.authors.map(authorStr => `<span class="people">${authorStr}</span>`).join(" &amp; ");
@@ -6,6 +6,7 @@ import { Level2Date as EdtfDate } from "@rr0/time";
6
6
  import { HtmlTag } from "../../../util/html/HtmlTag.js";
7
7
  import { RR0Datasource } from "./RR0Datasource.js";
8
8
  import { TimeTextBuilder } from "../../text/TimeTextBuilder.js";
9
+ import { EventTime } from "@rr0/data";
9
10
  import { RR0CaseSummaryMapper } from "./RR0CaseSummaryMapper.js";
10
11
  import { RR0FileDatasource } from "./RR0FileDatasource.js";
11
12
  import { NamedPlace } from "@rr0/place";
@@ -37,10 +38,12 @@ describe("RR0CaseSource", () => {
37
38
  super(mapping, sourceCases);
38
39
  }
39
40
  getTime(c) {
40
- return c.time;
41
+ return EventTime.start(c.time);
41
42
  }
42
43
  sortComparator(c1, c2) {
43
- return !c1.time || c2.time && c1.time.isBefore(c2.time) ? -1 : !c2.time || c1.time.isAfter(c2.time) ? 1 : 0;
44
+ const time1 = EventTime.start(c1.time);
45
+ const time2 = EventTime.start(c2.time);
46
+ return !time1 || time2 && time1.isBefore(time2) ? -1 : !time2 || time1.isAfter(time2) ? 1 : 0;
44
47
  }
45
48
  /**
46
49
  * Specialization of sources for RR0 cases
@@ -62,7 +65,7 @@ describe("RR0CaseSource", () => {
62
65
  }
63
66
  if (publication.time) {
64
67
  const sourceContext = context.clone();
65
- sourceContext.time.date = source.publication.time;
68
+ sourceContext.time.set(source.publication.time);
66
69
  const timeStr = this.timeTextBuilder.build(sourceContext);
67
70
  sourceItems.push(timeStr);
68
71
  }
@@ -60,7 +60,7 @@ describe("SCEAUCaseSource", () => {
60
60
  }
61
61
  if (publication.time) {
62
62
  const sourceContext = context.clone();
63
- sourceContext.time.date = source.publication.time;
63
+ sourceContext.time.set(source.publication.time);
64
64
  const timeStr = this.timeTextBuilder.build(sourceContext);
65
65
  sourceItems.push(timeStr);
66
66
  }
@@ -0,0 +1,36 @@
1
+ import { Level2Date, Level2Duration } from "@rr0/time";
2
+ /**
3
+ * The `datetime` attribute of a <time> and the EDTF it comes from.
4
+ *
5
+ * HTML knows its own formats only: a year, month, date, local or global date and time ("2004-06-11T09:12:33.123+01:00"),
6
+ * and durations in days, hours, minutes and seconds ("P1DT2H30M"). EDTF is richer: qualified values ("2004-~06", "~P2H"),
7
+ * a time zone written "+01", years and months in durations. So `datetime` holds what HTML can express, and what it cannot
8
+ * is kept as it was written in `data-edtf`, which is only set when it differs from `datetime`.
9
+ */
10
+ export declare class HtmlDatetime {
11
+ /**
12
+ * Sets the attributes of a <time> element for a time.
13
+ *
14
+ * @param el The element to set the attributes of.
15
+ * @param time The time the element is about.
16
+ */
17
+ static apply(el: HTMLElement, time: Level2Date | Level2Duration): void;
18
+ /**
19
+ * @return The `datetime` value that HTML can read for a time, or undefined if HTML has no equivalent.
20
+ */
21
+ static of(time: Level2Date | Level2Duration): string | undefined;
22
+ protected static ofDate(date: Level2Date): string | undefined;
23
+ protected static ofDuration(duration: Level2Duration): string | undefined;
24
+ /**
25
+ * @return The numeric value of a component, if it has one (not a range of values, as for unspecified digits) within bounds.
26
+ */
27
+ protected static value(comp: {
28
+ value: unknown;
29
+ } | undefined, min: number, max?: number): number | undefined;
30
+ protected static pad(n: number): string;
31
+ /**
32
+ * @param minutes The time zone offset, in minutes east of UTC.
33
+ * @return The time zone offset as HTML writes it ("Z", "+01:00").
34
+ */
35
+ protected static offset(minutes: number): string;
36
+ }
@@ -0,0 +1,102 @@
1
+ import { Level2Duration } from "@rr0/time";
2
+ /**
3
+ * The `datetime` attribute of a <time> and the EDTF it comes from.
4
+ *
5
+ * HTML knows its own formats only: a year, month, date, local or global date and time ("2004-06-11T09:12:33.123+01:00"),
6
+ * and durations in days, hours, minutes and seconds ("P1DT2H30M"). EDTF is richer: qualified values ("2004-~06", "~P2H"),
7
+ * a time zone written "+01", years and months in durations. So `datetime` holds what HTML can express, and what it cannot
8
+ * is kept as it was written in `data-edtf`, which is only set when it differs from `datetime`.
9
+ */
10
+ export class HtmlDatetime {
11
+ /**
12
+ * Sets the attributes of a <time> element for a time.
13
+ *
14
+ * @param el The element to set the attributes of.
15
+ * @param time The time the element is about.
16
+ */
17
+ static apply(el, time) {
18
+ const datetime = HtmlDatetime.of(time);
19
+ if (datetime !== undefined) {
20
+ el.dateTime = datetime;
21
+ }
22
+ const edtf = time.toString();
23
+ if (edtf !== datetime) {
24
+ el.dataset.edtf = edtf;
25
+ }
26
+ }
27
+ /**
28
+ * @return The `datetime` value that HTML can read for a time, or undefined if HTML has no equivalent.
29
+ */
30
+ static of(time) {
31
+ return time instanceof Level2Duration ? HtmlDatetime.ofDuration(time) : HtmlDatetime.ofDate(time);
32
+ }
33
+ static ofDate(date) {
34
+ var _a;
35
+ const year = HtmlDatetime.value(date.year, 1);
36
+ if (year === undefined) {
37
+ return undefined;
38
+ }
39
+ let str = String(year).padStart(4, "0");
40
+ const month = HtmlDatetime.value(date.month, 1, 12); // Seasons (21 to 24) are no month
41
+ if (month === undefined) {
42
+ return str;
43
+ }
44
+ str += "-" + HtmlDatetime.pad(month);
45
+ const day = HtmlDatetime.value(date.day, 1, 31);
46
+ if (day === undefined) {
47
+ return str;
48
+ }
49
+ str += "-" + HtmlDatetime.pad(day);
50
+ const hour = HtmlDatetime.value(date.hour, 0, 23);
51
+ const minute = HtmlDatetime.value(date.minute, 0, 59);
52
+ if (hour === undefined || minute === undefined) { // HTML has no time without minutes
53
+ return str;
54
+ }
55
+ str += "T" + HtmlDatetime.pad(hour) + ":" + HtmlDatetime.pad(minute);
56
+ const second = HtmlDatetime.value(date.second, 0, 59);
57
+ if (second !== undefined) {
58
+ str += ":" + HtmlDatetime.pad(second);
59
+ const millisecond = HtmlDatetime.value(date.millisecond, 0, 999); // @rr0/time 1.1.0
60
+ if (millisecond !== undefined) {
61
+ str += "." + String(millisecond).padStart(3, "0");
62
+ }
63
+ }
64
+ const timeshift = (_a = date.timeshift) === null || _a === void 0 ? void 0 : _a.value;
65
+ if (typeof timeshift === "number" && Number.isInteger(timeshift)) {
66
+ str += HtmlDatetime.offset(timeshift);
67
+ }
68
+ return str;
69
+ }
70
+ static ofDuration(duration) {
71
+ const spec = duration.toSpec();
72
+ if (spec.years || spec.months) { // HTML durations have no years nor months, whose length depends on the date
73
+ return undefined;
74
+ }
75
+ const numbers = [spec.days, spec.hours, spec.minutes, spec.seconds].map(comp => HtmlDatetime.value(comp, 0));
76
+ const [days, hours, minutes, seconds] = numbers;
77
+ const time = (hours ? hours + "H" : "") + (minutes ? minutes + "M" : "") + (seconds ? seconds + "S" : "");
78
+ const str = (days ? days + "D" : "") + (time ? "T" + time : "");
79
+ return str ? "P" + str : undefined;
80
+ }
81
+ /**
82
+ * @return The numeric value of a component, if it has one (not a range of values, as for unspecified digits) within bounds.
83
+ */
84
+ static value(comp, min, max = Number.MAX_SAFE_INTEGER) {
85
+ const value = comp === null || comp === void 0 ? void 0 : comp.value;
86
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max ? value : undefined;
87
+ }
88
+ static pad(n) {
89
+ return String(n).padStart(2, "0");
90
+ }
91
+ /**
92
+ * @param minutes The time zone offset, in minutes east of UTC.
93
+ * @return The time zone offset as HTML writes it ("Z", "+01:00").
94
+ */
95
+ static offset(minutes) {
96
+ if (minutes === 0) {
97
+ return "Z";
98
+ }
99
+ const abs = Math.abs(minutes);
100
+ return (minutes < 0 ? "-" : "+") + HtmlDatetime.pad(Math.floor(abs / 60)) + ":" + HtmlDatetime.pad(abs % 60);
101
+ }
102
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Level2Date, Level2Duration } from "@rr0/time";
3
+ import { HtmlDatetime } from "./HtmlDatetime.js";
4
+ import { cmsTestUtil } from "../../test/CMSTestUtil.js";
5
+ describe("HtmlDatetime", () => {
6
+ describe("date", () => {
7
+ test.each([
8
+ ["2004", "2004"],
9
+ ["0033", "0033"],
10
+ ["2004-06", "2004-06"],
11
+ ["2004-06-11", "2004-06-11"],
12
+ ["2004-06-11T09:12", "2004-06-11T09:12"],
13
+ ["2004-06-11T09:12:33", "2004-06-11T09:12:33"],
14
+ ["2004-06-11 09:12", "2004-06-11T09:12"],
15
+ ["2004-06-11T09:12:33Z", "2004-06-11T09:12:33Z"],
16
+ ["2004-06-11T09:12:33+01", "2004-06-11T09:12:33+01:00"],
17
+ ["2004-06-11T09:12-05", "2004-06-11T09:12-05:00"],
18
+ ["1947-06-24 21:45PST", "1947-06-24T21:45-08:00"]
19
+ ])("%s is %s", (edtf, expected) => {
20
+ expect(HtmlDatetime.of(Level2Date.fromString(edtf))).toBe(expected);
21
+ });
22
+ test("qualification is not part of the HTML value", () => {
23
+ expect(HtmlDatetime.of(Level2Date.fromString("2004-~06"))).toBe("2004-06");
24
+ expect(HtmlDatetime.of(Level2Date.fromString("2004-06~"))).toBe("2004-06");
25
+ expect(HtmlDatetime.of(Level2Date.fromString("?2004-06-11"))).toBe("2004-06-11");
26
+ });
27
+ test("an hour without minutes has no HTML equivalent, the date remains", () => {
28
+ expect(HtmlDatetime.of(new Level2Date({ year: 2004, month: 6, day: 11, hour: 9 }))).toBe("2004-06-11");
29
+ });
30
+ test("a season is no month", () => {
31
+ expect(HtmlDatetime.of(Level2Date.fromString("2004-21"))).toBe("2004");
32
+ });
33
+ test("a year before 1 has no HTML equivalent", () => {
34
+ expect(HtmlDatetime.of(Level2Date.fromString("-0033"))).toBeUndefined();
35
+ });
36
+ });
37
+ describe("duration", () => {
38
+ test.each([
39
+ ["P2D", "P2D"],
40
+ ["P20H", "PT20H"],
41
+ ["P90S", "PT1M30S"],
42
+ ["P2D10H23M45S", "P2DT10H23M45S"],
43
+ ["PT30M", "PT30M"]
44
+ ])("%s is %s", (edtf, expected) => {
45
+ expect(HtmlDatetime.of(Level2Duration.fromString(edtf))).toBe(expected);
46
+ });
47
+ test("months are written MM in the former notation, and have no HTML equivalent", () => {
48
+ expect(HtmlDatetime.of(Level2Duration.fromString("P2MM"))).toBeUndefined();
49
+ });
50
+ test("years have no HTML equivalent", () => {
51
+ expect(HtmlDatetime.of(Level2Duration.fromString("P1Y"))).toBeUndefined();
52
+ });
53
+ test("qualification is not part of the HTML value", () => {
54
+ expect(HtmlDatetime.of(Level2Duration.fromString("~P20H"))).toBe("PT20H");
55
+ });
56
+ });
57
+ describe("element", () => {
58
+ const newTimeEl = () => cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "").file.document.createElement("time");
59
+ test("data-edtf is not set when HTML reads the same value", () => {
60
+ const timeEl = newTimeEl();
61
+ HtmlDatetime.apply(timeEl, Level2Date.fromString("2004-06-11"));
62
+ expect(timeEl.getAttribute("datetime")).toBe("2004-06-11");
63
+ expect(timeEl.hasAttribute("data-edtf")).toBe(false);
64
+ });
65
+ test("data-edtf holds the EDTF when HTML reads it differently", () => {
66
+ const timeEl = newTimeEl();
67
+ const date = Level2Date.fromString("2004-06-11T09:12+01");
68
+ HtmlDatetime.apply(timeEl, date);
69
+ expect(timeEl.getAttribute("datetime")).toBe("2004-06-11T09:12+01:00");
70
+ expect(timeEl.getAttribute("data-edtf")).toBe(date.toString());
71
+ });
72
+ test("data-edtf holds the EDTF when HTML has no equivalent, and then datetime is not set", () => {
73
+ const timeEl = newTimeEl();
74
+ const duration = Level2Duration.fromString("P2MM");
75
+ HtmlDatetime.apply(timeEl, duration);
76
+ expect(timeEl.hasAttribute("datetime")).toBe(false);
77
+ expect(timeEl.getAttribute("data-edtf")).toBe(duration.toString());
78
+ });
79
+ });
80
+ });
@@ -77,7 +77,6 @@ export class TimeElementFactory {
77
77
  const duration = context.time.duration;
78
78
  const items = [];
79
79
  const messages = context.messages.context.time.duration;
80
- const datetime = duration.toString();
81
80
  const spec = duration.toSpec();
82
81
  const days = spec.days;
83
82
  if (days) {
@@ -105,7 +104,7 @@ export class TimeElementFactory {
105
104
  if (context.time.approximate) {
106
105
  replacementStr = messages.approximate(replacementStr);
107
106
  }
108
- replacement = TimeReplacer.resolvedTime(context, datetime);
107
+ replacement = TimeReplacer.resolvedTime(context, duration);
109
108
  replacement.classList.add("duration");
110
109
  replacement.textContent = replacementStr;
111
110
  }
@@ -28,7 +28,7 @@ export class TimeRenderer {
28
28
  const currentFileName = file.name;
29
29
  const doc = file.document;
30
30
  let replacement;
31
- const timeEl = TimeReplacer.resolvedTime(context, date.toString());
31
+ const timeEl = TimeReplacer.resolvedTime(context, date);
32
32
  if (title !== text) {
33
33
  timeEl.title = title;
34
34
  }
@@ -1,5 +1,6 @@
1
1
  import { HtmlRR0Context } from "../../RR0Context.js";
2
2
  import { DomReplacement } from "../DomReplacement.js";
3
+ import { Level2Date, Level2Duration } from "@rr0/time";
3
4
  import { TimeElementFactory } from "./TimeElementFactory.js";
4
5
  import { TimeRenderOptions } from "./TimeRenderer.js";
5
6
  import { TimeStringCompleter } from "./TimeStringCompleter.js";
@@ -15,7 +16,11 @@ export declare class TimeReplacer implements DomReplacement<HtmlRR0Context, HTML
15
16
  static readonly inlineTags: string[];
16
17
  protected readonly completer: TimeStringCompleter;
17
18
  constructor(factory: TimeElementFactory);
18
- static resolvedTime(context: HtmlRR0Context, dateTime: string): HTMLTimeElement;
19
+ /**
20
+ * @param context The context to create the element in.
21
+ * @param time The time the element is about, which `datetime` and `data-edtf` are set from.
22
+ */
23
+ static resolvedTime(context: HtmlRR0Context, time: Level2Date | Level2Duration): HTMLTimeElement;
19
24
  /**
20
25
  * @return The text that precedes an element in its sentence.
21
26
  */
@@ -1,4 +1,5 @@
1
1
  import { ObjectUtils } from "@rr0/common";
2
+ import { HtmlDatetime } from "./HtmlDatetime.js";
2
3
  import { TimeStringCompleter } from "./TimeStringCompleter.js";
3
4
  /**
4
5
  * Replaces a <time> tag.
@@ -8,9 +9,13 @@ export class TimeReplacer {
8
9
  this.factory = factory;
9
10
  this.completer = new TimeStringCompleter();
10
11
  }
11
- static resolvedTime(context, dateTime) {
12
+ /**
13
+ * @param context The context to create the element in.
14
+ * @param time The time the element is about, which `datetime` and `data-edtf` are set from.
15
+ */
16
+ static resolvedTime(context, time) {
12
17
  const replacement = context.file.document.createElement("time");
13
- replacement.dateTime = dateTime;
18
+ HtmlDatetime.apply(replacement, time);
14
19
  return replacement;
15
20
  }
16
21
  /**
@@ -34,7 +39,7 @@ export class TimeReplacer {
34
39
  }
35
40
  async replacement(pageContext, origEl) {
36
41
  let replacement;
37
- if (origEl.dateTime || origEl.dataset.format === "none") { // Already done, or not to be interpreted
42
+ if (origEl.dateTime || origEl.dataset.edtf || origEl.dataset.format === "none") { // Already done, or not to be interpreted
38
43
  replacement = origEl;
39
44
  }
40
45
  else {
@@ -153,7 +153,7 @@ describe("TimeReplacer", async () => {
153
153
  original.textContent = "2003-12-24T10:22CDT";
154
154
  const replacement = await replacer.replacement(context, original);
155
155
  expect(replacement.outerHTML)
156
- .toBe(`<span class="time-resolved"><a href="${path.join("/", timeOptions.rootDir, "2/0/0/3/12/24/")}"><time datetime="2003-12-24T10:22-05">mercredi 24 décembre 2003 à 10:22</time></a></span>`); // TODO: Text should have timezone info
156
+ .toBe(`<span class="time-resolved"><a href="${path.join("/", timeOptions.rootDir, "2/0/0/3/12/24/")}"><time datetime="2003-12-24T10:22-05:00" data-edtf="2003-12-24T10:22-05">mercredi 24 décembre 2003 à 10:22</time></a></span>`); // TODO: Text should have timezone info
157
157
  expect(context.time.getYear()).toBe(2003);
158
158
  expect(context.time.getMonth()).toBe(12);
159
159
  expect(context.time.getDayOfMonth()).toBe(24);
@@ -262,7 +262,7 @@ describe("TimeReplacer", async () => {
262
262
  const datetime = "P2D10H23M45S";
263
263
  timeEl.textContent = datetime;
264
264
  const replacement = await replacer.replacement(context, timeEl);
265
- expect(replacement.outerHTML).toBe(`<time datetime="${datetime}" class="duration">2 jours, 10 heures, 23 minutes et 45 secondes</time>`);
265
+ expect(replacement.outerHTML).toBe(`<time datetime="P2DT10H23M45S" data-edtf="${datetime}" class="duration">2 jours, 10 heures, 23 minutes et 45 secondes</time>`);
266
266
  });
267
267
  test("with context", async () => {
268
268
  const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
@@ -276,7 +276,7 @@ describe("TimeReplacer", async () => {
276
276
  const element = context.file.document.createElement("time");
277
277
  element.textContent = "P20M";
278
278
  const replacement = await replacer.replacement(context, element);
279
- expect(replacement.outerHTML).toBe(`<time datetime="P20M" class="duration">20 minutes</time>`);
279
+ expect(replacement.outerHTML).toBe(`<time datetime="PT20M" data-edtf="P20M" class="duration">20 minutes</time>`);
280
280
  }
281
281
  });
282
282
  test("with approximation", async () => {
@@ -284,7 +284,7 @@ describe("TimeReplacer", async () => {
284
284
  const original = context.file.document.createElement("time");
285
285
  original.textContent = "P~2H";
286
286
  const replacement = await replacer.replacement(context, original);
287
- expect(replacement.outerHTML).toBe(`<time datetime="P2H" class="duration">environ 2 heures</time>`);
287
+ expect(replacement.outerHTML).toBe(`<time datetime="PT2H" data-edtf="P~2H" class="duration">environ 2 heures</time>`);
288
288
  });
289
289
  });
290
290
  describe("parses hour", () => {
@@ -374,6 +374,34 @@ describe("TimeReplacer", async () => {
374
374
  const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
375
375
  expect(await render(context, "1947-06-24 21:45PST")).toBe("mardi 24 juin 1947 à 21:45");
376
376
  });
377
+ test("a time zone in parentheses, as pages write it, is read as a time zone", async () => {
378
+ var _a, _b;
379
+ for (const str of ["1947-06-24 21:45 (PST)", "1947-06-24 21:45 PST"]) {
380
+ const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
381
+ expect(await render(context, str)).toBe("mardi 24 juin 1947 à 21:45");
382
+ expect((_b = (_a = context.time.date) === null || _a === void 0 ? void 0 : _a.timeshift) === null || _b === void 0 ? void 0 : _b.value, str).toBe(-480);
383
+ }
384
+ });
385
+ test("a time zone that depends on the date", async () => {
386
+ var _a, _b;
387
+ for (const [str, hours] of [["1969-07-20 20:17 (PT)", -7], ["1969-12-20 20:17 PT", -8], ["2009-07-19 05:54 PT", -7]]) {
388
+ const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
389
+ await render(context, str);
390
+ expect((_b = (_a = context.time.date) === null || _a === void 0 ? void 0 : _a.timeshift) === null || _b === void 0 ? void 0 : _b.value, str).toBe(hours * 60);
391
+ }
392
+ });
393
+ test("GMT is a time zone", async () => {
394
+ var _a, _b;
395
+ const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
396
+ await render(context, "1969-07-20 20:17:40 GMT");
397
+ expect((_b = (_a = context.time.date) === null || _a === void 0 ? void 0 : _a.timeshift) === null || _b === void 0 ? void 0 : _b.value).toBe(0);
398
+ });
399
+ test("a sidereal time is not a time zone, and is kept as text", async () => {
400
+ var _a;
401
+ const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
402
+ expect(await render(context, "1967-05-13 15:40 (LST)")).toBe("samedi 13 mai 1967 à 15:40 (LST)");
403
+ expect((_a = context.time.date) === null || _a === void 0 ? void 0 : _a.timeshift).toBeUndefined();
404
+ });
377
405
  test("time inside a link gets no link of its own", async () => {
378
406
  const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
379
407
  const doc = context.file.document;
@@ -20,6 +20,12 @@ export declare class TimeStringCompleter {
20
20
  * A trailing time zone ("PST", "(CDT)", "UTC+1") is part of the value, not free text.
21
21
  */
22
22
  protected static readonly timeZone: RegExp;
23
+ /**
24
+ * @param str A word following a time.
25
+ * @return The time zone it names, as EDTF writes it right after the time ("EST" for "(EST)"), or undefined if it is not a time zone
26
+ * EDTF knows ("(LST)" is a local sidereal time, whose offset is unknown, so it remains free text).
27
+ */
28
+ protected static zone(str: string): string | undefined;
23
29
  /**
24
30
  * A duration ("P10M", "~P1H"), or a range of durations ("P10M/12M").
25
31
  */
@@ -1,3 +1,4 @@
1
+ import { timeZones } from "@rr0/time";
1
2
  import { TimeContext } from "../TimeContext.mjs";
2
3
  /**
3
4
  * Prepares a <time> string for EDTF parsing.
@@ -8,6 +9,16 @@ import { TimeContext } from "../TimeContext.mjs";
8
9
  * first. Free text written around the value ("le soir", "vers") is set apart so that it can be kept as is.
9
10
  */
10
11
  export class TimeStringCompleter {
12
+ /**
13
+ * @param str A word following a time.
14
+ * @return The time zone it names, as EDTF writes it right after the time ("EST" for "(EST)"), or undefined if it is not a time zone
15
+ * EDTF knows ("(LST)" is a local sidereal time, whose offset is unknown, so it remains free text).
16
+ */
17
+ static zone(str) {
18
+ var _a;
19
+ const name = (_a = TimeStringCompleter.timeZone.exec(str)) === null || _a === void 0 ? void 0 : _a[1];
20
+ return name && (/^UTC[+-]?\d*$/.test(name) || timeZones.some(zone => zone.name === name && zone.timeshift !== "?")) ? name : undefined;
21
+ }
11
22
  split(str) {
12
23
  let prefix = "";
13
24
  let value = str.replace(/[ \t\r\n]+/g, " ").trim(); // "1973-10-11\n 21:00/22:00"
@@ -23,9 +34,15 @@ export class TimeStringCompleter {
23
34
  // A bare number followed by a space may be a quantity ("1 h", "30 mn après", "8 jours plus tard"), so a day of the
24
35
  // month needs a T before its text ("18Tla nuit").
25
36
  const quantity = post && /^\d{1,2}$/.test(post[1]) && !post[2].includes("T");
26
- if (post && !quantity && !TimeStringCompleter.timeZone.test(post[3].trim())) {
27
- value = post[1];
28
- suffix = (post[2].includes("T") ? " " : post[2]) + post[3];
37
+ if (post && !quantity) {
38
+ const zone = TimeStringCompleter.zone(post[3].trim());
39
+ if (zone) {
40
+ value = post[1] + zone; // EDTF reads "21:45EST", not "21:45 EST" nor "21:45 (EST)"
41
+ }
42
+ else {
43
+ value = post[1];
44
+ suffix = (post[2].includes("T") ? " " : post[2]) + post[3];
45
+ }
29
46
  }
30
47
  return { prefix, value, suffix };
31
48
  }
@@ -121,7 +138,7 @@ export class TimeStringCompleter {
121
138
  /**
122
139
  * A trailing time zone ("PST", "(CDT)", "UTC+1") is part of the value, not free text.
123
140
  */
124
- TimeStringCompleter.timeZone = /^\(?(?:[A-Z]{1,5}|UTC[+-]?\d*)\)?$/;
141
+ TimeStringCompleter.timeZone = /^\(?([A-Z]{1,5}|UTC[+-]?\d*)\)?$/;
125
142
  /**
126
143
  * A duration ("P10M", "~P1H"), or a range of durations ("P10M/12M").
127
144
  */
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@rr0/cms",
3
3
  "type": "module",
4
4
  "author": "Jérôme Beau <rr0@rr0.org> (https://rr0.org)",
5
- "version": "0.3.80",
5
+ "version": "0.3.82",
6
6
  "description": "RR0 Content Management System (CMS)",
7
7
  "exports": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
@@ -31,10 +31,10 @@
31
31
  "dependencies": {
32
32
  "@javarome/fileutil": "^0.3.9",
33
33
  "@rr0/common": "^1.1.5",
34
- "@rr0/data": "^0.3.47",
34
+ "@rr0/data": "^0.3.49",
35
35
  "@rr0/lang": "^0.1.13",
36
36
  "@rr0/place": "^0.5.4",
37
- "@rr0/time": "^1.0.0",
37
+ "@rr0/time": "^2.2.0",
38
38
  "canvas": "^3.2.1",
39
39
  "csv-parser": "^3.2.0",
40
40
  "glob": "^11.0.3",