@domorium/validator 2.0.1 → 2.2.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to `@domorium/validator` are documented here.
4
4
 
5
+ ## 2.2.0
6
+
7
+ - **The lexer's mode stack no longer grows with the document.** Each line left
8
+ one or two frames behind until the lex ended — 3.56 million on a 50 MB export,
9
+ an estimated tens of megabytes at peak. Every line now returns the lexer to
10
+ where it began, so the stack is at most two deep. The tokens are unchanged
11
+ (#80).
12
+ - **`readDate` reads a date in a document checked against no dialect.** A file
13
+ whose header names no version, a Personal Ancestral File, or a version no
14
+ schema describes answered `null` for every date, so a consumer showing years
15
+ showed none for them. Such a document now reads a payload by the GEDCOM 5.5.1
16
+ grammar and, where that reads nothing, by the GEDCOM 7 one without `SCHMA`,
17
+ and answers `null` only where neither reads a possible date. Its diagnostics
18
+ are unchanged (#453).
19
+ - **Year 0 in the Gregorian or Julian calendar is reported** with the new code
20
+ `VAL016` (`ImpossibleYear`), in both dialects, and `readDate` answers `null`
21
+ for it. GEDCOM 7 states that neither calendar has a year 0; 5.5.1 is held to
22
+ the same rule. The year is judged as the grammar reads it, so `0BC` counts and
23
+ an extension calendar does not (#453).
24
+ - `GedcomDate.dualYear` documents that a date before the common era has none.
25
+
26
+ ## 2.1.0
27
+
28
+ - **`readDate(payload)` answers what a date says, not only whether it is
29
+ valid.** A consumer wanting the year of a birth had to parse the payload
30
+ again, and a regular expression that finds `1401` in `1401 BCE` or a year in
31
+ `ABT HEBREW 5761` hands on a wrong year as a fact. The document now returns
32
+ the parse its diagnostics apply: the modifier, each date with its calendar,
33
+ day, month, year and epoch, a 5.5.1 phrase and dual year, and the text. It
34
+ reads in the document's dialect, resolves a `SCHMA` alias of a month or a
35
+ calendar, and answers `null` for whatever the diagnostics reject as a date
36
+ value, including a day its month does not have (#378).
37
+
5
38
  ## 2.0.1
6
39
 
7
40
  - **A 5.5.1 date names its calendar where the specification puts it.** 5.5.1
package/README.md CHANGED
@@ -59,8 +59,58 @@ interface GedcomError {
59
59
  }
60
60
  ```
61
61
 
62
- `data` carries the facts a quick fix is built from — which xref failed to
63
- resolve, and what record tag it needed.
62
+ | Field | Type | What it holds |
63
+ | --------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
64
+ | `code` | `string`, a `GedcomErrorCode` | What kind of problem this is. Match on this. |
65
+ | `message` | `string` | Written for a person to read, and rewritten when a clearer wording is found. Do not match on it. |
66
+ | `hint` | `string?` | Declared for advice alongside the message. Nothing in this package sets it yet, so treat it as absent. |
67
+ | `data` | object? | The structured facts a quick fix is built from, where the diagnostic has any. |
68
+ | `range` | `Range` | Zero-based line and character, `end` exclusive, over the smallest text that is wrong — the payload, not the line. |
69
+ | `level` | `"error" \| "warning" \| "info"` | How much it matters. |
70
+
71
+ `data` is what makes a fix offerable rather than merely reportable: an
72
+ unresolved pointer reports the xref that failed and the record tag it needed, so
73
+ a host can offer to create that record.
74
+
75
+ For this file, where `SEX` carries a value its schema does not permit and `FAMC`
76
+ points at a record that is not there:
77
+
78
+ ```gedcom
79
+ 0 HEAD
80
+ 1 GEDC
81
+ 2 VERS 7.0
82
+ 0 @I1@ INDI
83
+ 1 NAME Lisa /Simpson/
84
+ 1 SEX Q
85
+ 1 FAMC @F9@
86
+ 0 TRLR
87
+ ```
88
+
89
+ `getErrors()` answers:
90
+
91
+ ```json
92
+ [
93
+ {
94
+ "code": "VAL005",
95
+ "message": "Value for SEX should be in set [F, M, U, X]",
96
+ "range": {
97
+ "start": { "line": 5, "character": 6 },
98
+ "end": { "line": 5, "character": 7 }
99
+ },
100
+ "level": "error"
101
+ },
102
+ {
103
+ "code": "unresolved-xref",
104
+ "message": "No FAM record carries @F9@",
105
+ "data": { "xref": "@F9@", "requiredRecordTag": "FAM" },
106
+ "range": {
107
+ "start": { "line": 6, "character": 7 },
108
+ "end": { "line": 6, "character": 11 }
109
+ },
110
+ "level": "error"
111
+ }
112
+ ]
113
+ ```
64
114
 
65
115
  Match on `code` rather than on `message`; messages are written for people and
66
116
  change. `GedcomErrorCode` is exported for this:
@@ -90,11 +140,79 @@ const unresolved = errors.filter(
90
140
  | `VAL013` | `SubstitutedVersion` | a version checked against a different version's schema |
91
141
  | `VAL014` | `ImpossibleDay` | a date naming a day its calendar does not have, such as `31 FEB` |
92
142
  | `VAL015` | `PersonalAncestralFile` | a header naming `SYST` before `GEDC`, so the file is a Personal Ancestral File |
143
+ | `VAL016` | `ImpossibleYear` | a Gregorian or Julian date naming year 0, which neither calendar numbers |
93
144
  | `unresolved-xref` | `UnresolvedXref` | an xref naming no record of the required type |
94
145
  | `invalid-level` | `InvalidLevel` | a level that cannot follow the line above it |
95
146
  | `LEXER` | `Lexer` | the text could not be tokenized |
96
147
  | `PARSER` | `Parser` | the tokens could not be assembled into a tree |
97
148
 
149
+ ## Reading the tree
150
+
151
+ `getNodes()` returns the records — the level-0 nodes — each holding its
152
+ substructures in `children`. A node carries its line's tokens in a `tokens` map
153
+ keyed by `TokenNames`, and which key holds the text is the thing to get right:
154
+
155
+ | Token | Written as | Holds |
156
+ | --------- | ------------- | -------------------------------------- |
157
+ | `LEVEL` | `1` | The level, as it was written. |
158
+ | `POINTER` | `0 @I1@ INDI` | The xref a record declares for itself. |
159
+ | `TAG` | `1 NAME` | The tag. |
160
+ | `XREF` | `1 FAMC @F1@` | An xref a line points at. |
161
+ | `VALUE` | `1 NAME Lisa` | A payload that is not a pointer. |
162
+
163
+ `POINTER` and `XREF` are the pair worth reading twice: a record's own xref is
164
+ never in `XREF`, and a pointer payload is never in `VALUE`.
165
+
166
+ Pulling every person's name out of a file:
167
+
168
+ ```typescript
169
+ import { GedcomDocument, TokenNames } from "@domorium/validator";
170
+ import type { ASTNode } from "@domorium/validator";
171
+
172
+ const document = new GedcomDocument().createDocument(gedcomString);
173
+
174
+ const payload = (node: ASTNode, tag: string): string | undefined =>
175
+ node.children.find((child) => child.tokens[TokenNames.TAG]?.value === tag)
176
+ ?.tokens[TokenNames.VALUE]?.value;
177
+
178
+ for (const record of document.getNodes()) {
179
+ if (record.tokens[TokenNames.TAG]?.value !== "INDI") continue;
180
+ console.log({
181
+ xref: record.tokens[TokenNames.POINTER]?.value,
182
+ name: payload(record, "NAME"),
183
+ line: record.range.start.line,
184
+ });
185
+ }
186
+ ```
187
+
188
+ ```text
189
+ { xref: '@I1@', name: 'Lisa /Simpson/', line: 3 }
190
+ ```
191
+
192
+ `range` on a node or a token is computed from the offsets on every access, not
193
+ stored: a large file has millions of tokens and a stored range costs three
194
+ objects apiece. Read it freely at the edges, and use `startOffset` and
195
+ `endOffset` inside a loop over the whole document.
196
+
197
+ ## What a document answers
198
+
199
+ Every method reads the document last given to `createDocument`, which returns
200
+ the same instance so the two can be chained.
201
+
202
+ | Method | Answers |
203
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
204
+ | `createDocument(text, options?)` | Parses and validates, and returns the document itself. |
205
+ | `getErrors()` | Every diagnostic, in the order found. |
206
+ | `getNodes()` | The records, each with its substructures. |
207
+ | `getVersion()` | The version read from `HEAD`.`GEDC`.`VERS`, as written. |
208
+ | `getDialect()` | Which rules were applied: `"5.5.1"` or `"7.0"`. |
209
+ | `getVersionResolution()` | How that was decided — `supported`, `substituted`, `unsupported`, `undetermined`, or `paf` for a file whose header names a writing program before its version. |
210
+ | `getLabel(node)` | The specification's name for a structure, for a UI to show instead of the tag: `HEAD` answers `Header`. |
211
+ | `getPointerTargetTag(node)` | The record tag a pointer line must name: `FAMC` answers `FAM`. |
212
+ | `isRecordDeclaration(node)` | Whether the node declares a record other lines can point at. |
213
+ | `getCompletions(position, lineText)` | What may be written at a position — `{ label, kind: "tag" \| "enum" \| "pointer", detail? }`. |
214
+ | `readDate(payload)` | What a date payload says, read in the document's dialect — or `null` where the diagnostics would reject it as a date value. A document checked against no dialect reads by 5.5.1, then 7.0. See `GedcomDateValue`. |
215
+
98
216
  ## Scripts
99
217
 
100
218
  | Command | Description |
@@ -3,6 +3,7 @@ import { ASTNode } from "../parser";
3
3
  import { type GedcomDialect, type VersionResolution } from "../validator/versionRegistry";
4
4
  import { type GedcomCompletion } from "../completion/completion";
5
5
  import type { Position } from "../types/position";
6
+ import type { GedcomDateValue } from "../types/date";
6
7
  export interface CreateDocumentOptions {
7
8
  /** Text that is part of a document rather than one of its own. */
8
9
  fragment?: boolean;
@@ -14,6 +15,7 @@ export declare class GedcomDocument {
14
15
  private pointers;
15
16
  private errors;
16
17
  private scheme;
18
+ private validatedAs;
17
19
  private resolution;
18
20
  private requiresSchmaDeclaration;
19
21
  private extensions;
@@ -28,6 +30,14 @@ export declare class GedcomDocument {
28
30
  getVersionResolution(): VersionResolution | undefined;
29
31
  getDialect(): GedcomDialect | undefined;
30
32
  getCompletions(position: Position, lineText: string): GedcomCompletion[];
33
+ /**
34
+ * Null wherever the document's own diagnostics would not call the payload a
35
+ * valid date value, and for one with nothing to read: an empty payload, or a
36
+ * 5.5.1 date in a calendar the schema gives no months. A document checked
37
+ * against no dialect reads by 5.5.1, then by 7.0 without its SCHMA, and is
38
+ * null only where neither reads a possible date.
39
+ */
40
+ readDate(value: string): GedcomDateValue | null;
31
41
  getErrors(): GedcomError[];
32
42
  getNodes(): ASTNode[];
33
43
  }