@rr0/cms 0.3.72 → 0.3.74
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/dist/tech/info/soft/APIFactory.js +2 -2
- package/dist/tech/info/soft/APIFactory.test.d.ts +1 -0
- package/dist/tech/info/soft/APIFactory.test.js +15 -0
- package/dist/time/SsiTitleReplaceCommand.d.ts +15 -0
- package/dist/time/SsiTitleReplaceCommand.js +27 -2
- package/dist/time/TimeContext.mjs +7 -2
- package/dist/time/TimeService.d.ts +4 -0
- package/dist/time/TimeService.js +14 -0
- package/dist/time/TitleReplaceCommand.test.js +21 -3
- package/dist/time/html/TimeElementFactory.d.ts +1 -1
- package/dist/time/html/TimeElementFactory.js +8 -1
- package/dist/time/html/TimeRenderer.d.ts +4 -0
- package/dist/time/html/TimeRenderer.js +1 -1
- package/dist/time/html/TimeReplacer.d.ts +7 -0
- package/dist/time/html/TimeReplacer.js +44 -4
- package/dist/time/html/TimeReplacer.test.js +78 -0
- package/dist/time/html/TimeStringCompleter.d.ts +55 -0
- package/dist/time/html/TimeStringCompleter.js +128 -0
- package/dist/time/text/RelativeTimeTextBuilder.d.ts +6 -1
- package/dist/time/text/RelativeTimeTextBuilder.js +22 -6
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@ export class APIFactory extends TypedDataFactory {
|
|
|
4
4
|
super(eventFactory, "api", ["index"]);
|
|
5
5
|
}
|
|
6
6
|
parse(apiJson) {
|
|
7
|
-
const events = apiJson.events.map(this.eventFactory.parse);
|
|
8
|
-
return { type: "api", id: apiJson.id, dirName: apiJson.dirName, url: apiJson.url, events };
|
|
7
|
+
const events = (apiJson.events || []).map(this.eventFactory.parse);
|
|
8
|
+
return { type: "api", id: apiJson.id, dirName: apiJson.dirName, title: apiJson.title, url: apiJson.url, events };
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { RR0EventFactory } from "@rr0/data";
|
|
3
|
+
import { APIFactory } from "./APIFactory.js";
|
|
4
|
+
describe("APIFactory", () => {
|
|
5
|
+
const factory = new APIFactory(new RR0EventFactory());
|
|
6
|
+
test("keeps the title, which becomes the page title", () => {
|
|
7
|
+
const api = factory.parse({ type: "api", title: "JNLP", url: "https://www.jcp.org/en/jsr/detail?id=56" });
|
|
8
|
+
expect(api.title).toBe("JNLP");
|
|
9
|
+
expect(api.url).toBe("https://www.jcp.org/en/jsr/detail?id=56");
|
|
10
|
+
});
|
|
11
|
+
test("accepts an api.json without events", () => {
|
|
12
|
+
const api = factory.parse({ type: "api", title: "JOGL" });
|
|
13
|
+
expect(api.events).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -6,10 +6,25 @@ import { HtmlRR0Context } from "../RR0Context.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare class SsiTitleReplaceCommand extends SsiEchoVarReplaceCommand {
|
|
8
8
|
protected defaultHandlers: StringContextHandler[];
|
|
9
|
+
protected static readonly setVarRegex: RegExp;
|
|
10
|
+
/**
|
|
11
|
+
* The title set by the file being processed, if any.
|
|
12
|
+
*/
|
|
13
|
+
protected setTitle: string | undefined;
|
|
9
14
|
/**
|
|
10
15
|
* @param defaultHandlers Will generate a title for a given context/file, if no title is found.
|
|
11
16
|
*/
|
|
12
17
|
constructor(defaultHandlers?: StringContextHandler[]);
|
|
18
|
+
/**
|
|
19
|
+
* Consumes a `<!--#set var="title" value="..." -->` directive first, as it would otherwise be read only after the
|
|
20
|
+
* title has been resolved (and then output as a second `<title>` inside the page's body).
|
|
21
|
+
*/
|
|
22
|
+
execute(context: HtmlRR0Context): Promise<void>;
|
|
13
23
|
protected createReplacer(context: HtmlRR0Context): Promise<RegexReplacer>;
|
|
14
24
|
protected getTitle(context: HtmlRR0Context): string;
|
|
25
|
+
/**
|
|
26
|
+
* @return The title implied by a file name: its directory's name for an index page (org/eu/fr/.../paris/index.html
|
|
27
|
+
* gives "Paris"), its own name otherwise.
|
|
28
|
+
*/
|
|
29
|
+
protected nameTitle(fileName: string): string;
|
|
15
30
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SsiEchoVarReplaceCommand } from "ssg-api";
|
|
2
|
+
import { StringUtil } from "../util/string/StringUtil.js";
|
|
3
|
+
import path from "path";
|
|
2
4
|
/**
|
|
3
5
|
* Replaces the SSI expression "<!--#echo var="title" -->" by the page's <title> content,
|
|
4
6
|
* with a link if there's a <meta name="url"> content.
|
|
@@ -11,6 +13,19 @@ export class SsiTitleReplaceCommand extends SsiEchoVarReplaceCommand {
|
|
|
11
13
|
super("title");
|
|
12
14
|
this.defaultHandlers = defaultHandlers;
|
|
13
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Consumes a `<!--#set var="title" value="..." -->` directive first, as it would otherwise be read only after the
|
|
18
|
+
* title has been resolved (and then output as a second `<title>` inside the page's body).
|
|
19
|
+
*/
|
|
20
|
+
async execute(context) {
|
|
21
|
+
const contents = context.file.contents;
|
|
22
|
+
const match = SsiTitleReplaceCommand.setVarRegex.exec(contents);
|
|
23
|
+
this.setTitle = match === null || match === void 0 ? void 0 : match[1];
|
|
24
|
+
if (match) {
|
|
25
|
+
context.file.contents = contents.replace(match[0], "");
|
|
26
|
+
}
|
|
27
|
+
await super.execute(context);
|
|
28
|
+
}
|
|
14
29
|
async createReplacer(context) {
|
|
15
30
|
return {
|
|
16
31
|
replace: (_match, ..._args) => {
|
|
@@ -22,13 +37,23 @@ export class SsiTitleReplaceCommand extends SsiEchoVarReplaceCommand {
|
|
|
22
37
|
};
|
|
23
38
|
}
|
|
24
39
|
getTitle(context) {
|
|
25
|
-
let title = context.file.title;
|
|
40
|
+
let title = context.file.title || this.setTitle;
|
|
26
41
|
if (!title) {
|
|
27
42
|
this.defaultHandlers.some(handle => !title && (title = handle(context)));
|
|
28
43
|
}
|
|
29
44
|
if (!title) {
|
|
30
|
-
title = context.file.name;
|
|
45
|
+
title = this.nameTitle(context.file.name);
|
|
31
46
|
}
|
|
32
47
|
return title;
|
|
33
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* @return The title implied by a file name: its directory's name for an index page (org/eu/fr/.../paris/index.html
|
|
51
|
+
* gives "Paris"), its own name otherwise.
|
|
52
|
+
*/
|
|
53
|
+
nameTitle(fileName) {
|
|
54
|
+
const baseName = path.basename(fileName, path.extname(fileName));
|
|
55
|
+
const name = /^index(_[a-z]{2})?$/.test(baseName) ? path.basename(path.dirname(fileName)) : baseName;
|
|
56
|
+
return StringUtil.camelToText(name);
|
|
57
|
+
}
|
|
34
58
|
}
|
|
59
|
+
SsiTitleReplaceCommand.setVarRegex = /<!--\s*#set\s+var="title"\s+value="(.+?)"\s*-->\s*/s;
|
|
@@ -287,10 +287,15 @@ export class TimeContext {
|
|
|
287
287
|
* @return {TimeContext}
|
|
288
288
|
*/
|
|
289
289
|
clone() {
|
|
290
|
-
var _a, _b, _c, _d, _e, _f;
|
|
290
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
291
291
|
const date = this.date;
|
|
292
292
|
const interval = this.interval;
|
|
293
|
-
|
|
293
|
+
const season = ((_a = date === null || date === void 0 ? void 0 : date.month) === null || _a === void 0 ? void 0 : _a.value) > 12; // "1954-23": EdtfDate's constructor rejects it, only its parser accepts it
|
|
294
|
+
const clone = new TimeContext((_b = date === null || date === void 0 ? void 0 : date.year) === null || _b === void 0 ? void 0 : _b.value, season ? undefined : (_c = date === null || date === void 0 ? void 0 : date.month) === null || _c === void 0 ? void 0 : _c.value, (_d = date === null || date === void 0 ? void 0 : date.day) === null || _d === void 0 ? void 0 : _d.value, (_e = date === null || date === void 0 ? void 0 : date.hour) === null || _e === void 0 ? void 0 : _e.value, (_f = date === null || date === void 0 ? void 0 : date.minute) === null || _f === void 0 ? void 0 : _f.value, (_g = date === null || date === void 0 ? void 0 : date.timeshift) === null || _g === void 0 ? void 0 : _g.value, this.approximate, this.approximateTime, interval === null || interval === void 0 ? void 0 : interval.from, interval === null || interval === void 0 ? void 0 : interval.to, this.duration);
|
|
295
|
+
if (season) {
|
|
296
|
+
clone.date = EdtfDate.fromString(date.toString());
|
|
297
|
+
}
|
|
298
|
+
return clone;
|
|
294
299
|
}
|
|
295
300
|
/**
|
|
296
301
|
*
|
|
@@ -7,6 +7,10 @@ export declare class TimeService extends AbstractDataService<RR0Event, RR0EventJ
|
|
|
7
7
|
protected options: TimeOptions;
|
|
8
8
|
readonly timePathRegex: RegExp;
|
|
9
9
|
static readonly defaultRegex: RegExp;
|
|
10
|
+
/**
|
|
11
|
+
* A year beyond 4 digits, which has its own directory (time/-10000/index.html).
|
|
12
|
+
*/
|
|
13
|
+
static readonly yearDirRegex: RegExp;
|
|
10
14
|
constructor(dataService: AllDataService, options: TimeOptions, timePathRegex?: RegExp);
|
|
11
15
|
parseFileName(fileName: string): RegExpExecArray | null;
|
|
12
16
|
titleFromFile(context: HtmlRR0Context, fileName: string, timeTextBuilder: TimeTextBuilder): string | undefined;
|
package/dist/time/TimeService.js
CHANGED
|
@@ -22,6 +22,16 @@ export class TimeService extends AbstractDataService {
|
|
|
22
22
|
}
|
|
23
23
|
contextFromFileName(context, fileName = context.file.name) {
|
|
24
24
|
let timeContext;
|
|
25
|
+
const yearDirExec = TimeService.yearDirRegex.exec(fileName);
|
|
26
|
+
if (yearDirExec) {
|
|
27
|
+
timeContext = context.clone().time;
|
|
28
|
+
timeContext.setYear(parseInt(yearDirExec[1], 10));
|
|
29
|
+
timeContext.setMonth(undefined);
|
|
30
|
+
timeContext.setDayOfMonth(undefined);
|
|
31
|
+
timeContext.setHour(undefined);
|
|
32
|
+
timeContext.setMinutes(undefined);
|
|
33
|
+
return timeContext;
|
|
34
|
+
}
|
|
25
35
|
let elems;
|
|
26
36
|
if (fileName.endsWith("index.html")) {
|
|
27
37
|
while ((elems = fileName.split("/")).length < 6) {
|
|
@@ -64,3 +74,7 @@ export class TimeService extends AbstractDataService {
|
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
TimeService.defaultRegex = /time\/(-)?(\d)\/(\d)\/(\d)\/(\d)\/?(\d{2})?\/?(\d{2})?\/?(index(_[a-z]{2})?.html)?/;
|
|
77
|
+
/**
|
|
78
|
+
* A year beyond 4 digits, which has its own directory (time/-10000/index.html).
|
|
79
|
+
*/
|
|
80
|
+
TimeService.yearDirRegex = /time\/(-?\d{5,})\/(index(_[a-z]{2})?\.html)?$/;
|
|
@@ -10,9 +10,14 @@ describe("TitleReplaceCommand", () => {
|
|
|
10
10
|
const command = new SsiTitleReplaceCommand();
|
|
11
11
|
const context = cmsTestUtil.newHtmlContext(fileName, `This is about <!--#echo var="title" -->!`);
|
|
12
12
|
await command.execute(context);
|
|
13
|
-
|
|
14
|
-
expect(context.file.
|
|
15
|
-
|
|
13
|
+
expect(context.file.title).toBe("4");
|
|
14
|
+
expect(context.file.contents).toBe(`<html><head><title>4</title></head><body>This is about 4!</body></html>`);
|
|
15
|
+
});
|
|
16
|
+
test("year beyond 4 digits", async () => {
|
|
17
|
+
const command = new SsiTitleReplaceCommand([timeDefaultHandler]);
|
|
18
|
+
const context = cmsTestUtil.newHtmlContext("time/-10000/index.html", `This is about <!--#echo var="title" -->!`);
|
|
19
|
+
await command.execute(context);
|
|
20
|
+
expect(context.file.title).toBe("10000 av. J.-C.");
|
|
16
21
|
});
|
|
17
22
|
test("default title with handler", async () => {
|
|
18
23
|
const command = new SsiTitleReplaceCommand([timeDefaultHandler]);
|
|
@@ -21,6 +26,13 @@ describe("TitleReplaceCommand", () => {
|
|
|
21
26
|
expect(context.file.title).toBe("1954");
|
|
22
27
|
expect(context.file.contents).toBe(`<html><head><title>1954</title></head><body>This is about 1954!</body></html>`);
|
|
23
28
|
});
|
|
29
|
+
test("title set by a SSI directive prevails over the default handler", async () => {
|
|
30
|
+
const command = new SsiTitleReplaceCommand([timeDefaultHandler]);
|
|
31
|
+
const context = cmsTestUtil.newHtmlContext(fileName, `<!--#set var="title" value="Les visiteurs du ciel" -->\nThis is about <!--#echo var="title" -->!`);
|
|
32
|
+
await command.execute(context);
|
|
33
|
+
expect(context.file.title).toBe("Les visiteurs du ciel");
|
|
34
|
+
expect(context.file.contents).toBe(`<html><head><title>Les visiteurs du ciel</title></head><body>This is about Les visiteurs du ciel!</body></html>`);
|
|
35
|
+
});
|
|
24
36
|
test("default month title with handler", async () => {
|
|
25
37
|
const command = new SsiTitleReplaceCommand([timeDefaultHandler]);
|
|
26
38
|
const context = cmsTestUtil.time.newHtmlContext("1/9/5/4/10/index.html", `This is about <!--#echo var="title" -->!`);
|
|
@@ -36,4 +48,10 @@ describe("TitleReplaceCommand", () => {
|
|
|
36
48
|
expect(context.file.contents).toBe(`<html><head><title>Vendredi 1 octobre 1954</title></head><body>This is about Vendredi 1 octobre 1954!</body></html>`);
|
|
37
49
|
});
|
|
38
50
|
});
|
|
51
|
+
test("directory name of an index page with no title", async () => {
|
|
52
|
+
const command = new SsiTitleReplaceCommand([timeDefaultHandler]);
|
|
53
|
+
const context = cmsTestUtil.newHtmlContext("org/eu/fr/cobeps/index.html", `This is about <!--#echo var="title" -->!`);
|
|
54
|
+
await command.execute(context);
|
|
55
|
+
expect(context.file.title).toBe("Cobeps");
|
|
56
|
+
});
|
|
39
57
|
});
|
|
@@ -7,7 +7,7 @@ export declare class TimeElementFactory {
|
|
|
7
7
|
readonly renderer: TimeRenderer;
|
|
8
8
|
constructor(renderer: TimeRenderer);
|
|
9
9
|
create(context: HtmlRR0Context, previousContext: HtmlRR0Context | undefined, options: TimeRenderOptions): HTMLElement | undefined;
|
|
10
|
-
|
|
10
|
+
createInterval(fromContext: HtmlRR0Context, toContext: HtmlRR0Context, previousContext: HtmlRR0Context, options: TimeRenderOptions): HTMLElement | undefined;
|
|
11
11
|
protected createStarting(fromContext: HtmlRR0Context, previousContext: HtmlRR0Context, options: TimeRenderOptions): HTMLElement | undefined;
|
|
12
12
|
protected valueReplacement(context: HtmlRR0Context, previousContext: RR0Context | undefined, options?: TimeRenderOptions): HTMLElement | undefined;
|
|
13
13
|
protected durationReplacement(context: HtmlRR0Context): HTMLTimeElement | undefined;
|
|
@@ -31,9 +31,16 @@ export class TimeElementFactory {
|
|
|
31
31
|
}
|
|
32
32
|
createInterval(fromContext, toContext, previousContext, options) {
|
|
33
33
|
let replacement;
|
|
34
|
+
options = { ...options, relativeWords: false }; // "the day after at 22 h" can't start or end an interval
|
|
34
35
|
const startReplacement = this.valueReplacement(fromContext, previousContext, options);
|
|
35
36
|
if (startReplacement) {
|
|
36
|
-
const
|
|
37
|
+
const from = fromContext.time;
|
|
38
|
+
const to = toContext.time;
|
|
39
|
+
const sameDay = from.getDayOfMonth() && from.getYear() === to.getYear() && from.getMonth() === to.getMonth()
|
|
40
|
+
&& from.getDayOfMonth() === to.getDayOfMonth();
|
|
41
|
+
const endReplacement = sameDay // "21:00/22:00" ends at "22 h", not at "Tuesday 22 May 1517 at 22 h"
|
|
42
|
+
? this.valueReplacement(toContext, fromContext, options)
|
|
43
|
+
: this.valueReplacement(toContext, previousContext, options);
|
|
37
44
|
if (endReplacement && endReplacement.outerHTML !== startReplacement.outerHTML) {
|
|
38
45
|
replacement = fromContext.file.document.createElement("span");
|
|
39
46
|
replacement.className = "time-interval";
|
|
@@ -9,6 +9,10 @@ export interface TimeRenderOptions {
|
|
|
9
9
|
* The text says "between" right before this interval, so it reads "between X and Y" rather than "between X to Y".
|
|
10
10
|
*/
|
|
11
11
|
between?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* If a wording like "the day after" may replace the time. Defaults to true.
|
|
14
|
+
*/
|
|
15
|
+
relativeWords?: boolean;
|
|
12
16
|
}
|
|
13
17
|
export declare class TimeRenderer {
|
|
14
18
|
readonly urlBuilder: TimeUrlBuilder;
|
|
@@ -23,7 +23,7 @@ export class TimeRenderer {
|
|
|
23
23
|
const date = time.date;
|
|
24
24
|
const absoluteTimeUrl = this.urlBuilder.fromEdtf(date);
|
|
25
25
|
const title = this.textBuilder.build(context, renderOptions);
|
|
26
|
-
const text = (previousContext ? this.relativeTextBuilder.build(previousContext, context) : undefined) || title;
|
|
26
|
+
const text = (previousContext ? this.relativeTextBuilder.build(previousContext, context, options.relativeWords !== false) : undefined) || title;
|
|
27
27
|
const file = context.file;
|
|
28
28
|
const currentFileName = file.name;
|
|
29
29
|
const doc = file.document;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { HtmlRR0Context } from "../../RR0Context.js";
|
|
2
2
|
import { DomReplacement } from "../DomReplacement.js";
|
|
3
3
|
import { TimeElementFactory } from "./TimeElementFactory.js";
|
|
4
|
+
import { TimeRenderOptions } from "./TimeRenderer.js";
|
|
5
|
+
import { TimeStringCompleter } from "./TimeStringCompleter.js";
|
|
4
6
|
/**
|
|
5
7
|
* Replaces a <time> tag.
|
|
6
8
|
*/
|
|
@@ -11,6 +13,7 @@ export declare class TimeReplacer implements DomReplacement<HtmlRR0Context, HTML
|
|
|
11
13
|
* ("between <strong><time>1989/1994</time></strong>").
|
|
12
14
|
*/
|
|
13
15
|
static readonly inlineTags: string[];
|
|
16
|
+
protected readonly completer: TimeStringCompleter;
|
|
14
17
|
constructor(factory: TimeElementFactory);
|
|
15
18
|
static resolvedTime(context: HtmlRR0Context, dateTime: string): HTMLTimeElement;
|
|
16
19
|
/**
|
|
@@ -18,4 +21,8 @@ export declare class TimeReplacer implements DomReplacement<HtmlRR0Context, HTML
|
|
|
18
21
|
*/
|
|
19
22
|
static precedingText(el: Element): string;
|
|
20
23
|
replacement(context: HtmlRR0Context, origEl: HTMLTimeElement): Promise<HTMLElement>;
|
|
24
|
+
/**
|
|
25
|
+
* Renders a range of durations ("P10M/12M") as "from 10 to 12 minutes".
|
|
26
|
+
*/
|
|
27
|
+
protected durationRange(context: HtmlRR0Context, previousContext: HtmlRR0Context | undefined, durations: RegExpExecArray, options: TimeRenderOptions): HTMLElement | undefined;
|
|
21
28
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ObjectUtils } from "@rr0/common";
|
|
2
|
+
import { TimeStringCompleter } from "./TimeStringCompleter.js";
|
|
2
3
|
/**
|
|
3
4
|
* Replaces a <time> tag.
|
|
4
5
|
*/
|
|
5
6
|
export class TimeReplacer {
|
|
6
7
|
constructor(factory) {
|
|
7
8
|
this.factory = factory;
|
|
9
|
+
this.completer = new TimeStringCompleter();
|
|
8
10
|
}
|
|
9
11
|
static resolvedTime(context, dateTime) {
|
|
10
12
|
const replacement = context.file.document.createElement("time");
|
|
@@ -30,10 +32,29 @@ export class TimeReplacer {
|
|
|
30
32
|
else {
|
|
31
33
|
const previousContext = origEl.dataset.context === "none" ? undefined : context.clone();
|
|
32
34
|
const timeStr = origEl.textContent;
|
|
33
|
-
const
|
|
34
|
-
const between = context.messages.context.time.between.test(TimeReplacer.precedingText(origEl));
|
|
35
|
+
const { prefix, value, suffix } = this.completer.split(timeStr);
|
|
36
|
+
const between = context.messages.context.time.between.test(TimeReplacer.precedingText(origEl) + prefix);
|
|
37
|
+
// A <time> already inside a link keeps that link: a link of its own would nest <a> in <a>, which the HTML
|
|
38
|
+
// parser splits, closing the enclosing elements (a note, a source) early.
|
|
39
|
+
const url = !origEl.closest("a");
|
|
40
|
+
const options = { url, contentOnly: true, between };
|
|
35
41
|
try {
|
|
36
|
-
|
|
42
|
+
const durations = /^(~?)P([^/]+)\/P?([^/]+)$/.exec(value.trim()); // "P10M/12M"
|
|
43
|
+
if (durations) {
|
|
44
|
+
replacement = this.durationRange(context, previousContext, durations, options);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const completed = this.completer.complete(value.trim(), context.time);
|
|
48
|
+
if (!this.completer.isInterpretable(completed)) {
|
|
49
|
+
context.warn("Could not interpret time", timeStr);
|
|
50
|
+
}
|
|
51
|
+
else if (this.completer.isDayless(completed)) {
|
|
52
|
+
context.warn("Could not resolve the day of time", timeStr);
|
|
53
|
+
}
|
|
54
|
+
else if (context.time.updateFromStr(completed)) {
|
|
55
|
+
replacement = this.factory.create(context, previousContext, options);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
37
58
|
}
|
|
38
59
|
catch (e) { // One unrenderable time must not stop the others of the page
|
|
39
60
|
context.warn("Could not render time", timeStr, e.message);
|
|
@@ -41,12 +62,31 @@ export class TimeReplacer {
|
|
|
41
62
|
}
|
|
42
63
|
if (!replacement) {
|
|
43
64
|
replacement = origEl;
|
|
44
|
-
|
|
65
|
+
}
|
|
66
|
+
else if (prefix || suffix) { // Keep the words written around the time ("vers", "le soir")
|
|
67
|
+
const wrapper = context.file.document.createElement("span");
|
|
68
|
+
wrapper.className = "time-described";
|
|
69
|
+
wrapper.append(prefix, replacement, suffix);
|
|
70
|
+
replacement = wrapper;
|
|
45
71
|
}
|
|
46
72
|
context.debug("\tReplacing time", origEl.outerHTML, "with", ObjectUtils.asSet(replacement).outerHTML);
|
|
47
73
|
}
|
|
48
74
|
return replacement;
|
|
49
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Renders a range of durations ("P10M/12M") as "from 10 to 12 minutes".
|
|
78
|
+
*/
|
|
79
|
+
durationRange(context, previousContext, durations, options) {
|
|
80
|
+
const [, approximate, from, to] = durations;
|
|
81
|
+
const fromContext = context.clone();
|
|
82
|
+
const toContext = context.clone();
|
|
83
|
+
let replacement;
|
|
84
|
+
if (fromContext.time.updateFromStr(`${approximate}P${from.toUpperCase()}`)
|
|
85
|
+
&& toContext.time.updateFromStr(`${approximate}P${to.toUpperCase()}`)) {
|
|
86
|
+
replacement = this.factory.createInterval(fromContext, toContext, previousContext, options);
|
|
87
|
+
}
|
|
88
|
+
return replacement;
|
|
89
|
+
}
|
|
50
90
|
}
|
|
51
91
|
/**
|
|
52
92
|
* Elements that don't break a sentence, so that the text before them is the text before the <time> they contain
|
|
@@ -282,4 +282,82 @@ describe("TimeReplacer", async () => {
|
|
|
282
282
|
test("with approximation", async () => {
|
|
283
283
|
});
|
|
284
284
|
});
|
|
285
|
+
describe("partial values", () => {
|
|
286
|
+
async function render(context, str) {
|
|
287
|
+
const timeEl = context.file.document.createElement("time");
|
|
288
|
+
timeEl.textContent = str;
|
|
289
|
+
const replacement = await replacer.replacement(context, timeEl);
|
|
290
|
+
return replacement.textContent;
|
|
291
|
+
}
|
|
292
|
+
test("day of the current month", async () => {
|
|
293
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
294
|
+
expect(await render(context, "19")).toBe("dimanche 19");
|
|
295
|
+
expect(context.time.toString()).toBe("1990-08-19");
|
|
296
|
+
});
|
|
297
|
+
test("hour of the current day", async () => {
|
|
298
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
299
|
+
await render(context, "12");
|
|
300
|
+
expect(await render(context, "21:00")).toBe("21 h");
|
|
301
|
+
expect(context.time.getDayOfMonth()).toBe(12);
|
|
302
|
+
expect(context.time.getHour()).toBe(21);
|
|
303
|
+
expect(await render(context, "23T~01:45")).toBe("jeudi 23 01:45");
|
|
304
|
+
expect(await render(context, "00:19/00:30")).toBe("00:19 à 00:30");
|
|
305
|
+
expect(context.time.getYear()).toBe(1990);
|
|
306
|
+
});
|
|
307
|
+
test("hour of an unknown day is not read as a year", async () => {
|
|
308
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/index.html", "");
|
|
309
|
+
expect(await render(context, "07:30")).toBe("07:30");
|
|
310
|
+
expect(context.time.getYear()).toBe(1990);
|
|
311
|
+
});
|
|
312
|
+
test("interval end relative to its start", async () => {
|
|
313
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
314
|
+
expect(await render(context, "1517-05-22 21:00/22:00")).toBe("mardi 22 mai 1517 à 21 h à 22 h");
|
|
315
|
+
expect(await render(context, "02 20:00/03 05:00")).toBe("mercredi 2 20 h à jeudi 3 05 h");
|
|
316
|
+
expect(await render(context, "27 23:00/03:00")).toBe("dimanche 27 23 h à lundi 28 03 h");
|
|
317
|
+
});
|
|
318
|
+
test("date without hour after an hour of the previous day", async () => {
|
|
319
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
320
|
+
await render(context, "1998-08-09 22:00");
|
|
321
|
+
expect(await render(context, "1998-08-10")).toBe("lundi 10");
|
|
322
|
+
});
|
|
323
|
+
test("range of durations", async () => {
|
|
324
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
325
|
+
expect(await render(context, "P10M/12M")).toBe("10 minutes à 12 minutes");
|
|
326
|
+
expect(await render(context, "P15S/20s")).toBe("15 secondes à 20 secondes");
|
|
327
|
+
});
|
|
328
|
+
test("words around the value are kept", async () => {
|
|
329
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
330
|
+
expect(await render(context, "1964-04-27Tle soir")).toBe("lundi 27 avril 1964 le soir");
|
|
331
|
+
expect(await render(context, "28Tpeu après minuit")).toBe("le lendemain peu après minuit");
|
|
332
|
+
expect(await render(context, "vers\u00a010:50")).toBe("vers\u00a010:50");
|
|
333
|
+
});
|
|
334
|
+
test("words are not read as a time", async () => {
|
|
335
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
336
|
+
expect(await render(context, "1 h après le coucher du soleil")).toBe("1 h après le coucher du soleil");
|
|
337
|
+
expect(await render(context, "12 mars 1977")).toBe("12 mars 1977");
|
|
338
|
+
expect(context.time.toString()).toBe("1990-08");
|
|
339
|
+
});
|
|
340
|
+
test("time zone is part of the value", async () => {
|
|
341
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
342
|
+
expect(await render(context, "1947-06-24 21:45PST")).toBe("mardi 24 juin 1947 à 21:45");
|
|
343
|
+
});
|
|
344
|
+
test("time inside a link gets no link of its own", async () => {
|
|
345
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
346
|
+
const doc = context.file.document;
|
|
347
|
+
const a = doc.createElement("a");
|
|
348
|
+
a.href = "/science/crypto/ufo/enquete/dossier/5Novembre/index.html";
|
|
349
|
+
const timeEl = doc.createElement("time");
|
|
350
|
+
timeEl.textContent = "2003";
|
|
351
|
+
a.append(timeEl);
|
|
352
|
+
doc.body.append(a);
|
|
353
|
+
const replacement = await replacer.replacement(context, timeEl);
|
|
354
|
+
expect(replacement.querySelector("a")).toBe(null);
|
|
355
|
+
expect(replacement.textContent).toBe("2003");
|
|
356
|
+
});
|
|
357
|
+
test("season does not break the next time", async () => {
|
|
358
|
+
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
359
|
+
expect(await render(context, "1990-21")).toBe("printemps 1990");
|
|
360
|
+
expect(await render(context, "-0033")).toBe("33 av. J.-C.");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
285
363
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { TimeContext } from "../TimeContext.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* A <time> string split into the time value itself and the free text around it ("vers 10:50", "03Tla nuit").
|
|
4
|
+
*/
|
|
5
|
+
export interface TimeStringParts {
|
|
6
|
+
prefix: string;
|
|
7
|
+
value: string;
|
|
8
|
+
suffix: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Prepares a <time> string for EDTF parsing.
|
|
12
|
+
*
|
|
13
|
+
* RR0 pages write times relative to the page and to the previous time: "19" is the 19th of the current month,
|
|
14
|
+
* "21:00" an hour of the current day, "1517-05-22 21:00/22:00" ends at 22:00 the same day. EDTF alone reads "19"
|
|
15
|
+
* as the year 19 and "07:30" as the year 7, so such partial values are completed from the current time context
|
|
16
|
+
* first. Free text written around the value ("le soir", "vers") is set apart so that it can be kept as is.
|
|
17
|
+
*/
|
|
18
|
+
export declare class TimeStringCompleter {
|
|
19
|
+
/**
|
|
20
|
+
* A trailing time zone ("PST", "(CDT)", "UTC+1") is part of the value, not free text.
|
|
21
|
+
*/
|
|
22
|
+
protected static readonly timeZone: RegExp;
|
|
23
|
+
/**
|
|
24
|
+
* A duration ("P10M", "~P1H"), or a range of durations ("P10M/12M").
|
|
25
|
+
*/
|
|
26
|
+
static readonly duration: RegExp;
|
|
27
|
+
split(str: string): TimeStringParts;
|
|
28
|
+
/**
|
|
29
|
+
* @param value A time value, possibly partial ("19", "21:00", "15 22:00/16 05:00").
|
|
30
|
+
* @param time The current time context, that the partial value is relative to.
|
|
31
|
+
* @return The completed value, or the value unchanged if it is not partial or cannot be completed.
|
|
32
|
+
*/
|
|
33
|
+
complete(value: string, time: TimeContext): string;
|
|
34
|
+
/**
|
|
35
|
+
* EDTF parsing reads "1 h après le coucher" as the year 1 and ignores the rest: words must not reach it.
|
|
36
|
+
*
|
|
37
|
+
* @return If the value only holds characters of a time value (and possibly a trailing time zone).
|
|
38
|
+
*/
|
|
39
|
+
isInterpretable(value: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* @return If the value still holds a time of an unknown day ("07:30", "12 05:45"), which EDTF would read as a year.
|
|
42
|
+
*/
|
|
43
|
+
isDayless(value: string): boolean;
|
|
44
|
+
protected completeOne(value: string, time: TimeContext): string;
|
|
45
|
+
protected isTime(value: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* An end hour before its start hour ("23:00/03:00") is on the next day.
|
|
48
|
+
*
|
|
49
|
+
* @param end The end, completed with the day of its start ("1981-10-27 03:00").
|
|
50
|
+
* @param start The start ("1981-10-27 23:00").
|
|
51
|
+
*/
|
|
52
|
+
protected dayAfterIfBefore(end: string, start: string): string;
|
|
53
|
+
protected year(year: number): string;
|
|
54
|
+
protected pad(n: number | string): string;
|
|
55
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { TimeContext } from "../TimeContext.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Prepares a <time> string for EDTF parsing.
|
|
4
|
+
*
|
|
5
|
+
* RR0 pages write times relative to the page and to the previous time: "19" is the 19th of the current month,
|
|
6
|
+
* "21:00" an hour of the current day, "1517-05-22 21:00/22:00" ends at 22:00 the same day. EDTF alone reads "19"
|
|
7
|
+
* as the year 19 and "07:30" as the year 7, so such partial values are completed from the current time context
|
|
8
|
+
* first. Free text written around the value ("le soir", "vers") is set apart so that it can be kept as is.
|
|
9
|
+
*/
|
|
10
|
+
export class TimeStringCompleter {
|
|
11
|
+
split(str) {
|
|
12
|
+
let prefix = "";
|
|
13
|
+
let value = str.replace(/[ \t\r\n]+/g, " ").trim(); // "1973-10-11\n 21:00/22:00"
|
|
14
|
+
let suffix = "";
|
|
15
|
+
if (!TimeStringCompleter.duration.test(value.trim())) {
|
|
16
|
+
const pre = /^([^\d~-]*[A-Za-zÀ-ÿ][^\d~]*?)(\s+)(~?\d.*)$/s.exec(value); // "vers 10:50", "Environ 00:30"
|
|
17
|
+
if (pre) {
|
|
18
|
+
prefix = pre[1] + pre[2];
|
|
19
|
+
value = pre[3];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const post = /^(.*?\d)(\s*T\s*|\s+)([^\d\s~].*)$/s.exec(value); // "03Tla nuit", "1976-08-03 le soir"
|
|
23
|
+
// 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
|
+
// month needs a T before its text ("18Tla nuit").
|
|
25
|
+
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];
|
|
29
|
+
}
|
|
30
|
+
return { prefix, value, suffix };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @param value A time value, possibly partial ("19", "21:00", "15 22:00/16 05:00").
|
|
34
|
+
* @param time The current time context, that the partial value is relative to.
|
|
35
|
+
* @return The completed value, or the value unchanged if it is not partial or cannot be completed.
|
|
36
|
+
*/
|
|
37
|
+
complete(value, time) {
|
|
38
|
+
const slash = value.indexOf("/");
|
|
39
|
+
if (slash > 0 && !TimeStringCompleter.duration.test(value)) {
|
|
40
|
+
const start = this.completeOne(value.substring(0, slash).trim(), time);
|
|
41
|
+
const startTime = new TimeContext();
|
|
42
|
+
if (!startTime.updateFromStr(start) || !startTime.date) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
let end = value.substring(slash + 1).trim();
|
|
46
|
+
const endMonth = /^\d{1,2}$/.exec(end);
|
|
47
|
+
if (endMonth && !startTime.getDayOfMonth() && startTime.getMonth()) { // "1989-05/06": May to June
|
|
48
|
+
end = `${this.year(startTime.getYear())}-${this.pad(end)}`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const completedEnd = this.completeOne(end, startTime);
|
|
52
|
+
end = completedEnd !== end && this.isTime(end) ? this.dayAfterIfBefore(completedEnd, start) : completedEnd;
|
|
53
|
+
}
|
|
54
|
+
return start + "/" + end;
|
|
55
|
+
}
|
|
56
|
+
return this.completeOne(value, time);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* EDTF parsing reads "1 h après le coucher" as the year 1 and ignores the rest: words must not reach it.
|
|
60
|
+
*
|
|
61
|
+
* @return If the value only holds characters of a time value (and possibly a trailing time zone).
|
|
62
|
+
*/
|
|
63
|
+
isInterpretable(value) {
|
|
64
|
+
const withoutZone = value.replace(/\s*\(?(?:[A-Z]{2,5}|UTC[+-]?\d*)\)?\s*(\/|$)/g, "$1"); // "01:00EST/03:00EST"
|
|
65
|
+
return /^[\d\s\-:/~?%.,+XTZPYMWDHS]+$/.test(withoutZone);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* @return If the value still holds a time of an unknown day ("07:30", "12 05:45"), which EDTF would read as a year.
|
|
69
|
+
*/
|
|
70
|
+
isDayless(value) {
|
|
71
|
+
return value.split("/").some(part => /^~?\d{1,2}[: T]\d/.test(part.trim()));
|
|
72
|
+
}
|
|
73
|
+
completeOne(value, time) {
|
|
74
|
+
var _a;
|
|
75
|
+
const year = time.getYear();
|
|
76
|
+
const month = time.getMonth();
|
|
77
|
+
const day = time.getDayOfMonth();
|
|
78
|
+
const knownMonth = year !== undefined && month >= 1 && month <= 12;
|
|
79
|
+
let m;
|
|
80
|
+
if ((m = /^(\d{1,2})$/.exec(value)) && knownMonth && Number(m[1]) <= 31) { // "19": a day of the month
|
|
81
|
+
return `${this.year(year)}-${this.pad(month)}-${this.pad(m[1])}`;
|
|
82
|
+
}
|
|
83
|
+
if ((m = /^(\d{1,2})[ T](~?\d{1,2}:\d{2}(?::\d{2})?.*)$/.exec(value)) && knownMonth && Number(m[1]) <= 31) {
|
|
84
|
+
return `${this.year(year)}-${this.pad(month)}-${this.pad(m[1])} ${m[2]}`; // "12 05:45", "23T~01:45"
|
|
85
|
+
}
|
|
86
|
+
if ((m = /^(\d{2})-(\d{2})( .+)?$/.exec(value)) && year !== undefined && Number(m[1]) <= 12) { // "02-11 02:00"
|
|
87
|
+
return `${this.year(year)}-${m[1]}-${m[2]}${(_a = m[3]) !== null && _a !== void 0 ? _a : ""}`;
|
|
88
|
+
}
|
|
89
|
+
if ((m = /^(~?\d{1,2}:\d{2}(?::\d{2})?)(.*)$/.exec(value)) && knownMonth && day) { // "21:00"
|
|
90
|
+
return `${this.year(year)}-${this.pad(month)}-${this.pad(day)} ${m[1]}${m[2]}`;
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
isTime(value) {
|
|
95
|
+
return /^~?\d{1,2}:\d{2}/.test(value);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* An end hour before its start hour ("23:00/03:00") is on the next day.
|
|
99
|
+
*
|
|
100
|
+
* @param end The end, completed with the day of its start ("1981-10-27 03:00").
|
|
101
|
+
* @param start The start ("1981-10-27 23:00").
|
|
102
|
+
*/
|
|
103
|
+
dayAfterIfBefore(end, start) {
|
|
104
|
+
const time = /^(-?\d{4})-(\d{2})-(\d{2}) (~?)(\d{1,2}:\d{2})(.*)$/.exec(end);
|
|
105
|
+
const startTime = /^-?\d{4}-\d{2}-\d{2}[ T]~?(\d{1,2}:\d{2})/.exec(start);
|
|
106
|
+
if (time && startTime && time[5].padStart(5, "0") < startTime[1].padStart(5, "0")) {
|
|
107
|
+
const next = new Date(Date.UTC(Number(time[1]), Number(time[2]) - 1, Number(time[3]) + 1));
|
|
108
|
+
next.setUTCFullYear(Number(time[1]), Number(time[2]) - 1, Number(time[3]) + 1); // Years before 100 too
|
|
109
|
+
const day = `${this.year(next.getUTCFullYear())}-${this.pad(next.getUTCMonth() + 1)}-${this.pad(next.getUTCDate())}`;
|
|
110
|
+
end = `${day} ${time[4]}${time[5]}${time[6]}`;
|
|
111
|
+
}
|
|
112
|
+
return end;
|
|
113
|
+
}
|
|
114
|
+
year(year) {
|
|
115
|
+
return (year < 0 ? "-" : "") + String(Math.abs(year)).padStart(4, "0");
|
|
116
|
+
}
|
|
117
|
+
pad(n) {
|
|
118
|
+
return String(n).padStart(2, "0");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A trailing time zone ("PST", "(CDT)", "UTC+1") is part of the value, not free text.
|
|
123
|
+
*/
|
|
124
|
+
TimeStringCompleter.timeZone = /^\(?(?:[A-Z]{1,5}|UTC[+-]?\d*)\)?$/;
|
|
125
|
+
/**
|
|
126
|
+
* A duration ("P10M", "~P1H"), or a range of durations ("P10M/12M").
|
|
127
|
+
*/
|
|
128
|
+
TimeStringCompleter.duration = /^~?P\d/;
|
|
@@ -3,5 +3,10 @@ import { RR0Context } from "../../RR0Context.js";
|
|
|
3
3
|
export declare class RelativeTimeTextBuilder {
|
|
4
4
|
protected timeTextBuilder: TimeTextBuilder;
|
|
5
5
|
constructor(timeTextBuilder: TimeTextBuilder);
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param oldContext The time to render relatively to.
|
|
8
|
+
* @param newContext The time to render.
|
|
9
|
+
* @param words If a wording like "the day after" may replace the time.
|
|
10
|
+
*/
|
|
11
|
+
build(oldContext: RR0Context, newContext: RR0Context, words?: boolean): string;
|
|
7
12
|
}
|
|
@@ -4,7 +4,12 @@ export class RelativeTimeTextBuilder {
|
|
|
4
4
|
constructor(timeTextBuilder) {
|
|
5
5
|
this.timeTextBuilder = timeTextBuilder;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @param oldContext The time to render relatively to.
|
|
9
|
+
* @param newContext The time to render.
|
|
10
|
+
* @param words If a wording like "the day after" may replace the time.
|
|
11
|
+
*/
|
|
12
|
+
build(oldContext, newContext, words = true) {
|
|
8
13
|
var _a, _b, _c, _d, _e;
|
|
9
14
|
let text;
|
|
10
15
|
const time = newContext.time;
|
|
@@ -34,7 +39,8 @@ export class RelativeTimeTextBuilder {
|
|
|
34
39
|
const deltaDurationMonth = (_b = deltaDuration.months) === null || _b === void 0 ? void 0 : _b.value;
|
|
35
40
|
const sameMonth = !deltaDurationMonth || deltaDurationMonth < 1;
|
|
36
41
|
const noMonth = !previousMonth;
|
|
37
|
-
|
|
42
|
+
// Calendar fields are compared too: 22:00 to the next midnight is less than a day, but another day
|
|
43
|
+
const shouldSetMonth = deltaDurationMonth || (noMonth || shouldSetYear) || month !== previousMonth;
|
|
38
44
|
if (shouldSetMonth) {
|
|
39
45
|
deltaTime.setMonth(month);
|
|
40
46
|
options.month = "long";
|
|
@@ -43,7 +49,8 @@ export class RelativeTimeTextBuilder {
|
|
|
43
49
|
const previousDay = previousTime.getDayOfMonth();
|
|
44
50
|
const dayOfMonthDelta = (_c = deltaDuration.days) === null || _c === void 0 ? void 0 : _c.value;
|
|
45
51
|
const sameDay = !dayOfMonthDelta || dayOfMonthDelta < 1;
|
|
46
|
-
const shouldSetDay = dayOfMonthDelta || (!sameDay && sameYear && sameMonth)
|
|
52
|
+
const shouldSetDay = dayOfMonthDelta || (!sameDay && sameYear && sameMonth) || shouldSetMonth
|
|
53
|
+
|| dayOfMonth !== previousDay;
|
|
47
54
|
const noDay = !previousDay;
|
|
48
55
|
if (shouldSetDay) {
|
|
49
56
|
deltaTime.setDayOfMonth(dayOfMonth);
|
|
@@ -52,7 +59,8 @@ export class RelativeTimeTextBuilder {
|
|
|
52
59
|
}
|
|
53
60
|
const hour = time.getHour();
|
|
54
61
|
const hourDelta = (_d = deltaDuration.hours) === null || _d === void 0 ? void 0 : _d.value;
|
|
55
|
-
const shouldSetHour = hourDelta || (hour && (noDay || (sameYear && sameMonth && sameDay))) || previousTime.getHour() !== time.getHour()
|
|
62
|
+
const shouldSetHour = hourDelta || (hour && (noDay || (sameYear && sameMonth && sameDay))) || previousTime.getHour() !== time.getHour()
|
|
63
|
+
|| (shouldSetDay && hour !== undefined); // "12 05:45" after "03 05:00" is not just "45"
|
|
56
64
|
if (shouldSetHour) {
|
|
57
65
|
deltaTime.setHour(hour);
|
|
58
66
|
options.hour = "2-digit";
|
|
@@ -60,8 +68,13 @@ export class RelativeTimeTextBuilder {
|
|
|
60
68
|
const minutes = time.getMinutes();
|
|
61
69
|
const minutesDelta = (_e = deltaDuration.minutes) === null || _e === void 0 ? void 0 : _e.value;
|
|
62
70
|
const sameHour = hourDelta === 0;
|
|
63
|
-
const shouldSetMinutes = minutesDelta || (minutes && (sameYear && sameMonth && sameDay && sameHour))
|
|
71
|
+
const shouldSetMinutes = minutesDelta || (minutes && (sameYear && sameMonth && sameDay && sameHour))
|
|
72
|
+
|| (shouldSetHour && minutes);
|
|
64
73
|
if (shouldSetMinutes) {
|
|
74
|
+
if (!shouldSetHour && hour !== undefined) { // Minutes alone ("19") don't tell a time, even at hour 0
|
|
75
|
+
deltaTime.setHour(hour);
|
|
76
|
+
options.hour = "2-digit";
|
|
77
|
+
}
|
|
65
78
|
deltaTime.setMinutes(minutes);
|
|
66
79
|
options.minute = "2-digit";
|
|
67
80
|
}
|
|
@@ -71,7 +84,10 @@ export class RelativeTimeTextBuilder {
|
|
|
71
84
|
// A relative wording ("the day after", "the month after"...) only makes sense against a previous time
|
|
72
85
|
// that is as precise: a month page (1947-07) is not "the day before" its first entry (1947-07-01).
|
|
73
86
|
const messages = newContext.messages.context.time.relative;
|
|
74
|
-
if (
|
|
87
|
+
if (!words) {
|
|
88
|
+
// Rendered in full
|
|
89
|
+
}
|
|
90
|
+
else if (dayOfMonthDelta && sameYear && sameMonth && !noDay) {
|
|
75
91
|
switch (dayOfMonthDelta) {
|
|
76
92
|
case -1:
|
|
77
93
|
text = messages.day.before;
|
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.
|
|
5
|
+
"version": "0.3.74",
|
|
6
6
|
"description": "RR0 Content Management System (CMS)",
|
|
7
7
|
"exports": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@rr0/data": "^0.3.43",
|
|
35
35
|
"@rr0/lang": "^0.1.13",
|
|
36
36
|
"@rr0/place": "^0.5.4",
|
|
37
|
-
"@rr0/time": "^0.11.
|
|
37
|
+
"@rr0/time": "^0.11.3",
|
|
38
38
|
"canvas": "^3.2.1",
|
|
39
39
|
"csv-parser": "^3.2.0",
|
|
40
40
|
"glob": "^11.0.3",
|