@rr0/cms 0.3.73 → 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/TimeService.d.ts +4 -0
- package/dist/time/TimeService.js +14 -0
- package/dist/time/TitleReplaceCommand.test.js +21 -3
- package/dist/time/html/TimeReplacer.js +4 -1
- package/dist/time/html/TimeReplacer.test.js +13 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -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
|
});
|
|
@@ -34,7 +34,10 @@ export class TimeReplacer {
|
|
|
34
34
|
const timeStr = origEl.textContent;
|
|
35
35
|
const { prefix, value, suffix } = this.completer.split(timeStr);
|
|
36
36
|
const between = context.messages.context.time.between.test(TimeReplacer.precedingText(origEl) + prefix);
|
|
37
|
-
|
|
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 };
|
|
38
41
|
try {
|
|
39
42
|
const durations = /^(~?)P([^/]+)\/P?([^/]+)$/.exec(value.trim()); // "P10M/12M"
|
|
40
43
|
if (durations) {
|
|
@@ -341,6 +341,19 @@ describe("TimeReplacer", async () => {
|
|
|
341
341
|
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
342
342
|
expect(await render(context, "1947-06-24 21:45PST")).toBe("mardi 24 juin 1947 à 21:45");
|
|
343
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
|
+
});
|
|
344
357
|
test("season does not break the next time", async () => {
|
|
345
358
|
const context = cmsTestUtil.time.newHtmlContext("1/9/9/0/08/index.html", "");
|
|
346
359
|
expect(await render(context, "1990-21")).toBe("printemps 1990");
|
package/package.json
CHANGED