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");