1 | import { ReferenceWithTimezone, ParsingComponents, ParsingResult } from "./results";
|
2 | import { Component, ParsedResult, ParsingOption, ParsingReference } from "./types";
|
3 | import { AsyncDebugBlock, DebugHandler } from "./debugging";
|
4 | import ENDefaultConfiguration from "./locales/en/configuration";
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | export interface Configuration {
|
11 | parsers: Parser[];
|
12 | refiners: Refiner[];
|
13 | }
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | export interface Parser {
|
27 | pattern(context: ParsingContext): RegExp;
|
28 | extract(
|
29 | context: ParsingContext,
|
30 | match: RegExpMatchArray
|
31 | ): ParsingComponents | ParsingResult | { [c in Component]?: number } | null;
|
32 | }
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | export interface Refiner {
|
41 | refine: (context: ParsingContext, results: ParsingResult[]) => ParsingResult[];
|
42 | }
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | export class Chrono {
|
48 | parsers: Array<Parser>;
|
49 | refiners: Array<Refiner>;
|
50 |
|
51 | defaultConfig = new ENDefaultConfiguration();
|
52 |
|
53 | constructor(configuration?: Configuration) {
|
54 | configuration = configuration || this.defaultConfig.createCasualConfiguration();
|
55 | this.parsers = [...configuration.parsers];
|
56 | this.refiners = [...configuration.refiners];
|
57 | }
|
58 |
|
59 | |
60 |
|
61 |
|
62 | clone(): Chrono {
|
63 | return new Chrono({
|
64 | parsers: [...this.parsers],
|
65 | refiners: [...this.refiners],
|
66 | });
|
67 | }
|
68 |
|
69 | |
70 |
|
71 |
|
72 |
|
73 | parseDate(text: string, referenceDate?: ParsingReference | Date, option?: ParsingOption): Date | null {
|
74 | const results = this.parse(text, referenceDate, option);
|
75 | return results.length > 0 ? results[0].start.date() : null;
|
76 | }
|
77 |
|
78 | parse(text: string, referenceDate?: ParsingReference | Date, option?: ParsingOption): ParsedResult[] {
|
79 | const context = new ParsingContext(text, referenceDate, option);
|
80 |
|
81 | let results = [];
|
82 | this.parsers.forEach((parser) => {
|
83 | const parsedResults = Chrono.executeParser(context, parser);
|
84 | results = results.concat(parsedResults);
|
85 | });
|
86 |
|
87 | results.sort((a, b) => {
|
88 | return a.index - b.index;
|
89 | });
|
90 |
|
91 | this.refiners.forEach(function (refiner) {
|
92 | results = refiner.refine(context, results);
|
93 | });
|
94 |
|
95 | return results;
|
96 | }
|
97 |
|
98 | private static executeParser(context: ParsingContext, parser: Parser) {
|
99 | const results = [];
|
100 | const pattern = parser.pattern(context);
|
101 |
|
102 | const originalText = context.text;
|
103 | let remainingText = context.text;
|
104 | let match = pattern.exec(remainingText);
|
105 |
|
106 | while (match) {
|
107 |
|
108 | const index = match.index + originalText.length - remainingText.length;
|
109 | match.index = index;
|
110 |
|
111 | const result = parser.extract(context, match);
|
112 | if (!result) {
|
113 |
|
114 | remainingText = originalText.substring(match.index + 1);
|
115 | match = pattern.exec(remainingText);
|
116 | continue;
|
117 | }
|
118 |
|
119 | let parsedResult: ParsingResult = null;
|
120 | if (result instanceof ParsingResult) {
|
121 | parsedResult = result;
|
122 | } else if (result instanceof ParsingComponents) {
|
123 | parsedResult = context.createParsingResult(match.index, match[0]);
|
124 | parsedResult.start = result;
|
125 | } else {
|
126 | parsedResult = context.createParsingResult(match.index, match[0], result);
|
127 | }
|
128 |
|
129 | const parsedIndex = parsedResult.index;
|
130 | const parsedText = parsedResult.text;
|
131 | context.debug(() =>
|
132 | console.log(`${parser.constructor.name} extracted (at index=${parsedIndex}) '${parsedText}'`)
|
133 | );
|
134 |
|
135 | results.push(parsedResult);
|
136 | remainingText = originalText.substring(parsedIndex + parsedText.length);
|
137 | match = pattern.exec(remainingText);
|
138 | }
|
139 |
|
140 | return results;
|
141 | }
|
142 | }
|
143 |
|
144 | export class ParsingContext implements DebugHandler {
|
145 | readonly text: string;
|
146 | readonly option: ParsingOption;
|
147 | readonly reference: ReferenceWithTimezone;
|
148 |
|
149 | |
150 |
|
151 |
|
152 | readonly refDate: Date;
|
153 |
|
154 | constructor(text: string, refDate?: ParsingReference | Date, option?: ParsingOption) {
|
155 | this.text = text;
|
156 | this.reference = new ReferenceWithTimezone(refDate);
|
157 | this.option = option ?? {};
|
158 |
|
159 | this.refDate = this.reference.instant;
|
160 | }
|
161 |
|
162 | createParsingComponents(components?: { [c in Component]?: number } | ParsingComponents): ParsingComponents {
|
163 | if (components instanceof ParsingComponents) {
|
164 | return components;
|
165 | }
|
166 |
|
167 | return new ParsingComponents(this.reference, components);
|
168 | }
|
169 |
|
170 | createParsingResult(
|
171 | index: number,
|
172 | textOrEndIndex: number | string,
|
173 | startComponents?: { [c in Component]?: number } | ParsingComponents,
|
174 | endComponents?: { [c in Component]?: number } | ParsingComponents
|
175 | ): ParsingResult {
|
176 | const text = typeof textOrEndIndex === "string" ? textOrEndIndex : this.text.substring(index, textOrEndIndex);
|
177 |
|
178 | const start = startComponents ? this.createParsingComponents(startComponents) : null;
|
179 | const end = endComponents ? this.createParsingComponents(endComponents) : null;
|
180 |
|
181 | return new ParsingResult(this.reference, index, text, start, end);
|
182 | }
|
183 |
|
184 | debug(block: AsyncDebugBlock): void {
|
185 | if (this.option.debug) {
|
186 | if (this.option.debug instanceof Function) {
|
187 | this.option.debug(block);
|
188 | } else {
|
189 | const handler: DebugHandler = <DebugHandler>this.option.debug;
|
190 | handler.debug(block);
|
191 | }
|
192 | }
|
193 | }
|
194 | }
|