@khanacademy/perseus-editor 0.0.1 → 0.1.2

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 (127) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/es/index.css +1020 -0
  4. package/dist/es/index.js +9 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +1020 -0
  7. package/dist/index.js +9 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +6 -1
  10. package/package.json +16 -4
  11. package/src/README.md +7 -0
  12. package/src/__stories__/editor.stories.js +33 -0
  13. package/src/__stories__/section-control-button.stories.jsx +27 -0
  14. package/src/__testdata__/input-number_testdata.js +28 -0
  15. package/src/__tests__/editor_test.jsx +123 -0
  16. package/src/__tests__/i18n_test.jsx +585 -0
  17. package/src/__tests__/traversal_test.jsx +378 -0
  18. package/src/all-editors.js +85 -0
  19. package/src/all-editors_test.js +17 -0
  20. package/src/all-widgets.js +10 -0
  21. package/src/article-editor.jsx +437 -0
  22. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  23. package/src/components/blur-input.jsx +62 -0
  24. package/src/components/checkbox.jsx +194 -0
  25. package/src/components/device-framer.jsx +91 -0
  26. package/src/components/drag-target.jsx +88 -0
  27. package/src/components/dropdown-option.jsx +318 -0
  28. package/src/components/form-wrapped-text-field.jsx +248 -0
  29. package/src/components/graph-settings.jsx +631 -0
  30. package/src/components/hover-behavior.jsx +178 -0
  31. package/src/components/json-editor.jsx +121 -0
  32. package/src/components/link.jsx +173 -0
  33. package/src/components/simple-button.jsx +86 -0
  34. package/src/components/sortable.jsx +186 -0
  35. package/src/components/util.js +39 -0
  36. package/src/components/viewport-resizer.jsx +73 -0
  37. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  38. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  39. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  40. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  41. package/src/diffs/__tests__/split-diff_test.js +34 -0
  42. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  43. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  44. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  45. package/src/diffs/article-diff.jsx +70 -0
  46. package/src/diffs/item-diff.jsx +77 -0
  47. package/src/diffs/renderer-diff.jsx +143 -0
  48. package/src/diffs/split-diff.jsx +30 -0
  49. package/src/diffs/string-array-diff.jsx +75 -0
  50. package/src/diffs/structured-item-diff.jsx +269 -0
  51. package/src/diffs/tags-diff.jsx +50 -0
  52. package/src/diffs/text-diff.jsx +194 -0
  53. package/src/diffs/widget-diff-performer.jsx +74 -0
  54. package/src/diffs/widget-diff.jsx +291 -0
  55. package/src/editor-page.jsx +351 -0
  56. package/src/editor.jsx +1364 -0
  57. package/src/hint-editor.jsx +447 -0
  58. package/src/i18n.jsx +139 -0
  59. package/src/iframe-content-renderer.jsx +201 -0
  60. package/src/index.js +27 -8
  61. package/src/item-editor.jsx +159 -0
  62. package/src/item-extras-editor.jsx +150 -0
  63. package/src/katex-error-view.jsx +87 -0
  64. package/src/multirenderer-editor.jsx +1074 -0
  65. package/src/section-control-button.jsx +53 -0
  66. package/src/stateful-article-editor.jsx +176 -0
  67. package/src/stateful-editor-page.jsx +90 -0
  68. package/src/styles/perseus-editor.less +1339 -0
  69. package/src/testing-widgets.js +12 -0
  70. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  71. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  72. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  73. package/src/widgets/categorizer-editor.jsx +103 -0
  74. package/src/widgets/cs-program-editor.jsx +258 -0
  75. package/src/widgets/definition-editor.jsx +75 -0
  76. package/src/widgets/dropdown-editor.jsx +201 -0
  77. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  78. package/src/widgets/example-graphie-widget.jsx +175 -0
  79. package/src/widgets/example-widget-editor.jsx +60 -0
  80. package/src/widgets/example-widget.jsx +153 -0
  81. package/src/widgets/explanation-editor.jsx +89 -0
  82. package/src/widgets/expression-editor.jsx +560 -0
  83. package/src/widgets/graded-group-editor.jsx +162 -0
  84. package/src/widgets/graded-group-set-editor.jsx +101 -0
  85. package/src/widgets/grapher-editor.jsx +154 -0
  86. package/src/widgets/group-editor.jsx +83 -0
  87. package/src/widgets/iframe-editor.jsx +200 -0
  88. package/src/widgets/image-editor.jsx +326 -0
  89. package/src/widgets/input-number-editor.jsx +269 -0
  90. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  91. package/src/widgets/interaction-editor.jsx +1389 -0
  92. package/src/widgets/interactive-graph-editor.jsx +322 -0
  93. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  94. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  95. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  96. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  97. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  98. package/src/widgets/label-image/answer-choices.jsx +231 -0
  99. package/src/widgets/label-image/behavior.jsx +92 -0
  100. package/src/widgets/label-image/marker.jsx +292 -0
  101. package/src/widgets/label-image/question-markers.jsx +181 -0
  102. package/src/widgets/label-image/select-image.jsx +86 -0
  103. package/src/widgets/label-image-editor.jsx +257 -0
  104. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  105. package/src/widgets/matcher-editor.jsx +147 -0
  106. package/src/widgets/matrix-editor.jsx +155 -0
  107. package/src/widgets/measurer-editor.jsx +245 -0
  108. package/src/widgets/molecule-editor.jsx +63 -0
  109. package/src/widgets/number-line-editor.jsx +498 -0
  110. package/src/widgets/numeric-input-editor.jsx +554 -0
  111. package/src/widgets/orderer-editor.jsx +166 -0
  112. package/src/widgets/passage-editor.jsx +115 -0
  113. package/src/widgets/passage-ref-editor.jsx +77 -0
  114. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  115. package/src/widgets/plotter-editor.jsx +537 -0
  116. package/src/widgets/radio/editor.jsx +398 -0
  117. package/src/widgets/reaction-diagram-editor.jsx +127 -0
  118. package/src/widgets/sequence-editor.jsx +205 -0
  119. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  120. package/src/widgets/simple-markdown-tester.jsx +91 -0
  121. package/src/widgets/simulator-editor.jsx +144 -0
  122. package/src/widgets/sorter-editor.jsx +100 -0
  123. package/src/widgets/table-editor.jsx +164 -0
  124. package/src/widgets/transformer-editor.jsx +1006 -0
  125. package/src/widgets/unit-editor.jsx +293 -0
  126. package/src/widgets/video-editor.jsx +74 -0
  127. package/src/__tests__/index.test.js +0 -15
