@khanacademy/perseus 0.4.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.eslintrc.js +11 -0
  2. package/CHANGELOG.md +47 -0
  3. package/dist/es/index.js +1078 -1299
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.js +1077 -1304
  6. package/dist/index.js.map +1 -1
  7. package/dist/strings.js +0 -4
  8. package/package.json +9 -8
  9. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +110 -110
  10. package/src/perseus-api.jsx +10 -36
  11. package/src/perseus-types.js +0 -138
  12. package/src/styles/constants.js +0 -2
  13. package/src/styles/variables.less +0 -2
  14. package/src/types.js +18 -35
  15. package/src/util.js +0 -6
  16. package/src/version.js +1 -1
  17. package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
  18. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +110 -110
  19. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +1136 -1160
  20. package/src/widgets/__tests__/radio/base-radio_test.jsx +243 -41
  21. package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
  22. package/src/widgets/__tests__/radio/choice_test.jsx +201 -0
  23. package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
  24. package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
  25. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
  26. package/src/widgets/passage/get-line-height-for-node.js +4 -5
  27. package/src/widgets/passage/passage-markdown.jsx +176 -120
  28. package/src/widgets/passage.jsx +48 -46
  29. package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
  30. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
  31. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
  32. package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
  33. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
  34. package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
  35. package/src/widgets/radio/base-radio.jsx +483 -556
  36. package/src/widgets/radio/choice-icon/choice-icon.jsx +155 -0
  37. package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
  38. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +219 -0
  39. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +129 -0
  40. package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
  41. package/src/widgets/radio/choice-none-above.jsx +49 -40
  42. package/src/widgets/radio/choice.jsx +282 -353
  43. package/src/widgets/radio/choice.jsx.fixture.js +0 -18
  44. package/src/widgets/radio/focus-ring.jsx +29 -22
  45. package/src/widgets/radio/option-status.jsx +58 -36
  46. package/src/widgets/radio/widget.jsx +181 -136
  47. package/src/widgets/radio.jsx +16 -17
  48. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
  49. package/src/widgets/radio/choice-icon.jsx +0 -469
  50. package/src/widgets/radio/cross-out-button.jsx +0 -134
@@ -7,7 +7,6 @@ import _ from "underscore";
7
7
  import PassageMarkdown from "../passage-markdown.jsx";
8
8
 
9
9
  const parse = PassageMarkdown.parse;
10
- const rules = PassageMarkdown._rulesForTesting;
11
10
 
12
11
  const validateParse = (parsed, expected) => {
13
12
  if (!_.isEqual(parsed, expected)) {
@@ -352,14 +351,6 @@ describe.skip("passage markdown", () => {
352
351
  });
353
352
  });
354
353
 
