Skip to content

Commit bbf7f3b

Browse files
authored
Enable parsePerseusItem to handle all published Perseus content (#2082)
This PR fixes the remaining cases where the parser couldn't handle some data in our content corpus -- notably, in articles and international content. After this PR is merged, we will be able to use the parser in Webapp! Note that running the exhaustive test tool still produces some failures. However, I suspect the failing content isn't published, because it either doesn't render (crashes the page) or can't be scored (throws an exception when you click the "check answer" button). We'll find out when we start logging parser errors in production whether I'm right about this. The remaining errors are: ``` (root).question.widgets["grapher N"].options.correct.coords -- expected array of length 2; got [] (root).question.widgets["matcher N"].options -- expected object; got undefined (root).question.widgets["graded-group N"].options.widgets["numeric-input N"].options.answers[N].answerForms[N] -- expected "integer", "mixed", "improper", "proper", "decimal", "percent", "pi"; got "number" (root).question.widgets["example-graphie-widget N"] -- expected a valid widget type; got "example-graphie-widget" (root).question.widgets["image N"]["(widget key)"][1] -- expected a string representing a positive integer; got "0" (root).question.widgets["explanation N"]["(widget key)"][1] -- expected a string representing a positive integer; got "0" ``` Issue: LEMS-2582 ## Test plan: `yarn test` Author: benchristel Reviewers: benchristel, jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x) Pull Request URL: #2082
1 parent 766d335 commit bbf7f3b

23 files changed

+3704
-270
lines changed

.changeset/hot-cougars-laugh.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
"@khanacademy/perseus-core": minor
4+
---
5+
6+
Enable parsePerseusItem to parse all published content, upgrading old formats to the current one.

packages/perseus-core/src/data-schema.ts

+22-19
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,9 @@ export type GraphRange = [
550550
export type GrapherAnswerTypes =
551551
| {
552552
type: "absolute_value";
553-
coords: [
554-
// The vertex
555-
Coord, // A point along one line of the absolute value "V" lines
556-
Coord,
557-
];
553+
// If `coords` is null, the graph will not be gradable. All answers
554+
// will be scored as invalid.
555+
coords: null | [vertex: Coord, secondPoint: Coord];
558556
}
559557
| {
560558
type: "exponential";
@@ -563,38 +561,46 @@ export type GrapherAnswerTypes =
563561
asymptote: [Coord, Coord];
564562
// Two points along the exponential curve. One end of the curve
565563
// trends towards the asymptote.
566-
coords: [Coord, Coord];
564+
// If `coords` is null, the graph will not be gradable. All answers
565+
// will be scored as invalid.
566+
coords: null | [Coord, Coord];
567567
}
568568
| {
569569
type: "linear";
570570
// Two points along the straight line
571-
coords: [Coord, Coord];
571+
// If coords is null, the graph will not be gradable. All answers
572+
// will be scored as invalid.
573+
coords: null | [Coord, Coord];
572574
}
573575
| {
574576
type: "logarithm";
575577
// Two points along the asymptote line.
576578
asymptote: [Coord, Coord];
577579
// Two points along the logarithmic curve. One end of the curve
578580
// trends towards the asymptote.
579-
coords: [Coord, Coord];
581+
// If coords is null, the graph will not be gradable. All answers
582+
// will be scored as invalid.
583+
coords: null | [Coord, Coord];
580584
}
581585
| {
582586
type: "quadratic";
583-
coords: [
584-
// The vertex of the parabola
585-
Coord, // A point along the parabola
586-
Coord,
587-
];
587+
// If coords is null, the graph will not be gradable. All answers
588+
// will be scored as invalid.
589+
coords: null | [vertex: Coord, secondPoint: Coord];
588590
}
589591
| {
590592
type: "sinusoid";
591593
// Two points on the same slope in the sinusoid wave line.
592-
coords: [Coord, Coord];
594+
// If coords is null, the graph will not be gradable. All answers
595+
// will be scored as invalid.
596+
coords: null | [Coord, Coord];
593597
}
594598
| {
595599
type: "tangent";
596600
// Two points on the same slope in the tangent wave line.
597-
coords: [Coord, Coord];
601+
// If coords is null, the graph will not be gradable. All answers
602+
// will be scored as invalid.
603+
coords: null | [Coord, Coord];
598604
};
599605

600606
export type PerseusGrapherWidgetOptions = {
@@ -1615,9 +1621,6 @@ export type PerseusCSProgramWidgetOptions = {
16151621
showEditor: boolean;
16161622
// Whether to show the execute buttons
16171623
showButtons: boolean;
1618-
// TODO(benchristel): width is not used. Delete it?
1619-
// The width of the widget
1620-
width: number;
16211624
// The height of the widget
16221625
height: number;
16231626
// TODO(benchristel): static is not used. Delete it?
@@ -1643,7 +1646,7 @@ export type PerseusIFrameWidgetOptions = {
16431646
// A URL to display OR a CS Program ID
16441647
url: string;
16451648
// Settings that you add here are available to the program as an object returned by Program.settings()
1646-
settings: ReadonlyArray<PerseusCSProgramSetting>;
1649+
settings?: ReadonlyArray<PerseusCSProgramSetting>;
16471650
// The width of the widget
16481651
width: number | string;
16491652
// The height of the widget

packages/perseus/src/util/parse-perseus-json/perseus-parsers/cs-program-widget.ts

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export const parseCSProgramWidget: Parser<CSProgramWidget> = parseWidget(
2222
settings: array(object({name: string, value: string})),
2323
showEditor: boolean,
2424
showButtons: boolean,
25-
width: number,
2625
height: number,
2726
static: defaulted(boolean, () => false),
2827
}),

packages/perseus/src/util/parse-perseus-json/perseus-parsers/grapher-widget.ts

+7-15
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
string,
1212
union,
1313
} from "../general-purpose-parsers";
14-
import {defaulted} from "../general-purpose-parsers/defaulted";
1514
import {discriminatedUnionOn} from "../general-purpose-parsers/discriminated-union";
1615

1716
import {parseWidget} from "./widget";
@@ -42,58 +41,51 @@ export const parseGrapherWidget: Parser<GrapherWidget> = parseWidget(
4241
"absolute_value",
4342
object({
4443
type: constant("absolute_value"),
45-
coords: pairOfPoints,
44+
coords: nullable(pairOfPoints),
4645
}),
4746
)
4847
.withBranch(
4948
"exponential",
5049
object({
5150
type: constant("exponential"),
5251
asymptote: pairOfPoints,
53-
coords: pairOfPoints,
52+
coords: nullable(pairOfPoints),
5453
}),
5554
)
5655
.withBranch(
5756
"linear",
5857
object({
5958
type: constant("linear"),
60-
coords: defaulted(
61-
pairOfPoints,
62-
() =>
63-
[
64-
[-5, 5],
65-
[5, 5],
66-
] as [[number, number], [number, number]],
67-
),
59+
coords: nullable(pairOfPoints),
6860
}),
6961
)
7062
.withBranch(
7163
"logarithm",
7264
object({
7365
type: constant("logarithm"),
7466
asymptote: pairOfPoints,
75-
coords: pairOfPoints,
67+
coords: nullable(pairOfPoints),
7668
}),
7769
)
7870
.withBranch(
7971
"quadratic",
8072
object({
8173
type: constant("quadratic"),
82-
coords: pairOfPoints,
74+
coords: nullable(pairOfPoints),
8375
}),
8476
)
8577
.withBranch(
8678
"sinusoid",
8779
object({
8880
type: constant("sinusoid"),
89-
coords: pairOfPoints,
81+
coords: nullable(pairOfPoints),
9082
}),
9183
)
9284
.withBranch(
9385
"tangent",
9486
object({
9587
type: constant("tangent"),
96-
coords: pairOfPoints,
88+
coords: nullable(pairOfPoints),
9789
}),
9890
).parser,
9991
graph: object({

packages/perseus/src/util/parse-perseus-json/perseus-parsers/iframe-widget.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ export const parseIframeWidget: Parser<IFrameWidget> = parseWidget(
1919
constant("iframe"),
2020
object({
2121
url: string,
22-
settings: array(object({name: string, value: string})),
22+
settings: optional(array(object({name: string, value: string}))),
2323
width: union(number).or(string).parser,
2424
height: union(number).or(string).parser,
25-
allowFullScreen: boolean,
25+
allowFullScreen: defaulted(boolean, () => false),
2626
allowTopNavigation: optional(boolean),
2727
static: defaulted(boolean, () => false),
2828
}),

packages/perseus/src/util/parse-perseus-json/perseus-parsers/interaction-widget.ts

+20-24
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
object,
88
optional,
99
pair,
10+
pipeParsers,
1011
string,
1112
union,
1213
} from "../general-purpose-parsers";
14+
import {convert} from "../general-purpose-parsers/convert";
1315
import {defaulted} from "../general-purpose-parsers/defaulted";
1416
import {discriminatedUnionOn} from "../general-purpose-parsers/discriminated-union";
1517

@@ -25,11 +27,12 @@ import type {
2527
const pairOfNumbers = pair(number, number);
2628
const stringOrEmpty = defaulted(string, () => "");
2729

30+
const parseKey = pipeParsers(optional(string)).then(convert(String)).parser;
31+
2832
type FunctionElement = Extract<PerseusInteractionElement, {type: "function"}>;
29-
const parseFunctionType = constant("function");
3033
const parseFunctionElement: Parser<FunctionElement> = object({
31-
type: parseFunctionType,
32-
key: string,
34+
type: constant("function"),
35+
key: parseKey,
3336
options: object({
3437
value: string,
3538
funcName: string,
@@ -42,10 +45,9 @@ const parseFunctionElement: Parser<FunctionElement> = object({
4245
});
4346

4447
type LabelElement = Extract<PerseusInteractionElement, {type: "label"}>;
45-
const parseLabelType = constant("label");
4648
const parseLabelElement: Parser<LabelElement> = object({
47-
type: parseLabelType,
48-
key: string,
49+
type: constant("label"),
50+
key: parseKey,
4951
options: object({
5052
label: string,
5153
color: string,
@@ -55,10 +57,9 @@ const parseLabelElement: Parser<LabelElement> = object({
5557
});
5658

5759
type LineElement = Extract<PerseusInteractionElement, {type: "line"}>;
58-
const parseLineType = constant("line");
5960
const parseLineElement: Parser<LineElement> = object({
60-
type: parseLineType,
61-
key: string,
61+
type: constant("line"),
62+
key: parseKey,
6263
options: object({
6364
color: string,
6465
startX: string,
@@ -75,10 +76,9 @@ type MovableLineElement = Extract<
7576
PerseusInteractionElement,
7677
{type: "movable-line"}
7778
>;
78-
const parseMovableLineType = constant("movable-line");
7979
const parseMovableLineElement: Parser<MovableLineElement> = object({
80-
type: parseMovableLineType,
81-
key: string,
80+
type: constant("movable-line"),
81+
key: parseKey,
8282
options: object({
8383
startX: string,
8484
startY: string,
@@ -100,10 +100,9 @@ type MovablePointElement = Extract<
100100
PerseusInteractionElement,
101101
{type: "movable-point"}
102102
>;
103-
const parseMovablePointType = constant("movable-point");
104103
const parseMovablePointElement: Parser<MovablePointElement> = object({
105-
type: parseMovablePointType,
106-
key: string,
104+
type: constant("movable-point"),
105+
key: parseKey,
107106
options: object({
108107
startX: string,
109108
startY: string,
@@ -122,10 +121,9 @@ type ParametricElement = Extract<
122121
PerseusInteractionElement,
123122
{type: "parametric"}
124123
>;
125-
const parseParametricType = constant("parametric");
126124
const parseParametricElement: Parser<ParametricElement> = object({
127-
type: parseParametricType,
128-
key: string,
125+
type: constant("parametric"),
126+
key: parseKey,
129127
options: object({
130128
x: string,
131129
y: string,
@@ -138,10 +136,9 @@ const parseParametricElement: Parser<ParametricElement> = object({
138136
});
139137

140138
type PointElement = Extract<PerseusInteractionElement, {type: "point"}>;
141-
const parsePointType = constant("point");
142139
const parsePointElement: Parser<PointElement> = object({
143-
type: parsePointType,
144-
key: string,
140+
type: constant("point"),
141+
key: parseKey,
145142
options: object({
146143
color: string,
147144
coordX: string,
@@ -150,10 +147,9 @@ const parsePointElement: Parser<PointElement> = object({
150147
});
151148

152149
type RectangleElement = Extract<PerseusInteractionElement, {type: "rectangle"}>;
153-
const parseRectangleType = constant("rectangle");
154150
const parseRectangleElement: Parser<RectangleElement> = object({
155-
type: parseRectangleType,
156-
key: string,
151+
type: constant("rectangle"),
152+
key: parseKey,
157153
options: object({
158154
color: string,
159155
coordX: string,

packages/perseus/src/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ const parseLockedLineType: Parser<LockedLineType> = object({
213213
points: pair(parseLockedPointType, parseLockedPointType),
214214
color: parseLockedFigureColor,
215215
lineStyle: parseLockedLineStyle,
216-
showPoint1: boolean,
217-
showPoint2: boolean,
216+
showPoint1: defaulted(boolean, () => false),
217+
showPoint2: defaulted(boolean, () => false),
218218
// TODO(benchristel): default labels to empty array?
219219
labels: optional(array(parseLockedLabelType)),
220220
ariaLabel: optional(string),
@@ -266,13 +266,14 @@ const parseLockedFunctionType: Parser<LockedFunctionType> = object({
266266
ariaLabel: optional(string),
267267
});
268268

269-
const parseLockedFigure: Parser<LockedFigure> = union(parseLockedPointType)
270-
.or(parseLockedLineType)
271-
.or(parseLockedVectorType)
272-
.or(parseLockedEllipseType)
273-
.or(parseLockedPolygonType)
274-
.or(parseLockedFunctionType)
275-
.or(parseLockedLabelType).parser;
269+
const parseLockedFigure: Parser<LockedFigure> = discriminatedUnionOn("type")
270+
.withBranch("point", parseLockedPointType)
271+
.withBranch("line", parseLockedLineType)
272+
.withBranch("vector", parseLockedVectorType)
273+
.withBranch("ellipse", parseLockedEllipseType)
274+
.withBranch("polygon", parseLockedPolygonType)
275+
.withBranch("function", parseLockedFunctionType)
276+
.withBranch("label", parseLockedLabelType).parser;
276277

277278
export const parseInteractiveGraphWidget: Parser<InteractiveGraphWidget> =
278279
parseWidget(

packages/perseus/src/util/parse-perseus-json/perseus-parsers/measurer-widget.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ import type {MeasurerWidget} from "@khanacademy/perseus-core";
1717
export const parseMeasurerWidget: Parser<MeasurerWidget> = parseWidget(
1818
constant("measurer"),
1919
object({
20-
image: parsePerseusImageBackground,
20+
// The default value for image comes from measurer.tsx.
21+
// See parse-perseus-json/README.md for why we want to duplicate the
22+
// defaults here.
23+
image: defaulted(parsePerseusImageBackground, () => ({
24+
url: null,
25+
top: 0,
26+
left: 0,
27+
})),
2128
showProtractor: boolean,
2229
showRuler: boolean,
2330
rulerLabel: string,

packages/perseus/src/util/parse-perseus-json/perseus-parsers/plotter-widget.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ export const parsePlotterWidget: Parser<PlotterWidget> = parseWidget(
2424
categories: array(string),
2525
type: enumeration(...plotterPlotTypes),
2626
maxY: number,
27-
scaleY: number,
27+
// The default value for scaleY comes from plotter.tsx.
28+
// See parse-perseus-json/README.md for why we want to duplicate the
29+
// defaults here.
30+
scaleY: defaulted(number, () => 1),
2831
labelInterval: optional(nullable(number)),
29-
snapsPerLine: number,
32+
// The default value for snapsPerLine comes from plotter.tsx.
33+
// See parse-perseus-json/README.md for why we want to duplicate the
34+
// defaults here.
35+
snapsPerLine: defaulted(number, () => 2),
3036
starting: array(number),
3137
correct: array(number),
3238
picUrl: optional(nullable(string)),

0 commit comments

Comments
 (0)