From 682264ccfb48a3a770ea0c3dd5bc405941557e1a Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Thu, 5 Dec 2024 16:59:03 -0500 Subject: [PATCH 01/10] fix: wrap Intl.<>() calls in try/catch --- src/relative-time-element.ts | 81 ++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 2130065..1b59ed4 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -120,14 +120,27 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor // // Returns a formatted time String. #getFormattedTitle(date: Date): string | undefined { - return new Intl.DateTimeFormat(this.#lang, { - day: 'numeric', - month: 'short', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - timeZoneName: 'short', - }).format(date) + let dateTimeFormat + try { + dateTimeFormat = new Intl.DateTimeFormat(this.#lang, { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + }) + } catch (_e) { + dateTimeFormat = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + }) + } + return dateTimeFormat.format(date) } #resolveFormat(duration: Duration): ResolvedFormat { @@ -172,10 +185,20 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor } #getRelativeFormat(duration: Duration): string { - const relativeFormat = new Intl.RelativeTimeFormat(this.#lang, { - numeric: 'auto', - style: this.formatStyle, - }) + let relativeFormat + + try { + relativeFormat = new Intl.RelativeTimeFormat(this.#lang, { + numeric: 'auto', + style: this.formatStyle, + }) + } catch (_e) { + relativeFormat = new Intl.RelativeTimeFormat('default', { + numeric: 'auto', + style: this.formatStyle, + }) + } + const tense = this.tense if (tense === 'future' && duration.sign !== 1) duration = emptyDuration if (tense === 'past' && duration.sign !== -1) duration = emptyDuration @@ -187,16 +210,30 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor } #getDateTimeFormat(date: Date): string { - const formatter = new Intl.DateTimeFormat(this.#lang, { - second: this.second, - minute: this.minute, - hour: this.hour, - weekday: this.weekday, - day: this.day, - month: this.month, - year: this.year, - timeZoneName: this.timeZoneName, - }) + let formatter + try { + formatter = new Intl.DateTimeFormat(this.#lang, { + second: this.second, + minute: this.minute, + hour: this.hour, + weekday: this.weekday, + day: this.day, + month: this.month, + year: this.year, + timeZoneName: this.timeZoneName, + }) + } catch (_e) { + formatter = new Intl.DateTimeFormat('default', { + second: this.second, + minute: this.minute, + hour: this.hour, + weekday: this.weekday, + day: this.day, + month: this.month, + year: this.year, + timeZoneName: this.timeZoneName, + }) + } return `${this.prefix} ${formatter.format(date)}`.trim() } From 0a42e68e491c1f68bdc0929c35e9725360460ec9 Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Fri, 6 Dec 2024 16:11:11 -0500 Subject: [PATCH 02/10] fix: add try/catch to lang getter --- src/relative-time-element.ts | 92 ++++++++++++------------------------ 1 file changed, 29 insertions(+), 63 deletions(-) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 1b59ed4..4eac55b 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -82,11 +82,13 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor #updating: false | Promise = false get #lang() { - return ( - this.closest('[lang]')?.getAttribute('lang') || - this.ownerDocument.documentElement.getAttribute('lang') || - 'default' - ) + const lang = this.closest('[lang]')?.getAttribute('lang') || + this.ownerDocument.documentElement.getAttribute('lang') + try { + return new Intl.Locale(lang ?? '').toString() + } catch { + return 'default' + } } #renderRoot: Node = this.shadowRoot ? this.shadowRoot : this.attachShadow ? this.attachShadow({mode: 'open'}) : this @@ -120,27 +122,14 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor // // Returns a formatted time String. #getFormattedTitle(date: Date): string | undefined { - let dateTimeFormat - try { - dateTimeFormat = new Intl.DateTimeFormat(this.#lang, { - day: 'numeric', - month: 'short', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - timeZoneName: 'short', - }) - } catch (_e) { - dateTimeFormat = new Intl.DateTimeFormat('default', { - day: 'numeric', - month: 'short', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - timeZoneName: 'short', - }) - } - return dateTimeFormat.format(date) + return new Intl.DateTimeFormat(this.#lang, { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + }).format(date) } #resolveFormat(duration: Duration): ResolvedFormat { @@ -185,19 +174,10 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor } #getRelativeFormat(duration: Duration): string { - let relativeFormat - - try { - relativeFormat = new Intl.RelativeTimeFormat(this.#lang, { - numeric: 'auto', - style: this.formatStyle, - }) - } catch (_e) { - relativeFormat = new Intl.RelativeTimeFormat('default', { - numeric: 'auto', - style: this.formatStyle, - }) - } + const relativeFormat = new Intl.RelativeTimeFormat(this.#lang, { + numeric: 'auto', + style: this.formatStyle, + }) const tense = this.tense if (tense === 'future' && duration.sign !== 1) duration = emptyDuration @@ -210,30 +190,16 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor } #getDateTimeFormat(date: Date): string { - let formatter - try { - formatter = new Intl.DateTimeFormat(this.#lang, { - second: this.second, - minute: this.minute, - hour: this.hour, - weekday: this.weekday, - day: this.day, - month: this.month, - year: this.year, - timeZoneName: this.timeZoneName, - }) - } catch (_e) { - formatter = new Intl.DateTimeFormat('default', { - second: this.second, - minute: this.minute, - hour: this.hour, - weekday: this.weekday, - day: this.day, - month: this.month, - year: this.year, - timeZoneName: this.timeZoneName, - }) - } + const formatter = new Intl.DateTimeFormat(this.#lang, { + second: this.second, + minute: this.minute, + hour: this.hour, + weekday: this.weekday, + day: this.day, + month: this.month, + year: this.year, + timeZoneName: this.timeZoneName, + }) return `${this.prefix} ${formatter.format(date)}`.trim() } From 2449b0c54c88a6492480a5056d755cd637436d22 Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Fri, 6 Dec 2024 16:11:45 -0500 Subject: [PATCH 03/10] fix: remove extra space --- src/relative-time-element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 4eac55b..24c5713 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -178,7 +178,6 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor numeric: 'auto', style: this.formatStyle, }) - const tense = this.tense if (tense === 'future' && duration.sign !== 1) duration = emptyDuration if (tense === 'past' && duration.sign !== -1) duration = emptyDuration From 048df51331840f2c1f1d7c8a4bbeb94bee71b78f Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Fri, 6 Dec 2024 16:40:30 -0500 Subject: [PATCH 04/10] fix: lint --- src/relative-time-element.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index 24c5713..068183d 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -82,8 +82,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor #updating: false | Promise = false get #lang() { - const lang = this.closest('[lang]')?.getAttribute('lang') || - this.ownerDocument.documentElement.getAttribute('lang') + const lang = this.closest('[lang]')?.getAttribute('lang') || this.ownerDocument.documentElement.getAttribute('lang') try { return new Intl.Locale(lang ?? '').toString() } catch { From b6136cc111a74e0765c444014b5bc2da842410e7 Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Mon, 9 Dec 2024 16:04:17 -0500 Subject: [PATCH 05/10] test: add relative-time test for invalid lang --- test/relative-time.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/relative-time.js b/test/relative-time.js index b00e592..3fb26d3 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -434,6 +434,18 @@ suite('relative-time', function () { }) } + test('renders correctly when given an invalid lang', async () => { + const now = new Date().toISOString() + + const element = document.createElement('relative-time') + element.setAttribute('datetime', now) + element.setAttribute('lang', 'does-not-exist') + assert.doesNotThrow(() => element.attributeChangedCallback('lang', null, null)) + + await Promise.resolve() + assert.equal(element.shadowRoot.textContent, 'now') + }) + suite('[tense=past]', function () { test('always uses relative dates', async () => { freezeTime(new Date(2033, 1, 1)) From b6dbae3ea0ef37057fe6fd80e93aebdb378f4030 Mon Sep 17 00:00:00 2001 From: Marie Lucca <40550942+francinelucca@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:14:20 -0500 Subject: [PATCH 06/10] Update test/relative-time.js Co-authored-by: Keith Cirkel --- test/relative-time.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/relative-time.js b/test/relative-time.js index 3fb26d3..0d13096 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -440,7 +440,6 @@ suite('relative-time', function () { const element = document.createElement('relative-time') element.setAttribute('datetime', now) element.setAttribute('lang', 'does-not-exist') - assert.doesNotThrow(() => element.attributeChangedCallback('lang', null, null)) await Promise.resolve() assert.equal(element.shadowRoot.textContent, 'now') From d37caedd64792abd8fb4026c3a6092880b83c9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Duy=20Quang?= Date: Mon, 30 Dec 2024 22:40:54 +0700 Subject: [PATCH 07/10] Make `applyDuration` reversible. --- src/duration.ts | 33 +++++++++++++++++++++++++++------ test/duration.ts | 19 +++++++++++-------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/duration.ts b/src/duration.ts index 74cb017..28c3bfe 100644 --- a/src/duration.ts +++ b/src/duration.ts @@ -84,14 +84,35 @@ export class Duration { } } +const durationApplicationActionsForward = [ + (r: Date, duration: Duration) => { + r.setUTCFullYear(r.getUTCFullYear() + duration.years) + }, + (r: Date, duration: Duration) => { + r.setUTCMonth(r.getUTCMonth() + duration.months) + }, + (r: Date, duration: Duration) => { + r.setUTCDate(r.getUTCDate() + duration.weeks * 7 + duration.days) + }, + (r: Date, duration: Duration) => { + r.setUTCHours(r.getUTCHours() + duration.hours) + }, + (r: Date, duration: Duration) => { + r.setUTCMinutes(r.getUTCMinutes() + duration.minutes) + }, + (r: Date, duration: Duration) => { + r.setUTCSeconds(r.getUTCSeconds() + duration.seconds) + }, +] +const durationApplicationActionsBackward = [...durationApplicationActionsForward].reverse() + export function applyDuration(date: Date | number, duration: Duration): Date { const r = new Date(date) - r.setFullYear(r.getFullYear() + duration.years) - r.setMonth(r.getMonth() + duration.months) - r.setDate(r.getDate() + duration.weeks * 7 + duration.days) - r.setHours(r.getHours() + duration.hours) - r.setMinutes(r.getMinutes() + duration.minutes) - r.setSeconds(r.getSeconds() + duration.seconds) + if (duration.sign < 0) { + for (const action of durationApplicationActionsBackward) action(r, duration) + } else { + for (const action of durationApplicationActionsForward) action(r, duration) + } return r } diff --git a/test/duration.ts b/test/duration.ts index 3b35600..ecb9e68 100644 --- a/test/duration.ts +++ b/test/duration.ts @@ -81,16 +81,19 @@ suite('duration', function () { }) suite('applyDuration', function () { - const referenceDate = '2022-10-21T16:48:44.104Z' const tests = new Set([ - {input: 'P4Y', expected: '2026-10-21T16:48:44.104Z'}, - {input: '-P4Y', expected: '2018-10-21T16:48:44.104Z'}, - {input: '-P3MT5M', expected: '2022-07-21T16:43:44.104Z'}, - {input: 'P1Y2M3DT4H5M6S', expected: '2023-12-24T20:53:50.104Z'}, - {input: 'P5W', expected: '2022-11-25T16:48:44.104Z'}, - {input: '-P5W', expected: '2022-09-16T16:48:44.104Z'}, + {referenceDate: '2022-10-21T16:48:44.104Z', input: 'P5W', expected: '2022-11-25T16:48:44.104Z'}, + {referenceDate: '2022-11-25T16:48:44.104Z', input: '-P5W', expected: '2022-10-21T16:48:44.104Z'}, + {referenceDate: '2022-07-21T16:43:44.104Z', input: 'P3MT5M', expected: '2022-10-21T16:48:44.104Z'}, + {referenceDate: '2022-10-21T16:48:44.104Z', input: '-P3MT5M', expected: '2022-07-21T16:43:44.104Z'}, + {referenceDate: '2022-10-21T16:48:44.104Z', input: 'P4Y', expected: '2026-10-21T16:48:44.104Z'}, + {referenceDate: '2026-10-21T16:48:44.104Z', input: '-P4Y', expected: '2022-10-21T16:48:44.104Z'}, + {referenceDate: '2022-10-21T16:48:44.104Z', input: 'P1Y2M3DT4H5M6S', expected: '2023-12-24T20:53:50.104Z'}, + {referenceDate: '2023-12-24T20:53:50.104Z', input: '-P1Y2M3DT4H5M6S', expected: '2022-10-21T16:48:44.104Z'}, + {referenceDate: '2023-08-15T00:00:00.000Z', input: 'P1Y3M25D', expected: '2024-12-10T00:00:00.000Z'}, + {referenceDate: '2024-12-10T00:00:00.000Z', input: '-P1Y3M25D', expected: '2023-08-15T00:00:00.000Z'}, ]) - for (const {input, expected} of tests) { + for (const {referenceDate, input, expected} of tests) { test(`${referenceDate} -> ${input} -> ${expected}`, () => { assert.equal(applyDuration(new Date(referenceDate), Duration.from(input))?.toISOString(), expected) }) From 5e4a7e304e8a821363e5bd46a418cbfc810499cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Duy=20Quang?= Date: Mon, 13 Jan 2025 15:16:11 +0700 Subject: [PATCH 08/10] Unrolled duration application steps. --- src/duration.ts | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/duration.ts b/src/duration.ts index 28c3bfe..58492b2 100644 --- a/src/duration.ts +++ b/src/duration.ts @@ -84,34 +84,22 @@ export class Duration { } } -const durationApplicationActionsForward = [ - (r: Date, duration: Duration) => { +export function applyDuration(date: Date | number, duration: Duration): Date { + const r = new Date(date) + if (duration.sign < 0) { + r.setUTCSeconds(r.getUTCSeconds() + duration.seconds) + r.setUTCMinutes(r.getUTCMinutes() + duration.minutes) + r.setUTCHours(r.getUTCHours() + duration.hours) + r.setUTCDate(r.getUTCDate() + duration.weeks * 7 + duration.days) + r.setUTCMonth(r.getUTCMonth() + duration.months) + r.setUTCFullYear(r.getUTCFullYear() + duration.years) + } else { r.setUTCFullYear(r.getUTCFullYear() + duration.years) - }, - (r: Date, duration: Duration) => { r.setUTCMonth(r.getUTCMonth() + duration.months) - }, - (r: Date, duration: Duration) => { r.setUTCDate(r.getUTCDate() + duration.weeks * 7 + duration.days) - }, - (r: Date, duration: Duration) => { r.setUTCHours(r.getUTCHours() + duration.hours) - }, - (r: Date, duration: Duration) => { r.setUTCMinutes(r.getUTCMinutes() + duration.minutes) - }, - (r: Date, duration: Duration) => { r.setUTCSeconds(r.getUTCSeconds() + duration.seconds) - }, -] -const durationApplicationActionsBackward = [...durationApplicationActionsForward].reverse() - -export function applyDuration(date: Date | number, duration: Duration): Date { - const r = new Date(date) - if (duration.sign < 0) { - for (const action of durationApplicationActionsBackward) action(r, duration) - } else { - for (const action of durationApplicationActionsForward) action(r, duration) } return r } From c518ee0e68d719793ba8b180415a25fa16d30f8b Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Mon, 13 Jan 2025 11:18:30 +0000 Subject: [PATCH 09/10] get main branch green --- test/relative-time.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/relative-time.js b/test/relative-time.js index 0d13096..c98a1ae 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -528,7 +528,7 @@ suite('relative-time', function () { time.setAttribute('datetime', datetime) time.setAttribute('format', 'micro') await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '10y') + assert.equal(time.shadowRoot.textContent, '11y') }) test('micro formats future times', async () => { From 4789b31e6147dea5425487ff1810a0760c874dbf Mon Sep 17 00:00:00 2001 From: Cameron Dutro Date: Mon, 13 Jan 2025 10:38:45 -0800 Subject: [PATCH 10/10] Use node v22 --- .github/workflows/publish.yml | 8 ++++---- .github/workflows/test.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 20bebdd..26e8fd5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 22 registry-url: https://registry.npmjs.org/ cache: npm - run: npm install -g npm@latest @@ -29,15 +29,15 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.npm_token}} publish-github: name: Publish to GitHub Packages - runs-on: ubuntu-latest - permissions: + runs-on: ubuntu-latest + permissions: contents: read packages: write steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 22 registry-url: https://npm.pkg.github.com cache: npm scope: '@github' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20ee438..7a23343 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 22 - name: npm install, build, and test run: | npm it