355
- describe("orders", () => {
356
- it("should always be numbers", () => {
357
- _.each(rules, (rule) => {
358
- expect(typeof rule.order === "number").toBeTruthy();
359
- });
360
- });
361
- });
362
-
363
354
  describe("output", () => {
364
355
  it("should output some basic formatted passage text", () => {
365
356
  assertParsesToReact(
@@ -1,10 +1,9 @@
1
1
  // @flow
2
-
3
- type JQueryCollection = $FlowFixMe;
2
+ import $ from "jquery";
4
3
 
5
4
  export const getLineHeightForNode = (
6
- $line1: JQueryCollection,
7
- $line2: JQueryCollection,
5
+ line1: HTMLDivElement,
6
+ line2: HTMLDivElement,
8
7
  ): number => {
9
- return $line2.offset().top - $line1.offset().top;
8
+ return $(line2).offset().top - $(line1).offset().top;
10
9
  };
@@ -1,85 +1,96 @@
1
1
  /* eslint-disable react/sort-comp */
2
2
  // @flow
3
+
3
4
  import SimpleMarkdown from "@khanacademy/simple-markdown";
4
5
  import * as i18n from "@khanacademy/wonder-blocks-i18n";
5
- import PropTypes from "prop-types";
6
6
  import * as React from "react";
7
7
  import _ from "underscore";
8
8
 
9
- const START_REF_PREFIX = "start-ref-";
10
- const END_REF_PREFIX = "end-ref-";
11
- const REF_STYLE = {
12
- display: "inline-block",
13
- width: 0,
14
- visibility: "hidden",
15
- };
9
+ import type {
10
+ Capture,
11
+ Parser,
12
+ SingleASTNode,
13
+ State,
14
+ } from "@khanacademy/simple-markdown";
16
15
 
17
- const LABEL_OUTER_STYLE = {
18
- // for some reason we need these to keep the nbsp from wrapping when the
19
- // inner circle/square is display: inline-block
20
- display: "inline",
21
- whiteSpace: "nowrap",
22
- };
16
+ export type ParseState = {|
17
+ ...State,
18
+ currentRef: number[],
19
+ useRefs: boolean,
20
+ lastRef: number,
21
+ firstSentenceRef: ?string,
22
+ firstQuestionRef: ?string,
23
+ lastFootnote: {id: number, text: string},
24
+ |};
23
25
 
24
- const SQUARE_LABEL_STYLE = {
25
- display: "inline-block",
26
- color: "rgb(255, 255, 255)",
27
- backgroundColor: "rgb(90, 90, 90)",
28
- paddingLeft: 10,
29
- paddingRight: 10,
30
- userSelect: "none",
31
- WebkitUserSelect: "none",
32
- };
26
+ type OutputFun = $FlowFixMe;
33
27
 
34
- const CIRCLE_LABEL_STYLE = {
35
- display: "inline-block",
36
- color: "rgb(255, 255, 255)",
37
- backgroundColor: "rgb(90, 90, 90)",
38
- userSelect: "none",
39
- WebkitUserSelect: "none",
40
- width: 22,
41
- height: 22,
42
- borderRadius: "50%",
43
- textAlign: "center",
44
- };
28
+ type FootnoteType = {|
29
+ id: number,
30
+ text: string,
31
+ |};
32
+
33
+ type RefStartNode = {|
34
+ ref: ?number,
35
+ refContent: React.Node,
36
+ |};
45
37
 
46
- class RefStart extends React.Component<$FlowFixMe> {
47
- static propTypes = {
48
- refContent: PropTypes.node.isRequired,
38
+ type RefEndNode = {|
39
+ ref: ?number,
40
+ |};
41
+
42
+ type LabelNode = {|
43
+ content: string,
44
+ space: boolean,
45
+ |};
46
+
47
+ type HighlightNode = {|
48
+ content: string,
49
+ |};
50
+
51
+ type RefStartProps = {|
52
+ refContent: React.Node,
53
+ |};
54
+
55
+ function getInitialParseState(): ParseState {
56
+ return {
57
+ currentRef: [],
58
+ useRefs: true,
59
+ lastRef: 0,
60
+ firstSentenceRef: null,
61
+ firstQuestionRef: null,
62
+ lastFootnote: {id: 0, text: ""},
49
63
  };
64
+ }
50
65
 
66
+ class RefStart extends React.Component<RefStartProps> {
51
67
  render(): React.Node {
52
68
  return <span style={REF_STYLE}>{i18n.doNotTranslate("_")}</span>;
53
69
  }
54
70
 
55
- getRefContent = () => {
71
+ getRefContent: () => React.Node = () => {
56
72
  return this.props.refContent;
57
73
  };
58
74
  }
59
75
 
60
- class RefEnd extends React.Component<$FlowFixMe> {
76
+ class RefEnd extends React.Component<{}> {
61
77
  render(): React.Node {
62
78
  return <span style={REF_STYLE}>{i18n.doNotTranslate("_")}</span>;
63
79
  }
64
80
  }
65
81
 
66
- const rules: $FlowFixMe = {
67
- // $FlowFixMe[prop-missing]
68
- // $FlowFixMe[incompatible-use]
82
+ const rules = {
69
83
  newline: SimpleMarkdown.defaultRules.newline,
70
- // $FlowFixMe[prop-missing]
71
- // $FlowFixMe[incompatible-use]
72
84
  paragraph: SimpleMarkdown.defaultRules.paragraph,
73
- // $FlowFixMe[prop-missing]
74
- // $FlowFixMe[incompatible-use]
75
85
  escape: SimpleMarkdown.defaultRules.escape,
76
86
  passageFootnote: {
77
- // $FlowFixMe[prop-missing]
78
- // $FlowFixMe[incompatible-use]
79
87
  order: SimpleMarkdown.defaultRules.escape.order + 0.1,
80
- // $FlowFixMe[prop-missing]
81
88
  match: SimpleMarkdown.inlineRegex(/^\^/),
82
- parse: (capture, parse, state) => {
89
+ parse: (
90
+ capture: Capture,
91
+ parse: Parser,
92
+ state: ParseState,
93
+ ): FootnoteType => {
83
94
  // if no footnotes have been seen, we're id 1. otherwise,
84
95
  // we're the next subsequent id
85
96
  const id = state.lastFootnote.id + 1;
@@ -100,15 +111,13 @@ const rules: $FlowFixMe = {
100
111
  state.lastFootnote = footnote;
101
112
  return footnote;
102
113
  },
103
- react: (node, output, state) => {
114
+ react: (node: FootnoteType, output: OutputFun, state: ParseState) => {
104
115
  return <sup key={state.key}>{node.text}</sup>;
105
116
  },
106
117
  },
107
118
  refStart: {
108
- // $FlowFixMe[prop-missing]
109
- // $FlowFixMe[incompatible-use]
110
119
  order: SimpleMarkdown.defaultRules.escape.order + 0.2,
111
- match: function (source, state) {
120
+ match: function (source) {
112
121
  const capture = /^\{\{/.exec(source);
113
122
  if (capture) {
114
123
  // We need to do extra processing here to capture the
@@ -138,15 +147,20 @@ const rules: $FlowFixMe = {
138
147
  closeIndex++;
139
148
  }
140
149
 
141
- const refText = source.slice(2, closeIndex);
142
-
143
150
  // A "magic" capture that matches the opening {{
144
151
  // but captures the full ref text internally :D
145
- return [capture[0], refText];
152
+ // unfortunately the "magic" makes Flow upset,
153
+ // so there's this questionable workaround
154
+ capture[1] = source.slice(2, closeIndex);
155
+ return capture;
146
156
  }
147
157
  return null;
148
158
  },
149
- parse: (capture, parse, state) => {
159
+ parse: (
160
+ capture: Capture,
161
+ parse: Parser,
162
+ state: ParseState,
163
+ ): RefStartNode => {
150
164
  if (!state.useRefs) {
151
165
  return {
152
166
  ref: null,
@@ -169,7 +183,7 @@ const rules: $FlowFixMe = {
169
183
  // passage, for instance!
170
184
  useRefs: false,
171
185
  },
172
- INITIAL_PARSE_STATE,
186
+ getInitialParseState(),
173
187
  ),
174
188
  );
175
189
 
@@ -178,8 +192,9 @@ const rules: $FlowFixMe = {
178
192
  refContent: refContent,
179
193
  };
180
194
  },
181
- react: (node, output, state) => {
182
- if (node.ref == null) {
195
+ react: (node: RefStartNode, output: OutputFun, state: ParseState) => {
196
+ const ref = node.ref;
197
+ if (ref == null) {
183
198
  return null;
184
199
  }
185
200
 
@@ -187,22 +202,26 @@ const rules: $FlowFixMe = {
187
202
  // and output out-of-band. We don't want to affect
188
203
  // our state by the double-output here :).
189
204
  const refContent = output(node.refContent, {});
205
+
206
+ // note(matthewc) the refs created here become the refs
207
+ // pulled from `this.refs` in passage.jsx
190
208
  return (
191
209
  <RefStart
192
- ref={START_REF_PREFIX + node.ref}
193
- key={START_REF_PREFIX + node.ref}
210
+ ref={START_REF_PREFIX + ref}
211
+ key={START_REF_PREFIX + ref}
194
212
  refContent={refContent}
195
213
  />
196
214
  );
197
215
  },
198
216
  },
199
217
  refEnd: {
200
- // $FlowFixMe[prop-missing]
201
- // $FlowFixMe[incompatible-use]
202
218
  order: SimpleMarkdown.defaultRules.escape.order + 0.3,
203
- // $FlowFixMe[prop-missing]
204
219
  match: SimpleMarkdown.inlineRegex(/^\}\}/),
205
- parse: (capture, parse, state) => {
220
+ parse: (
221
+ capture: Capture,
222
+ parse: Parser,
223
+ state: ParseState,
224
+ ): RefEndNode => {
206
225
  if (!state.useRefs) {
207
226
  return {
208
227
  ref: null,
@@ -214,8 +233,10 @@ const rules: $FlowFixMe = {
214
233
  ref: ref,
215
234
  };
216
235
  },
217
- react: (node, output, state) => {
236
+ react: (node: RefEndNode, output: OutputFun, state: ParseState) => {
218
237
  if (node.ref != null) {
238
+ // note(matthewc) the refs created here become the refs
239
+ // pulled from `this.refs` in passage.jsx
219
240
  return (
220
241
  <RefEnd
221
242
  ref={END_REF_PREFIX + node.ref}
@@ -230,12 +251,13 @@ const rules: $FlowFixMe = {
230
251
  },
231
252
  },
232
253
  squareLabel: {
233
- // $FlowFixMe[prop-missing]
234
- // $FlowFixMe[incompatible-use]
235
254
  order: SimpleMarkdown.defaultRules.escape.order + 0.4,
236
- // $FlowFixMe[prop-missing]
237
255
  match: SimpleMarkdown.inlineRegex(/^\[\[(\w+)\]\]( *)/),
238
- parse: (capture, parse, state) => {
256
+ parse: (
257
+ capture: Capture,
258
+ parse: Parser,
259
+ state: ParseState,
260
+ ): LabelNode => {
239
261
  if (!state.firstQuestionRef) {
240
262
  state.firstQuestionRef = capture[1];
241
263
  }
@@ -244,7 +266,7 @@ const rules: $FlowFixMe = {
244
266
  space: capture[2].length > 0,
245
267
  };
246
268
  },
247
- react: (node, output, state) => {
269
+ react: (node: LabelNode, output: OutputFun, state: ParseState) => {
248
270
  return [
249
271
  <span
250
272
  key="visual-square"
@@ -264,18 +286,23 @@ const rules: $FlowFixMe = {
264
286
  },
265
287
  },
266
288
  circleLabel: {
267
- // $FlowFixMe[prop-missing]
268
- // $FlowFixMe[incompatible-use]
269
289
  order: SimpleMarkdown.defaultRules.escape.order + 0.5,
270
- // $FlowFixMe[prop-missing]
271
290
  match: SimpleMarkdown.inlineRegex(/^\(\((\w+)\)\)( *)/),
272
- parse: (capture, parse, state) => {
291
+ parse: (
292
+ capture: Capture,
293
+ parse: Parser,
294
+ state: ParseState,
295
+ ): LabelNode => {
273
296
  return {
274
297
  content: capture[1],
275
298
  space: capture[2].length > 0,
276
299
  };
277
300
  },
278
- react: (node, output, state) => {
301
+ react: (
302
+ node: LabelNode,
303
+ output: OutputFun,
304
+ state: ParseState,
305
+ ): React.Node => {
279
306
  return [
280
307
  <span
281
308
  key="visual-circle"
@@ -295,12 +322,13 @@ const rules: $FlowFixMe = {
295
322
  },
296
323
  },
297
324
  squareBracketRef: {
298
- // $FlowFixMe[prop-missing]
299
- // $FlowFixMe[incompatible-use]
300
325
  order: SimpleMarkdown.defaultRules.escape.order + 0.6,
301
- // $FlowFixMe[prop-missing]
302
326
  match: SimpleMarkdown.inlineRegex(/^\[(\d+)\]( *)/),
303
- parse: (capture, parse, state) => {
327
+ parse: (
328
+ capture: Capture,
329
+ parse: Parser,
330
+ state: ParseState,
331
+ ): LabelNode => {
304
332
  if (!state.firstSentenceRef) {
305
333
  state.firstSentenceRef = capture[1];
306
334
  }
@@ -309,7 +337,7 @@ const rules: $FlowFixMe = {
309
337
  space: capture[2].length > 0,
310
338
  };
311
339
  },
312
- react: (node, output, state) => {
340
+ react: (node: LabelNode, output: OutputFun, state: ParseState) => {
313
341
  return [
314
342
  <span
315
343
  key="visual-brackets"
@@ -326,19 +354,20 @@ const rules: $FlowFixMe = {
326
354
  },
327
355
  },
328
356
  highlight: {
329
- // $FlowFixMe[prop-missing]
330
- // $FlowFixMe[incompatible-use]
331
357
  order: SimpleMarkdown.defaultRules.escape.order + 0.7,
332
- // $FlowFixMe[prop-missing]
333
358
  match: SimpleMarkdown.inlineRegex(
334
359
  /^{highlighting.start}(.+?){highlighting.end}/,
335
360
  ),
336
- parse: (capture, parse, state) => {
361
+ parse: (
362
+ capture: Capture,
363
+ parse: Parser,
364
+ state: ParseState,
365
+ ): HighlightNode => {
337
366
  return {
338
367
  content: capture[1],
339
368
  };
340
369
  },
341
- react: (node, output, state) => {
370
+ react: (node: HighlightNode, output: OutputFun, state: ParseState) => {
342
371
  return [
343
372
  <span key={0} className="perseus-highlight">
344
373
  {node.content}
@@ -347,19 +376,20 @@ const rules: $FlowFixMe = {
347
376
  },
348
377
  },
349
378
  reviewHighlight: {
350
- // $FlowFixMe[prop-missing]
351
- // $FlowFixMe[incompatible-use]
352
379
  order: SimpleMarkdown.defaultRules.escape.order + 0.7,
353
- // $FlowFixMe[prop-missing]
354
380
  match: SimpleMarkdown.inlineRegex(
355
381
  /^{review-highlighting.start}(.+?){review-highlighting.end}/,
356
382
  ),
357
- parse: (capture, parse, state) => {
383
+ parse: (
384
+ capture: Capture,
385
+ parse: Parser,
386
+ state: ParseState,
387
+ ): HighlightNode => {
358
388
  return {
359
389
  content: capture[1],
360
390
  };
361
391
  },
362
- react: (node, output, state) => {
392
+ react: (node: HighlightNode, output: OutputFun, state: ParseState) => {
363
393
  return [
364
394
  <span key={0} className="perseus-review-highlight">
365
395
  {node.content}
@@ -367,21 +397,11 @@ const rules: $FlowFixMe = {
367
397
  ];
368
398
  },
369
399
  },
370
- // $FlowFixMe[prop-missing]
371
- // $FlowFixMe[incompatible-use]
372
400
  strong: SimpleMarkdown.defaultRules.strong,
373
- // $FlowFixMe[prop-missing]
374
- // $FlowFixMe[incompatible-use]
375
401
  u: SimpleMarkdown.defaultRules.u,
376
- // $FlowFixMe[prop-missing]
377
- // $FlowFixMe[incompatible-use]
378
402
  em: SimpleMarkdown.defaultRules.em,
379
- // $FlowFixMe[prop-missing]
380
- // $FlowFixMe[incompatible-use]
381
403
  del: SimpleMarkdown.defaultRules.del,
382
404
  text: {
383
- // $FlowFixMe[prop-missing]
384
- // $FlowFixMe[incompatible-use]
385
405
  ...SimpleMarkdown.defaultRules.text,
386
406
  react(node, output, state) {
387
407
  return <span key={state.key}>{node.content}</span>;
@@ -389,28 +409,64 @@ const rules: $FlowFixMe = {
389
409
  },
390
410
  };
391
411
 
392
- const INITIAL_PARSE_STATE = {
393
- currentRef: [],
394
- useRefs: true,
395
- lastRef: 0,
396
- lastFootnote: {id: 0, text: ""},
412
+ const START_REF_PREFIX = "start-ref-";
413
+ const END_REF_PREFIX = "end-ref-";
414
+ const REF_STYLE = {
415
+ display: "inline-block",
416
+ width: 0,
417
+ visibility: "hidden",
418
+ };
419
+
420
+ const LABEL_OUTER_STYLE = {
421
+ // for some reason we need these to keep the nbsp from wrapping when the
422
+ // inner circle/square is display: inline-block
423
+ display: "inline",
424
+ whiteSpace: "nowrap",
397
425
  };
398
- // $FlowFixMe[prop-missing]
426
+
427
+ const SQUARE_LABEL_STYLE = {
428
+ display: "inline-block",
429
+ color: "rgb(255, 255, 255)",
430
+ backgroundColor: "rgb(90, 90, 90)",
431
+ paddingLeft: 10,
432
+ paddingRight: 10,
433
+ userSelect: "none",
434
+ WebkitUserSelect: "none",
435
+ };
436
+
437
+ const CIRCLE_LABEL_STYLE = {
438
+ display: "inline-block",
439
+ color: "rgb(255, 255, 255)",
440
+ backgroundColor: "rgb(90, 90, 90)",
441
+ userSelect: "none",
442
+ WebkitUserSelect: "none",
443
+ width: 22,
444
+ height: 22,
445
+ borderRadius: "50%",
446
+ textAlign: "center",
447
+ };
448
+
399
449
  const builtParser = SimpleMarkdown.parserFor(rules);
400
- const parse: (string, $FlowFixMe) => $FlowFixMe = (source, state) => {
450
+ const parse: (string, ?ParseState) => Array<SingleASTNode> = (
451
+ source,
452
+ state,
453
+ ) => {
401
454
  state = state || {};
402
455
  const paragraphedSource = source + "\n\n";
403
- return builtParser(paragraphedSource, _.extend(state, INITIAL_PARSE_STATE));
456
+ return builtParser(
457
+ paragraphedSource,
458
+ _.extend(state, getInitialParseState()),
459
+ );
404
460
  };
405
461
 
462
+ const output: (Array<SingleASTNode>) => React.Node = SimpleMarkdown.reactFor(
463
+ SimpleMarkdown.ruleOutput(rules, "react"),
464
+ );
465
+
406
466
  export default {
407
- parse: parse,
408
- // $FlowFixMe[prop-missing]
409
- output: (SimpleMarkdown.reactFor(
410
- // $FlowFixMe[prop-missing]
411
- SimpleMarkdown.ruleOutput(rules, "react"),
412
- ): $FlowFixMe),
413
- START_REF_PREFIX: START_REF_PREFIX,
414
- END_REF_PREFIX: END_REF_PREFIX,
415
- _rulesForTesting: rules,
467
+ parse,
468
+ output,
469
+ START_REF_PREFIX,
470
+ END_REF_PREFIX,
471
+ getInitialParseState,
416
472
  };