Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758import moment from 'moment'import { isStrictDate, isValidDateTime } from './iso-temporal-value'
export interface TemporalFormats { readonly dateFormat: string; readonly timeFormat: string }export const DEFAULT_TEMPORAL_FORMATS: TemporalFormats = Object.freeze({ dateFormat: 'YYYY-MM-DD', timeFormat: 'HH:mm' })
/** ISO has a stable meaning. Other text is interpreted only by the current preference. */export function parseTemporalValue(value: unknown, kind: 'date' | 'datetime', formats: TemporalFormats = DEFAULT_TEMPORAL_FORMATS): string | undefined { if (typeof value !== 'string' || value.length > 1024) return undefined if (kind === 'date' && isStrictDate(value) || kind === 'datetime' && isValidDateTime(value)) return value // Never reinterpret a malformed ISO value using a permissive alternate pattern. if (/^\d{4}-\d{2}-\d{2}(?:$|T)/.test(value)) return undefined const format = kind === 'date' ? formats.dateFormat : `${formats.dateFormat} ${formats.timeFormat}` // A partial date pattern would let Moment invent the missing calendar parts. if (!['Y', 'M', 'D'].every(token => hasMomentToken(formats.dateFormat, token)) || format.length > 256) return undefined const parsed = moment.utc(value, format, 'en', true) if (!parsed.isValid() || parsed.locale('en').format(format) !== value) return undefined const iso = parsed.format(kind === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH:mm:ss.SSS[Z]') return (kind === 'date' ? isStrictDate(iso) : isValidDateTime(iso)) ? iso : undefined}
export function displayTemporalValue(value: unknown, kind: 'date' | 'datetime', formats: TemporalFormats = DEFAULT_TEMPORAL_FORMATS): string { const iso = parseTemporalValue(value, kind, formats) if (!iso) return typeof value === 'string' ? value : '' const parsed = kind === 'date' ? moment.utc(iso, 'YYYY-MM-DD', 'en', true) : moment.parseZone(iso) return parsed.locale('en').format(kind === 'date' ? formats.dateFormat : `${formats.dateFormat} ${formats.timeFormat}`)}
/** Editing a wall time retains an existing offset and hidden sub-minute precision. */export function temporalInputValue(value: string, kind: 'date' | 'datetime', formats: TemporalFormats, current?: string): string | undefined { const iso = parseTemporalValue(value, kind, formats) if (!iso || kind === 'date' || isValidDateTime(value)) return iso const previous = parseTemporalValue(current, 'datetime', formats) const zone = previous?.match(/(?:Z|[+-]\d{2}:\d{2})$/)?.[0] ?? 'Z' const parsed = moment.utc(iso) const before = previous ? moment.parseZone(previous) : undefined if (before && !hasMomentToken(formats.timeFormat, 'm')) parsed.minute(before.minute()) if (before && !hasMomentToken(formats.timeFormat, 's')) parsed.second(before.second()) if (before && !hasMomentToken(formats.timeFormat, 'S')) parsed.millisecond(before.millisecond()) const seconds = hasMomentToken(formats.timeFormat, 's') || /T\d{2}:\d{2}:\d{2}/.test(previous ?? '') const fraction = hasMomentToken(formats.timeFormat, 'S') || /\.\d+/.test(previous ?? '') return `${parsed.format(fraction ? 'YYYY-MM-DDTHH:mm:ss.SSS' : seconds ? 'YYYY-MM-DDTHH:mm:ss' : 'YYYY-MM-DDTHH:mm')}${zone}`}
/** Match actual Moment tokens while ignoring bracketed literals and escapes. */export function hasMomentToken(format: string, token: string): boolean { let escaped = false let bracketed = false for (const character of format) { if (escaped) { escaped = false; continue } if (character === '\\') { escaped = true; continue } if (character === '[') { bracketed = true; continue } if (character === ']') { bracketed = false; continue } if (!bracketed && character === token) return true } return false}