diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index 9bf5265..d0a3692 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -555,17 +555,13 @@ Rest: 5:00 50% `); }); - it("parses intervals with negative comment offsets", () => { + it("parses last comment with negative offset", () => { expect( parse(` Name: My Workout Interval: 10:00 90% @ 0:10 Find your rythm. @ -0:10 Final push. YOU GOT IT! - -Rest: 5:00 50% - @ -4:30 Great effort! - @ -2:00 Cool down well after all of this. `), ).toMatchInlineSnapshot(` Object { @@ -604,37 +600,120 @@ Rest: 5:00 50% }, "type": "Interval", }, + ], + "name": "My Workout", + "tags": Array [], + } + `); + }); + + it("parses comment with negative offset before absolutely offset comment", () => { + expect( + parse(` +Name: My Workout +Interval: 1:00 90% + @ -0:10 Before last + @ 0:50 Last! +`), + ).toMatchInlineSnapshot(` + Object { + "author": "", + "description": "", + "intervals": Array [ Object { "cadence": undefined, "comments": Array [ Object { "loc": Object { "col": 4, - "row": 7, + "row": 3, }, "offset": Duration { - "seconds": 30, + "seconds": 40, }, - "text": "Great effort!", + "text": "Before last", }, Object { "loc": Object { "col": 4, - "row": 8, + "row": 4, }, "offset": Duration { - "seconds": 180, + "seconds": 50, }, - "text": "Cool down well after all of this.", + "text": "Last!", }, ], "duration": Duration { - "seconds": 300, + "seconds": 60, }, "intensity": ConstantIntensity { - "_value": 0.5, + "_value": 0.9, }, - "type": "Rest", + "type": "Interval", + }, + ], + "name": "My Workout", + "tags": Array [], + } + `); + }); + + it("parses multiple comments with negative offsets in row", () => { + expect( + parse(` +Name: My Workout +Interval: 1:00 90% + @ -0:10 One more before last + @ -0:10 Before last + @ -0:10 Last! +`), + ).toMatchInlineSnapshot(` + Object { + "author": "", + "description": "", + "intervals": Array [ + Object { + "cadence": undefined, + "comments": Array [ + Object { + "loc": Object { + "col": 4, + "row": 3, + }, + "offset": Duration { + "seconds": 30, + }, + "text": "One more before last", + }, + Object { + "loc": Object { + "col": 4, + "row": 4, + }, + "offset": Duration { + "seconds": 40, + }, + "text": "Before last", + }, + Object { + "loc": Object { + "col": 4, + "row": 5, + }, + "offset": Duration { + "seconds": 50, + }, + "text": "Last!", + }, + ], + "duration": Duration { + "seconds": 60, + }, + "intensity": ConstantIntensity { + "_value": 0.9, + }, + "type": "Interval", }, ], "name": "My Workout", @@ -780,6 +859,70 @@ Interval: 10:00 90% `); }); + it("throws error when negative offset is followed by positive offset", () => { + expect(() => + parse(` +Name: My Workout +Interval: 2:00 90% + @ -0:10 Comment 1 + @ +0:30 Comment 2 + @ 1:30 Comment 3 +`), + ).toThrowErrorMatchingInlineSnapshot(`"Negative offset followed by positive offset at line 5 char 5"`); + }); + + it("works fine when positive offset is followed by negative offset", () => { + expect( + parse(` +Name: My Workout +Interval: 1:00 90% + @ +0:10 Comment 1 + @ -0:10 Comment 2 +`), + ).toMatchInlineSnapshot(` + Object { + "author": "", + "description": "", + "intervals": Array [ + Object { + "cadence": undefined, + "comments": Array [ + Object { + "loc": Object { + "col": 4, + "row": 3, + }, + "offset": Duration { + "seconds": 10, + }, + "text": "Comment 1", + }, + Object { + "loc": Object { + "col": 4, + "row": 4, + }, + "offset": Duration { + "seconds": 50, + }, + "text": "Comment 2", + }, + ], + "duration": Duration { + "seconds": 60, + }, + "intensity": ConstantIntensity { + "_value": 0.9, + }, + "type": "Interval", + }, + ], + "name": "My Workout", + "tags": Array [], + } + `); + }); + it("throws error when comment offset is outside of interval length", () => { expect(() => parse(` diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 34954e0..bb886ed 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -58,8 +58,14 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => { return [header, tokens]; }; +type PartialComment = { + offsetToken: OffsetToken; + text: string; + loc: SourceLocation; +}; + const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Comment[], Token[]] => { - const comments: Comment[] = []; + const comments: PartialComment[] = []; while (tokens[0]) { const [start, offset, text, ...rest] = tokens; if (start.type === "comment-start") { @@ -73,7 +79,7 @@ const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Co throw new ParseError(`Expected [comment text] instead got ${tokenToString(text)}`, text?.loc || offset.loc); } comments.push({ - offset: absoluteOffset(offset, intervalDuration, last(comments)), + offsetToken: offset, text: text.value, loc: offset.loc, }); @@ -82,16 +88,54 @@ const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Co break; } } - return [comments, tokens]; + + return [computeAbsoluteOffsets(comments, intervalDuration), tokens]; }; -const absoluteOffset = (offset: OffsetToken, intervalDuration: Duration, previousComment?: Comment): Duration => { - if (offset.kind === "relative-minus") { - return new Duration(intervalDuration.seconds - offset.value); - } else if (offset.kind === "relative-plus" && previousComment) { - return new Duration(previousComment.offset.seconds + offset.value); - } else { - return new Duration(offset.value); +const computeAbsoluteOffsets = (partialComments: PartialComment[], intervalDuration: Duration): Comment[] => { + const comments: Comment[] = []; + for (let i = 0; i < partialComments.length; i++) { + const pComment = partialComments[i]; + const offsetToken = pComment.offsetToken; + + // Assume absolute offset by default + let offset: Duration = new Duration(offsetToken.value); + + if (offsetToken.kind === "relative-plus") { + // Position relative to previous already-computed comment offset + const previousComment = last(comments); + if (previousComment) { + offset = new Duration(previousComment.offset.seconds + offset.seconds); + } + } else if (offsetToken.kind === "relative-minus") { + // Position relative to next comment or interval end + offset = new Duration(nextCommentOffset(partialComments, i, intervalDuration).seconds - offset.seconds); + } + + comments.push({ + offset, + loc: pComment.loc, + text: pComment.text, + }); + } + return comments; +}; + +const nextCommentOffset = (partialComments: PartialComment[], i: number, intervalDuration: Duration): Duration => { + const nextComment = partialComments[i + 1]; + if (!nextComment) { + return intervalDuration; + } + switch (nextComment.offsetToken.kind) { + case "relative-minus": + return new Duration( + nextCommentOffset(partialComments, i + 1, intervalDuration).seconds - nextComment.offsetToken.value, + ); + case "relative-plus": + throw new ParseError("Negative offset followed by positive offset", nextComment.offsetToken.loc); + case "absolute": + default: + return new Duration(nextComment.offsetToken.value); } };