diff --git a/rr0.css b/rr0.css index d76b579ad2..5429f5fd70 100644 --- a/rr0.css +++ b/rr0.css @@ -659,7 +659,7 @@ a, details summary, .toggle, .note-id, .source-id { a:hover, details summary:hover, .toggle:hover, .indexed li:hover, form label:hover, .multilang:hover { border: none; border-radius: .2em; - background: rgba(184, 197, 212, .49) + background: rgba(184, 197, 212, .19) } .canular, .deprecated { diff --git a/source/Source.ts b/source/Source.ts index 09ef0fa601..1e80ae2e69 100644 --- a/source/Source.ts +++ b/source/Source.ts @@ -3,8 +3,8 @@ export type Publication = { time: string } -export abstract class Source { - protected constructor( +export class Source { + constructor( readonly title: string, readonly authors: string[], readonly publication: Publication, readonly subTitle: string = undefined, readonly series: string = undefined, readonly summary: string = undefined, readonly dirName: string = undefined diff --git a/time/RR0CaseRenderer.ts b/time/RR0CaseRenderer.ts index 1558a09073..1db0030f32 100644 --- a/time/RR0CaseRenderer.ts +++ b/time/RR0CaseRenderer.ts @@ -1,14 +1,37 @@ import { HtmlRR0SsgContext } from "../RR0SsgContext" -import { RR0CaseSummary } from "./RR0CaseSummary" import { Source } from "../source/Source" import { OnlineSource } from "../source/OnlineSource" +import { RR0CaseSummary } from "./datasource/rr0/RR0CaseSummary" +import { TimeTextBuilder } from "./TimeTextBuilder" export class RR0CaseRenderer { + render(context: HtmlRR0SsgContext, rr0Case: RR0CaseSummary): HTMLLIElement { + const outDoc = context.outputFile.document + const item = outDoc.createElement("li") + const time = rr0Case.time + const timeEl = outDoc.createElement("time") as HTMLTimeElement + timeEl.dateTime = time.toString() + const caseContext = context.clone() + Object.assign(caseContext, {time}) + timeEl.textContent = TimeTextBuilder.build(caseContext) + item.append(timeEl) + item.append(" À ") + const placeEl = outDoc.createElement("span") + placeEl.className = "place" + placeEl.textContent = rr0Case.place?.name || "" + item.append(placeEl, ", ", rr0Case.description) + rr0Case.sources.forEach(source => { + const sourceEl = this.thisSourceElement(context, source) + item.append(" ", sourceEl) + }) + return item + } + protected thisSourceElement(context: HtmlRR0SsgContext, source: Source) { const sourceEl = context.outputFile.document.createElement("span") sourceEl.className = "source" - sourceEl.innerHTML = source.authors.join(" & ") + `: ` + sourceEl.innerHTML = source.authors?.join(" & ") + `: ` const doc = context.outputFile.document if (source instanceof OnlineSource) { const onlineSource = source as OnlineSource @@ -20,26 +43,11 @@ export class RR0CaseRenderer { sourceEl.textContent = source.title } const copyright = doc.createElement("i") - copyright.textContent = source.publication.publisher - sourceEl.append(", ", copyright, ", ", source.publication.time) + const publication = source.publication + if (publication) { + copyright.textContent = publication.publisher + sourceEl.append(", ", copyright, ", ", publication.time) + } return sourceEl } - - render(context: HtmlRR0SsgContext, rr0Case: RR0CaseSummary): HTMLLIElement { - const outDoc = context.outputFile.document - const item = outDoc.createElement("li") - const timeEl = outDoc.createElement("time") - timeEl.textContent = rr0Case.time.toString() - item.append(timeEl) - item.append(" À ") - const placeEl = outDoc.createElement("span") - placeEl.className = "place" - placeEl.textContent = rr0Case.place.name - item.append(placeEl, ", ", rr0Case.description) - rr0Case.sources.forEach(source => { - const sourceEl = this.thisSourceElement(context, source) - item.append(" ", sourceEl) - }) - return item - } } diff --git a/time/TimeContext.ts b/time/TimeContext.ts index 5b92335c54..6e1f9f73f4 100644 --- a/time/TimeContext.ts +++ b/time/TimeContext.ts @@ -1,4 +1,3 @@ - /** * Time context for a RR0 page. */ @@ -116,4 +115,12 @@ export class TimeContext { protected isSet(value: any) { return value != void 0 && value != null } + + isBefore(other: TimeContext): boolean { + return this.toString().localeCompare(other.toString()) < 0 + } + + isAfter(other: TimeContext): boolean { + return this.toString().localeCompare(other.toString()) > 0 + } } diff --git a/time/TimeReplacer.ts b/time/TimeReplacer.ts index 4334e519e9..9c48c84a27 100644 --- a/time/TimeReplacer.ts +++ b/time/TimeReplacer.ts @@ -21,33 +21,12 @@ export class TimeReplacer implements DomReplacement { - if (i % 2 !== 0) { - reduced.push(current) - } - return reduced - }, []) - replacement = this.durationReplacement(context, daysStr, hoursStr, minutesStr, secondsStr, approximate) - } + return dateTimeValues.slice(1) } - return replacement + return undefined } static replaceElement( @@ -55,7 +34,6 @@ export class TimeReplacer implements DomReplacement { + if (i % 2 !== 0) { + reduced.push(current) + } + return reduced + }, []) + replacement = this.durationReplacement(context, daysStr, hoursStr, minutesStr, secondsStr, approximate) + } + } + return replacement + } + async replacement(context: HtmlRR0SsgContext, original: HTMLTimeElement): Promise { let replacement: HTMLElement | undefined if (original.dateTime) { // Already done? diff --git a/time/datasource/DatasourceTestCase.ts b/time/datasource/DatasourceTestCase.ts index dfd97353cd..aa89610eea 100644 --- a/time/datasource/DatasourceTestCase.ts +++ b/time/datasource/DatasourceTestCase.ts @@ -3,6 +3,7 @@ import { RR0CaseRenderer } from "../RR0CaseRenderer" import { RR0CaseMapping } from "./ChronologyReplacer" import { HtmlRR0SsgContext, RR0SsgContext } from "../../RR0SsgContext" import { TimeContext } from "../TimeContext" +import { TimeTextBuilder } from "../TimeTextBuilder" export class DatasourceTestCase { @@ -25,18 +26,22 @@ export class DatasourceTestCase { checkCaseHTML(context: HtmlRR0SsgContext, nativeCase: S, item: HTMLLIElement, dataDate: Date) { const datasource = this.mapping.datasource const expected = this.mapping.mapper.map(context, nativeCase, dataDate) - const nativeTime = this.getTime(nativeCase) - const dayOfMonth = nativeTime.getDayOfMonth() - const dateStr = `${nativeTime.getYear()}-${String(nativeTime.getMonth()).padStart(2, "0")}` - + (dayOfMonth ? "-" + String(dayOfMonth).padStart(2, "0") : "") - const hour = nativeTime.getHour() - const timeStr = hour ? " " + String(hour).padStart(2, "0") + ":" - + String(nativeTime.getMinutes()).padStart(2, "0") : "" + const time = this.getTime(nativeCase) + const caseContext = context.clone() + Object.assign(caseContext, {time}) + const timeStr = TimeTextBuilder.build(caseContext) const caseNumber = nativeCase.caseNumber - expect(item.innerHTML).toBe( - ` À ${expected.place.name}, ${expected.description} ${datasource.author}: ${expected.place.name}` : "" + let sources = expected.sources + if (sources?.length > 0) { + const source = sources[0] + const publicationStr = source.publication ? `, ${source.publication.time}` : "" + const sourceStr = ` ${datasource.author}: cas n° ${caseNumber}, ${datasource.copyright}, ${expected.sources[0].publication.time}`) + "&")}">cas n° ${caseNumber}, ${datasource.copyright}${publicationStr}` + expect(item.innerHTML).toBe( + `${placeStr}, ${expected.description}${sourceStr}`) + } } async testRender(context: HtmlRR0SsgContext) { diff --git a/time/datasource/rr0/RR0CaseSummary.ts b/time/datasource/rr0/RR0CaseSummary.ts index 87c1f2b1c3..3232aceb66 100644 --- a/time/datasource/rr0/RR0CaseSummary.ts +++ b/time/datasource/rr0/RR0CaseSummary.ts @@ -1,6 +1,6 @@ -import { Place } from "../place/Place" -import { TimeContext } from "./TimeContext" -import { Source } from "../source/Source" +import { Place } from "../../../place/Place" +import { TimeContext } from "../../TimeContext" +import { Source } from "../../../source/Source" export type NamedPlace = { readonly place: Place @@ -9,7 +9,7 @@ export type NamedPlace = { export type RR0CaseSummary = { readonly time: TimeContext - readonly place: NamedPlace + readonly place?: NamedPlace readonly description: string readonly sources: Source[] } diff --git a/time/datasource/rr0/RR0HttpDatasource.ts b/time/datasource/rr0/RR0HttpDatasource.ts index 5980714ee2..e52846f6fa 100644 --- a/time/datasource/rr0/RR0HttpDatasource.ts +++ b/time/datasource/rr0/RR0HttpDatasource.ts @@ -4,8 +4,10 @@ import { UrlUtil } from "../../../util/url/UrlUtil" import { JSDOM } from "jsdom" import { RR0Datasource } from "./RR0Datasource" import { TimeContext } from "../../TimeContext" -import { NamedPlace } from "./RR0CaseSummary" +import { NamedPlace, RR0CaseSummary } from "./RR0CaseSummary" import { Place } from "../../../place/Place" +import { Source } from "../../../source/Source" +import { TimeReplacer } from "../../TimeReplacer" export class RR0HttpDatasource extends RR0Datasource { protected readonly http = new HttpSource() @@ -48,32 +50,78 @@ export class RR0HttpDatasource extends RR0Datasource { const caseLink = context.inputFile.name const url = new URL(caseLink, this.baseUrl) const timeEl = row.querySelector("time") as HTMLTimeElement - let time: TimeContext + const itemContext = context.clone() + let timeContext = itemContext.time if (timeEl) { - const itemContext = context.clone() - time = itemContext.time - url.hash = time - const dateTime = new Date(timeEl.dateTime) - time.setYear(dateTime.getFullYear()) - time.setMonth(dateTime.getMonth() + 1) - time.setDayOfMonth(dateTime.getDate()) + url.hash = timeEl.dateTime + this.getTime(timeContext, timeEl) timeEl.remove() } let namedPlace: NamedPlace - const placeEl = row.querySelector(".place") + const placeEl = row.querySelector(".plac") if (placeEl) { - const name = placeEl.textContent - const place = new Place() - namedPlace = {name, place} - placeEl.remove() + namedPlace = this.getPlace(placeEl) + const toRemove = ["", " À ", " A ", ", "] + Array.from(row.childNodes).forEach(childNode => { + if (childNode.nodeType === 3 && toRemove.includes(childNode.nodeValue)) { + childNode.remove() + } + }) } - const description = row.textContent.trim().replaceAll("\n", "").replaceAll(/ /g, " ") - return { - url, - place: namedPlace, - time, - description + const sources = this.getSources(row) + const description = this.getDescription(row) + return {url, place: namedPlace, time: timeContext, description, sources} + } + + protected getSources(row: HTMLElement): Source[] { + const sources: Source[] = [] + const sourceIds = row.querySelectorAll(".source-id") + for (const sourceId of sourceIds) { + const sourceContent = sourceId.querySelector(".source-contents") + const title = this.getDescription(sourceContent) + sourceId.remove() + sources.push(new Source(title)) + } + return sources + } + + protected getTime(time: TimeContext, timeEl: HTMLTimeElement) { + const result = TimeReplacer.parseDateTime(timeEl.dateTime) + if (result) { + const [yearStr, monthStr, dayOfMonthStr, hour, minutes, timeZone] = result + time.setYear(parseInt(yearStr, 10)) + if (monthStr) { + time.setMonth(parseInt(monthStr, 10)) + } + if (dayOfMonthStr) { + time.setDayOfMonth(parseInt(dayOfMonthStr, 10)) + } + if (hour) { + time.setHour(parseInt(hour, 10)) + } + if (minutes) { + time.setMinutes(parseInt(minutes, 10)) + } + if (timeZone) { + time.setTimeZone(timeZone) + } + } + } + + protected getPlace(placeEl: HTMLElement): NamedPlace { + const name = placeEl.textContent + const place = new Place() + placeEl.remove() + return {name, place} + } + + protected getDescription(el: HTMLElement): string { + const notes = el.querySelectorAll(".note-id") + for (const note of notes) { + const noteContents = note.querySelector(".note-contents") + note.replaceWith(` (${noteContents.textContent})`) } + return el.textContent.trim().replaceAll("\n", "").replace(/\s{2,}/g, " ").replaceAll(" .", ".") } protected queryUrl(year: number, month: number, day: number): string { diff --git a/time/datasource/rr0/RR0Mapping.ts b/time/datasource/rr0/RR0Mapping.ts index a8e9709684..8cff44c6da 100644 --- a/time/datasource/rr0/RR0Mapping.ts +++ b/time/datasource/rr0/RR0Mapping.ts @@ -12,7 +12,7 @@ export const rr0Mapper = new RR0CaseSummaryMapper(cityService, rr0Datasource.bas export const rr0Mapping = {datasource: rr0Datasource, mapper: rr0Mapper} export const rr0SortComparator - = (c1: RR0CaseSummary, - c2: RR0CaseSummary) => c1.time < c2.time ? -1 : c1.time > c2.time ? 1 : 0 + = (c1: RR0CaseSummary, c2: RR0CaseSummary) => !c1.time || c2.time && c1.time.isBefore( + c2.time) ? -1 : !c2.time || c1.time.isAfter(c2.time) ? 1 : 0 export const rr0TimeAccessor = (c: RR0CaseSummary) => c.time diff --git a/time/datasource/rr0/RR0TestCases.ts b/time/datasource/rr0/RR0TestCases.ts index 9314b76747..a828de7b04 100644 --- a/time/datasource/rr0/RR0TestCases.ts +++ b/time/datasource/rr0/RR0TestCases.ts @@ -5,18 +5,37 @@ import { RR0CaseSummary } from "./RR0CaseSummary" import { UrlUtil } from "../../../util/url/UrlUtil" export const rr0TestCases: RR0CaseSummary[] = [ + { + url: new URL(UrlUtil.join(rr0Datasource.searchPath, "1/9/7/0/03/index.html"), rr0Datasource.baseUrl), + time: new TimeContext(rr0TestUtil.intlOptions, 1970, 3), + description: "L'armée de l'Air indique : Le BPE (Bureau Prospective et Etudes) s'assure qu'aucun des témoignages qui lui sont transmis, soit par les régions aériennes ou militaires, soit par la gendarmerie nationale, ne contienne des informations pouvant intéresser l'armée de l'air ou la mettre en cause. Transmet ces documents à monsieur Claude Poher, ingénieur du Cnes, habilité \"Secret Défense\", qui a été désigné par cet organisme pour suivre officiellement cette question. Reçoit du GEPA toutes les informations sur les ovnis dans le monde. Exploite les conclusions des travaux que Monsieur Poher transmet périodiquement au BPE et qui ont permis, entre autres, d'établir la fiche pour le ministre l'année dernière.", + sources: [{title: "Velasco, Jean-Jacques: 2004"}] + }, { url: new URL(UrlUtil.join(rr0Datasource.searchPath, "1/9/7/0/03/index.html#1970-03-04"), rr0Datasource.baseUrl), time: new TimeContext(rr0TestUtil.intlOptions, 1970, 3, 4), description: "Disparition du sous-marin français Eurydice au large de Saint-Tropez, avec 50 hommes à bord. Ce bâtiment était conçu pour la lutte contre les sous-marins à propulsion nucléaire et ne lança pas le moindre appel, alors qu'il était en parfait état de marche.", - place: undefined, sources: [] }, { url: new URL(UrlUtil.join(rr0Datasource.searchPath, "1/9/7/0/03/index.html#1970-03-07"), rr0Datasource.baseUrl), time: new TimeContext(rr0TestUtil.intlOptions, 1970, 3, 7), - description: "", - place: undefined, + description: "Le satellite ATS 3 prend la photo d'une éclipse totale qui montre l'ombre de la Lune sur les USA. Ce rond sombre de 160 km de diamètre se déplace du sud-ouest au nord à la vitesse de 2400 km/h.", sources: [] + }, + { + url: new URL(UrlUtil.join(rr0Datasource.searchPath, "1/9/7/0/03/index.html#1970-03-28%2023:00"), + rr0Datasource.baseUrl), + place: { + name: "Warminster", + place: {} + }, + time: new TimeContext(rr0TestUtil.intlOptions, 1970, 3, 28, 23, 0), + description: "canular de David I. Simpson visant à tester les ufologues. Le photographe dupera Charles Bowen qui, ayant une confiance aveugle en ce dernier, incitera également Pierre Guérin à valider à tort les 4 photos. La supercherie ne sera révélée qu'en mars 1976.", + sources: [ + {title: "Guérin, P.: FSR, vol. 16,n° 6, p.7-8."}, + {title: "Simpson, D. I.: Revue du MUFOB, nouvelle série, n° 2, 1976-03"}, + {title: "Guérin, P.: \"Quand les 'rationalistes' fabriquent de fausses photos d'ovnis\" in Ovni : les mécanismes d'une désinformation, Albin Michel, p. x-y"} + ] } ]