diff --git a/src/ast.ts b/src/ast.ts index fc20753..88f1786 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,3 +1,5 @@ +import { IntervalLabelTokenValue } from "./tokenizer"; + export type Workout = { name: string; author: string; @@ -5,9 +7,12 @@ export type Workout = { intervals: Interval[]; }; -export type Interval = { - type: "Warmup" | "Cooldown" | "Interval" | "Rest"; +export type IntervalData = { duration: number; power: { from: number; to: number }; cadence?: number; }; + +export type Interval = IntervalData & { + type: IntervalLabelTokenValue; +}; diff --git a/src/parser.ts b/src/parser.ts index c5d164d..1739a70 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,5 @@ -import { Interval, Workout } from "./ast"; -import { LabelTokenValue, Token } from "./tokenizer"; +import { Interval, IntervalData, Workout } from "./ast"; +import { isIntervalLabelTokenValue, Token } from "./tokenizer"; type Header = { name?: string; @@ -25,23 +25,17 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => { while (tokens[0]) { const token = tokens[0]; - if (token.type === "label" && token.value === LabelTokenValue.Name) { + if (token.type === "label" && token.value === "Name") { tokens.shift(); const [name, rest] = extractText(tokens); header.name = name; tokens = rest; - } else if ( - token.type === "label" && - token.value === LabelTokenValue.Author - ) { + } else if (token.type === "label" && token.value === "Author") { tokens.shift(); const [author, rest] = extractText(tokens); header.author = author; tokens = rest; - } else if ( - token.type === "label" && - token.value === LabelTokenValue.Description - ) { + } else if (token.type === "label" && token.value === "Description") { tokens.shift(); const [description, rest] = extractText(tokens); header.description = description; @@ -55,8 +49,60 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => { return [header, tokens]; }; +const parseIntervalParams = (tokens: Token[]): [IntervalData, Token[]] => { + const data: Partial = {}; + + while (tokens[0]) { + const token = tokens[0]; + if (token.type === "duration") { + data.duration = token.value; + tokens.shift(); + } else if (token.type === "cadence") { + data.cadence = token.value; + tokens.shift(); + } else if (token.type === "power") { + data.power = { from: token.value, to: token.value }; + tokens.shift(); + } else if (token.type === "power-range") { + data.power = { from: token.value[0], to: token.value[1] }; + tokens.shift(); + } else { + break; + } + } + + if (!("duration" in data)) { + throw new Error("Duration not specified"); + } + if (!("power" in data)) { + throw new Error("Power not specified"); + } + + return [data as IntervalData, tokens]; +}; + const parseIntervals = (tokens: Token[]): Interval[] => { - return []; + const intervals: Interval[] = []; + + while (tokens[0]) { + const token = tokens.shift() as Token; + if (token.type === "label" && isIntervalLabelTokenValue(token.value)) { + const [{ duration, power, cadence }, rest] = parseIntervalParams(tokens); + intervals.push({ + type: token.value, + duration, + power, + cadence, + }); + tokens = rest; + } else if (token.type === "text" && token.value === "") { + // Ignore empty lines + } else { + throw new Error(`Unexpected token [${token.type} ${token.value}]`); + } + } + + return intervals; }; export const parse = (tokens: Token[]): Workout => { diff --git a/src/tokenizer.ts b/src/tokenizer.ts index 6e2cb67..04e47bd 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -1,12 +1,25 @@ -export enum LabelTokenValue { - Name = "Name", - Author = "Author", - Description = "Description", - Warmup = "Warmup", - Rest = "Rest", - Interval = "Interval", - Cooldown = "Cooldown", -} +export type HeaderLabelTokenValue = "Name" | "Author" | "Description"; +export type IntervalLabelTokenValue = + | "Warmup" + | "Rest" + | "Interval" + | "Cooldown"; +export type LabelTokenValue = HeaderLabelTokenValue | IntervalLabelTokenValue; + +export const isHeaderLabelTokenValue = ( + value: string +): value is HeaderLabelTokenValue => { + return ["Name", "Author", "Description"].includes(value); +}; +export const isIntervalLabelTokenValue = ( + value: string +): value is IntervalLabelTokenValue => { + return ["Warmup", "Rest", "Interval", "Cooldown"].includes(value); +}; +export const isLabelTokenValue = (value: string): value is LabelTokenValue => { + return isHeaderLabelTokenValue(value) || isIntervalLabelTokenValue(value); +}; + export type LabelToken = { type: "label"; value: LabelTokenValue; @@ -53,15 +66,15 @@ const tokenizeValueParam = (text: string): Token => { const tokenizeParams = (type: LabelTokenValue, text: string): Token[] => { switch (type) { - case LabelTokenValue.Name: - case LabelTokenValue.Author: - case LabelTokenValue.Description: { + case "Name": + case "Author": + case "Description": { return [{ type: "text", value: text }]; } - case LabelTokenValue.Warmup: - case LabelTokenValue.Rest: - case LabelTokenValue.Interval: - case LabelTokenValue.Cooldown: + case "Warmup": + case "Rest": + case "Interval": + case "Cooldown": return text.split(" ").map(tokenizeValueParam); } }; @@ -71,7 +84,7 @@ const tokenizeRule = (line: string): Token[] => { if (!matches) { return [{ type: "text", value: line.trim() }]; } - if (!Object.keys(LabelTokenValue).includes(matches[1])) { + if (!isLabelTokenValue(matches[1])) { return [{ type: "text", value: line.trim() }]; }