diff --git a/README.md b/README.md index acc3432..f49284b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ To use MODAQ in your product as an npm package, do the following 4. If you want to use the packet parser (instead of passing in a packet parameter), you need to include a URL to YAPP +To see what each prop does, visit [this page](https://github.com/alopezlago/QuizBowlDiscordScoreTracker/wiki/ModaqControl-props). + # Getting Started You will need to have [npm](https://www.npmjs.com/get-npm) and [yarn](https://yarnpkg.com/getting-started/install) installed on your system. diff --git a/package.json b/package.json index e15ba72..49a7153 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modaq", - "version": "1.2.2", + "version": "1.2.3", "description": "Quiz Bowl Reader using TypeScript, React, and MobX", "repository": { "type": "git", diff --git a/server.js b/server.js index 6a2d2dd..8390669 100644 --- a/server.js +++ b/server.js @@ -6,10 +6,10 @@ new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: false, historyApiFallback: true, -}).listen(8080, "localhost.qbreader", function (err, result) { +}).listen(8080, "localhost.quizbowlreader.com", function (err, result) { if (err) { console.log(err); } - console.log("Listening at localhost.qbreader:8080"); + console.log("Listening at localhost.quizbowlreader.com:8080"); }); diff --git a/src/components/BonusQuestion.tsx b/src/components/BonusQuestion.tsx index 28c0f75..cc8b234 100644 --- a/src/components/BonusQuestion.tsx +++ b/src/components/BonusQuestion.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { observer } from "mobx-react-lite"; import { mergeStyleSets } from "@fluentui/react"; -import * as FormattedTextParser from "src/parser/FormattedTextParser"; +import * as PacketState from "../state/PacketState"; import { BonusQuestionPart } from "./BonusQuestionPart"; import { Bonus } from "src/state/PacketState"; import { Cycle } from "src/state/Cycle"; @@ -22,7 +22,10 @@ export const BonusQuestion = observer((props: IBonusQuestionProps) => { const throwOutClickHandler: () => void = React.useCallback(() => { props.cycle.addThrownOutBonus(props.bonusIndex); }, [props]); - const formattedLeadin: IFormattedText[] = FormattedTextParser.parseFormattedText(props.bonus.leadin.trim()); + const formattedLeadin: IFormattedText[] = React.useMemo( + () => PacketState.getBonusWords(props.bonus.leadin, props.appState.game.gameFormat), + [props.bonus.leadin, props.appState.game.gameFormat] + ); const parts: JSX.Element[] = props.bonus.parts.map((bonusPartProps, index) => { return ( diff --git a/src/components/BonusQuestionPart.tsx b/src/components/BonusQuestionPart.tsx index de8004e..93cc416 100644 --- a/src/components/BonusQuestionPart.tsx +++ b/src/components/BonusQuestionPart.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Checkbox, Dropdown, IDropdownOption, IDropdownStyles, mergeStyleSets } from "@fluentui/react"; import { observer } from "mobx-react-lite"; -import * as FormattedTextParser from "src/parser/FormattedTextParser"; +import * as PacketState from "src/state/PacketState"; import { BonusPart } from "src/state/PacketState"; import { Cycle } from "src/state/Cycle"; import { Answer } from "./Answer"; @@ -66,7 +66,10 @@ export const BonusQuestionPart = observer((props: IBonusQuestionPartProps) => { ); } - const bonusPartText: IFormattedText[] = FormattedTextParser.parseFormattedText(props.bonusPart.question); + const bonusPartText: IFormattedText[] = React.useMemo( + () => PacketState.getBonusWords(props.bonusPart.question, props.gameFormat), + [props.bonusPart.question, props.gameFormat] + ); // TODO: We should try to resize the checkbox's box to match the font size return ( diff --git a/src/components/FormattedText.tsx b/src/components/FormattedText.tsx index fd5a214..3c1c471 100644 --- a/src/components/FormattedText.tsx +++ b/src/components/FormattedText.tsx @@ -9,7 +9,7 @@ export const FormattedText = observer( const classes: IFormattedTextClassNames = useStyles(); const elements: JSX.Element[] = []; for (let i = 0; i < props.segments.length; i++) { - elements.push(); + elements.push(); } const className: string = props.className ? `${classes.text} ${props.className}` : classes.text; @@ -42,6 +42,10 @@ const FormattedSegment = observer((props: IFormattedSegmentProps) => { ); } + if (props.segment.pronunciation) { + element = {element}; + } + return element; }); @@ -52,10 +56,12 @@ export interface IFormattedTextProps { interface IFormattedSegmentProps { segment: IFormattedText; + classNames: IFormattedTextClassNames; } interface IFormattedTextClassNames { text: string; + pronunciationGuide: string; } const useStyles = memoizeFunction( @@ -64,5 +70,8 @@ const useStyles = memoizeFunction( text: { display: "inline", }, + pronunciationGuide: { + color: "rgb(128, 128, 128)", + }, }) ); diff --git a/src/components/QuestionWord.tsx b/src/components/QuestionWord.tsx index ce3f76d..d4009de 100644 --- a/src/components/QuestionWord.tsx +++ b/src/components/QuestionWord.tsx @@ -7,13 +7,7 @@ import { FormattedText } from "./FormattedText"; export const QuestionWord = observer( (props: IQuestionWordProps): JSX.Element => { - const classes = getClassNames( - props.selected, - props.correct, - props.wrong, - props.inPronunciationGuide, - props.index != undefined - ); + const classes = getClassNames(props.selected, props.correct, props.wrong, props.index != undefined); return ( @@ -26,7 +20,6 @@ export const QuestionWord = observer( interface IQuestionWordProps { word: IFormattedText[]; index: number | undefined; - inPronunciationGuide?: boolean; selected?: boolean; correct?: boolean; wrong?: boolean; @@ -44,7 +37,6 @@ const getClassNames = memoizeFunction( selected: boolean | undefined, correct: boolean | undefined, wrong: boolean | undefined, - isPronunciation: boolean | undefined, isIndexDefined: boolean ): IQuestionWordClassNames => mergeStyleSets({ @@ -67,9 +59,6 @@ const getClassNames = memoizeFunction( background: "rgba(128, 128, 128, 0.2)", textDecoration: "underline double", }, - isPronunciation && { - color: "rgb(128, 128, 128)", - }, // Only highlight a word on hover if it's not in an existing state from selected/correct/wrong isIndexDefined && !(selected || correct || wrong) && { "&:hover": { background: "rgba(200, 200, 0, 0.15)" } }, diff --git a/src/components/TossupQuestion.tsx b/src/components/TossupQuestion.tsx index b983f4a..9ea919d 100644 --- a/src/components/TossupQuestion.tsx +++ b/src/components/TossupQuestion.tsx @@ -38,7 +38,6 @@ export const TossupQuestion = observer( correctBuzzIndex={correctBuzzIndex} index={word.canBuzzOn ? word.wordIndex : undefined} isLastWord={word.canBuzzOn && word.isLastWord} - inPronunciationGuide={!word.canBuzzOn && word.inPronunciationGuide} selectedWordRef={selectedWordRef} word={word.word} wrongBuzzIndexes={wrongBuzzIndexes} @@ -112,7 +111,6 @@ const QuestionWordWrapper = observer((props: IQuestionWordWrapperProps) => { index={props.index} word={props.word} selected={props.index === uiState.selectedWordIndex} - inPronunciationGuide={props.inPronunciationGuide} correct={props.index === props.correctBuzzIndex} wrong={props.wrongBuzzIndexes.findIndex((position) => position === props.index) >= 0} componentRef={selected ? props.selectedWordRef : undefined} @@ -170,7 +168,6 @@ interface IQuestionWordWrapperProps { cycle: Cycle; index?: number; isLastWord: boolean; - inPronunciationGuide: boolean; selectedWordRef: React.MutableRefObject; tossup: Tossup; tossupNumber: number; diff --git a/src/parser/IFormattedText.ts b/src/parser/IFormattedText.ts index 6132abd..d01d0e5 100644 --- a/src/parser/IFormattedText.ts +++ b/src/parser/IFormattedText.ts @@ -2,6 +2,7 @@ export interface IFormattedText { text: string; bolded: boolean; emphasized: boolean; + pronunciation?: boolean; required?: boolean; underlined?: boolean; } diff --git a/src/state/IPacket.ts b/src/state/IPacket.ts index 907c383..4356978 100644 --- a/src/state/IPacket.ts +++ b/src/state/IPacket.ts @@ -6,14 +6,12 @@ export interface IPacket { export interface ITossup { question: string; answer: string; - number: number; } export interface IBonus { leadin: string; parts: string[]; answers: string[]; - number: number; values: number[]; difficultyModifiers?: string[]; } diff --git a/src/state/PacketState.ts b/src/state/PacketState.ts index 9faa83f..dd9ad3e 100644 --- a/src/state/PacketState.ts +++ b/src/state/PacketState.ts @@ -1,4 +1,4 @@ -import { observable, makeObservable, makeAutoObservable } from "mobx"; +import { makeAutoObservable } from "mobx"; import { format } from "mobx-sync"; import * as FormattedTextParser from "src/parser/FormattedTextParser"; @@ -169,11 +169,14 @@ export class Tossup implements IQuestion { canBuzzOn, }); } else { + for (const segment of word) { + segment.pronunciation = !canBuzzOn && inPronunciationGuide; + } + words.push({ nonWordIndex: index, textIndex: i, word, - inPronunciationGuide, canBuzzOn, }); } @@ -198,15 +201,51 @@ export class Bonus { constructor(leadin: string, parts: BonusPart[]) { // We don't use makeAutoObservable because leadin doesn't need to be observable (never changes) - makeObservable(this, { - parts: observable, - }); + makeAutoObservable(this); - this.leadin = leadin; + this.leadin = leadin.trim(); this.parts = parts; } } +export function getBonusWords(text: string, format: IGameFormat): IFormattedText[] { + if ( + format.pronunciationGuideMarkers == undefined || + format.pronunciationGuideMarkers.length !== 2 || + format.pronunciationGuideMarkers.some((guide) => guide == undefined) + ) { + return FormattedTextParser.parseFormattedText(text); + } + + const formattedText: IFormattedText[][] = FormattedTextParser.splitFormattedTextIntoWords(text); + + const pronunciationGuideMarkers: [string, string] = format.pronunciationGuideMarkers; + let inPronunciationGuide = false; + for (let i = 0; i < formattedText.length; i++) { + const word: IFormattedText[] = formattedText[i]; + const fullText = word.reduce((result, text) => result + text.text, ""); + + if (fullText.startsWith(pronunciationGuideMarkers[0])) { + inPronunciationGuide = true; + } + + for (const segment of word) { + segment.pronunciation = inPronunciationGuide; + } + + if (inPronunciationGuide && fullText.indexOf(pronunciationGuideMarkers[1]) >= 0) { + inPronunciationGuide = false; + } + + // Add the space back to all but the last word + if (i !== formattedText.length - 1) { + word[word.length - 1].text += " "; + } + } + + return formattedText.reduce((previous, next) => previous.concat(next), []); +} + export type ITossupWord = IBuzzableTossupWord | INonbuzzableTossupWord; export interface IBuzzableTossupWord extends IBaseTossupWord { @@ -217,7 +256,6 @@ export interface IBuzzableTossupWord extends IBaseTossupWord { export interface INonbuzzableTossupWord extends IBaseTossupWord { nonWordIndex: number; - inPronunciationGuide: boolean; canBuzzOn: false; } diff --git a/tests/PacketStateTossupTests.ts b/tests/PacketStateTests.ts similarity index 69% rename from tests/PacketStateTossupTests.ts rename to tests/PacketStateTests.ts index a047729..30f6fd7 100644 --- a/tests/PacketStateTossupTests.ts +++ b/tests/PacketStateTests.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; import * as GameFormats from "src/state/GameFormats"; +import * as PacketState from "src/state/PacketState"; import { Tossup } from "src/state/PacketState"; import { IFormattedText } from "src/parser/IFormattedText"; import { IGameFormat } from "src/state/IGameFormat"; @@ -18,7 +19,7 @@ const superpowersGameFormat: IGameFormat = { ], }; -describe("PacketStateTossupTests", () => { +describe("PacketStateTests", () => { describe("formattedQuestionText", () => { // Most of these tests are handled by FormattedTextParserTests, so just test that it's hooked up to it and that // we include the end character @@ -45,6 +46,113 @@ describe("PacketStateTossupTests", () => { }); }); + // Need tests for getBonusWords? + describe("getBonusWords", () => { + it("No pronunciation guide in format", () => { + const formattedText: IFormattedText[] = PacketState.getBonusWords("This is my bonus part", { + ...GameFormats.ACFGameFormat, + pronunciationGuideMarkers: undefined, + }); + + expect(formattedText.length).to.equal(2); + + const firstSegment: IFormattedText = formattedText[0]; + expect(firstSegment.text).to.equal("This is"); + expect(firstSegment.pronunciation).to.be.undefined; + expect(firstSegment.bolded).to.be.true; + + const secondSegment: IFormattedText = formattedText[1]; + expect(secondSegment.text).to.equal(" my bonus part"); + expect(secondSegment.pronunciation).to.be.undefined; + expect(secondSegment.bolded).to.be.false; + }); + + it("With pronunciation guide", () => { + const formattedText: IFormattedText[] = PacketState.getBonusWords( + "This is my bonus (BONE-us) part", + GameFormats.ACFGameFormat + ); + + expect(formattedText.length).to.equal(6); + + expect(formattedText[0].text).to.equal("This "); + expect(formattedText[0].pronunciation).to.be.false; + expect(formattedText[0].bolded).to.be.true; + + expect(formattedText[1].text).to.equal("is "); + expect(formattedText[1].pronunciation).to.be.false; + expect(formattedText[1].bolded).to.be.true; + + expect(formattedText[2].text).to.equal("my "); + expect(formattedText[2].pronunciation).to.be.false; + expect(formattedText[2].bolded).to.be.false; + + expect(formattedText[3].text).to.equal("bonus "); + expect(formattedText[3].pronunciation).to.be.false; + expect(formattedText[3].bolded).to.be.false; + + expect(formattedText[4].text).to.equal("(BONE-us) "); + expect(formattedText[4].pronunciation).to.be.true; + expect(formattedText[4].bolded).to.be.false; + + expect(formattedText[5].text).to.equal("part"); + expect(formattedText[5].pronunciation).to.be.false; + expect(formattedText[5].bolded).to.be.false; + }); + + it("With multiple pronunciation guide", () => { + const formattedText: IFormattedText[] = PacketState.getBonusWords( + "Another (an-OTH-er) bonus (BONE-us) part", + GameFormats.ACFGameFormat + ); + + expect(formattedText.length).to.equal(5); + + expect(formattedText[0].text).to.equal("Another "); + expect(formattedText[0].pronunciation).to.be.false; + expect(formattedText[0].underlined).to.be.true; + + expect(formattedText.slice(1).map((text) => text.underlined)).to.not.contain(true); + + expect(formattedText[1].text).to.equal("(an-OTH-er) "); + expect(formattedText[1].pronunciation).to.be.true; + + expect(formattedText[2].text).to.equal("bonus "); + expect(formattedText[2].pronunciation).to.be.false; + + expect(formattedText[3].text).to.equal("(BONE-us) "); + expect(formattedText[3].pronunciation).to.be.true; + + expect(formattedText[4].text).to.equal("part"); + expect(formattedText[4].pronunciation).to.be.false; + }); + + it("No pronunication guide, but defined in format", () => { + const formattedText: IFormattedText[] = PacketState.getBonusWords( + "This is my bonus part", + GameFormats.ACFGameFormat + ); + + expect(formattedText.length).to.equal(5); + expect(formattedText.map((text) => text.pronunciation)).to.not.contain(true); + + expect(formattedText[0].text).to.equal("This "); + expect(formattedText[0].bolded).to.be.true; + + expect(formattedText[1].text).to.equal("is "); + expect(formattedText[1].bolded).to.be.true; + + expect(formattedText[2].text).to.equal("my "); + expect(formattedText[2].bolded).to.be.false; + + expect(formattedText[3].text).to.equal("bonus "); + expect(formattedText[3].bolded).to.be.false; + + expect(formattedText[4].text).to.equal("part"); + expect(formattedText[4].bolded).to.be.false; + }); + }); + describe("getPointsAtPosition", () => { it("No powers", () => { const tossup: Tossup = new Tossup("This is my question", "Answer");