@@ -0,0 +1,70 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // @flow
3
+ /**
4
+ * A side by side diff view for Perseus articles.
5
+ */
6
+
7
+ import PropTypes from "prop-types";
8
+ import * as React from "react";
9
+ import _ from "underscore";
10
+
11
+ import RendererDiff from "./renderer-diff.jsx";
12
+
13
+ const rendererProps = PropTypes.shape({
14
+ content: PropTypes.string,
15
+ images: PropTypes.objectOf(PropTypes.any),
16
+ widgets: PropTypes.objectOf(PropTypes.any),
17
+ });
18
+
19
+ type Props = $FlowFixMe;
20
+ type State = $FlowFixMe;
21
+
22
+ class ArticleDiff extends React.Component<Props, State> {
23
+ static propTypes = {
24
+ // TODO(alex): Check whether we still have any Perseus articles whose
25
+ // top-level json is an object, not an array. If not, simplify here.
26
+ after: PropTypes.oneOfType([
27
+ rendererProps,
28
+ PropTypes.arrayOf(rendererProps),
29
+ ]).isRequired,
30
+ before: PropTypes.oneOfType([
31
+ rendererProps,
32
+ PropTypes.arrayOf(rendererProps),
33
+ ]).isRequired,
34
+ };
35
+
36
+ static _stateFromProps: (Props) => State = (props) => {
37
+ const {before, after} = props;
38
+ return {
39
+ before: Array.isArray(before) ? before : [before],
40
+ after: Array.isArray(after) ? after : [after],
41
+ };
42
+ };
43
+
44
+ state: State = ArticleDiff._stateFromProps(this.props);
45
+
46
+ UNSAFE_componentWillReceiveProps(nextProps: Props) {
47
+ this.setState(ArticleDiff._stateFromProps(nextProps));
48
+ }
49
+
50
+ render(): React.Node {
51
+ const {before, after} = this.state;
52
+
53
+ const sectionCount = Math.max(before.length, after.length);
54
+
55
+ const sections = _.times(sectionCount, (n) => (
56
+ <RendererDiff
57
+ before={n < before.length ? before[n] : undefined}
58
+ after={n < after.length ? after[n] : undefined}
59
+ title={`Section ${n + 1}`}
60
+ showAlignmentOptions={true}
61
+ showSeparator={n < sectionCount - 1}
62
+ key={n}
63
+ />
64
+ ));
65
+
66
+ return <div className="framework-perseus">{sections}</div>;
67
+ }
68
+ }
69
+
70
+ export default ArticleDiff;
@@ -0,0 +1,77 @@
1
+ // @flow
2
+ /**
3
+ * A side by side diff view for Perseus exercise items
4
+ * in the standard layout.
5
+ */
6
+
7
+ import PropTypes from "prop-types";
8
+ import * as React from "react";
9
+ import _ from "underscore";
10
+
11
+ import RendererDiff from "./renderer-diff.jsx";
12
+ import WidgetDiff from "./widget-diff.jsx";
13
+
14
+ const itemProps = PropTypes.shape({
15
+ question: PropTypes.shape({}).isRequired,
16
+ answerArea: PropTypes.shape({}).isRequired,
17
+ hints: PropTypes.arrayOf(PropTypes.any).isRequired,
18
+ });
19
+
20
+ type Props = $FlowFixMe;
21
+
22
+ class ItemDiff extends React.Component<Props> {
23
+ static propTypes = {
24
+ after: itemProps.isRequired,
25
+ before: itemProps.isRequired,
26
+ };
27
+
28
+ render(): React.Node {
29
+ const {before, after} = this.props;
30
+
31
+ const hintCount = Math.max(before.hints.length, after.hints.length);
32
+
33
+ const question = (
34
+ <RendererDiff
35
+ before={before.question}
36
+ after={after.question}
37
+ title="Question"
38
+ showAlignmentOptions={false}
39
+ showSeparator={true}
40
+ />
41
+ );
42
+
43
+ const extras = (
44
+ <WidgetDiff
45
+ before={before.answerArea}
46
+ after={after.answerArea}
47
+ title="Question extras"
48
+ />
49
+ );
50
+
51
+ const hints = _.times(hintCount, function (n) {
52
+ return (
53
+ <RendererDiff
54
+ before={
55
+ n < before.hints.length ? before.hints[n] : undefined
56
+ }
57
+ after={n < after.hints.length ? after.hints[n] : undefined}
58
+ title={`Hint ${n + 1}`}
59
+ showAlignmentOptions={false}
60
+ showSeparator={n < hintCount - 1}
61
+ key={n}
62
+ />
63
+ );
64
+ });
65
+
66
+ return (
67
+ <div className="framework-perseus">
68
+ {question}
69
+ {extras}
70
+ {hints && <div className="diff-separator" />}
71
+ {hints}
72
+ </div>
73
+ );
74
+ }
75
+ }
76
+
77
+ export default ItemDiff;
@@ -0,0 +1,143 @@
1
+ // @flow
2
+ /**
3
+ * A side by side diff view for Perseus renderers.
4
+ */
5
+ import {Widgets} from "@khanacademy/perseus";
6
+ import * as React from "react";
7
+ import _ from "underscore";
8
+
9
+ import TextDiff from "./text-diff.jsx";
10
+ import WidgetDiff from "./widget-diff.jsx";
11
+
12
+ import type {PerseusRenderer} from "@khanacademy/perseus";
13
+
14
+ // In diffs, only show the widgetInfo props that can change
15
+ const filterWidgetInfo = function (widgetInfo, showAlignmentOptions) {
16
+ const {alignment, graded, options, type} = widgetInfo || {};
17
+
18
+ const filteredWidgetInfo = {options};
19
+
20
+ // Show alignment options iff multiple valid ones exist for this widget
21
+ if (
22
+ showAlignmentOptions &&
23
+ Widgets.getSupportedAlignments(type).length > 1
24
+ ) {
25
+ // $FlowFixMe[prop-missing]
26
+ filteredWidgetInfo.alignment = alignment;
27
+ }
28
+
29
+ if (type === "transformer") {
30
+ // $FlowFixMe[prop-missing]
31
+ filteredWidgetInfo.graded = graded;
32
+ }
33
+
34
+ if (Widgets.supportsStaticMode(type)) {
35
+ // $FlowFixMe[prop-missing]
36
+ filteredWidgetInfo.static = widgetInfo?.static ?? undefined;
37
+ }
38
+
39
+ return filteredWidgetInfo;
40
+ };
41
+
42
+ // TODO(michaelpolyak): This type is very similar to `PerseusRenderer` type
43
+ // found in `perseus-all-package/perseus-type.js`, consider just using it.
44
+ type RendererProps = {|
45
+ content: string,
46
+
47
+ // NOTE: images and widgets may not be set for some items hints,
48
+ // specifically in old revisions, which may only be loaded for diffing.
49
+ widgets: ?PerseusRenderer["widgets"],
50
+ images: ?PerseusRenderer["images"],
51
+ |};
52
+
53
+ type Props = {|
54
+ // The "after" props of the renderer. Will be displayed on the right.
55
+ after: RendererProps,
56
+
57
+ // The "before" props of the renderer. Will be displayed on the left.
58
+ before: RendererProps,
59
+
60
+ // If true, show widget alignment options in the diff.
61
+ showAlignmentOptions: boolean,
62
+
63
+ // If true, render a horizontal rule after this diff.
64
+ showSeparator: boolean,
65
+
66
+ // The heading to render above the side by side diff.
67
+ // (In a code review tool this would be the filename.)
68
+ title: string,
69
+ |};
70
+
71
+ class RendererDiff extends React.Component<Props> {
72
+ static defaultProps: $Rest<Props, {|title: string|}> = {
73
+ after: {
74
+ content: "",
75
+ images: {},
76
+ widgets: {},
77
+ },
78
+ before: {
79
+ content: "",
80
+ images: {},
81
+ widgets: {},
82
+ },
83
+ showAlignmentOptions: false,
84
+ showSeparator: false,
85
+ };
86
+
87
+ render(): React.Node {
88
+ const {after, before, showAlignmentOptions, showSeparator, title} =
89
+ this.props;
90
+
91
+ let textDiff;
92
+ let widgetsDiff;
93
+
94
+ if (before.content || after.content) {
95
+ textDiff = (
96
+ <TextDiff
97
+ before={before.content}
98
+ after={after.content}
99
+ title={title}
100
+ />
101
+ );
102
+ }
103
+
104
+ const beforeWidgets = Object.keys(before.widgets ?? {}).filter(
105
+ (widget) => before.content.includes(widget),
106
+ );
107
+ const afterWidgets = Object.keys(after.widgets ?? {}).filter((widget) =>
108
+ after.content.includes(widget),
109
+ );
110
+
111
+ if (beforeWidgets.length || afterWidgets.length) {
112
+ const widgets = _.union(beforeWidgets, afterWidgets);
113
+ widgetsDiff = widgets.map((widget) => (
114
+ <WidgetDiff
115
+ before={filterWidgetInfo(
116
+ before.widgets?.[widget],
117
+ showAlignmentOptions,
118
+ )}
119
+ after={filterWidgetInfo(
120
+ after.widgets?.[widget],
121
+ showAlignmentOptions,
122
+ )}
123
+ title={widget}
124
+ type={
125
+ (before.widgets?.[widget] ?? {}).type ||
126
+ (after.widgets?.[widget] ?? {}).type
127
+ }
128
+ key={widget}
129
+ />
130
+ ));
131
+ }
132
+
133
+ return (
134
+ <div>
135
+ {textDiff}
136
+ {widgetsDiff}
137
+ {showSeparator && <div className="diff-separator" />}
138
+ </div>
139
+ );
140
+ }
141
+ }
142
+
143
+ export default RendererDiff;
@@ -0,0 +1,30 @@
1
+ // @flow
2
+ import _ from "underscore";
3
+
4
+ // Split a word-wise diff generated by jsdiff into multiple lines, for the
5
+ // purpose of breaking up the diffs into lines, so that modified lines can be
6
+ // faintly highlighted
7
+
8
+ const splitDiff = function (diffEntries: $FlowFixMe): $FlowFixMe {
9
+ const lines = [];
10
+ let currentLine = [];
11
+ _.each(diffEntries, (entry) => {
12
+ const values = entry.value.split("\n");
13
+ _.each(values, (value, i) => {
14
+ const isNewline = i > 0;
15
+ if (isNewline) {
16
+ lines.push(currentLine);
17
+ currentLine = [];
18
+ }
19
+ const newEntry = _.extend({}, entry, {value: value});
20
+ currentLine.push(newEntry);
21
+ });
22
+ });
23
+
24
+ if (currentLine.length) {
25
+ lines.push(currentLine);
26
+ }
27
+ return lines;
28
+ };
29
+
30
+ export default splitDiff;
@@ -0,0 +1,75 @@
1
+ // @flow
2
+ // eslint-disable-next-line import/no-extraneous-dependencies
3
+ import jsdiff from "jsdiff";
4
+ import _ from "underscore";
5
+
6
+ const statusFor = function (chunk) {
7
+ if (chunk.added) {
8
+ return "added";
9
+ }
10
+ if (chunk.removed) {
11
+ return "removed";
12
+ }
13
+ return "unchanged";
14
+ };
15
+
16
+ // Turn a chunk (which contains an array of values and a status)
17
+ // into an array of values, each with the same status
18
+ const splitUpChunk = (chunk) =>
19
+ _.map(chunk.value, (value) => {
20
+ return {
21
+ value: value,
22
+ status: statusFor(chunk),
23
+ };
24
+ });
25
+
26
+ // Apply `fn` to every element in `lst` and then concatenate all the results
27
+ // http://clojuredocs.org/clojure_core/clojure.core/mapcat
28
+ const mapcat = function (lst, fn) {
29
+ return _.flatten(_.map(lst, fn), true /* only flatten one level */);
30
+ };
31
+
32
+ // > ArrayDiff.diff([1,2,3], [2,3,4]);
33
+ // = [{ "value": [1],
34
+ // "removed": true },
35
+ // { "value": [2, 3] },
36
+ // { "value": [4],
37
+ // "added": true }]
38
+ const ArrayDiff = new jsdiff.Diff();
39
+ ArrayDiff.tokenize = (array) => _.map(array, (elem) => [elem]);
40
+ // The default is `+` for string concatenation, which doesn't work for array
41
+ // concatenation.
42
+ ArrayDiff.join = (a, b) => a.concat(b);
43
+ // By default jsDiff uses ===
44
+ ArrayDiff.equals = _.isEqual;
45
+
46
+ // Take the output of jsdiff's function (which concatenates adjacent entries)
47
+ // and make it just one entry per chunk
48
+ // > flattenChunks([{ "value": [1],
49
+ // "removed": true },
50
+ // { "value": [2, 3] },
51
+ // { "value": [4],
52
+ // "added": true }])
53
+ // = [{ "value":1, "status":"removed"},
54
+ // { "value":2, "status":"unchanged"},
55
+ // { "value":3, "status":"unchanged"},
56
+ // { "value":4, "status":"added"}]
57
+ const flattenChunks = (chunks) => mapcat(chunks, splitUpChunk);
58
+
59
+ // Take two arrays and create a diff for them. The result is two arrays of
60
+ // objects, one for the things that should be included in a 'before', and one
61
+ // for 'after'
62
+ const stringArrayDiff = function (
63
+ a: $ReadOnlyArray<$FlowFixMe>,
64
+ b: $ReadOnlyArray<$FlowFixMe>,
65
+ ): $FlowFixMe {
66
+ const diffResult = ArrayDiff.diff(a, b);
67
+ const flattened = flattenChunks(diffResult);
68
+
69
+ return {
70
+ before: _.filter(flattened, (entry) => entry.status !== "added"),
71
+ after: _.filter(flattened, (entry) => entry.status !== "removed"),
72
+ };
73
+ };
74
+
75
+ export default stringArrayDiff;
@@ -0,0 +1,269 @@
1
+ // @flow
2
+ /**
3
+ * A side by side diff view for Perseus exercise items
4
+ * that do not have the standard question layout.
5
+ */
6
+ import {
7
+ buildEmptyItemTreeForShape,
8
+ buildMapper,
9
+ itemToTree,
10
+ shapes,
11
+ } from "@khanacademy/perseus";
12
+ import * as React from "react";
13
+
14
+ import RendererDiff from "./renderer-diff.jsx";
15
+ import TagsDiff from "./tags-diff.jsx";
16
+
17
+ import type {Item, Path, Shape} from "@khanacademy/perseus";
18
+
19
+ type ItemList = [mixed, Path];
20
+
21
+ /**
22
+ * Outputs true if path begins with beginPath, false otherwise.
23
+ */
24
+ function beginsWith(path: Path, beginPath: Path): boolean {
25
+ let matches = true;
26
+ for (let i = 0; i < beginPath.length; i++) {
27
+ if (i >= path.length) {
28
+ return false;
29
+ }
30
+ if (beginPath[i] !== path[i]) {
31
+ matches = false;
32
+ }
33
+ }
34
+ return matches;
35
+ }
36
+
37
+ /**
38
+ * Outputs true if beforePath and afterPath are the same.
39
+ */
40
+ function checkPath(beforePath: Path, afterPath: Path): boolean {
41
+ if (beforePath.length !== afterPath.length) {
42
+ return false;
43
+ }
44
+ for (let i = 0, l = beforePath.length; i < l; i++) {
45
+ if (beforePath[i] !== afterPath[i]) {
46
+ return false;
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * Given a path, returns a title. Puts colons after numbers in the path.
54
+ */
55
+ function getTitle(path: Path): string {
56
+ const title = [];
57
+ for (let i = 0; i < path.length; i++) {
58
+ if (typeof path[i] === "number") {
59
+ title.push((path[i] + 1).toString() + ":");
60
+ } else {
61
+ title.push(path[i]);
62
+ }
63
+ }
64
+ return title.join(" ");
65
+ }
66
+
67
+ type Tag = {
68
+ idToName: (string) => string,
69
+ nameToId: (string) => string,
70
+ names: $ReadOnlyArray<string>,
71
+ ...
72
+ };
73
+
74
+ type Props = {|
75
+ after: Item,
76
+ before: Item,
77
+ shape: Shape,
78
+ tags: Tag,
79
+ |};
80
+
81
+ class StructuredItemDiff extends React.Component<Props> {
82
+ /**
83
+ * Traverses the given shape and adds paths that are present in
84
+ * beforeList and afterList to result. Note that this method assumes
85
+ * the order of elements in beforeList and afterList, which are
86
+ * from buildMapper(), is the same order they appear in in the shape.
87
+ */
88
+ static generateCompletePathsList(
89
+ // eslint-disable-next-line flowtype/no-mutable-array
90
+ beforeList: Array<ItemList>,
91
+ // eslint-disable-next-line flowtype/no-mutable-array
92
+ afterList: Array<ItemList>,
93
+ // eslint-disable-next-line flowtype/no-mutable-array
94
+ result: Array<Path>,
95
+ shape: Shape,
96
+ path: $ReadOnlyArray<any>,
97
+ ): void {
98
+ if (
99
+ shape.type === "content" ||
100
+ shape.type === "hint" ||
101
+ shape.type === "tags"
102
+ ) {
103
+ const beforePath =
104
+ beforeList.length > 0 && checkPath(path, beforeList[0][1]);
105
+ const afterPath =
106
+ afterList.length > 0 && checkPath(path, afterList[0][1]);
107
+ if (beforePath && afterPath) {
108
+ result.push(path);
109
+ beforeList.splice(0, 1);
110
+ afterList.splice(0, 1);
111
+ } else if (beforePath) {
112
+ result.push(path);
113
+ beforeList.splice(0, 1);
114
+ } else if (afterPath) {
115
+ result.push(path);
116
+ afterList.splice(0, 1);
117
+ }
118
+ } else if (shape.type === "array") {
119
+ let index = 0;
120
+ let newPath = path.concat(index);
121
+
122
+ // For array types, the paths will be in the form [<path>, n],
123
+ // where n is an integer > 0 and increments.
124
+ // As long as either beforeList or afterList has a next element that
125
+ // matches [<path>, n], we recurse into that item with the new path.
126
+ while (
127
+ (beforeList.length > 0 &&
128
+ beginsWith(beforeList[0][1], newPath)) ||
129
+ (afterList.length > 0 && beginsWith(afterList[0][1], newPath))
130
+ ) {
131
+ StructuredItemDiff.generateCompletePathsList(
132
+ beforeList,
133
+ afterList,
134
+ result,
135
+ shape.elementShape,
136
+ newPath,
137
+ );
138
+ index++;
139
+ newPath = path.concat(index);
140
+ }
141
+ } else if (shape.type === "object") {
142
+ const keys = Object.keys(shape.shape);
143
+ for (let i = 0; i < keys.length; i++) {
144
+ const newPath = path.concat([keys[i]]);
145
+ StructuredItemDiff.generateCompletePathsList(
146
+ beforeList,
147
+ afterList,
148
+ result,
149
+ shape.shape[keys[i]],
150
+ newPath,
151
+ );
152
+ }
153
+ }
154
+ }
155
+
156
+ render(): React.Element<"div"> {
157
+ const {before, after, shape, tags} = this.props;
158
+
159
+ const beforeList = [];
160
+ const afterList = [];
161
+
162
+ buildMapper()
163
+ .setContentMapper((c, _, p) => beforeList.push([c, p]))
164
+ .setHintMapper((c, _, p) => beforeList.push([c, p]))
165
+ .setTagsMapper((c, _, p) => beforeList.push([c, p]))
166
+ .mapTree(itemToTree(before), shape);
167
+
168
+ buildMapper()
169
+ .setContentMapper((c, _, p) => afterList.push([c, p]))
170
+ .setHintMapper((c, _, p) => afterList.push([c, p]))
171
+ .setTagsMapper((c, _, p) => afterList.push([c, p]))
172
+ .mapTree(itemToTree(after), shape);
173
+
174
+ // These are used in generateCompletePathsList()
175
+ // and are modified in that method.
176
+ const beforeListModified = beforeList.slice();
177
+ const afterListModified = afterList.slice();
178
+
179
+ const allDiffPaths = [];
180
+ StructuredItemDiff.generateCompletePathsList(
181
+ beforeListModified,
182
+ afterListModified,
183
+ allDiffPaths,
184
+ shape,
185
+ [],
186
+ );
187
+
188
+ const diffCount = allDiffPaths.length;
189
+
190
+ const diffs: React.Node = allDiffPaths.map((path, n) => {
191
+ const isTag = path[path.length - 1] === "tags";
192
+ const currentTitle = getTitle(path);
193
+
194
+ let before = beforeList.find((e) => {
195
+ return checkPath(e[1], path);
196
+ });
197
+ let after = afterList.find((e) => {
198
+ return checkPath(e[1], path);
199
+ });
200
+
201
+ if (isTag) {
202
+ if (!before) {
203
+ before = [[], path];
204
+ }
205
+ if (!after) {
206
+ after = [[], path];
207
+ }
208
+
209
+ const beforeTags = [];
210
+ if (Array.isArray(before[0])) {
211
+ before[0].forEach((tagId) => {
212
+ if (typeof tagId === "string") {
213
+ beforeTags.push(tags.idToName(tagId));
214
+ }
215
+ });
216
+ }
217
+ const afterTags = [];
218
+ if (Array.isArray(after[0])) {
219
+ after[0].forEach((tagId) => {
220
+ if (typeof tagId === "string") {
221
+ afterTags.push(tags.idToName(tagId));
222
+ }
223
+ });
224
+ }
225
+
226
+ const intersection = beforeTags.filter((tag) =>
227
+ afterTags.includes(tag),
228
+ );
229
+ const beforeOnly = beforeTags.filter(
230
+ (tag) => !afterTags.includes(tag),
231
+ );
232
+ const afterOnly = afterTags.filter(
233
+ (tag) => !beforeTags.includes(tag),
234
+ );
235
+
236
+ return (
237
+ <TagsDiff
238
+ beforeOnly={beforeOnly}
239
+ afterOnly={afterOnly}
240
+ intersection={intersection}
241
+ title={currentTitle}
242
+ showSeparator={n < diffCount - 1}
243
+ key={n}
244
+ />
245
+ );
246
+ }
247
+ if (!before) {
248
+ before = [buildEmptyItemTreeForShape(shapes.content), path];
249
+ }
250
+ if (!after) {
251
+ after = [buildEmptyItemTreeForShape(shapes.content), path];
252
+ }
253
+ return (
254
+ <RendererDiff
255
+ before={(before[0]: $FlowFixMe)}
256
+ after={(after[0]: $FlowFixMe)}
257
+ title={currentTitle}
258
+ showAlignmentOptions={false}
259
+ showSeparator={n < diffCount - 1}
260
+ key={n}
261
+ />
262
+ );
263
+ });
264
+
265
+ return <div className="framework-perseus">{diffs}</div>;
266
+ }
267
+ }
268
+
269
+ export default StructuredItemDiff;