@astryxdesign/core 0.7.9-canary.4d7dca6 → 0.7.9-canary.52e65f3

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.
@@ -71,6 +71,8 @@ export interface CalendarDateLexicon {
71
71
  readonly eras: ReadonlyMap<string, string>;
72
72
  readonly yearAliases: ReadonlyMap<string, string>;
73
73
  readonly trailers: ReadonlySet<string>;
74
+ readonly yearMarkers: ReadonlySet<string>;
75
+ readonly separators: ReadonlySet<string>;
74
76
  readonly weekdays: ReadonlySet<string>;
75
77
  readonly vocabulary: ReadonlyArray<string>;
76
78
  readonly vocabularySet: ReadonlySet<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"calendarDateParser.d.ts","sourceRoot":"","sources":["../../src/utils/calendarDateParser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"calendarDateParser.d.ts","sourceRoot":"","sources":["../../src/utils/calendarDateParser.ts"],"names":[],"mappings":"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"}
@@ -291,6 +291,8 @@ function buildLexicon(locale, calendar) {
291
291
  const yearAliases = new Map();
292
292
  const skip = new Set();
293
293
  const trailers = new Set();
294
+ const yearMarkers = new Set();
295
+ const separators = new Set();
294
296
  const weekdays = new Set();
295
297
  const addSkip = (text, words = skip) => {
296
298
  for (const word of norm(text).replace(NOT_WORDS, ' ').split(/\s+/)) {
@@ -370,10 +372,13 @@ function buildLexicon(locale, calendar) {
370
372
  for (const options of literalFormats) {
371
373
  for (const pd of literalDates) {
372
374
  const parts = partsOf(loc, calendar, options, pd);
373
- for (const part of parts) {
375
+ for (const [k, part] of parts.entries()) {
374
376
  if (part.type === 'literal' || partType(part) === 'yearName') {
375
377
  addSkip(part.value);
376
378
  }
379
+ if (part.type === 'literal' && parts[k - 1]?.type === 'year') {
380
+ addSkip(part.value, yearMarkers);
381
+ }
377
382
  }
378
383
  if (loc === locale) {
379
384
  const tail = parts.at(-1);
@@ -405,6 +410,13 @@ function buildLexicon(locale, calendar) {
405
410
  }
406
411
  }
407
412
  }
413
+ const hint = displayOptions(locale, calendar, NUMERIC);
414
+ for (const part of partsOf(locale, calendar, hint, literalDates[0])) {
415
+ const separator = norm(part.value).replace(/\s+/g, '');
416
+ if (part.type === 'literal' && separator) {
417
+ separators.add(separator);
418
+ }
419
+ }
408
420
  if (LUNISOLAR.has(calendar)) {
409
421
  // Year names cycle every 60 years.
410
422
  for (let year = 1996; year < 2057; year++) {
@@ -496,6 +508,8 @@ function buildLexicon(locale, calendar) {
496
508
  eras,
497
509
  yearAliases,
498
510
  trailers,
511
+ yearMarkers,
512
+ separators,
499
513
  weekdays,
500
514
  vocabulary: [...vocabularySet].sort((a, b) => b.length - a.length),
501
515
  vocabularySet,
@@ -517,8 +531,9 @@ function calendarEraYear(locale, calendar, pd) {
517
531
  // Tokens
518
532
  // =============================================================================
519
533
 
520
- function tokenize(s, lex) {
534
+ function tokenize(s, lex, isDigitNameRead = true) {
521
535
  const out = [];
536
+ let hasDigitName = false;
522
537
  let i = 0;
523
538
  const end = s.trimEnd().length;
524
539
  while (i < end) {
@@ -533,6 +548,13 @@ function tokenize(s, lex) {
533
548
  i = template.end;
534
549
  continue;
535
550
  }
551
+ const digitName = isDigitNameRead && /\d/.test(ch) && matchWord(s, i, lex);
552
+ if (digitName) {
553
+ out.push(wordToken(digitName, lex));
554
+ hasDigitName = true;
555
+ i += digitName.length;
556
+ continue;
557
+ }
536
558
  if (/\d/.test(ch)) {
537
559
  let j = i;
538
560
  while (j < s.length && /\d/.test(s[j])) {
@@ -588,6 +610,17 @@ function tokenize(s, lex) {
588
610
  }
589
611
  return 'unreadable';
590
612
  }
613
+ // Latvian "1. adars" is a month, and also day 1 of "adars". It is the day
614
+ // when no other number can be the day. A number before the year word, as in
615
+ // "26. g.", is the year.
616
+ const canBeDay = (token, k) => {
617
+ const next = out.slice(k + 1).find(t => t.t !== 'sep');
618
+ const isYear = next?.t === 'word' && next.isYearMarker === true;
619
+ return token.t === 'num' && Number(token.v) <= 31 && !isYear;
620
+ };
621
+ if (hasDigitName && !out.some(canBeDay)) {
622
+ return tokenize(s, lex, false);
623
+ }
591
624
  return out;
592
625
  }
593
626
  function matchTemplate(s, i, lex) {
@@ -681,7 +714,8 @@ function wordToken(word, lex) {
681
714
  };
682
715
  }
683
716
  return {
684
- t: 'word'
717
+ t: 'word',
718
+ isYearMarker: lex.yearMarkers.has(word)
685
719
  };
686
720
  }
687
721
 
@@ -803,7 +837,7 @@ export function parseCalendarDate(text, lex, options = {}) {
803
837
  if (nums.length < 2) {
804
838
  return INCOMPLETE;
805
839
  }
806
- if (nums.length > 3 || !sameSeparators(tokens)) {
840
+ if (nums.length > 3 || !sameSeparators(tokens, lex)) {
807
841
  return UNREADABLE;
808
842
  }
809
843
  let order = nums.length === 2 ? lex.order.filter(field => field !== 'year') : lex.order;
@@ -912,7 +946,12 @@ function finish(date, isComplete, isDateDisabled) {
912
946
  isComplete
913
947
  };
914
948
  }
915
- function sameSeparators(tokens) {
949
+
950
+ /**
951
+ * Whether the numbers share one separator. Two separators of the locale's own
952
+ * numeric pattern also match, such as Hindi Buddhist "1/1`/2443".
953
+ */
954
+ function sameSeparators(tokens, lex) {
916
955
  let previous = null;
917
956
  let seenNumber = false;
918
957
  let pending = null;
@@ -920,7 +959,7 @@ function sameSeparators(tokens) {
920
959
  if (token.t === 'num') {
921
960
  if (seenNumber) {
922
961
  const separator = pending ?? '';
923
- if (previous != null && separator !== previous) {
962
+ if (previous != null && separator !== previous && !(lex.separators.has(separator) && lex.separators.has(previous))) {
924
963
  return false;
925
964
  }
926
965
  previous = separator;
@@ -1057,11 +1096,35 @@ function resolveDate(lex, fields) {
1057
1096
  // =============================================================================
1058
1097
 
1059
1098
  const displayOptionCache = new WeakMap();
1099
+ const eraChangeCache = new Map();
1100
+
1101
+ /**
1102
+ * Whether the era changes between 1900 and 2100. Only then can a year without
1103
+ * its era read as two dates.
1104
+ */
1105
+ function hasEraChange(calendar) {
1106
+ let result = eraChangeCache.get(calendar);
1107
+ if (result == null) {
1108
+ const eraOf = date => calendarEraYear('en', calendar, date).era;
1109
+ result = eraOf({
1110
+ year: 1900,
1111
+ month: 1,
1112
+ day: 1
1113
+ }) !== eraOf({
1114
+ year: 2100,
1115
+ month: 12,
1116
+ day: 31
1117
+ });
1118
+ eraChangeCache.set(calendar, result);
1119
+ }
1120
+ return result;
1121
+ }
1060
1122
 
1061
1123
  /**
1062
1124
  * Adds the era to a format that omits it, so a year never reads as a
1063
1125
  * Gregorian year. Keeps the locale's own pattern when the era would reorder
1064
- * the fields.
1126
+ * the fields. Where the era changes, it uses a narrow era that keeps the
1127
+ * order instead, such as Amharic Japanese "1/1/8 R".
1065
1128
  */
1066
1129
  function displayOptions(locale, calendar, options) {
1067
1130
  if (WITHOUT_ERA.has(calendar)) {
@@ -1082,14 +1145,19 @@ function displayOptions(locale, calendar, options) {
1082
1145
  };
1083
1146
  const parts = partsOf(locale, calendar, options, sample);
1084
1147
  result = options;
1148
+ const order = fieldOrder(parts).join();
1085
1149
  if (!parts.some(p => p.type === 'era')) {
1086
- const withEra = {
1087
- ...options,
1088
- era: 'short'
1089
- };
1090
- const eraParts = partsOf(locale, calendar, withEra, sample);
1091
- if (fieldOrder(parts).join() === fieldOrder(eraParts).join()) {
1092
- result = withEra;
1150
+ const widths = hasEraChange(calendar) ? ['short', 'narrow'] : ['short'];
1151
+ for (const era of widths) {
1152
+ const withEra = {
1153
+ ...options,
1154
+ era
1155
+ };
1156
+ const eraParts = partsOf(locale, calendar, withEra, sample);
1157
+ if (fieldOrder(eraParts).join() === order) {
1158
+ result = withEra;
1159
+ break;
1160
+ }
1093
1161
  }
1094
1162
  }
1095
1163
  byLocale.set(key, result);
@@ -1102,10 +1170,30 @@ function formatInCalendar(pd, options, locale, calendar) {
1102
1170
  relatedYear: true
1103
1171
  });
1104
1172
  }
1105
- return plainDateFormat(pd, {
1106
- ...displayOptions(locale, calendar, options),
1173
+ const display = displayOptions(locale, calendar, options);
1174
+ const text = plainDateFormat(pd, {
1175
+ ...display,
1107
1176
  calendar
1108
1177
  }, locale);
1178
+ // When every era width reorders the fields, such as the Amharic short-month
1179
+ // date, the text has no era. Text without an era reads in the latest era.
1180
+ // So a date of an earlier era adds the era. It goes at the start or the
1181
+ // end, as in the era format.
1182
+ const eraOf = date => calendarEraYear('en', calendar, date).era;
1183
+ if (!hasEraChange(calendar) || eraOf(pd) === eraOf({
1184
+ year: 9999,
1185
+ month: 12,
1186
+ day: 31
1187
+ }) || partsOf(locale, calendar, display, pd).some(p => p.type === 'era')) {
1188
+ return text;
1189
+ }
1190
+ const withEra = partsOf(locale, calendar, {
1191
+ ...options,
1192
+ era: 'short'
1193
+ }, pd);
1194
+ const at = withEra.findIndex(p => p.type === 'era');
1195
+ const era = withEra.slice(at === 0 ? 0 : at - 1, at === 0 ? 2 : at + 1).map(p => p.value).join('');
1196
+ return at === 0 ? era + text : text + era;
1109
1197
  }
1110
1198
 
1111
1199
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.9-canary.4d7dca6",
3
+ "version": "0.7.9-canary.52e65f3",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -880,9 +880,9 @@
880
880
  "react-dom": ">=19.0.0"
881
881
  },
882
882
  "devDependencies": {
883
- "@astryxdesign/a11y-spec": "0.7.9-canary.4d7dca6",
884
- "@astryxdesign/cli": "0.7.9-canary.4d7dca6",
885
- "@astryxdesign/interaction-harness": "0.7.9-canary.4d7dca6",
883
+ "@astryxdesign/a11y-spec": "0.7.9-canary.52e65f3",
884
+ "@astryxdesign/cli": "0.7.9-canary.52e65f3",
885
+ "@astryxdesign/interaction-harness": "0.7.9-canary.52e65f3",
886
886
  "@babel/cli": "^7.29.7",
887
887
  "@babel/core": "^7.29.7",
888
888
  "@babel/preset-react": "^7.29.7",
@@ -91,6 +91,8 @@ export interface CalendarDateLexicon {
91
91
  readonly eras: ReadonlyMap<string, string>;
92
92
  readonly yearAliases: ReadonlyMap<string, string>;
93
93
  readonly trailers: ReadonlySet<string>;
94
+ readonly yearMarkers: ReadonlySet<string>;
95
+ readonly separators: ReadonlySet<string>;
94
96
  readonly weekdays: ReadonlySet<string>;
95
97
  readonly vocabulary: ReadonlyArray<string>;
96
98
  readonly vocabularySet: ReadonlySet<string>;
@@ -352,6 +354,8 @@ function buildLexicon(
352
354
  const yearAliases = new Map<string, string>();
353
355
  const skip = new Set<string>();
354
356
  const trailers = new Set<string>();
357
+ const yearMarkers = new Set<string>();
358
+ const separators = new Set<string>();
355
359
  const weekdays = new Set<string>();
356
360
 
357
361
  const addSkip = (text: string, words = skip) => {
@@ -439,10 +443,13 @@ function buildLexicon(
439
443
  for (const options of literalFormats) {
440
444
  for (const pd of literalDates) {
441
445
  const parts = partsOf(loc, calendar, options, pd);
442
- for (const part of parts) {
446
+ for (const [k, part] of parts.entries()) {
443
447
  if (part.type === 'literal' || partType(part) === 'yearName') {
444
448
  addSkip(part.value);
445
449
  }
450
+ if (part.type === 'literal' && parts[k - 1]?.type === 'year') {
451
+ addSkip(part.value, yearMarkers);
452
+ }
446
453
  }
447
454
  if (loc === locale) {
448
455
  const tail = parts.at(-1);
@@ -472,6 +479,13 @@ function buildLexicon(
472
479
  }
473
480
  }
474
481
  }
482
+ const hint = displayOptions(locale, calendar, NUMERIC);
483
+ for (const part of partsOf(locale, calendar, hint, literalDates[0])) {
484
+ const separator = norm(part.value).replace(/\s+/g, '');
485
+ if (part.type === 'literal' && separator) {
486
+ separators.add(separator);
487
+ }
488
+ }
475
489
  if (LUNISOLAR.has(calendar)) {
476
490
  // Year names cycle every 60 years.
477
491
  for (let year = 1996; year < 2057; year++) {
@@ -576,6 +590,8 @@ function buildLexicon(
576
590
  eras,
577
591
  yearAliases,
578
592
  trailers,
593
+ yearMarkers,
594
+ separators,
579
595
  weekdays,
580
596
  vocabulary: [...vocabularySet].sort((a, b) => b.length - a.length),
581
597
  vocabularySet,
@@ -617,13 +633,18 @@ type Token =
617
633
  | {readonly t: 'monthNum'; readonly v: string; readonly isLeap: boolean}
618
634
  | {readonly t: 'era'; readonly code: string; readonly isWeekday?: boolean}
619
635
  | {readonly t: 'leap'}
620
- | {readonly t: 'word'}
636
+ | {readonly t: 'word'; readonly isYearMarker?: boolean}
621
637
  | {readonly t: 'sep'; readonly v: string};
622
638
 
623
639
  type Tokens = Token[] | 'incomplete' | 'unreadable';
624
640
 
625
- function tokenize(s: string, lex: CalendarDateLexicon): Tokens {
641
+ function tokenize(
642
+ s: string,
643
+ lex: CalendarDateLexicon,
644
+ isDigitNameRead = true,
645
+ ): Tokens {
626
646
  const out: Token[] = [];
647
+ let hasDigitName = false;
627
648
  let i = 0;
628
649
  const end = s.trimEnd().length;
629
650
  while (i < end) {
@@ -638,6 +659,13 @@ function tokenize(s: string, lex: CalendarDateLexicon): Tokens {
638
659
  i = template.end;
639
660
  continue;
640
661
  }
662
+ const digitName = isDigitNameRead && /\d/.test(ch) && matchWord(s, i, lex);
663
+ if (digitName) {
664
+ out.push(wordToken(digitName, lex));
665
+ hasDigitName = true;
666
+ i += digitName.length;
667
+ continue;
668
+ }
641
669
  if (/\d/.test(ch)) {
642
670
  let j = i;
643
671
  while (j < s.length && /\d/.test(s[j])) {
@@ -684,6 +712,17 @@ function tokenize(s: string, lex: CalendarDateLexicon): Tokens {
684
712
  }
685
713
  return 'unreadable';
686
714
  }
715
+ // Latvian "1. adars" is a month, and also day 1 of "adars". It is the day
716
+ // when no other number can be the day. A number before the year word, as in
717
+ // "26. g.", is the year.
718
+ const canBeDay = (token: Token, k: number) => {
719
+ const next = out.slice(k + 1).find(t => t.t !== 'sep');
720
+ const isYear = next?.t === 'word' && next.isYearMarker === true;
721
+ return token.t === 'num' && Number(token.v) <= 31 && !isYear;
722
+ };
723
+ if (hasDigitName && !out.some(canBeDay)) {
724
+ return tokenize(s, lex, false);
725
+ }
687
726
  return out;
688
727
  }
689
728
 
@@ -777,7 +816,7 @@ function wordToken(word: string, lex: CalendarDateLexicon): Token {
777
816
  if (alias != null) {
778
817
  return {t: 'num', v: alias, isAlias: true};
779
818
  }
780
- return {t: 'word'};
819
+ return {t: 'word', isYearMarker: lex.yearMarkers.has(word)};
781
820
  }
782
821
 
783
822
  /** The month a typed abbreviation names when it starts only one month name. */
@@ -913,7 +952,7 @@ export function parseCalendarDate(
913
952
  if (nums.length < 2) {
914
953
  return INCOMPLETE;
915
954
  }
916
- if (nums.length > 3 || !sameSeparators(tokens)) {
955
+ if (nums.length > 3 || !sameSeparators(tokens, lex)) {
917
956
  return UNREADABLE;
918
957
  }
919
958
  let order: ReadonlyArray<DateField> =
@@ -1022,7 +1061,11 @@ function finish(
1022
1061
  return {kind: 'value', date, isComplete};
1023
1062
  }
1024
1063
 
1025
- function sameSeparators(tokens: Token[]): boolean {
1064
+ /**
1065
+ * Whether the numbers share one separator. Two separators of the locale's own
1066
+ * numeric pattern also match, such as Hindi Buddhist "1/1`/2443".
1067
+ */
1068
+ function sameSeparators(tokens: Token[], lex: CalendarDateLexicon): boolean {
1026
1069
  let previous: string | null = null;
1027
1070
  let seenNumber = false;
1028
1071
  let pending: string | null = null;
@@ -1030,7 +1073,11 @@ function sameSeparators(tokens: Token[]): boolean {
1030
1073
  if (token.t === 'num') {
1031
1074
  if (seenNumber) {
1032
1075
  const separator = pending ?? '';
1033
- if (previous != null && separator !== previous) {
1076
+ if (
1077
+ previous != null &&
1078
+ separator !== previous &&
1079
+ !(lex.separators.has(separator) && lex.separators.has(previous))
1080
+ ) {
1034
1081
  return false;
1035
1082
  }
1036
1083
  previous = separator;
@@ -1195,10 +1242,30 @@ const displayOptionCache = new WeakMap<
1195
1242
  Map<string, Intl.DateTimeFormatOptions>
1196
1243
  >();
1197
1244
 
1245
+ const eraChangeCache = new Map<CalendarSystem, boolean>();
1246
+
1247
+ /**
1248
+ * Whether the era changes between 1900 and 2100. Only then can a year without
1249
+ * its era read as two dates.
1250
+ */
1251
+ function hasEraChange(calendar: CalendarSystem): boolean {
1252
+ let result = eraChangeCache.get(calendar);
1253
+ if (result == null) {
1254
+ const eraOf = (date: PlainDate) =>
1255
+ calendarEraYear('en', calendar, date).era;
1256
+ result =
1257
+ eraOf({year: 1900, month: 1, day: 1}) !==
1258
+ eraOf({year: 2100, month: 12, day: 31});
1259
+ eraChangeCache.set(calendar, result);
1260
+ }
1261
+ return result;
1262
+ }
1263
+
1198
1264
  /**
1199
1265
  * Adds the era to a format that omits it, so a year never reads as a
1200
1266
  * Gregorian year. Keeps the locale's own pattern when the era would reorder
1201
- * the fields.
1267
+ * the fields. Where the era changes, it uses a narrow era that keeps the
1268
+ * order instead, such as Amharic Japanese "1/1/8 R".
1202
1269
  */
1203
1270
  function displayOptions(
1204
1271
  locale: Locale,
@@ -1219,11 +1286,18 @@ function displayOptions(
1219
1286
  const sample = {year: 2026, month: 11, day: 2};
1220
1287
  const parts = partsOf(locale, calendar, options, sample);
1221
1288
  result = options;
1289
+ const order = fieldOrder(parts).join();
1222
1290
  if (!parts.some(p => p.type === 'era')) {
1223
- const withEra = {...options, era: 'short'} as const;
1224
- const eraParts = partsOf(locale, calendar, withEra, sample);
1225
- if (fieldOrder(parts).join() === fieldOrder(eraParts).join()) {
1226
- result = withEra;
1291
+ const widths = hasEraChange(calendar)
1292
+ ? (['short', 'narrow'] as const)
1293
+ : (['short'] as const);
1294
+ for (const era of widths) {
1295
+ const withEra = {...options, era};
1296
+ const eraParts = partsOf(locale, calendar, withEra, sample);
1297
+ if (fieldOrder(eraParts).join() === order) {
1298
+ result = withEra;
1299
+ break;
1300
+ }
1227
1301
  }
1228
1302
  }
1229
1303
  byLocale.set(key, result);
@@ -1242,11 +1316,27 @@ function formatInCalendar(
1242
1316
  relatedYear: true,
1243
1317
  });
1244
1318
  }
1245
- return plainDateFormat(
1246
- pd,
1247
- {...displayOptions(locale, calendar, options), calendar},
1248
- locale,
1249
- );
1319
+ const display = displayOptions(locale, calendar, options);
1320
+ const text = plainDateFormat(pd, {...display, calendar}, locale);
1321
+ // When every era width reorders the fields, such as the Amharic short-month
1322
+ // date, the text has no era. Text without an era reads in the latest era.
1323
+ // So a date of an earlier era adds the era. It goes at the start or the
1324
+ // end, as in the era format.
1325
+ const eraOf = (date: PlainDate) => calendarEraYear('en', calendar, date).era;
1326
+ if (
1327
+ !hasEraChange(calendar) ||
1328
+ eraOf(pd) === eraOf({year: 9999, month: 12, day: 31}) ||
1329
+ partsOf(locale, calendar, display, pd).some(p => p.type === 'era')
1330
+ ) {
1331
+ return text;
1332
+ }
1333
+ const withEra = partsOf(locale, calendar, {...options, era: 'short'}, pd);
1334
+ const at = withEra.findIndex(p => p.type === 'era');
1335
+ const era = withEra
1336
+ .slice(at === 0 ? 0 : at - 1, at === 0 ? 2 : at + 1)
1337
+ .map(p => p.value)
1338
+ .join('');
1339
+ return at === 0 ? era + text : text + era;
1250
1340
  }
1251
1341
 
1252
1342
  /**