diff --git a/index.ts b/index.ts index 4220d3d..95ba616 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,14 @@ import { IGameFormat as gameFormat } from "./src/state/IGameFormat"; import { IBonus as bonus, IPacket as packet, ITossup as tossup } from "./src/state/IPacket"; import { IPlayer as player } from "./src/state/TeamState"; import { ModaqControl as control, IModaqControlProps as controlProps } from "./src/components/ModaqControl"; +import { + IFormattingOptions as iFormattingOptions, + parseFormattedText as ftpParseFormattedText, + splitFormattedTextIntoWords as ftpSplitFormattedTextIntoWords, + defaultPronunciationGuideMarkers as ftpDefaultPronunciationGuideMarkers, + defaultReaderDirectives as ftpDefaultReaderDirectives, +} from "src/parser/FormattedTextParser"; +import { IFormattedText as iFormattedText } from "src/parser/IFormattedText"; export const ModaqControl = control; @@ -18,4 +26,16 @@ export type IPlayer = player; export type IGameFormat = gameFormat; +export type IFormattingOptions = iFormattingOptions; + +export type IFormattedText = iFormattedText; + export const GameFormats = gameFormats; + +export const defaultPronunciationGuideMarkers = ftpDefaultPronunciationGuideMarkers; + +export const defaultReaderDirectives = ftpDefaultReaderDirectives; + +export const parseFormattedText = ftpParseFormattedText; + +export const splitFormattedTextIntoWords = ftpSplitFormattedTextIntoWords; diff --git a/package.json b/package.json index bacda4a..9c744f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modaq", - "version": "1.28.0", + "version": "1.29.0", "description": "Quiz Bowl Reader using TypeScript, React, and MobX", "repository": { "type": "git", diff --git a/src/components/Answer.tsx b/src/components/Answer.tsx index f73c20e..8cd5eae 100644 --- a/src/components/Answer.tsx +++ b/src/components/Answer.tsx @@ -9,10 +9,9 @@ import { AppState } from "../state/AppState"; export const Answer = observer(function Answer(props: IAnswerProps): JSX.Element { const appState: AppState = React.useContext(StateContext); - const formattedText: IFormattedText[] = FormattedTextParser.parseFormattedText( - props.text.trimLeft(), - appState.game.gameFormat.pronunciationGuideMarkers - ); + const formattedText: IFormattedText[] = FormattedTextParser.parseFormattedText(props.text.trimLeft(), { + pronunciationGuideMarkers: appState.game.gameFormat.pronunciationGuideMarkers, + }); return (
diff --git a/src/components/dialogs/ScoresheetDialog.tsx b/src/components/dialogs/ScoresheetDialog.tsx index 7af2785..b74e775 100644 --- a/src/components/dialogs/ScoresheetDialog.tsx +++ b/src/components/dialogs/ScoresheetDialog.tsx @@ -162,7 +162,9 @@ function getUnformattedAnswer(game: GameState, answer: string): string { answer = answer.substring(0, alternateIndex).trim(); } - const text = FormattedTextParser.parseFormattedText(answer, game.gameFormat.pronunciationGuideMarkers) + const text = FormattedTextParser.parseFormattedText(answer, { + pronunciationGuideMarkers: game.gameFormat.pronunciationGuideMarkers, + }) .map((line) => line.text) .join(""); diff --git a/src/parser/FormattedTextParser.ts b/src/parser/FormattedTextParser.ts index 288a02f..d0cb6af 100644 --- a/src/parser/FormattedTextParser.ts +++ b/src/parser/FormattedTextParser.ts @@ -1,12 +1,74 @@ import { IFormattedText } from "./IFormattedText"; -export function parseFormattedText(text: string, pronunciationGuideMarkers?: [string, string]): IFormattedText[] { +/** + * Default pronunciation guide markers used if none are passed into `IFormattingOptions` + */ +export const defaultPronunciationGuideMarkers: [string, string] = ["(", ")"]; + +/** + * Default reader directives used if none are passed into `IFormattingOptions` + */ +export const defaultReaderDirectives: string[] = ["(emphasize)", "(pause)", "(read slowly)"]; + +/** + * Options for how to parse and format text + */ +export interface IFormattingOptions { + /** + * Two-element array where the first string is the tag for the start of a pronunciation guide and the second string + * is the tag for the end. For example, if the pronuncation guide looks like "(guide)", the array would be + * [ "(", ")" ]. Pronunciation guides don't count as words and are formatted differently from the rest of the + * question text. + * If no value is provided, then `defaultPronunciationGuideMarkers` will be used. + */ + pronunciationGuideMarkers?: [string, string]; + + /** + * Directives for the reader, like "(read slowly)". These don't count as words and are formatted differently from + * the rest of the question text. + * If no value is provided, then `defaultReaderDirectives` will be used. + */ + readerDirectives?: string[]; +} + +/** + * Takes text with formatting tags and turns it into an array of texts with formatting information included, such as + * which words are bolded. + * Note that if the '"' character is used in a pronunciation guide, it will also support '“' and '”', and vice versa. + * @param text The text to format, such a question or answerline. + * @param options Formtating options, such as what indicates the start of a pronunciation guide. + * @returns An array of `IFormattedText` that represents the text with formatting metadata, such as which words are + * bolded, underlined, etc. + */ +export function parseFormattedText(text: string, options?: IFormattingOptions): IFormattedText[] { const result: IFormattedText[] = []; if (text == undefined) { return result; } + options = options ?? {}; + const pronunciationGuideMarkers: [[string, string]] = [ + options.pronunciationGuideMarkers ?? defaultPronunciationGuideMarkers, + ]; + + // Normalize quotes in pronunciation guides + if (pronunciationGuideMarkers[0][0].includes('"') || pronunciationGuideMarkers[0][1].includes('"')) { + pronunciationGuideMarkers.push([ + pronunciationGuideMarkers[0][0].replace(/"/g, "“"), + pronunciationGuideMarkers[0][1].replace(/"/g, "”"), + ]); + } + + if (pronunciationGuideMarkers[0][0].includes("“") || pronunciationGuideMarkers[0][1].includes("”")) { + pronunciationGuideMarkers.push([ + pronunciationGuideMarkers[0][0].replace(/“/g, '"'), + pronunciationGuideMarkers[0][1].replace(/”/g, '"'), + ]); + } + + const readerDirectives: string[] | undefined = options.readerDirectives ?? defaultReaderDirectives; + let bolded = false; let emphasized = false; let underlined = false; @@ -15,26 +77,34 @@ export function parseFormattedText(text: string, pronunciationGuideMarkers?: [st let pronunciation = false; let startIndex = 0; + let extraTags = ""; + for (const pronunciationGuideMarker of pronunciationGuideMarkers) { + extraTags += `|${escapeRegExp(pronunciationGuideMarker[0])}|${escapeRegExp(pronunciationGuideMarker[1])}`; + } + + if (readerDirectives) { + extraTags += `|${readerDirectives.map((directive) => escapeRegExp(directive)).join("|")}`; + } + // If we need to support older browswers, use RegExp, exec, and a while loop. See // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll - const matchIterator: IterableIterator = - pronunciationGuideMarkers == undefined - ? text.matchAll(/<\/?em>|<\/?req>|<\/?b>|<\/?u>|<\/?sub>|<\/?sup>/gi) - : text.matchAll( - new RegExp( - `<\\/?em>|<\\/?req>|<\\/?b>|<\\/?u>|<\\/?sub>|<\\/?sup>|${escapeRegExp( - pronunciationGuideMarkers[0] - )}|${escapeRegExp(pronunciationGuideMarkers[1])}`, - "gi" - ) - ); + const matchIterator: IterableIterator = text.matchAll( + new RegExp(`<\\/?em>|<\\/?req>|<\\/?b>|<\\/?u>|<\\/?sub>|<\\/?sup>${extraTags}`, "gi") + ); for (const match of matchIterator) { // For the end of the pronunciation guide, we want to include it in the string, so add it to the current slice - const tagInTextLength: number = - pronunciationGuideMarkers != undefined && match[0].toLowerCase() === pronunciationGuideMarkers[1] - ? pronunciationGuideMarkers[1].length - : 0; + // TODO: Do we need to do this with reader directives? + const tag: string = match[0]; + const normalizedTag: string = tag.toLowerCase(); + let tagInTextLength = 0; + for (const pronunciationGuideMarker of pronunciationGuideMarkers) { + if (normalizedTag === pronunciationGuideMarker[1].toLowerCase()) { + tagInTextLength = pronunciationGuideMarker[1].length; + break; + } + } + const matchIndex: number = match.index ?? 0; const slice: string = text.substring(startIndex, matchIndex + tagInTextLength); @@ -52,9 +122,8 @@ export function parseFormattedText(text: string, pronunciationGuideMarkers?: [st } // Once we got the slice of text, toggle the attribute for the next slice - const tag: string = match[0]; let skipTag = true; - switch (tag.toLowerCase()) { + switch (normalizedTag) { case "": emphasized = true; break; @@ -94,16 +163,37 @@ export function parseFormattedText(text: string, pronunciationGuideMarkers?: [st superscripted = false; break; default: - if (pronunciationGuideMarkers) { - if (tag === pronunciationGuideMarkers[0].toLowerCase()) { + let pronunciationGuideMatched = false; + for (const pronunciationGuideMarker of pronunciationGuideMarkers) { + if (normalizedTag === pronunciationGuideMarker[0].toLowerCase()) { skipTag = false; pronunciation = true; - break; - } else if (tag === pronunciationGuideMarkers[1].toLowerCase()) { + pronunciationGuideMatched = true; + } else if (normalizedTag === pronunciationGuideMarker[1].toLowerCase()) { pronunciation = false; - break; + pronunciationGuideMatched = true; } } + + if (pronunciationGuideMatched) { + break; + } + + if (readerDirectives.some((directive) => directive.trim().toLowerCase() === normalizedTag)) { + // Treat it like a pronunciation guide for this one specific word + const readerDirectiveText: IFormattedText = { + text: tag, + bolded, + emphasized, + underlined, + subscripted, + superscripted, + pronunciation: true, + }; + result.push(readerDirectiveText); + break; + } + throw `Unknown match: ${tag}`; } @@ -133,17 +223,20 @@ export function parseFormattedText(text: string, pronunciationGuideMarkers?: [st // TODO: Look into removing the dependency with parseFormattedText, so that we only do one pass over the string instead // of two passes. -export function splitFormattedTextIntoWords( - text: string, - pronunciationGuideMarkers?: [string, string] -): IFormattedText[][] { +/** + * Takes text with formatting tags and splits it into an array of words with formatting information for each word. + * @param text The text to format, such a question or answerline. + * @param options Formtating options, such as what indicates the start of a pronunciation guide. + * @returns An array of words represented as an `IFormattedText[]` representing all the formatting in that word. + */ +export function splitFormattedTextIntoWords(text: string, options?: IFormattingOptions): IFormattedText[][] { // We need to take the list of formatted text and split them up into individual words. // Algorithm: For each piece of formatted text, go through and split the text by the spaces in it. // If there are no spaces, then add it to a variable tracking the last word. // If there are spaces, add the last word to the list, and then add each non-empty segment (i.e. non-space) to the // list, except for the last one. If the last segment isn't empty, set that as the "last word", and continue going // through the list of formatted texts. - const formattedText: IFormattedText[] = parseFormattedText(text, pronunciationGuideMarkers); + const formattedText: IFormattedText[] = parseFormattedText(text, options); const splitFormattedText: IFormattedText[][] = []; diff --git a/src/parser/IFormattedText.ts b/src/parser/IFormattedText.ts index c398e2f..decbdfb 100644 --- a/src/parser/IFormattedText.ts +++ b/src/parser/IFormattedText.ts @@ -1,8 +1,23 @@ export interface IFormattedText { + /** + * The text of this fragment + */ text: string; bolded: boolean; + + /** + * If text is emphasized, which is italicized. + */ emphasized: boolean; + + /** + * `true` if this text should be formatted like a pronunciation guide or reader directive. + */ pronunciation?: boolean; + + /** + * Obsolete. Use bolded and underlined instead. + */ required?: boolean; underlined?: boolean; subscripted?: boolean; diff --git a/src/state/PacketState.ts b/src/state/PacketState.ts index 3b3f0b5..3f4c233 100644 --- a/src/state/PacketState.ts +++ b/src/state/PacketState.ts @@ -76,7 +76,9 @@ export class Tossup implements IQuestion { let powerMarkerIndex = 0; for (let i = 0; i < format.powers.length; i++) { const powerMarker: string = format.powers[i].marker.trim(); - const currentPowerMarkerIndex = words.indexOf(powerMarker, powerMarkerIndex); + const currentPowerMarkerIndex = words.findIndex( + (value, index) => index >= powerMarkerIndex && value.startsWith(powerMarker) + ); if (currentPowerMarkerIndex === -1) { continue; } @@ -129,7 +131,7 @@ export class Tossup implements IQuestion { let canBuzzOn = true; let index: number = wordIndex; const trimmedText: string = fullText.trim(); - const powerMarkerIndex: number = format.powers.findIndex((power) => power.marker === trimmedText); + const powerMarkerIndex: number = format.powers.findIndex((power) => trimmedText.startsWith(power.marker)); if (isLastWord) { // Last word should always be the terminal character, which can't be a power or in a pronunciation guide wordIndex++; @@ -173,9 +175,9 @@ export class Tossup implements IQuestion { private formattedQuestionText(format: IGameFormat): IFormattedText[][] { // Include the ■ to give an end of question marker - return FormattedTextParser.splitFormattedTextIntoWords(this.question, format.pronunciationGuideMarkers).concat([ - [{ text: "■END■", bolded: true, emphasized: false, required: false, pronunciation: false }], - ]); + return FormattedTextParser.splitFormattedTextIntoWords(this.question, { + pronunciationGuideMarkers: format.pronunciationGuideMarkers, + }).concat([[{ text: "■END■", bolded: true, emphasized: false, required: false, pronunciation: false }]]); } } @@ -197,7 +199,9 @@ export class Bonus { } export function getBonusWords(text: string, format: IGameFormat): IFormattedText[] { - return FormattedTextParser.parseFormattedText(text, format.pronunciationGuideMarkers); + return FormattedTextParser.parseFormattedText(text, { + pronunciationGuideMarkers: format.pronunciationGuideMarkers, + }); } export type ITossupWord = IBuzzableTossupWord | INonbuzzableTossupWord; diff --git a/tests/FormattedTextParserTests.ts b/tests/FormattedTextParserTests.ts index 24ebeb1..41d2cac 100644 --- a/tests/FormattedTextParserTests.ts +++ b/tests/FormattedTextParserTests.ts @@ -119,10 +119,9 @@ describe("FormattedTextParserTests", () => { }); it("Pronunciation guide", () => { const textToFormat = "This text is mine (mein)."; - const result: IFormattedText[] = FormattedTextParser.parseFormattedText( - textToFormat, - GameFormats.ACFGameFormat.pronunciationGuideMarkers - ); + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: GameFormats.ACFGameFormat.pronunciationGuideMarkers, + }); expect(result).to.deep.equal([ { text: "This text is mine ", @@ -155,10 +154,9 @@ describe("FormattedTextParserTests", () => { }); it("Bolded pronunciation guide", () => { const textToFormat = "Solano Lopez (LOW-pez) was in this war."; - const result: IFormattedText[] = FormattedTextParser.parseFormattedText( - textToFormat, - GameFormats.ACFGameFormat.pronunciationGuideMarkers - ); + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: GameFormats.ACFGameFormat.pronunciationGuideMarkers, + }); expect(result).to.deep.equal([ { text: "Solano Lopez ", @@ -191,7 +189,9 @@ describe("FormattedTextParserTests", () => { }); it("Non-parentheses pronunciation guide", () => { const textToFormat = "This text is mine [mein]."; - const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, ["[", "]"]); + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["[", "]"], + }); expect(result).to.deep.equal([ { text: "This text is mine ", @@ -224,7 +224,9 @@ describe("FormattedTextParserTests", () => { }); it("Different pronunciation guide", () => { const textToFormat = "This text is mine (mein)."; - const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, ["[", "]"]); + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["[", "]"], + }); expect(result).to.deep.equal([ { text: "This text is mine (mein).", @@ -237,6 +239,307 @@ describe("FormattedTextParserTests", () => { }, ]); }); + it("Special quotes in text with normal quotes in pronunciation guide", () => { + const textToFormat = "This text is mine (“mein”)."; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ['("', '")'], + }); + expect(result).to.deep.equal([ + { + text: "This text is mine ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: "(“mein”)", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: ".", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); + it("Normal quotes in text with special quotes in pronunciation guide", () => { + const textToFormat = 'This text is mine ("mein").'; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["(“", "”)"], + }); + expect(result).to.deep.equal([ + { + text: "This text is mine ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: '("mein")', + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: ".", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); + it("Mixed quotes in text (special, normal) in pronunciation guide", () => { + const textToFormat = 'This text is mine (“mein").'; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["(“", "”)"], + }); + expect(result).to.deep.equal([ + { + text: "This text is mine ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: '(“mein")', + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: ".", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); + it("Mixed quotes in text (normal, special) in pronunciation guide", () => { + const textToFormat = 'This text is mine ("mein”).'; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["(“", "”)"], + }); + expect(result).to.deep.equal([ + { + text: "This text is mine ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: '("mein”)', + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: ".", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); + it("Special quotes in wrong order in pronunciation guide", () => { + const textToFormat = "This text is mine (”mein“)."; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ['("', '")'], + }); + expect(result).to.deep.equal([ + { + text: "This text is mine (”mein“).", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); + it("Case insensitive normal quotes in text with special quotes in pronunciation guide", () => { + const textToFormat = 'This text is mine (a"mein"a).'; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["(A“", "”A)"], + }); + expect(result).to.deep.equal([ + { + text: "This text is mine ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: '(a"mein"a)', + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: ".", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); + it("Default reader directives", () => { + const textToFormat = + "This (Emphasize) equation is proportional to (read slowly) a minus x, plus (pause) 1."; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["[", "]"], + // readerDirectives: ["(emphasize)", "(read slowly)", "(pause)"], + }); + expect(result).to.deep.equal([ + { + text: "This ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: "(Emphasize)", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: " equation is proportional to ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: "(read slowly)", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: " a minus x, plus ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: "(pause)", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: " 1.", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); + it("Explicit reader directives", () => { + const textToFormat = "This (Emphasize) equation is proportional to (slowly) a minus x, plus (pause) 1."; + const result: IFormattedText[] = FormattedTextParser.parseFormattedText(textToFormat, { + pronunciationGuideMarkers: ["[", "]"], + readerDirectives: ["(slowly)"], + }); + expect(result).to.deep.equal([ + { + text: "This (Emphasize) equation is proportional to ", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + { + text: "(slowly)", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: true, + }, + { + text: " a minus x, plus (pause) 1.", + bolded: false, + emphasized: false, + underlined: false, + subscripted: false, + superscripted: false, + pronunciation: false, + }, + ]); + }); it("Emphasized then required", () => { const text = "Before emphasized then in between required then done."; const result: IFormattedText[] = FormattedTextParser.parseFormattedText(text); @@ -473,10 +776,9 @@ describe("FormattedTextParserTests", () => { }); it("Pronunciation", () => { const textToFormat = "There is a pronunciation guide (GUY-de) in this question."; - const result: IFormattedText[][] = FormattedTextParser.splitFormattedTextIntoWords( - textToFormat, - GameFormats.ACFGameFormat.pronunciationGuideMarkers - ); + const result: IFormattedText[][] = FormattedTextParser.splitFormattedTextIntoWords(textToFormat, { + pronunciationGuideMarkers: GameFormats.ACFGameFormat.pronunciationGuideMarkers, + }); const expected: IFormattedText[][] = textToFormat.split(/\s+/g).map((word, index) => { return [ { diff --git a/tests/PacketStateTests.ts b/tests/PacketStateTests.ts index d855388..62957c7 100644 --- a/tests/PacketStateTests.ts +++ b/tests/PacketStateTests.ts @@ -58,6 +58,37 @@ describe("PacketStateTests", () => { expect(formattedWord.underlined).to.be.false; expect(formattedWord.pronunciation).to.be.true; }); + it("formattedQuestionText has power marker", () => { + const tossup: Tossup = new Tossup("The power marker (*) is here.", "Answer"); + const formattedText: IFormattedText[][] = tossup.getWords(powersGameFormat).map((word) => word.word); + expect(formattedText.length).to.be.greaterThan(1); + expect(formattedText[3].length).to.equal(1); + const formattedWord: IFormattedText = formattedText[3][0]; + expect(formattedWord.text).to.equal("(*)"); + expect(formattedWord.bolded).to.be.false; + expect(formattedWord.emphasized).to.be.false; + expect(formattedWord.underlined).to.be.false; + expect(formattedWord.pronunciation).to.be.false; + }); + it("formattedQuestionText has power marker with punctuation after it", () => { + const tossup: Tossup = new Tossup("The power marker (*), I think.", "Answer"); + const formattedText: IFormattedText[][] = tossup.getWords(powersGameFormat).map((word) => word.word); + expect(formattedText.length).to.be.greaterThan(1); + expect(formattedText[3].length).to.equal(2); + const formattedFirstPart: IFormattedText = formattedText[3][0]; + expect(formattedFirstPart.text).to.equal("(*)"); + expect(formattedFirstPart.bolded).to.be.false; + expect(formattedFirstPart.emphasized).to.be.false; + expect(formattedFirstPart.underlined).to.be.false; + expect(formattedFirstPart.pronunciation).to.be.false; + + const formattedSecondWord: IFormattedText = formattedText[3][1]; + expect(formattedSecondWord.text).to.equal(","); + expect(formattedSecondWord.bolded).to.be.false; + expect(formattedSecondWord.emphasized).to.be.false; + expect(formattedSecondWord.underlined).to.be.false; + expect(formattedSecondWord.pronunciation).to.be.false; + }); }); // Need tests for getBonusWords? @@ -246,6 +277,14 @@ describe("PacketStateTests", () => { const points: number = tossup.getPointsAtPosition(superpowersGameFormat, 2); expect(points).to.equal(15); }); + it("In power with punctuation after power marker", () => { + const tossup: Tossup = new Tossup("This is my (*), question", "Answer"); + const points: number = tossup.getPointsAtPosition(powersGameFormat, 2); + expect(points).to.equal(15); + + const pointsAfter: number = tossup.getPointsAtPosition(powersGameFormat, 3); + expect(pointsAfter).to.equal(10); + }); // Tossups include a special character to mark the end of the question, which is after the last word in the // question, so the last index will be one greater than the number of words in the question.