var PERSEUS_PREFIX = "assets/perseus/";
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ var parentJsonpFunction = window["webpackJsonp"];
/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, callbacks = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId])
/******/ callbacks.push.apply(callbacks, installedChunks[chunkId]);
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
/******/ while(callbacks.length)
/******/ callbacks.shift().call(null, __webpack_require__);
/******/
/******/ };
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // object to store loaded and loading chunks
/******/ // "0" means "already loaded"
/******/ // Array means "loading", array contains callbacks
/******/ var installedChunks = {
/******/ 2:0
/******/ };
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId, callback) {
/******/ // "0" is the signal for "already loaded"
/******/ if(installedChunks[chunkId] === 0)
/******/ return callback.call(null, __webpack_require__);
/******/
/******/ // an array means "currently loading".
/******/ if(installedChunks[chunkId] !== undefined) {
/******/ installedChunks[chunkId].push(callback);
/******/ } else {
/******/ // start chunk loading
/******/ installedChunks[chunkId] = [callback];
/******/ var head = document.getElementsByTagName('head')[0];
/******/ var script = document.createElement('script');
/******/ script.type = 'text/javascript';
/******/ script.charset = 'utf-8';
/******/ script.async = true;
/******/ script.src = PERSEUS_PREFIX + __webpack_require__.p + "" + chunkId + "." + ({"1":"extra-widgets"}[chunkId]||chunkId) + ".js";
/******/ head.appendChild(script);
/******/ }
/******/ };
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "build/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
/**
* Loads the Perseus preview frame
*
* This is loaded inside the iframe, where it sets up the PreviewFrame component
* that handles all communication between the iframe and its parent.
* NOTE(amy): The KA CMS preview relies on frame-exercise.jsx in webapp's
* perseus-preview-package (which largely duplicates this logic)
*/
__webpack_require__(1);
// We only apply the stub implementation of window.Khan when it is not detected.
// When the stub implementation is not used, mathJaxLoaded is defined on
// window.Khan so we wait for mathJaxLoaded to complete before initializing
// Perseus
if (!window.Khan) {
window.Khan = {
Util: KhanUtil,
error: function error() {},
query: { debug: "" },
imageBase: "/images/"
};
}
var Perseus = window.Perseus = __webpack_require__(9);
var ReactDOM = window.ReactDOM = React.__internalReactDOM;
var PreviewFrame = __webpack_require__(10);
var constants = __webpack_require__(11);
// const afterMathJaxLoad = () => {
// Perseus.init({skipMathJax: false, loadExtraWidgets: true})
// .then(function() {
// const isMobile =
// window.frameElement.getAttribute("data-mobile") === "true";
// const styles = {};
// if (
// window.frameElement.getAttribute("data-lint-gutter") === "true"
// ) {
// // When we're being used in "edit mode", we need to draw our own
// // border and allocate space on the right to display lint
// // indicators in. IframeContentRenderer tells us to do this by
// // setting the data-lint-gutter attribute. If that attribute is
// // not set we don't need to allocate extra space or draw a
// // border. The DeviceFramer has already done it for us.
// styles.marginRight = constants.lintGutterWidth;
// styles.borderWidth = constants.perseusFrameBorderWidth;
// styles.borderColor = "black";
// styles.borderStyle = "solid";
// }
// ReactDOM.render(
//
//
//
,
// document.getElementById("content-container")
// );
// })
// .then(
// function() {},
// function(err) {
// console.error(err); // @Nolint
// }
// );
// };
// if (window.Khan.mathJaxLoaded) {
// window.Khan.mathJaxLoaded.then(afterMathJaxLoad);
// } else {
// afterMathJaxLoad();
// }
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
/**
* Sets up the basic environment for running Perseus in.
*/
window.icu = {
getDecimalFormatSymbols: function getDecimalFormatSymbols() {
return {
decimal_separator: ".",
grouping_separator: ",",
minus: "-"
};
}
};
window.KhanUtil = {
debugLog: function debugLog() {},
localeToFixed: function localeToFixed(num, precision) {
return num.toFixed(precision);
}
};
window.Exercises = {
localMode: true,
useKatex: true,
khanExercisesUrlBase: "../",
getCurrentFramework: function getCurrentFramework() {
return "khan-exercises";
},
PerseusBridge: {
cleanupProblem: function cleanupProblem() {
return false;
}
}
};
/***/ },
/* 2 */,
/* 3 */,
/* 4 */,
/* 5 */,
/* 6 */,
/* 7 */,
/* 8 */,
/* 9 */
/***/ function(module, exports, __webpack_require__) {
/**
* Main entry point
*/
var version = __webpack_require__(50);
var Widgets = __webpack_require__(31);
var basicWidgets = __webpack_require__(32);
// const basicWidgets = require("./all-widgets.js");
Widgets.registerMany(basicWidgets);
module.exports = {
apiVersion: version.apiVersion,
itemDataVersion: version.itemDataVersion,
init: __webpack_require__(33),
ArticleRenderer: __webpack_require__(34),
ItemRenderer: __webpack_require__(13),
ServerItemRenderer: __webpack_require__(35),
HintsRenderer: __webpack_require__(36),
Renderer: __webpack_require__(37),
MultiItems: __webpack_require__(30)
};
/***/ },
/* 10 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/**
* Demonstrates the rendered result of a Perseus question within an iframe.
*
* This mounts an ItemRenderer, HintRenderer, or ArticleRenderer(s)
* (depending on the content given) and applies mobile styling if necessary.
* NOTE(amy): The KA CMS preview relies on preview-frame.jsx in webapp's
* perseus-preview-package (which largely duplicates this logic)
*/
var React = __webpack_require__(43);
var ItemRenderer = __webpack_require__(13);
var HintRenderer = __webpack_require__(38);
var ArticleRenderer = __webpack_require__(34);
var TouchEmulator = __webpack_require__(42);
var PreviewFrame = React.createClass({
displayName: "PreviewFrame",
propTypes: {
isMobile: React.PropTypes.bool.isRequired
},
getInitialState: function getInitialState() {
return {};
},
componentDidMount: function componentDidMount() {
var _this = this;
window.addEventListener("message", function (event) {
var data = window.parent.iframeDataStore[event.data];
if (data) {
_this.setState(data);
}
});
window.parent.postMessage(window.frameElement.getAttribute("data-id"), "*");
this._updateParentWithHeight();
if (window.MutationObserver) {
// To know when to update the parent with the new iframe content
// height, we listen to DOM mutations inside the iframe and
// update the parent with the latest height every time a
// mutation is detected.
this._observer = new MutationObserver(function () {
_this._updateParentWithHeight();
});
this._observer.observe(document.getElementById("measured"), {
childList: true,
subtree: true,
attributes: true
});
}
// In addition to mutation observers, we also periodically check the
// height to capture the result of animations.
setInterval(function () {
_this._updateParentWithHeight();
}, 500);
if (this.props.isMobile) {
TouchEmulator();
}
// article-all means that we are rendering a full preview of an article
if (this.state.type === "article-all") {
document.body.style.overflow = "scroll";
} else {
document.body.style.overflow = "hidden";
}
},
componentDidUpdate: function componentDidUpdate() {
if (this.state.type === "article-all") {
document.body.style.overflow = "scroll";
} else {
document.body.style.overflow = "hidden";
}
},
componentWillUnmount: function componentWillUnmount() {
if (this._observer) {
this._observer.disconnect();
}
},
_updateParentWithHeight: function _updateParentWithHeight() {
var lowest = 0;
["#content-container", ".preview-measure"].forEach(function (selector) {
Array.from(document.querySelectorAll(selector)).forEach(function (element) {
lowest = Math.max(lowest, element.getBoundingClientRect().bottom);
});
});
var bottomMargin = 30;
window.parent.postMessage({
id: window.frameElement.getAttribute("data-id"),
height: lowest + bottomMargin
}, "*");
},
render: function render() {
var _this2 = this;
if (this.state.data) {
var makeUpdatedData = function makeUpdatedData(data) {
return _extends({}, data, {
workAreaSelector: "#workarea",
hintsAreaSelector: "#hintsarea",
apiOptions: _extends({}, data.apiOptions, {
isMobile: _this2.props.isMobile
})
});
};
var isExercise = this.state.type === "question" || this.state.type === "hint";
var perseusClass = "framework-perseus fonts-loaded " + (isExercise ? "bibliotron-exercise " : "bibliotron-article ") + (this.props.isMobile ? "perseus-mobile" : "");
var linterContext = this.state.data.linterContext;
if (this.state.type === "question") {
return React.createElement(
"div",
{
className: perseusClass,
style: this.props.isMobile ? {} : { margin: "30px 0" },
ref: "container"
},
React.createElement(ItemRenderer, _extends({}, makeUpdatedData(this.state.data), {
linterContext: linterContext
})),
React.createElement("div", { id: "workarea", style: { marginLeft: 0 } }),
React.createElement("div", { id: "hintsarea" })
);
} else if (this.state.type === "hint") {
return React.createElement(
"div",
{
className: perseusClass,
style: this.props.isMobile ? {} : { margin: "30px 0" },
ref: "container"
},
React.createElement(HintRenderer, _extends({}, makeUpdatedData(this.state.data), {
linterContext: linterContext
}))
);
} else if (this.state.type === "article") {
return React.createElement(
"div",
{
className: perseusClass,
style: this.props.isMobile ? {} : { margin: "30px 0" }
},
React.createElement(ArticleRenderer, _extends({}, makeUpdatedData(this.state.data), {
linterContext: linterContext
}))
);
} else if (this.state.type === "article-all") {
return React.createElement(
"div",
{
className: perseusClass,
style: this.props.isMobile ? {} : { margin: "30px 0" }
},
this.state.data.map(function (data, i) {
return React.createElement(ArticleRenderer, _extends({
key: i
}, makeUpdatedData(data), {
linterContext: linterContext
}));
})
);
} else {
return React.createElement("div", null);
}
} else {
return React.createElement("div", null);
}
}
});
module.exports = PreviewFrame;
/***/ },
/* 11 */
/***/ function(module, exports, __webpack_require__) {
var devices = {
PHONE: "phone",
TABLET: "tablet",
DESKTOP: "desktop"
};
module.exports = {
devices: devices,
// How many pixels do we reserve on the right-hand side of a preview
// for displaying lint indicators? This space needs to be reserved
// in DeviceFramer, but it is actually allocated in PerseusFrame
lintGutterWidth: 36,
// How wide a border does PerseusFrame draw? We need to allocate enough
// space for it in DeviceFramer.
perseusFrameBorderWidth: 1
};
/***/ },
/* 12 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable brace-style */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
/**
* [Most of] the Perseus client API.
*
* If making a change to this file, or otherwise to the perseus
* API, you should increment:
* * the perseus api major version if it is a breaking change
* * the perseus api minor version if it is an additive-only change
* * nothing if it is purely a bug fix.
*
* Callbacks passed to Renderer/ItemRenderer:
* * onInputError:
* Called when there is an error grading a widget
* * onFocusChange: (newFocusPath, oldFocusPath, keypadDOMNode)
* Called when the user focus changes. The first two parameters are `path`
* arrays uniquely identifying the respect inputs. The third parameter,
* `keypadDOMNode`, is the DOM node of the custom keypad, or `null` if the
* keypad is disabled, which can be used by clients to accommodate for the
* appearance of the keypad on the screen.
* When focus changes to or from nothing being selected, `path` will be null.
* * interactionCallback: Called when the user interacts with a widget.
* * answerableCallback: Called with the current `answerability` of the
* problem, e.g. whether all required fields have input.
* * getAnotherHint: If provided, a button is rendered at the bottom of the
* hints (only when at least one hint has been shown, and not all hints
* have been shown) allowing the user to take another hint. This function
* is then called when the user clicks the button.
*
* Stable CSS ClassNames:
* These are css class names that will continue to preserve their
* semantic meaning across the same perseus api major version.
*/
var React = __webpack_require__(43);
var StubTagEditor = __webpack_require__(55);
module.exports = {
Options: {
propTypes: React.PropTypes.shape({
isArticle: React.PropTypes.bool.isRequired,
satStyling: React.PropTypes.bool.isRequired,
onInputError: React.PropTypes.func.isRequired,
onFocusChange: React.PropTypes.func.isRequired,
staticRender: React.PropTypes.bool.isRequired,
GroupMetadataEditor: React.PropTypes.func.isRequired,
showAlignmentOptions: React.PropTypes.bool.isRequired,
readOnly: React.PropTypes.bool.isRequired,
answerableCallback: React.PropTypes.func,
getAnotherHint: React.PropTypes.func,
interactionCallback: React.PropTypes.func,
// A function that takes in the relative problem number (starts at
// 0 and is incremented for each group widget), and the ID of the
// group widget, then returns a react component that will be added
// immediately above the renderer in the group widget. If the
// function returns null, no annotation will be added.
groupAnnotator: React.PropTypes.func.isRequired,
// If imagePlaceholder or widgetPlaceholder are set, perseus will
// render the placeholder instead of the image or widget node.
imagePlaceholder: React.PropTypes.node,
widgetPlaceholder: React.PropTypes.node,
// Base React elements that can be used in place of the standard DOM
// DOM elements. For example, when provided, will be used
// in place of . This allows clients to provide pre-styled
// components or components with custom behavior.
baseElements: React.PropTypes.shape({
// The component provided here must adhere to the same
// interface as React's base component.
Link: React.PropTypes.func
}),
// Function that takes dimensions and returns a React component
// to display while an image is loading
imagePreloader: React.PropTypes.func,
// Function that takes an object argument. The object should
// include type and id, both strings, at least and can optionally
// include a boolean "correct" value. This is used for keeping
// track of widget interactions.
trackInteraction: React.PropTypes.func,
// A boolean that indicates whether or not a custom keypad is
// being used. For mobile web this will be the ProvidedKeypad
// component. In this situation we use the MathInput component
// from the math-input repo instead of the existing perseus math
// input components.
// TODO(charlie): Make this mutually exclusive with `staticRender`.
// Internally, we defer to `customKeypad` over `staticRender`, but
// they should really be represented as an enum or some other data
// structure that forbids them both being enabled at once.
customKeypad: React.PropTypes.bool,
// Indicates whether or not to use mobile styling.
isMobile: React.PropTypes.bool,
// A function, called with a bool indicating whether use of the
// drawing area (scratchpad) should be allowed/disallowed.
// Previously handled by `Khan.scratchpad.enable/disable`
setDrawingAreaAvailable: React.PropTypes.func,
// Whether to use the Draft.js editor or the legacy textarea
useDraftEditor: React.PropTypes.bool,
// Styling options that control the visual behavior of Perseus
// items.
// TODO(mdr): If we adopt this pattern, we'll need to think about
// how to make individual `styling` options be optional, and
// how to set their default values without overwriting provided
// values. For now, though, you must either specify all fields
// of `styling`, or omit the `styling` option entirely.
styling: React.PropTypes.shape({
// Which version of radio widget styles to use in non-SAT
// contexts.
//
// "legacy" was the version of the widget display after XOM but
// before we started adding MCR styles. It doesn't have support
// for rationales. It has since been removed.
//
// "intermediate" is a design which adds several new additions
// to the "legacy" styles such as:
// 1. Using the XOM "desktop" styles (with a visible check icon
// and lines in between choices) on mobile devices.
// 2. Designs for rationales
// 3. Using the single-select styles for multi-select styles
//
// "final" is a design which will build off of the
// "intermediate" designs and adds some improved designs as well
// as:
// 1. a/b/c/d/etc. letters inside of the prompt check box
// 2. New iconography and styles to indicate choice correctness
//
// The "legacy" and "intermediate" designs will be A/B tested
// against each other to ensure that its changes don't cause
// problems due to the new designs. Once the "intermediate"
// designs are finished, they will be A/B tested against the
// "final" designs.
//
// If no flag is provided, "legacy" styles will be shown.
//
// TODO(emily): Remove this by Aug 1, 2017, at which point all
// callsites should have been switched to using the "final"
// designs.
radioStyleVersion: React.PropTypes.oneOf(["intermediate", "final"])
}),
// The color used for the hint progress indicator (eg. 1 / 3)
hintProgressColor: React.PropTypes.string
}).isRequired,
defaults: {
isArticle: false,
isMobile: false,
satStyling: false,
onInputError: function onInputError() {},
onFocusChange: function onFocusChange() {},
staticRender: false,
GroupMetadataEditor: StubTagEditor,
showAlignmentOptions: false,
readOnly: false,
groupAnnotator: function groupAnnotator() {
return null;
},
baseElements: {
Link: function Link(props) {
return React.createElement("a", props);
}
},
setDrawingAreaAvailable: function setDrawingAreaAvailable() {},
useDraftEditor: false,
styling: {
radioStyleVersion: "final"
}
}
},
ClassNames: {
RENDERER: "perseus-renderer",
TWO_COLUMN_RENDERER: "perseus-renderer-two-columns",
RESPONSIVE_RENDERER: "perseus-renderer-responsive",
INPUT: "perseus-input",
FOCUSED: "perseus-focused",
RADIO: {
OPTION: "perseus-radio-option",
SELECTED: "perseus-radio-selected",
OPTION_CONTENT: "perseus-radio-option-content"
},
INTERACTIVE: "perseus-interactive",
CORRECT: "perseus-correct",
INCORRECT: "perseus-incorrect",
UNANSWERED: "perseus-unanswered",
MOBILE: "perseus-mobile"
}
};
/***/ },
/* 13 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable no-var, prefer-spread */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var _ = __webpack_require__(56);
var ApiOptions = __webpack_require__(12).Options;
var HintsRenderer = __webpack_require__(36);
var Renderer = __webpack_require__(37);
var ProvideKeypad = __webpack_require__(65);
var Util = __webpack_require__(17);
var _require = __webpack_require__(80),
mapObject = _require.mapObject;
var Gorgon = __webpack_require__(41);
var _require2 = __webpack_require__(52),
linterContextProps = _require2.linterContextProps,
linterContextDefault = _require2.linterContextDefault;
var RP = React.PropTypes;
var ItemRenderer = React.createClass({
displayName: "ItemRenderer",
propTypes: _extends({}, ProvideKeypad.propTypes, {
// defaults are set in `this.update()` so as to adhere to
// `ApiOptions.PropTypes`, though the API options that are passed in
// can be in any degree of completeness
apiOptions: RP.shape({
interactionCallback: RP.func,
onFocusChange: RP.func,
setDrawingAreaAvailable: RP.func
}),
// Whether this component should control hiding/showing peripheral
// item-related components (for list, see item.answerArea below).
// TODO(alex): Generalize this to an 'expectsToBeInTemplate' prop
controlPeripherals: RP.bool,
hintsAreaSelector: RP.string,
initialHintsVisible: RP.number,
item: RP.shape({
answerArea: RP.shape({
calculator: RP.bool,
chi2Table: RP.bool,
periodicTable: RP.bool,
tTable: RP.bool,
zTable: RP.bool
}),
hints: RP.arrayOf(RP.object),
question: RP.object
}).isRequired,
onShowCalculator: RP.func,
onShowChi2Table: RP.func,
onShowPeriodicTable: RP.func,
onShowTTable: RP.func,
onShowZTable: RP.func,
problemNum: RP.number,
reviewMode: React.PropTypes.bool,
savedState: RP.any,
workAreaSelector: RP.string,
linterContext: linterContextProps,
legacyPerseusLint: React.PropTypes.arrayOf(React.PropTypes.string)
}),
getDefaultProps: function getDefaultProps() {
return {
apiOptions: {}, // defaults are set in `this.update()`
controlPeripherals: true,
hintsAreaSelector: "#hintsarea",
initialHintsVisible: 0,
workAreaSelector: "#workarea",
reviewMode: false,
linterContext: linterContextDefault
};
},
getInitialState: function getInitialState() {
return _extends({}, ProvideKeypad.getInitialState(), {
hintsVisible: this.props.initialHintsVisible,
questionCompleted: false,
questionHighlightedWidgets: []
});
},
componentDidMount: function componentDidMount() {
ProvideKeypad.componentDidMount.call(this);
if (this.props.controlPeripherals && this.props.apiOptions.setDrawingAreaAvailable) {
this.props.apiOptions.setDrawingAreaAvailable(true);
}
this._currentFocus = null;
this.update();
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
this.setState({
questionHighlightedWidgets: []
});
},
componentDidUpdate: function componentDidUpdate() {
this.update();
},
componentWillUnmount: function componentWillUnmount() {
ProvideKeypad.componentWillUnmount.call(this);
ReactDOM.unmountComponentAtNode(document.querySelector(this.props.workAreaSelector));
ReactDOM.unmountComponentAtNode(document.querySelector(this.props.hintsAreaSelector));
if (this.props.controlPeripherals) {
var answerArea = this.props.item.answerArea || {};
if (answerArea.calculator) {
$("#calculator").hide();
}
if (answerArea.periodicTable) {
$(".periodic-table-info-box").hide();
}
if (answerArea.zTable) {
$(".z-table-info-box").hide();
}
if (answerArea.tTable) {
$(".t-table-info-box").hide();
}
if (answerArea.chi2Table) {
$(".chi2-table-info-box").hide();
}
}
},
keypadElement: function keypadElement() {
return ProvideKeypad.keypadElement.call(this);
},
update: function update() {
var apiOptions = _extends({}, ApiOptions.defaults, this.props.apiOptions, {
onFocusChange: this._handleFocusChange
});
// Since the item renderer works by rendering things into three divs
// that have completely different places in the DOM, we have to do this
// strangeness instead of relying on React's normal render() method.
// TODO(alpert): Figure out how to clean this up somehow
this.questionRenderer = ReactDOM.render(React.createElement(Renderer, _extends({
keypadElement: this.keypadElement(),
problemNum: this.props.problemNum,
onInteractWithWidget: this.handleInteractWithWidget,
highlightedWidgets: this.state.questionHighlightedWidgets,
apiOptions: apiOptions,
questionCompleted: this.state.questionCompleted,
reviewMode: this.props.reviewMode,
savedState: this.props.savedState,
linterContext: Gorgon.pushContextStack(this.props.linterContext, "question")
}, this.props.item.question, {
legacyPerseusLint: this.props.legacyPerseusLint
})), document.querySelector(this.props.workAreaSelector));
this.hintsRenderer = ReactDOM.render(React.createElement(HintsRenderer, {
hints: this.props.item.hints,
hintsVisible: this.state.hintsVisible,
apiOptions: apiOptions,
linterContext: Gorgon.pushContextStack(this.props.linterContext, "hints")
}), document.querySelector(this.props.hintsAreaSelector));
var answerArea = this.props.item.answerArea || {};
if (this.props.controlPeripherals) {
$("#calculator").toggle(answerArea.calculator || false);
$(".periodic-table-info-box").toggle(answerArea.periodicTable || false);
$(".z-table-info-box").toggle(answerArea.zTable || false);
$(".t-table-info-box").toggle(answerArea.tTable || false);
$(".chi2-table-info-box").toggle(answerArea.chi2Table || false);
} else {
if (answerArea.calculator) {
this.props.onShowCalculator && this.props.onShowCalculator();
}
if (answerArea.periodicTable) {
this.props.onShowPeriodicTable && this.props.onShowPeriodicTable();
}
if (answerArea.zTable) {
this.props.onShowZTable && this.props.onShowZTable();
}
if (answerArea.tTable) {
this.props.onShowTTable && this.props.onShowTTable();
}
if (answerArea.chi2Table) {
this.props.onShowChi2Table && this.props.onShowChi2Table();
}
}
if (apiOptions.answerableCallback) {
var isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
apiOptions.answerableCallback(isAnswerable);
}
},
_handleFocusChange: function _handleFocusChange(newFocus, oldFocus) {
if (newFocus != null) {
this._setCurrentFocus(newFocus);
} else {
this._onRendererBlur(oldFocus);
}
},
// Sets the current focus path and element and send an onChangeFocus event
// back to our parent.
_setCurrentFocus: function _setCurrentFocus(newFocus) {
var _this = this;
var keypadElement = this.keypadElement();
// By the time this happens, newFocus cannot be a prefix of
// prevFocused, since we must have either been called from
// an onFocusChange within a renderer, which is only called when
// this is not a prefix, or between the question and answer areas,
// which can never prefix each other.
var prevFocus = this._currentFocus;
this._currentFocus = newFocus;
// Determine whether the newly focused path represents an input.
var inputPaths = this.getInputPaths();
var didFocusInput = this._currentFocus && inputPaths.some(function (inputPath) {
return Util.inputPathsEqual(inputPath, _this._currentFocus);
});
if (this.props.apiOptions.onFocusChange != null) {
this.props.apiOptions.onFocusChange(this._currentFocus, prevFocus, didFocusInput && keypadElement && ReactDOM.findDOMNode(keypadElement));
}
if (keypadElement) {
if (didFocusInput) {
keypadElement.activate();
} else {
keypadElement.dismiss();
}
}
},
_onRendererBlur: function _onRendererBlur(blurPath) {
var _this2 = this;
var blurringFocusPath = this._currentFocus;
// Failsafe: abort if ID is different, because focus probably happened
// before blur.
if (!Util.inputPathsEqual(blurPath, blurringFocusPath)) {
return;
}
// Wait until after any new focus events fire this tick before
// declaring that nothing is focused, since if there were a focus change
// across Renderers (e.g., from the HintsRenderer to the
// QuestionRenderer), we could receive the blur before the focus.
setTimeout(function () {
if (Util.inputPathsEqual(_this2._currentFocus, blurringFocusPath)) {
_this2._setCurrentFocus(null);
}
});
},
/**
* Accepts a question area widgetId, or an answer area widgetId of
* the form "answer-input-number 1", or the string "answer-area"
* for the whole answer area (if the answer area is a single widget).
*/
_setWidgetProps: function _setWidgetProps(widgetId, newProps, callback) {
this.questionRenderer._setWidgetProps(widgetId, newProps, callback);
},
_handleAPICall: function _handleAPICall(functionName, path) {
// Get arguments to pass to function, including `path`.
var functionArgs = _.rest(arguments);
// TODO(charlie): Extend this API to support inputs in the
// HintsRenderer as well.
var caller = this.questionRenderer;
return caller[functionName].apply(caller, functionArgs);
},
setInputValue: function setInputValue(path, newValue, focus) {
return this._handleAPICall("setInputValue", path, newValue, focus);
},
focusPath: function focusPath(path) {
return this._handleAPICall("focusPath", path);
},
blurPath: function blurPath(path) {
return this._handleAPICall("blurPath", path);
},
getDOMNodeForPath: function getDOMNodeForPath(path) {
return this._handleAPICall("getDOMNodeForPath", path);
},
getGrammarTypeForPath: function getGrammarTypeForPath(path) {
return this._handleAPICall("getGrammarTypeForPath", path);
},
getInputPaths: function getInputPaths() {
var questionAreaInputPaths = this.questionRenderer.getInputPaths();
return questionAreaInputPaths;
},
handleInteractWithWidget: function handleInteractWithWidget(widgetId) {
var withRemoved = _.difference(this.state.questionHighlightedWidgets, [widgetId]);
this.setState({
questionCompleted: false,
questionHighlightedWidgets: withRemoved
});
if (this.props.apiOptions.interactionCallback) {
this.props.apiOptions.interactionCallback();
}
},
focus: function focus() {
return this.questionRenderer.focus();
},
blur: function blur() {
if (this._currentFocus) {
this.blurPath(this._currentFocus);
}
},
showHint: function showHint() {
if (this.state.hintsVisible < this.getNumHints()) {
this.setState({
hintsVisible: this.state.hintsVisible + 1
});
}
},
getNumHints: function getNumHints() {
return this.props.item.hints.length;
},
/**
* Grades the item.
*
* Returns a KE-style score of {
* empty: bool,
* correct: bool,
* message: string|null,
* guess: Array
* }
*/
scoreInput: function scoreInput() {
var guessAndScore = this.questionRenderer.guessAndScore();
var guess = guessAndScore[0];
var score = guessAndScore[1];
// Continue to include an empty guess for the now defunct answer area.
// TODO(alex): Check whether we rely on the format here for
// analyzing ProblemLogs. If not, remove this layer.
var maxCompatGuess = [guess, []];
var keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
var emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
this.setState({
questionCompleted: keScore.correct,
questionHighlightedWidgets: emptyQuestionAreaWidgets
});
return keScore;
},
/**
* Returns an array of all widget IDs in the order they occur in
* the question content.
*/
getWidgetIds: function getWidgetIds() {
return this.questionRenderer.getWidgetIds();
},
/**
* Returns an object mapping from widget ID to KE-style score.
* The keys of this object are the values of the array returned
* from `getWidgetIds`.
*/
scoreWidgets: function scoreWidgets() {
var qScore = this.questionRenderer.scoreWidgets();
var qGuess = this.questionRenderer.getUserInputForWidgets();
var state = this.questionRenderer.getSerializedState();
return mapObject(qScore, function (score, id) {
return Util.keScoreFromPerseusScore(score, qGuess[id], state);
});
},
/**
* Get a representation of the current state of the item.
*/
getSerializedState: function getSerializedState() {
return {
question: this.questionRenderer.getSerializedState(),
hints: this.hintsRenderer.getSerializedState()
};
},
restoreSerializedState: function restoreSerializedState(state, callback) {
// We need to wait for both the question renderer and the hints
// renderer to finish restoring their states.
var numCallbacks = 2;
var fireCallback = function fireCallback() {
--numCallbacks;
if (callback && numCallbacks === 0) {
callback();
}
};
this.questionRenderer.restoreSerializedState(state.question, fireCallback);
this.hintsRenderer.restoreSerializedState(state.hints, fireCallback);
},
showRationalesForCurrentlySelectedChoices: function showRationalesForCurrentlySelectedChoices() {
this.questionRenderer.showRationalesForCurrentlySelectedChoices();
},
deselectIncorrectSelectedChoices: function deselectIncorrectSelectedChoices() {
this.questionRenderer.deselectIncorrectSelectedChoices();
},
render: function render() {
return React.createElement("div", null);
}
});
module.exports = ItemRenderer;
/***/ },
/* 14 */,
/* 15 */,
/* 16 */,
/* 17 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable brace-style, comma-dangle, indent, max-len, no-var, one-var, prefer-spread */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var _ = __webpack_require__(56);
var KhanAnswerTypes = __webpack_require__(82);
var nestedMap = function nestedMap(children, func, context) {
if (_.isArray(children)) {
return _.map(children, function (child) {
return nestedMap(child, func);
});
} else {
return func.call(context, children);
}
};
var Util = {
/**
* Used to compare equality of two input paths, which are represented as
* arrays of strings.
*/
inputPathsEqual: function inputPathsEqual(a, b) {
if (a == null || b == null) {
return a == null === (b == null);
}
return a.length === b.length && a.every(function (item, index) {
return b[index] === item;
});
},
nestedMap: nestedMap,
rWidgetParts: /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]$/,
rWidgetRule: /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/,
rTypeFromWidgetId: /^([a-z-]+) ([0-9]+)$/,
snowman: "\u2603",
noScore: {
type: "points",
earned: 0,
total: 0,
message: null
},
seededRNG: function seededRNG(seed) {
var randomSeed = seed;
return function () {
// Robert Jenkins' 32 bit integer hash function.
var seed = randomSeed;
seed = seed + 0x7ed55d16 + (seed << 12) & 0xffffffff;
seed = (seed ^ 0xc761c23c ^ seed >>> 19) & 0xffffffff;
seed = seed + 0x165667b1 + (seed << 5) & 0xffffffff;
seed = (seed + 0xd3a2646c ^ seed << 9) & 0xffffffff;
seed = seed + 0xfd7046c5 + (seed << 3) & 0xffffffff;
seed = (seed ^ 0xb55a4f09 ^ seed >>> 16) & 0xffffffff;
return (randomSeed = seed & 0xfffffff) / 0x10000000;
};
},
// Shuffle an array using a given random seed or function.
// If `ensurePermuted` is true, the input and ouput are guaranteed to be
// distinct permutations.
shuffle: function shuffle(array, randomSeed, ensurePermuted) {
// Always return a copy of the input array
var shuffled = _.clone(array);
// Handle edge cases (input array is empty or uniform)
if (!shuffled.length || _.all(shuffled, function (value) {
return _.isEqual(value, shuffled[0]);
})) {
return shuffled;
}
var random;
if (_.isFunction(randomSeed)) {
random = randomSeed;
} else {
random = Util.seededRNG(randomSeed);
}
do {
// Fischer-Yates shuffle
for (var top = shuffled.length; top > 0; top--) {
var newEnd = Math.floor(random() * top),
temp = shuffled[newEnd];
shuffled[newEnd] = shuffled[top - 1];
shuffled[top - 1] = temp;
}
} while (ensurePermuted && _.isEqual(array, shuffled));
return shuffled;
},
// In IE8, split doesn't work right. Implement it ourselves.
split: "x".split(/(.)/g).length ? function (str, r) {
return str.split(r);
} : function (str, r) {
// Based on Steven Levithan's MIT-licensed split, available at
// http://blog.stevenlevithan.com/archives/cross-browser-split
var output = [];
var lastIndex = r.lastIndex = 0;
var match;
while (match = r.exec(str)) {
output.push(str.slice(lastIndex, match.index));
output.push.apply(output, match.slice(1));
lastIndex = match.index + match[0].length;
}
output.push(str.slice(lastIndex));
return output;
},
/**
* Given two score objects for two different widgets, combine them so that
* if one is wrong, the total score is wrong, etc.
*/
combineScores: function combineScores(scoreA, scoreB) {
var message;
if (scoreA.type === "points" && scoreB.type === "points") {
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
// TODO(alpert): Figure out how to combine messages usefully
message = null;
} else {
message = scoreA.message || scoreB.message;
}
return {
type: "points",
earned: scoreA.earned + scoreB.earned,
total: scoreA.total + scoreB.total,
message: message
};
} else if (scoreA.type === "points" && scoreB.type === "invalid") {
return scoreB;
} else if (scoreA.type === "invalid" && scoreB.type === "points") {
return scoreA;
} else if (scoreA.type === "invalid" && scoreB.type === "invalid") {
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
// TODO(alpert): Figure out how to combine messages usefully
message = null;
} else {
message = scoreA.message || scoreB.message;
}
return {
type: "invalid",
message: message
};
}
},
keScoreFromPerseusScore: function keScoreFromPerseusScore(score, guess, state) {
if (score.type === "points") {
return {
empty: false,
correct: score.earned >= score.total,
message: score.message,
guess: guess,
state: state
};
} else if (score.type === "invalid") {
return {
empty: true,
correct: false,
message: score.message,
guess: guess,
state: state
};
} else {
throw new Error("Invalid score type: " + score.type);
}
},
/**
* Return the first valid interpretation of 'text' as a number, in the form
* {value: 2.3, exact: true}.
*/
firstNumericalParse: function firstNumericalParse(text) {
// TODO(alpert): This is sort of hacky...
var first;
var val = KhanAnswerTypes.predicate.createValidatorFunctional(function (ans) {
first = ans;
return true; /* break */
}, {
simplify: "optional",
inexact: true,
forms: "integer, proper, improper, pi, log, mixed, decimal"
});
val(text);
return first;
},
stringArrayOfSize: function stringArrayOfSize(size) {
return _(size).times(function () {
return "";
});
},
/**
* For a graph's x or y dimension, given the tick step,
* the ranges extent (e.g. [-10, 10]), the pixel dimension constraint,
* and the grid step, return a bunch of configurations for that dimension.
*
* Example:
* gridDimensionConfig(10, [-50, 50], 400, 5)
*
* Returns: {
* scale: 4,
* snap: 2.5,
* tickStep: 2,
* unityLabel: true
* };
*/
gridDimensionConfig: function gridDimensionConfig(absTickStep, extent, dimensionConstraint, gridStep) {
var scale = Util.scaleFromExtent(extent, dimensionConstraint);
var stepPx = absTickStep * scale;
var unityLabel = stepPx > 30;
return {
scale: scale,
tickStep: absTickStep / gridStep,
unityLabel: unityLabel
};
},
/**
* Given the range, step, and boxSize, calculate the reasonable gridStep.
* Used for when one was not given explicitly.
*
* Example:
* getGridStep([[-10, 10], [-10, 10]], [1, 1], 340)
*
* Returns: [1, 1]
*/
getGridStep: function getGridStep(range, step, boxSize) {
return _(2).times(function (i) {
var scale = Util.scaleFromExtent(range[i], boxSize);
var gridStep = Util.gridStepFromTickStep(step[i], scale);
return gridStep;
});
},
snapStepFromGridStep: function snapStepFromGridStep(gridStep) {
return _.map(gridStep, function (step) {
return step / 2;
});
},
/**
* Given the range and a dimension, come up with the appropriate
* scale.
* Example:
* scaleFromExtent([-25, 25], 500) // returns 10
*/
scaleFromExtent: function scaleFromExtent(extent, dimensionConstraint) {
var span = extent[1] - extent[0];
var scale = dimensionConstraint / span;
return scale;
},
/**
* Return a reasonable tick step given extent and dimension.
* (extent is [begin, end] of the domain.)
* Example:
* tickStepFromExtent([-10, 10], 300) // returns 2
*/
tickStepFromExtent: function tickStepFromExtent(extent, dimensionConstraint) {
var span = extent[1] - extent[0];
var tickFactor;
// If single number digits
if (15 < span && span <= 20) {
tickFactor = 23;
// triple digit or decimal
} else if (span > 100 || span < 5) {
tickFactor = 10;
// double digit
} else {
tickFactor = 16;
}
var constraintFactor = dimensionConstraint / 500;
var desiredNumTicks = tickFactor * constraintFactor;
return Util.tickStepFromNumTicks(span, desiredNumTicks);
},
/**
* Given the tickStep and the graph's scale, find a
* grid step.
* Example:
* gridStepFromTickStep(200, 0.2) // returns 100
*/
gridStepFromTickStep: function gridStepFromTickStep(tickStep, scale) {
var tickWidth = tickStep * scale;
var x = tickStep;
var y = Math.pow(10, Math.floor(Math.log(x) / Math.LN10));
var leadingDigit = Math.floor(x / y);
if (tickWidth < 25) {
return tickStep;
}
if (tickWidth < 50) {
if (leadingDigit === 5) {
return tickStep;
} else {
return tickStep / 2;
}
}
if (leadingDigit === 1) {
return tickStep / 2;
}
if (leadingDigit === 2) {
return tickStep / 4;
}
if (leadingDigit === 5) {
return tickStep / 5;
}
},
/**
* Find a good tick step for the desired number of ticks in the range
* Modified from d3.scale.linear: d3_scale_linearTickRange.
* Thanks, mbostock!
* Example:
* tickStepFromNumTicks(50, 6) // returns 10
*/
tickStepFromNumTicks: function tickStepFromNumTicks(span, numTicks) {
var step = Math.pow(10, Math.floor(Math.log(span / numTicks) / Math.LN10));
var err = numTicks / span * step;
// Filter ticks to get closer to the desired count.
if (err <= 0.15) {
step *= 10;
} else if (err <= 0.35) {
step *= 5;
} else if (err <= 0.75) {
step *= 2;
}
// Round start and stop values to step interval.
return step;
},
/**
* Constrain tick steps intended for desktop size graphs
* to something more suitable for mobile size graphs.
* Specifically, we aim for 10 or fewer ticks per graph axis.
*/
constrainedTickStepsFromTickSteps: function constrainedTickStepsFromTickSteps(tickSteps, ranges) {
var steps = [];
for (var i = 0; i < 2; i++) {
var span = ranges[i][1] - ranges[i][0];
var numTicks = span / tickSteps[i];
if (numTicks <= 10) {
// Will displays fine on mobile
steps[i] = tickSteps[i];
} else if (numTicks <= 20) {
// Will be crowded on mobile, so hide every other tick
steps[i] = tickSteps[i] * 2;
} else {
// Fallback in case we somehow have more than 20 ticks
// Note: This shouldn't happen due to GraphSettings.validStep
steps[i] = Util.tickStepFromNumTicks(span, 10);
}
}
return steps;
},
/**
* Transparently update deprecated props so that the code to deal
* with them only lives in one place: (Widget).deprecatedProps
*
* For example, if a boolean `foo` was deprecated in favor of a
* number 'bar':
* deprecatedProps: {
* foo: function(props) {
* return {bar: props.foo ? 1 : 0};
* }
* }
*/
DeprecationMixin: {
// This lifecycle stage is only called before first render
componentWillMount: function componentWillMount() {
var newProps = {};
_.each(this.deprecatedProps, function (func, prop) {
if (_.has(this.props, prop)) {
_.extend(newProps, func(this.props));
}
}, this);
if (!_.isEmpty(newProps)) {
// Set new props directly so that widget renders correctly
// when it first mounts, even though these will be overwritten
// almost immediately afterwards...
_.extend(this.props, newProps);
// ...when we propagate the new props upwards and they come
// back down again.
setTimeout(this.props.onChange, 0, newProps);
}
}
},
/**
* Approximate equality on numbers and primitives.
*/
eq: function eq(x, y) {
if (_.isNumber(x) && _.isNumber(y)) {
return Math.abs(x - y) < 1e-9;
} else {
return x === y;
}
},
/**
* Deep approximate equality on primitives, numbers, arrays, and objects.
*/
deepEq: function deepEq(x, y) {
if (_.isArray(x) && _.isArray(y)) {
if (x.length !== y.length) {
return false;
}
for (var i = 0; i < x.length; i++) {
if (!Util.deepEq(x[i], y[i])) {
return false;
}
}
return true;
} else if (_.isArray(x) || _.isArray(y)) {
return false;
} else if (_.isFunction(x) && _.isFunction(y)) {
return Util.eq(x, y);
} else if (_.isFunction(x) || _.isFunction(y)) {
return false;
} else if (_.isObject(x) && _.isObject(y)) {
return x === y || _.all(x, function (v, k) {
return Util.deepEq(y[k], v);
}) && _.all(y, function (v, k) {
return Util.deepEq(x[k], v);
});
} else if (_.isObject(x) || _.isObject(y)) {
return false;
} else {
return Util.eq(x, y);
}
},
/**
* Query String Parser
*
* Original from:
* http://stackoverflow.com/questions/901115/get-querystring-values-in-javascript/2880929#2880929
*/
parseQueryString: function parseQueryString(query) {
query = query || window.location.search.substring(1);
var urlParams = {},
e,
a = /\+/g,
// Regex for replacing addition symbol with a space
r = /([^&=]+)=?([^&]*)/g,
d = function d(s) {
return decodeURIComponent(s.replace(a, " "));
};
while (e = r.exec(query)) {
urlParams[d(e[1])] = d(e[2]);
}
return urlParams;
},
/**
* Query string adder
* Works for URLs without #.
* Original from:
* http://stackoverflow.com/questions/5999118/add-or-update-query-string-parameter
*/
updateQueryString: function updateQueryString(uri, key, value) {
value = encodeURIComponent(value);
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf("?") !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, "$1" + key + "=" + value + "$2");
} else {
return uri + separator + key + "=" + value;
}
},
/**
* A more strict encodeURIComponent that escapes `()'!`s
* Especially useful for creating URLs that are embeddable in markdown
*
* Adapted from
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
* This function and the above original available under the
* CC-BY-SA 2.5 license.
*/
strongEncodeURIComponent: function strongEncodeURIComponent(str) {
return encodeURIComponent(str)
// Note that although RFC3986 reserves "!", RFC5987 does not,
// so we do not need to escape it
.replace(/['()!]/g, window.escape) // i.e., %27 %28 %29
.replace(/\*/g, "%2A");
},
// There are certain widgets where we don't want to provide the "answered"
// highlight indicator.
// The issue with just using the `graded` flag on questions is that showing
// that a certain widget is ungraded can sometimes reveal the answer to a
// question ("is this transformation possible? if so, do it")
// This is kind of a hack to get around this.
widgetShouldHighlight: function widgetShouldHighlight(widget) {
if (!widget) {
return false;
}
var HIGHLIGHT_BAR_BLACKLIST = ["measurer", "protractor"];
return !_.contains(HIGHLIGHT_BAR_BLACKLIST, widget.type);
},
/**
* If a widget says that it is empty once it is graded.
* Trying to encapsulate references to the score format.
*/
scoreIsEmpty: function scoreIsEmpty(score) {
// HACK(benkomalo): ugh. this isn't great; the Perseus score objects
// overload the type "invalid" for what should probably be three
// distinct cases:
// - truly empty or not fully filled out
// - invalid or malformed inputs
// - "almost correct" like inputs where the widget wants to give
// feedback (e.g. a fraction needs to be reduced, or `pi` should
// be used instead of 3.14)
//
// Unfortunately the coercion happens all over the place, as these
// Perseus style score objects are created *everywhere* (basically
// in every widget), so it's hard to change now. We assume that
// anything with a "message" is not truly empty, and one of the
// latter two cases for now.
return score.type === "invalid" && (!score.message || score.message.length === 0);
},
/**
* Extracts the location of a touch or mouse event, allowing you to pass
* in a "mouseup", "mousedown", or "mousemove" event and receive the
* correct coordinates. Shouldn't be used with "vmouse" events.
*
* The Util.touchHandlers are used to track the current state of the touch
* event, such as whether or not the user is currently pressed down (either
* through touch or mouse) on the screen.
*/
touchHandlers: {
pointerDown: false,
currentTouchIdentifier: null
},
resetTouchHandlers: function resetTouchHandlers() {
_.extend(Util.touchHandlers, {
pointerDown: false,
currentTouchIdentifier: null
});
},
extractPointerLocation: function extractPointerLocation(event) {
var touchOrEvent;
if (Util.touchHandlers.pointerDown) {
// Look for the touch matching the one we're tracking; ignore others
if (Util.touchHandlers.currentTouchIdentifier != null) {
var len = event.changedTouches ? event.changedTouches.length : 0;
for (var i = 0; i < len; i++) {
if (event.changedTouches[i].identifier === Util.touchHandlers.currentTouchIdentifier) {
touchOrEvent = event.changedTouches[i];
}
}
} else {
touchOrEvent = event;
}
var isEndish = event.type === "touchend" || event.type === "touchcancel";
if (touchOrEvent && isEndish) {
Util.touchHandlers.pointerDown = false;
Util.touchHandlers.currentTouchIdentifier = null;
}
} else {
// touchstart or mousedown
Util.touchHandlers.pointerDown = true;
if (event.changedTouches) {
touchOrEvent = event.changedTouches[0];
Util.touchHandlers.currentTouchIdentifier = touchOrEvent.identifier;
} else {
touchOrEvent = event;
}
}
if (touchOrEvent) {
return {
left: touchOrEvent.pageX,
top: touchOrEvent.pageY
};
}
},
/**
* Pass this function as the touchstart for an element to
* avoid sending the touch to the mobile scratchpad
*/
captureScratchpadTouchStart: function captureScratchpadTouchStart(e) {
e.stopPropagation();
},
getImageSize: function getImageSize(url, callback) {
var img = new Image();
img.onload = function () {
// IE 11 seems to have problems calculating the heights of svgs
// if they're not in the DOM. To solve this, we add the element to
// the dom, wait for a rerender, and use `.clientWidth` and
// `.clientHeight`. I think we could also solve the problem by
// adding the image to the document before setting the src, but then
// the experience would be worse for other browsers.
if (img.width === 0 && img.height === 0) {
document.body.appendChild(img);
_.defer(function () {
callback(img.clientWidth, img.clientHeight);
document.body.removeChild(img);
});
} else {
callback(img.width, img.height);
}
};
// Require here to prevent recursive imports
var SvgImage = __webpack_require__(67);
img.src = SvgImage.getRealImageUrl(url);
},
textarea: {
/**
* Gets the word right before where the textarea cursor is
*
* @param {Element} textarea - The textarea DOM element
* @return {JSON} - An object with the word and its starting and ending positions in the textarea
*/
getWordBeforeCursor: function getWordBeforeCursor(textarea) {
var text = textarea.value;
var endPos = textarea.selectionStart - 1;
var startPos = Math.max(text.lastIndexOf("\n", endPos), text.lastIndexOf(" ", endPos)) + 1;
return {
string: text.substring(startPos, endPos + 1),
pos: {
start: startPos,
end: endPos
}
};
},
/**
* Moves the textarea cursor at the specified position
*
* @param {Element} textarea - The textarea DOM element
* @param {int} pos - The position where the cursor will be moved
*/
moveCursor: function moveCursor(textarea, pos) {
textarea.selectionStart = pos;
textarea.selectionEnd = pos;
}
}
};
Util.random = Util.seededRNG(new Date().getTime() & 0xffffffff);
module.exports = Util;
/***/ },
/* 18 */,
/* 19 */,
/* 20 */,
/* 21 */,
/* 22 */,
/* 23 */,
/* 24 */,
/* 25 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable max-lines, no-console, no-var, react/prop-types, react/sort-comp */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
/* globals katex */
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var ReactCreateFragment = __webpack_require__(168);
var $ = __webpack_require__(169);
var _ = __webpack_require__(56);
var ApiOptions = __webpack_require__(12).Options;
var DragTarget = __webpack_require__(88);
var _require = __webpack_require__(47),
iconChevronDown = _require.iconChevronDown,
iconChevronRight = _require.iconChevronRight,
iconTrash = _require.iconTrash;
var InlineIcon = __webpack_require__(48);
var KatexErrorView = __webpack_require__(89);
var PerseusMarkdown = __webpack_require__(49);
var PropCheckBox = __webpack_require__(90);
var Util = __webpack_require__(17);
var Widgets = __webpack_require__(31);
var preprocessTex = __webpack_require__(91);
var PerseusEditor = __webpack_require__(92);
var WIDGET_PROP_BLACKLIST = __webpack_require__(93);
// like [[snowman input-number 1]]
var widgetPlaceholder = "[[\u2603 {id}]]";
var widgetRegExp = "(\\[\\[\u2603 {id}\\]\\])";
var rWidgetSplit = new RegExp(widgetRegExp.replace("{id}", "[a-z-]+ [0-9]+"), "g");
var shortcutRegexp = /^\[\[([a-z\-]+)$/; // like [[nu, [[int, etc
var ENDS_WITH_A_PARAGRAPH = /(?:\n{2,}|^\n*)$/;
var TRAILING_NEWLINES = /(\n*)$/;
var LEADING_NEWLINES = /^(\n*)/;
var commafyInteger = function commafyInteger(n) {
var str = n.toString();
if (str.length >= 5) {
str = str.replace(/(\d)(?=(\d{3})+$)/g, "$1{,}");
}
return str;
};
var makeEndWithAParagraphIfNecessary = function makeEndWithAParagraphIfNecessary(content) {
if (!ENDS_WITH_A_PARAGRAPH.test(content)) {
var newlines = TRAILING_NEWLINES.exec(content)[1];
return content + "\n\n".slice(0, 2 - newlines.length);
} else {
return content;
}
};
var makeStartWithAParagraphAlways = function makeStartWithAParagraphAlways(content) {
var newlines = LEADING_NEWLINES.exec(content)[1];
return "\n\n".slice(0, 2 - newlines.length) + content;
};
var WidgetSelect = React.createClass({
displayName: "WidgetSelect",
shouldComponentUpdate: function shouldComponentUpdate() {
return false;
},
handleChange: function handleChange(e) {
var widgetType = e.target.value;
if (widgetType === "") {
// TODO(alpert): Not sure if change will trigger here
// but might as well be safe
return;
}
if (this.props.onChange) {
this.props.onChange(widgetType);
}
},
render: function render() {
var widgets = Widgets.getPublicWidgets();
var orderedWidgetNames = _.sortBy(_.keys(widgets), function (name) {
return widgets[name].displayName;
});
var addWidgetString = "Add a widget\u2026";
return React.createElement(
"select",
{ value: "", onChange: this.handleChange },
React.createElement(
"option",
{ value: "" },
addWidgetString
),
React.createElement(
"option",
{ disabled: true },
"--"
),
_.map(orderedWidgetNames, function (name) {
return React.createElement(
"option",
{ key: name, value: name },
widgets[name].displayName
);
})
);
}
});
// This component handles upgading widget editor props via prop
// upgrade transforms. Widget editors will always be rendered
// with all available transforms applied, but the results of those
// transforms will not be propogated upwards until serialization.
var WidgetEditor = React.createClass({
displayName: "WidgetEditor",
propTypes: {
// Unserialized props
id: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
onRemove: React.PropTypes.func.isRequired,
apiOptions: ApiOptions.propTypes,
// Serialized props
type: React.PropTypes.string.isRequired,
alignment: React.PropTypes.string,
static: React.PropTypes.bool,
graded: React.PropTypes.bool,
options: React.PropTypes.any,
version: React.PropTypes.shape({
major: React.PropTypes.number.isRequired,
minor: React.PropTypes.number.isRequired
})
},
getInitialState: function getInitialState() {
return {
showWidget: false
};
},
componentWillMount: function componentWillMount() {
this._upgradeWidgetInfo(this.props);
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
this._upgradeWidgetInfo(nextProps);
},
_upgradeWidgetInfo: function _upgradeWidgetInfo(props) {
// We can't call serialize here because this.refs.widget
// doesn't exist before this component is mounted.
var filteredProps = _.omit(props, WIDGET_PROP_BLACKLIST);
var info = Widgets.upgradeWidgetInfoToLatestVersion(filteredProps);
this.setState({
widgetInfo: info
});
},
_toggleWidget: function _toggleWidget(e) {
e.preventDefault();
this.setState({ showWidget: !this.state.showWidget });
},
_handleWidgetChange: function _handleWidgetChange(newProps, cb, silent) {
var newWidgetInfo = _.clone(this.state.widgetInfo);
newWidgetInfo.options = _.extend(this.refs.widget.serialize(), newProps);
this.props.onChange(newWidgetInfo, cb, silent);
},
_toggleStatic: function _toggleStatic(e) {
e.preventDefault();
var newWidgetInfo = _.extend({}, this.state.widgetInfo, {
static: !this.state.widgetInfo.static
});
this.props.onChange(newWidgetInfo);
},
_handleAlignmentChange: function _handleAlignmentChange(e) {
var newAlignment = e.target.value;
var newWidgetInfo = _.clone(this.state.widgetInfo);
newWidgetInfo.alignment = newAlignment;
this.props.onChange(newWidgetInfo);
},
render: function render() {
var _this = this;
var widgetInfo = this.state.widgetInfo;
var Ed = Widgets.getEditor(widgetInfo.type);
var supportedAlignments;
if (this.props.apiOptions.showAlignmentOptions) {
supportedAlignments = Widgets.getSupportedAlignments(widgetInfo.type);
} else {
supportedAlignments = ["default"];
}
var supportsStaticMode = Widgets.supportsStaticMode(widgetInfo.type);
var isUngradedEnabled = widgetInfo.type === "transformer";
var gradedPropBox = React.createElement(PropCheckBox, {
label: "Graded:",
graded: widgetInfo.graded,
onChange: this.props.onChange
});
return React.createElement(
"div",
{ className: "perseus-widget-editor" },
React.createElement(
"div",
{
className: "perseus-widget-editor-title " + (this.state.showWidget ? "open" : "closed")
},
React.createElement(
"a",
{
className: "perseus-widget-editor-title-id",
href: "#",
onClick: this._toggleWidget
},
this.props.id,
this.state.showWidget ? React.createElement(InlineIcon, iconChevronDown) : React.createElement(InlineIcon, iconChevronRight)
),
supportsStaticMode && React.createElement("input", {
type: "button",
onClick: this._toggleStatic,
className: "simple-button--small",
value: widgetInfo.static ? "Unset as static" : "Set as static"
}),
supportedAlignments.length > 1 && React.createElement(
"select",
{
className: "alignment",
value: widgetInfo.alignment,
onChange: this._handleAlignmentChange
},
supportedAlignments.map(function (alignment) {
return React.createElement(
"option",
{ key: alignment },
alignment
);
})
),
React.createElement(
"a",
{
href: "#",
className: "remove-widget " + "simple-button simple-button--small orange",
onClick: function onClick(e) {
e.preventDefault();
_this.props.onRemove();
}
},
React.createElement(InlineIcon, iconTrash)
)
),
React.createElement(
"div",
{
className: "perseus-widget-editor-content " + (this.state.showWidget ? "enter" : "leave")
},
isUngradedEnabled && gradedPropBox,
React.createElement(Ed, _extends({
ref: "widget",
onChange: this._handleWidgetChange,
"static": widgetInfo.static,
apiOptions: this.props.apiOptions
}, widgetInfo.options))
)
);
},
getSaveWarnings: function getSaveWarnings() {
var issuesFunc = this.refs.widget.getSaveWarnings;
return issuesFunc ? issuesFunc() : [];
},
serialize: function serialize() {
// TODO(alex): Make this properly handle the case where we load json
// with a more recent widget version than this instance of Perseus
// knows how to handle.
var widgetInfo = this.state.widgetInfo;
return {
type: widgetInfo.type,
alignment: widgetInfo.alignment,
static: widgetInfo.static,
graded: widgetInfo.graded,
options: this.refs.widget.serialize(),
version: widgetInfo.version
};
}
});
// This is more general than the actual markdown image parsing regex,
// which is fine for correctness since it should only mean we could
// store extra image dimensions, unless the question is insanely
// formatted.
// A simplified regex here should hopefully be easier to keep in
// sync if the markdown parsing changes, though if it becomes
// easy to hook into the actual markdown regex without copy-pasting
// it, we should do that.
var IMAGE_REGEX = /!\[[^\]]*\]\(([^\s\)]+)[^\)]*\)/g;
/**
* Find all the matches to a /g regex.
*
* Returns an array of the regex matches. Infinite loops if `regex` does not
* have a /g modifier.
*
* Note: Returns an array of the capture objects, whereas String::match
* ignores captures. If you don't need captures, use String::match
*/
var allMatches = function allMatches(regex, str) {
var result = [];
while (true) {
// eslint-disable-line no-constant-condition
var match = regex.exec(str);
if (!match) {
break;
}
result.push(match);
}
return result;
};
/**
* Return an array of URLs of all the images in the given renderer
* markdown.
*/
var imageUrlsFromContent = function imageUrlsFromContent(content) {
return _.map(allMatches(IMAGE_REGEX, content), function (capture) {
return capture[1];
});
};
/**
* NOTE: This Editor class contains a ton of legacy logic which is not used,
* as a rewrite using Draft.js was implemented in perseus-editor.jsx.
* This code remains as a backup in case bugs in the rewrite block
* content creators.
* If you are going to make Editor changes, you likely want to navigate
* to perseus-editor.jsx
* TODO: Clear out all the textarea code and replace with Draft.js once we are
* comfortable that it is working well consistently
*/
var Editor = React.createClass({
displayName: "Editor",
propTypes: {
apiOptions: ApiOptions.propTypes,
imageUploader: React.PropTypes.func,
onChange: React.PropTypes.func
},
getDefaultProps: function getDefaultProps() {
return {
content: "",
placeholder: "",
widgets: {},
images: {},
disabled: false,
widgetEnabled: true,
immutableWidgets: false,
showWordCount: false,
warnNoPrompt: false,
warnNoWidgets: false
};
},
getInitialState: function getInitialState() {
return {
showKatexErrors: false
};
},
getWidgetEditor: function getWidgetEditor(id, type) {
if (!Widgets.getEditor(type)) {
return;
}
return React.createElement(WidgetEditor, _extends({
ref: id,
id: id,
type: type,
onChange: this._handleWidgetEditorChange.bind(this, id),
onRemove: this._handleWidgetEditorRemove.bind(this, id),
apiOptions: this.props.apiOptions
}, this.props.widgets[id]));
},
_handleWidgetEditorChange: function _handleWidgetEditorChange(id, newProps, cb, silent) {
var widgets = _.clone(this.props.widgets);
widgets[id] = _.extend({}, widgets[id], newProps);
if (this.props.apiOptions.useDraftEditor) {
this.refs.textarea.updateWidget(id, newProps);
}
this.props.onChange({ widgets: widgets }, cb, silent);
},
_handleWidgetEditorRemove: function _handleWidgetEditorRemove(id) {
var textarea = this.refs.textarea;
if (this.props.apiOptions.useDraftEditor) {
textarea.removeWidget(id);
} else {
var re = new RegExp(widgetRegExp.replace("{id}", id), "gm");
this.props.onChange({ content: textarea.value.replace(re, "") });
}
},
/**
* Calculate the size of all the images in props.content, and send
* those sizes to this.props.images using props.onChange.
*/
_sizeImages: function _sizeImages(props) {
var imageUrls = imageUrlsFromContent(props.content);
// Discard any images in our dimension table that no
// longer exist in content.
var images = _.pick(props.images, imageUrls);
// Only calculate sizes for images that were not present previously.
// Most content edits shouldn't have new images.
// This could get weird in the case of multiple images with the same
// URL, if you've changed the backing image size, but given graphie
// hashes it's probably an edge case.
var newImageUrls = _.filter(imageUrls, function (url) {
return !images[url];
});
// TODO(jack): Q promises would make this nicer and only
// fire once.
_.each(newImageUrls, function (url) {
Util.getImageSize(url, function (width, height) {
// We keep modifying the same image object rather than a new
// copy from this.props because all changes here are additive.
// Maintaining old changes isn't strictly necessary if
// props.onChange calls are not batched, but would be if they
// were, so this is nice from that anti-race-condition
// perspective as well.
images[url] = {
width: width,
height: height
};
props.onChange({
images: _.clone(images)
}, null, // callback
true // silent
);
});
});
},
componentDidMount: function componentDidMount() {
// This can't be in componentWillMount because that's happening during
// the middle of our parent's render, so we can't call
// this.props.onChange during that, since it calls our parent's
// setState
this._sizeImages(this.props);
if (!this.props.apiOptions.useDraftEditor) {
$(ReactDOM.findDOMNode(this.refs.textarea)).on("copy cut", this._maybeCopyWidgets).on("paste", this._maybePasteWidgets);
}
},
componentDidUpdate: function componentDidUpdate(prevProps) {
// TODO(alpert): Maybe fix React so this isn't necessary
var textarea = ReactDOM.findDOMNode(this.refs.textarea);
textarea.value = this.props.content;
// This can't be in componentWillReceiveProps because that's happening
// during the middle of our parent's render.
if (this.props.content !== prevProps.content) {
this._sizeImages(this.props);
}
},
handleDrop: function handleDrop(e) {
var _this2 = this;
if (this.props.apiOptions.useDraftEditor) {
return;
}
var content = this.props.content;
var dataTransfer = e.nativeEvent.dataTransfer;
// files will hold something if the drag was from the desktop or a file
// located on the user's computer.
var files = dataTransfer.files;
// ... but we only get a url if the drag originated in another window
if (files.length === 0) {
var imageUrl = dataTransfer.getData("URL");
if (imageUrl) {
// TODO(joel) - relocate when the image upload dialog lands
var newContent = content + "\n\n";
this.props.onChange({ content: newContent });
}
return;
}
/* For each file we make sure it's an image, then create a sentinel -
* snowman + identifier to insert into the current text. The sentinel
* only lives there temporarily until we get a response back from the
* server that the image is now hosted on AWS, at which time we replace
* the temporary sentinel with the permanent url for the image.
*
* There is an abuse of tap in the middle of the pipeline to make sure
* everything is sequenced in the correct order. We want to modify the
* content (given any number of images) at the same time, i.e. only
* once, so we do that step with the tap. After the content has been
* changed we send off the request for each image.
*
* Note that the snowman doesn't do anything special in this case -
* it's effectively just part of a broken link. Perseus could be
* extended to recognize this sentinel and highlight it like for
* widgets.
*/
_(files).chain().map(function (file) {
if (!file.type.match("image.*")) {
return null;
}
var sentinel = "\u2603 " + _.uniqueId("image_");
// TODO(joel) - figure out how to temporarily include the image
// before the server returns.
content += "\n\n";
return { file: file, sentinel: sentinel };
}).reject(_.isNull).tap(function () {
_this2.props.onChange({ content: content });
}).each(function (fileAndSentinel) {
_this2.props.imageUploader(fileAndSentinel.file, function (url) {
_this2.props.onChange({
content: _this2.props.content.replace(fileAndSentinel.sentinel, url)
});
});
});
},
handleChange: function handleChange() {
var textarea = ReactDOM.findDOMNode(this.refs.textarea);
this.props.onChange({ content: textarea.value });
},
_handleKeyDown: function _handleKeyDown(e) {
// Tab-completion of widgets. For example, to insert an image:
// type `[[im`, then tab.
if (e.key === "Tab") {
var textarea = ReactDOM.findDOMNode(this.refs.textarea);
var word = Util.textarea.getWordBeforeCursor(textarea);
var matches = word.string.toLowerCase().match(shortcutRegexp);
if (matches != null) {
var text = matches[1];
var widgets = Widgets.getAllWidgetTypes();
var matchingWidgets = _.filter(widgets, function (name) {
return name.substring(0, text.length) === text;
});
if (matchingWidgets.length === 1) {
var widgetType = matchingWidgets[0];
this._addWidgetToContent(this.props.content, [word.pos.start, word.pos.end + 1], widgetType);
}
e.preventDefault();
}
}
},
_maybeCopyWidgets: function _maybeCopyWidgets(e) {
// If there are widgets being cut/copied, put the widget JSON in
// localStorage.perseusLastCopiedWidgets to allow copy-pasting of
// widgets between Editors. Also store the text to be pasted in
// localStorage.perseusLastCopiedText since we want to know if the user
// is actually pasting something originally from Perseus later.
var textarea = e.target;
var selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
var widgetNames = _.map(selectedText.match(rWidgetSplit), function (syntax) {
return Util.rWidgetParts.exec(syntax)[1];
});
var widgetData = _.pick(this.serialize().widgets, widgetNames);
localStorage.perseusLastCopiedText = selectedText;
localStorage.perseusLastCopiedWidgets = JSON.stringify(widgetData);
console.log("Widgets copied: " + localStorage.perseusLastCopiedWidgets);
},
_maybePasteWidgets: function _maybePasteWidgets(e) {
// Use the data from localStorage to paste any widgets we copied
// before. Avoid name conflicts by renumbering pasted widgets so that
// their numbers are always higher than the highest numbered widget of
// their type.
// TODO(sam): Fix widget numbering in the widget editor titles
var widgetJSON = localStorage.perseusLastCopiedWidgets;
var lastCopiedText = localStorage.perseusLastCopiedText;
var textToBePasted = e.originalEvent.clipboardData.getData("text");
// Only intercept if we have widget data to paste and the user is
// pasting something originally from Perseus.
// TODO(sam/aria/alex): Make it so that you can paste arbitrary text
// (e.g. from a text editor) instead of exactly what was copied, and
// let the widgetJSON match up with it. This would let you copy text
// into a buffer, perform complex operations on it, then paste it back.
if (widgetJSON && lastCopiedText === textToBePasted) {
e.preventDefault();
var widgetData = JSON.parse(widgetJSON);
var safeWidgetMapping = this._safeWidgetNameMapping(widgetData);
// Use safe widget name map to construct the new widget data
// TODO(aria/alex): Don't use `rWidgetSplit` or other piecemeal
// regexes directly; abstract this out so that we don't have to
// worry about potential edge cases.
var safeWidgetData = {};
for (var _iterator = Object.entries(widgetData), _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) {
var _ref;
if (_isArray) {
if (_i >= _iterator.length) break;
_ref = _iterator[_i++];
} else {
_i = _iterator.next();
if (_i.done) break;
_ref = _i.value;
}
var _ref2 = _ref,
key = _ref2[0],
data = _ref2[1];
safeWidgetData[safeWidgetMapping[key]] = data;
}
var newWidgets = _.extend(safeWidgetData, this.props.widgets);
// Use safe widget name map to construct new text
var safeText = lastCopiedText.replace(rWidgetSplit, function (syntax) {
var match = Util.rWidgetParts.exec(syntax);
var completeWidget = match[0];
var widget = match[1];
return completeWidget.replace(widget, safeWidgetMapping[widget]);
});
// Add pasted text to previous content, replacing selected text to
// replicate normal paste behavior.
var textarea = e.target;
var selectionStart = textarea.selectionStart;
var newContent = this.props.content.substr(0, selectionStart) + safeText + this.props.content.substr(textarea.selectionEnd);
this.props.onChange({ content: newContent, widgets: newWidgets }, function () {
var expectedCursorPosition = selectionStart + safeText.length;
Util.textarea.moveCursor(textarea, expectedCursorPosition);
});
}
},
_safeWidgetNameMapping: function _safeWidgetNameMapping(widgetData) {
// Helper function for _maybePasteWidgets.
// For each widget about to be pasted, construct a mapping from
// old widget name to a new widget name that doesn't have conflicts
// with widgets already in the editor.
// eg. If there is an "image 2" already present in the editor and we're
// about to paste in two new images, return
// { "image 1": "image 3", "image 2": "image 4" }
// List of widgets about to be pasted as [[name, number], ...]
var widgets = _.keys(widgetData).map(function (name) {
return name.split(" ");
});
var widgetTypes = _.uniq(widgets.map(function (widget) {
return widget[0];
}));
// List of existing widgets as [[name, number], ...]
var existingWidgets = _.keys(this.props.widgets).map(function (name) {
return name.split(" ");
});
// Mapping of widget type to a safe (non-conflicting) number
// eg. { "image": 2, "dropdown": 1 }
var safeWidgetNums = {};
_.each(widgetTypes, function (type) {
safeWidgetNums[type] = _.chain(existingWidgets).filter(function (existingWidget) {
return existingWidget[0] === type;
}).map(function (existingWidget) {
return +existingWidget[1] + 1;
}).max().value();
// If there are no existing widgets _.max returns -Infinity
safeWidgetNums[type] = Math.max(safeWidgetNums[type], 1);
});
// Construct mapping, incrementing the vals in safeWidgetNums as we go
var safeWidgetMapping = {};
_.each(widgets, function (widget) {
var widgetName = widget.join(" ");
var widgetType = widget[0];
safeWidgetMapping[widgetName] = widgetType + " " + safeWidgetNums[widgetType];
safeWidgetNums[widgetType]++;
});
return safeWidgetMapping;
},
_addWidgetToContent: function _addWidgetToContent(oldContent, cursorRange, widgetType) {
var textarea = ReactDOM.findDOMNode(this.refs.textarea);
// Note: we have to use _.map here instead of Array::map
// because the results of a .match might be null if no
// widgets were found.
var allWidgetIds = _.map(oldContent.match(rWidgetSplit), function (syntax) {
var match = Util.rWidgetParts.exec(syntax);
var type = match[2];
var num = +match[3];
return [type, num];
});
var widgetNum = _.reduce(allWidgetIds, function (currentNum, otherId) {
var otherType = otherId[0],
otherNum = otherId[1];
if (otherType === widgetType) {
return Math.max(otherNum + 1, currentNum);
} else {
return currentNum;
}
}, 1);
var id = widgetType + " " + widgetNum;
var widgetContent = widgetPlaceholder.replace("{id}", id);
// Add newlines before block-display widgets like graphs
var isBlock = Widgets.getDefaultAlignment(widgetType) === "block";
var prelude = oldContent.slice(0, cursorRange[0]);
var postlude = oldContent.slice(cursorRange[1]);
var newPrelude = isBlock ? makeEndWithAParagraphIfNecessary(prelude) : prelude;
var newPostlude = isBlock ? makeStartWithAParagraphAlways(postlude) : postlude;
var newContent = newPrelude + widgetContent + newPostlude;
var newWidgets = _.clone(this.props.widgets);
newWidgets[id] = {
options: Widgets.getEditor(widgetType).defaultProps,
type: widgetType,
// Track widget version on creation, so that a widget editor
// without a valid version prop can only possibly refer to a
// pre-versioning creation time.
version: Widgets.getVersion(widgetType)
};
this.props.onChange({
content: newContent,
widgets: newWidgets
}, function () {
Util.textarea.moveCursor(textarea,
// We want to put the cursor after the widget
// and after any added newlines
newContent.length - postlude.length);
});
},
_addWidget: function _addWidget(widgetType) {
var textarea = this.refs.textarea;
if (this.props.apiOptions.useDraftEditor) {
textarea.addWidget(widgetType);
} else {
this._addWidgetToContent(this.props.content, [textarea.selectionStart, textarea.selectionEnd], widgetType);
textarea.focus();
}
},
// NOTE: These templates are all duplicated verbatim in perseus-editor.jsx
// so any changes should also be done there, until this code is all
// deleted in favor of perseus-editor.jsx
addTemplate: function addTemplate(e) {
var templateType = e.target.value;
if (templateType === "") {
return;
}
e.target.value = "";
if (this.props.apiOptions.useDraftEditor) {
this.refs.textarea.addTemplate(templateType);
return;
}
var oldContent = this.props.content;
// Force templates to have a blank line before them,
// as they are usually used as block elements
// (especially important for tables)
oldContent = oldContent.replace(/\n*$/, "\n\n");
var template;
if (templateType === "table") {
template = "header 1 | header 2 | header 3\n" + "- | - | -\n" + "data 1 | data 2 | data 3\n" + "data 4 | data 5 | data 6\n" + "data 7 | data 8 | data 9";
} else if (templateType === "titledTable") {
template = "|| **Table title** ||\n" + "header 1 | header 2 | header 3\n" + "- | - | -\n" + "data 1 | data 2 | data 3\n" + "data 4 | data 5 | data 6\n" + "data 7 | data 8 | data 9";
} else if (templateType === "alignment") {
template = "$\\begin{align} x+5 &= 30 \\\\\n" + "x+5-5 &= 30-5 \\\\\n" + "x &= 25 \\end{align}$";
} else if (templateType === "piecewise") {
template = "$f(x) = \\begin{cases}\n" + "7 & \\text{if }x=1 \\\\\n" + "f(x-1)+5 & \\text{if }x > 1\n" + "\\end{cases}$";
} else if (templateType === "allWidgets") {
template = Widgets.getAllWidgetTypes().map(function (type) {
return "[[" + Util.snowman + " " + type + " 1]]";
}).join("\n\n");
} else {
throw new Error("Invalid template type: " + templateType);
}
var newContent = oldContent + template;
this.props.onChange({ content: newContent }, this.focusAndMoveToEnd);
},
getSaveWarnings: function getSaveWarnings() {
var _this3 = this;
var widgetIds = _.intersection(this.widgetIds, _.keys(this.refs));
var warnings = _(widgetIds).chain().map(function (id) {
var issuesFunc = _this3.refs[id].getSaveWarnings;
var issues = issuesFunc ? issuesFunc() : [];
return _.map(issues, function (issue) {
return id + ": " + issue;
});
}).flatten(true).value();
return warnings;
},
focus: function focus() {
ReactDOM.findDOMNode(this.refs.textarea).focus();
},
focusAndMoveToEnd: function focusAndMoveToEnd() {
this.focus();
var textarea = ReactDOM.findDOMNode(this.refs.textarea);
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.value.length;
},
render: function render() {
var pieces;
var widgets;
var underlayPieces;
var widgetsDropDown;
var templatesDropDown;
var widgetsAndTemplates;
var wordCountDisplay;
var katexErrorList = [];
if (this.props.showWordCount) {
var numChars = PerseusMarkdown.characterCount(this.props.content);
var numWords = Math.floor(numChars / 6);
wordCountDisplay = React.createElement(
"span",
{
className: "perseus-editor-word-count",
title: "~" + commafyInteger(numWords) + " words (" + commafyInteger(numChars) + " characters)"
},
commafyInteger(numWords)
);
}
if (this.props.widgetEnabled) {
pieces = Util.split(this.props.content, rWidgetSplit);
widgets = {};
underlayPieces = [];
for (var i = 0; i < pieces.length; i++) {
if (i % 2 === 0) {
// Normal text
underlayPieces.push(pieces[i]);
var ast = PerseusMarkdown.parse(pieces[i]);
PerseusMarkdown.traverseContent(ast, function (node) {
if (node.type === "math" || node.type === "blockMath") {
var content = preprocessTex(node.content);
try {
katex.renderToString(content);
} catch (e) {
katexErrorList.push({
math: content,
message: e.message
});
}
}
});
} else {
// Widget reference
var match = Util.rWidgetParts.exec(pieces[i]);
var id = match[1];
var type = match[2];
var selected = false;
// TODO(alpert):
// var selected = focused && selStart === selEnd &&
// offset <= selStart &&
// selStart < offset + text.length;
// if (selected) {
// selectedWidget = id;
// }
var duplicate = id in widgets;
widgets[id] = this.getWidgetEditor(id, type);
var classes = (duplicate || !widgets[id] ? "error " : "") + (selected ? "selected " : "");
var key = duplicate ? i : id;
underlayPieces.push(React.createElement(
"b",
{ className: classes, key: key },
pieces[i]
));
}
}
// TODO(alpert): Move this to the content-change event handler
// _.each(_.keys(this.props.widgets), function(id) {
// if (!(id in widgets)) {
// // It's strange if these preloaded options stick around
// // since it's inconsistent with how things work if you
// // don't have the serialize/deserialize step in the
// // middle
// // TODO(alpert): Save options in a consistent manner so
// // that you can undo the deletion of a widget
// delete this.props.widgets[id];
// }
// }, this);
this.widgetIds = _.keys(widgets);
widgetsDropDown = React.createElement(WidgetSelect, { ref: "widgetSelect", onChange: this._addWidget });
var insertTemplateString = "Insert template\u2026";
templatesDropDown = React.createElement(
"select",
{ onChange: this.addTemplate },
React.createElement(
"option",
{ value: "" },
insertTemplateString
),
React.createElement(
"option",
{ disabled: true },
"--"
),
React.createElement(
"option",
{ value: "table" },
"Table"
),
React.createElement(
"option",
{ value: "titledTable" },
"Titled table"
),
React.createElement(
"option",
{ value: "alignment" },
"Aligned equations"
),
React.createElement(
"option",
{ value: "piecewise" },
"Piecewise function"
),
React.createElement(
"option",
{ disabled: true },
"--"
),
React.createElement(
"option",
{ value: "allWidgets" },
"All widgets (for testing)"
)
);
if (!this.props.immutableWidgets) {
widgetsAndTemplates = React.createElement(
"div",
{ className: "perseus-editor-widgets" },
React.createElement(
"div",
{ className: "perseus-editor-widgets-selectors" },
widgetsDropDown,
templatesDropDown,
wordCountDisplay
),
ReactCreateFragment(widgets)
);
// Prevent word count from being displayed elsewhere
wordCountDisplay = null;
}
} else {
underlayPieces = [this.props.content];
}
// Without this, the underlay isn't the proper size when the text ends
// with a newline.
underlayPieces.push(React.createElement("br", { key: "end" }));
var completeTextarea = [React.createElement(
"div",
{
className: "perseus-textarea-underlay",
ref: "underlay",
key: "underlay"
},
underlayPieces
), React.createElement("textarea", {
ref: "textarea",
key: "textarea",
onChange: this.handleChange,
onKeyDown: this._handleKeyDown,
placeholder: this.props.placeholder,
disabled: this.props.disabled,
value: this.props.content
})];
if (this.props.apiOptions.useDraftEditor) {
completeTextarea = React.createElement(PerseusEditor, {
ref: "textarea",
onChange: this.props.onChange,
content: this.props.content,
placeholder: this.props.placeholder,
initialWidgets: this.props.widgets,
imageUploader: this.props.imageUploader,
widgetEnabled: this.props.widgetEnabled
});
}
var textareaWrapper;
if (this.props.imageUploader) {
textareaWrapper = React.createElement(
DragTarget,
{
onDrop: this.handleDrop,
className: "perseus-textarea-pair"
},
completeTextarea
);
} else {
textareaWrapper = React.createElement(
"div",
{ className: "perseus-textarea-pair" },
completeTextarea
);
}
var contentWithoutWidgets = this.props.content.replace(/\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/g, "");
var noPrompt = contentWithoutWidgets.trim().length === 0;
var noWidgets = !/\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/g.test(this.props.content);
var warningStyle = {
borderTop: "none",
padding: 4,
backgroundColor: "pink"
};
return React.createElement(
"div",
{
className: "perseus-single-editor " + (this.props.className || "")
},
textareaWrapper,
katexErrorList.length > 0 && React.createElement(KatexErrorView, { errorList: katexErrorList }),
this.props.warnNoPrompt && noPrompt && React.createElement(
"div",
{ style: warningStyle },
"Graded Groups should contain a prompt"
),
this.props.warnNoWidgets && noWidgets && React.createElement(
"div",
{ style: warningStyle },
"Graded Groups should contain at least one widget"
),
wordCountDisplay,
widgetsAndTemplates
);
},
serialize: function serialize(options) {
var _this4 = this;
// need to serialize the widgets since the state might not be
// completely represented in props. ahem //transformer// (and
// interactive-graph and plotter).
var widgets = {};
var widgetIds = _.intersection(this.widgetIds, _.keys(this.refs));
_.each(widgetIds, function (id) {
widgets[id] = _this4.refs[id].serialize();
});
// Preserve the data associated with deleted widgets in their last
// modified form. This is only intended to be useful in the context of
// immediate cut and paste operations if Editor.serialize() is called
// in between the two (which ideally should not be happening).
// TODO(alex): Remove this once all widget.serialize() methods
// have been fixed to only return props,
// and the above no longer applies.
if (options && options.keepDeletedWidgets) {
_.chain(this.props.widgets).keys().reject(function (id) {
return _.contains(widgetIds, id);
}).each(function (id) {
widgets[id] = _this4.props.widgets[id];
});
}
return {
replace: this.props.replace,
content: this.props.content,
images: this.props.images,
widgets: widgets
};
}
});
module.exports = Editor;
/***/ },
/* 26 */,
/* 27 */,
/* 28 */,
/* 29 */
/***/ function(module, exports, __webpack_require__) {
/*
We are currently in a situation where Crowdin adds extra backslashes
to some strings, but not all. However, we can trust that an individual
string is in its entirety either escaped or not. This will allow us to use
a heuristic to determine with a high probability whether a string is
escaped or not, and thus whether to unescape it or not in
our `before_dom_insert()` JIPT hook.
TODO(aasmund): Delete this file when we have converted all our strings
to the new, unescaped Crowdin format. Calls to `maybeUnescape()` should
be deleted (unescaping will never be needed anymore).
The heuristic is as follows:
- For each LaTeX-like token (one or more backslashes followed by a special
character or by at least one letter) in the string:
- If the token cannot be unescaped (e.g. \e ), return the original string.
Otherwise, we now have two candidate tokens: the original tokens
and the escaped tokens.
- For both the original and the unescaped token, compute a number that
indicates *how* LaTeX-like the token is:
- 6 if there's an odd number of backslashes followed by a valid LaTeX
macro name (all backslashes but the last form a sequence of LaTeX
newlines, and the last one starts the LaTeX macro)
- 4 if there's one backslash followed by a dollar sign
(this is likely intended to actually display a dollar sign)
- 3 if there's an even number of backslashes (this is a sequence of
LaTeX newlines, and the letters that follow are LaTeX math variables)
- 2 if there are no backslashes (this is valid, but not LaTeX)
- 1 if there's an odd number of backslashes (but more than one)
followed by a dollar sign (our strings don't currently contain this)
- 0 otherwise (if there's an odd number of backslashes followed by
something that isn't a valid LaTeX macro name)
- If exactly one of the candidate strings contains one or more tokens
that scored 0, return the other string. Otherwise, return the string
with the highest sum of token scores, choosing the unescaped string
if there's a tie.
This algorithm was implemented in Python and run against all of our current
Crowdin strings (about 440,000), both in original form and escaped form.
It *always* makes the correct guess for the original strings, and makes
the wrong guess only for 30 of the escaped strings.
*/
// This regex captures sequences that might represent LaTeX or escape sequences
var MAYBE_LATEX_REGEX = /\\+([ !#$%*,.:;\[\]\^_{|}]|[a-zA-Z]*)/g;
// These are the LaTeX macros that are currently in use in our strings.
// They were collected by applying the above regex to all of our
// Crowdin strings, and manually removing most invalid macro names. Macro name
// validity was tested by pasting the macros into the Perseus editor.
// Invalid macros would typically be the result of the regex finding a sequence
// of LaTeX newlines followed by regular text, e.g. "\\\\xy = z". However,
// there are some actual misspellings around, so we've kept those.
// Note that \$ is handled separately.
var LATEX_MACROS_LIST = [" ", "!", "#", "%", "*", ",", ".", ":", ";", "[", "]", "^", "_", "{", "|", "}", "alpha", "angle", "approx", "arccos", "arcsin", "arctan", "arrow", "bar", "barwedge", "begin", "beta", "bf", "big", "Big", "bigg", "Bigg", "bigl", "Bigl", "bigr", "Bigr", "bigstar", "bigtriangledown", "bigtriangleup", "binom", "blacklozenge", "blue", "blueA", "blueB", "blueC", "blueD", "blueE", "boldsymbol", "Box", "boxdot", "boxed", "bullet", "cancel", "cap", "cdot", "cdots", "checkmark", "chi", "circ", "circledcirc", "clubsuit", "colon", "color", "cong", "cos", "cot", "csc", "cup", "curvearrowright", "dagger", "dbinom", "ddots", "delta", "Delta", "det", "dfrac", "diamond", "diamondsuit", "displaystyle", "div", "dot", "dots", "downarrow", "Downarrow", "ell", "end", "enspace", "epsilon", "equiv", "eta", "fbox", "flat", "footnotesize", "frac", "frown", "gamma", "Gamma", "gcf", "ge", "geq", "gg", "goldB", "goldC", "goldD", "goldE", "gray", "grayD", "grayE", "grayF", "green", "greenB", "greenC", "greenD", "greenE", "gt", "hat", "hbox", "heartsuit", "hline", "hphantom", "huge", "Huge", "iff", "iiint", "iint", "implies", "in", "infty", "int", "intercal", "it", "kaBlue", "kappa", "kern", "lambda", "langle", "large", "Large", "LARGE", "lcm", "ldots", "le", "left", "leftarrow", "leftrightarrow", "Leftrightarrow", "leftrightharpoons", "leftroot", "leq", "lfloor", "lg", "lim", "limits", "llap", "ln", "log", "longrightarrow", "Longrightarrow", "lozenge", "lt", "lvert", "maroonB", "maroonC", "maroonD", "maroonE", "mathbb", "mathbf", "mathcal", "mathop", "mathrm", "mathsf", "max", "mbox", "mid", "mp", "mu", "nabla", "ne", "nearrow", "neq", "ngeq", "ngtr", "nleq", "nless", "normalsize", "not", "nu", "nx", "odot", "oint", "omega", "Omega", "operatorname", "oplus", "orange", "oslash", "otimes", "overbrace", "overleftarrow", "overleftrightarrow", "overline", "overrightarrow", "overset", "parallel", "partial", "perp", "phantom", "phi", "Phi", "pi", "pink", "pm", "prime", "propto", "psi", "Psi", "purple", "purpleA", "purpleC", "purpleD", "purpleE", "qquad", "quad", "raise", "rangle", "red", "redA", "redB", "redC", "redD", "redE", "rfloor", "rho", "right", "rightarrow", "Rightarrow", "rightleftharpoons", "rvert", "scriptsize", "scriptstyle", "searrow", "sec", "setminus", "sharp", "sigma", "Sigma", "sim", "simeq", "sin", "small", "space", "sqrt", "square", "stackrel", "star", "substack", "sum", "swarrow", "tan", "tan", "tau", "tealA", "tealB", "tealC", "tealD", "tealE", "text", "textbf", "textit", "textrm", "tfrac", "therefore", "theta", "Theta", "tilde", "times", "tiny", "to", "triangle", "triangleleft", "triangleright", "underbrace", "underline", "underset", "uparrow", "uproot", "varphi", "vdots", "vec", "veebar", "vert", "vphantom", "widehat", "xi", "xrightarrow",
// These aren't valid LaTeX macros, but they are misspellings
// that also occur in our strings.
"Begin", "End", "inte", "lamba", "textb"];
var LATEX_MACROS = LATEX_MACROS_LIST.reduce(function (result, macro) {
result[macro] = null;
return result;
}, {});
// These escape sequences are the only ones that are in use. Note that newline,
// carriage return, tab, and backslash are the only characters we use that have
// standard escape sequences (we do not use vertical tab, form feed, etc.).
// We also expect that Unicode characters are never represented
// by their \u escape sequence.
var ESCAPE_SEQUENCES = {
n: "\n",
r: "\r",
t: "\t",
"\\": "\\"
};
// Returns a number representing how "LaTeX-like" a token is.
// Will only be run on tokens that match `MAYBE_LATEX_REGEX`, or the result of
// unescaping such a token. See the comment at the top of the file for details.
var getLatexLevel = function getLatexLevel(text) {
var backslashCount = 0;
while (backslashCount < text.length && text[backslashCount] === "\\") {
backslashCount++;
}
if (backslashCount === 0) {
return 2; // Valid, but doesn't contain any LaTeX syntax
} else if (backslashCount % 2 === 0) {
return 3; // Sequence of LaTeX newlines followed by other chars
} else {
// An odd number of backslashes would be a sequence of LaTeX newlines
// followed by a LaTeX macro
var maybeMacro = text.substring(backslashCount);
if (maybeMacro === "$") {
// Valid, but all our strings that use escaped dollars only have
// one backslash, so this is likely wrong if there are more
return backslashCount === 1 ? 4 : 1;
} else {
return LATEX_MACROS.hasOwnProperty(maybeMacro) ? 6 : 0;
}
}
};
var tryUnescape = function tryUnescape(text) {
var i = 0;
var result = "";
while (i < text.length) {
var c = text[i];
if (c === "\\") {
i += 1;
if (i === text.length) {
return null; // Odd number of backslashes - not unescapable
}
var e = text[i];
if (ESCAPE_SEQUENCES.hasOwnProperty(e)) {
result += ESCAPE_SEQUENCES[e];
} else {
return null; // Invalid escape sequence
}
} else {
result += c;
}
i += 1;
}
return result;
};
var shouldUnescape = function shouldUnescape(text) {
// - If there are no backslashes in the text, (un)escaping will have
// no effect, so we can't tell whether this string has been escaped
// by Crowdin. However, in order to help Manticore detect situations
// where the translation is escaped and the source string isn't,
// we will declare such strings not to be escaped.
// - For each token that might be LaTeX:
// - Try to unescape it. If that fails, we can say for certain
// that `text` is not escaped.
// - Compute the LaTeX level (see the comment at the top of the file)
// for the original and the unescaped token, and sum these values over
// all the tokens. Also, keep track of whether any of the original
// or escaped tokens contain invalid LaTeX.
// - If there existed invalid LaTeX only in the original tokens, we need to
// unescape; if there existed invalid LaTeX only in the escaped tokens,
// we need to keep the original.
// - Otherwise, select the version with the highest LaTeX level, preferring
// the unescaped version if there's a tie (because most of our strings
// are currently in the "old Crowdin style", meaning that they
// are escaped).
if (text.indexOf("\\") < 0) {
return false;
}
var levelSumOriginal = 0;
var levelSumUnescaped = 0;
var anyInvalidLatexInOriginal = false;
var anyInvalidLatexInUnescaped = false;
var match = void 0;
MAYBE_LATEX_REGEX.lastIndex = 0;
while ((match = MAYBE_LATEX_REGEX.exec(text)) !== null) {
var original = match[0];
var unescaped = tryUnescape(original);
if (unescaped === null) {
return false;
}
var originalLevel = getLatexLevel(original);
if (originalLevel === 0) {
anyInvalidLatexInOriginal = true;
}
var unescapedLevel = getLatexLevel(unescaped);
if (unescapedLevel === 0) {
anyInvalidLatexInUnescaped = true;
}
levelSumOriginal += originalLevel;
levelSumUnescaped += unescapedLevel;
}
if (anyInvalidLatexInOriginal !== anyInvalidLatexInUnescaped) {
return anyInvalidLatexInOriginal;
}
return levelSumUnescaped >= levelSumOriginal;
};
// Unescape the given string if it seems to be escaped.
var maybeUnescape = function maybeUnescape(text) {
if (shouldUnescape(text)) {
return tryUnescape(text);
} else {
return text;
}
};
// Unescape both of the given strings if the first one seems to be escaped.
// This is necessary because some Crowdin string pairs have a mismatch in
// their escaping: the source is new-style (not escaped), while the translation
// is old-style (escaped). Such strings will give an error in the translation
// download job, so we must ensure that they also give an error in the frontend.
// We do this by retaining the mismatch in their escaping, instead of giving
// them individual unescaping treatments.
var maybeUnescapeAccordingToSource = function maybeUnescapeAccordingToSource(source, translation) {
if (shouldUnescape(source)) {
// Note: We have not yet seen a situation where the source string is
// escaped and the translation is unescaped, so we choose not to care
// about that here. If it does happen, the second element in the
// returned array might be null.
return [tryUnescape(source), tryUnescape(translation)];
} else {
return [source, translation];
}
};
module.exports = {
maybeUnescape: maybeUnescape,
maybeUnescapeAccordingToSource: maybeUnescapeAccordingToSource,
shouldUnescape: shouldUnescape
};
/***/ },
/* 30 */
/***/ function(module, exports, __webpack_require__) {
/**
* This library provides support for Perseus multi-items: structured Perseus
* content that content creators can easily create, and that applications can
* easily render into different parts of the layout.
*
* For more details about application and motivation, see:
* https://sites.google.com/a/khanacademy.org/forge/for-developers/perseus-items-and-multi-items
*
* This file primarily exposes the `MultiRenderer` component, which performs
* multi-rendering. To multi-render a question, pass in the content of the item
* to the `MultiRenderer` component as a props. Then, pass in a function which
* takes an object of renderers (in the same structure as the content), and
* return a render tree. The `MultiRenderer` component will allow you to
* combine scores, serialized state, etc. without having to manually call on
* each of the functions. It also handles inter-widgets requests between the
* different renderers.
* For more details, see `multi-items/multi-renderer.jsx`.
*
* Example:
*
* item = {_multi: {
* left: ,
* right: [, ],
* }}
* shape = shapes.shape({
* left: shapes.content,
* right: shapes.arrayOf(shapes.content),
* })
*
*
* {({renderers}) =>
*
*
{renderers.left}
*
* {renderers.right.map(r =>
{r}
)}
*
*
* }
*
*
* This file also exposes `shapes`, which helps you construct a runtime type
* declaration for your particular class of multi-item. This can then be used
* to create a MultirendererEditor for your multi-item shape, and to validate
* that a multi-item conforms to the shape via `buildPropTypeForShape`.
* For more details, see `multi-items/shapes.js`.
*
* This file also exposes some utility functions for working with generic
* multi-items, like `findContentNodesInItem`, `findHintNodesInItem`,
* `inferItemShape`, and `buildEmptyItemForShape`.
* For more details, see `multi-items/items.js`.
*/
var _require = __webpack_require__(61),
buildEmptyItemForShape = _require.buildEmptyItemForShape,
findContentNodesInItem = _require.findContentNodesInItem,
findHintNodesInItem = _require.findHintNodesInItem,
inferItemShape = _require.inferItemShape;
var MultiRenderer = __webpack_require__(62);
var _require2 = __webpack_require__(63),
buildPropTypeForShape = _require2.buildPropTypeForShape;
var shapes = __webpack_require__(64);
module.exports = {
// Tools for rendering your multi-items
MultiRenderer: MultiRenderer,
// Tools for declaring your multi-item shapes
shapes: shapes,
buildPropTypeForShape: buildPropTypeForShape,
// Tools for generically manipulating multi-items
buildEmptyItemForShape: buildEmptyItemForShape,
findContentNodesInItem: findContentNodesInItem,
findHintNodesInItem: findHintNodesInItem,
inferItemShape: inferItemShape
};
/***/ },
/* 31 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable no-console, no-var, space-before-function-paren */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var _ = __webpack_require__(56);
var DEFAULT_ALIGNMENT = "block";
var DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
var DEFAULT_STATIC = false;
var DEFAULT_TRACKING = "";
var DEFAULT_LINTABLE = false;
var widgets = {};
var editors = {};
var Widgets = {
// Widgets must be registered to avoid circular dependencies with the
// core Editor and Renderer components.
register: function register(name, widget, editor) {
widgets[name] = widget;
editors[name] = editor;
},
registerMany: function registerMany(widgets) {
var _this = this;
widgets.forEach(function (_ref) {
var widget = _ref[0],
editor = _ref[1];
widget && _this.register(widget.name, widget, editor);
});
this.validateAlignments();
},
getWidget: function getWidget(name) {
// TODO(alex): Consider referring to these as renderers to avoid
// overloading "widget"
if (!_.has(widgets, name)) {
return null;
}
// Allow widgets to specify a widget directly or via a function
if (widgets[name].getWidget) {
return widgets[name].getWidget();
} else {
return widgets[name].widget;
}
},
getEditor: function getEditor(name) {
return _.has(editors, name) ? editors[name] : null;
},
getTransform: function getTransform(name) {
return _.has(widgets, name) ? widgets[name].transform || _.identity : null;
},
getVersion: function getVersion(name) {
var widgetInfo = widgets[name];
if (widgetInfo) {
return widgets[name].version || { major: 0, minor: 0 };
} else {
return null;
}
},
getVersionVector: function getVersionVector() {
var version = {};
_.each(_.keys(widgets), function (name) {
version[name] = Widgets.getVersion(name);
});
return version;
},
getPublicWidgets: function getPublicWidgets() {
// TODO(alex): Update underscore.js so that _.pick can take a function.
return _.pick(widgets, _.reject(_.keys(widgets), function (name) {
return widgets[name].hidden;
}));
},
isAccessible: function isAccessible(widgetInfo) {
var accessible = widgets[widgetInfo.type].accessible;
if (typeof accessible === "function") {
return accessible(widgetInfo.options);
} else {
return !!accessible;
}
},
getAllWidgetTypes: function getAllWidgetTypes() {
return _.keys(widgets);
},
upgradeWidgetInfoToLatestVersion: function upgradeWidgetInfoToLatestVersion(oldWidgetInfo) {
var type = oldWidgetInfo.type;
if (!_.isString(type)) {
throw new Error("widget type must be a string, but was: " + type);
}
var widgetExports = widgets[type];
if (widgetExports == null) {
// If we have a widget that isn't registered, we can't upgrade it
// TODO(aria): Figure out what the best thing to do here would be
return oldWidgetInfo;
}
// Unversioned widgets (pre-July 2014) are all implicitly 0.0
var initialVersion = oldWidgetInfo.version || { major: 0, minor: 0 };
var latestVersion = widgetExports.version || { major: 0, minor: 0 };
// If the widget version is later than what we understand (major
// version is higher than latest, or major versions are equal and minor
// version is higher than latest), don't perform any upgrades.
if (initialVersion.major > latestVersion.major || initialVersion.major === latestVersion.major && initialVersion.minor > latestVersion.minor) {
return oldWidgetInfo;
}
// We do a clone here so that it's safe to mutate the input parameter
// in propUpgrades functions (which I will probably accidentally do at
// some point, and we would like to not break when that happens).
var newEditorProps = _.clone(oldWidgetInfo.options) || {};
var upgradePropsMap = widgetExports.propUpgrades || {};
// Empty props usually mean a newly created widget by the editor,
// and are always considerered up-to-date.
// Mostly, we'd rather not run upgrade functions on props that are
// not complete.
if (_.keys(newEditorProps).length !== 0) {
// We loop through all the versions after the current version of
// the loaded widget, up to and including the latest version of the
// loaded widget, and run the upgrade function to bring our loaded
// widget's props up to that version.
// There is a little subtlety here in that we call
// upgradePropsMap[1] to upgrade *to* version 1,
// (not from version 1).
for (var nextVersion = initialVersion.major + 1; nextVersion <= latestVersion.major; nextVersion++) {
if (upgradePropsMap[nextVersion]) {
newEditorProps = upgradePropsMap[nextVersion](newEditorProps);
} else if (typeof console !== "undefined" && console.warn) {
// This is a warning because it is unlikely to be hit in
// local testing, and a warning is slightly less scary in
// prod than a `throw new Error`
console.warn("No upgrade found for widget `" + type + "` from " + "major version `" + (nextVersion - 1) + "` to " + "major version `" + nextVersion + "` found. This " + "is necessary to render this `" + type + "` correctly.");
// But try to keep going anyways (yolo!)
// (Throwing an error here would just break the page
// silently anyways, so that doesn't seem much better
// than a halfhearted attempt to continue, however
// shallow...)
}
}
}
// Minor version upgrades (eg. new optional props) don't have
// transform functions. Instead, we fill in the new props with their
// defaults.
var defaultProps = editors[type].defaultProps;
newEditorProps = _.extend({}, defaultProps, newEditorProps);
var alignment = oldWidgetInfo.alignment;
// Widgets that support multiple alignments will "lock in" the
// alignment to the alignment that would be listed first in the
// select box. If the widget only supports one alignment, the
// alignment value will likely just end up as "default".
if (alignment == null || alignment === "default") {
alignment = Widgets.getSupportedAlignments(type)[0];
}
var widgetStatic = oldWidgetInfo.static;
if (widgetStatic == null) {
widgetStatic = DEFAULT_STATIC;
}
return _.extend({}, oldWidgetInfo, {
// maintain other info, like type
// After upgrading we guarantee that the version is up-to-date
version: latestVersion,
// Default graded to true (so null/undefined becomes true):
graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true,
alignment: alignment,
static: widgetStatic,
options: newEditorProps
});
},
getRendererPropsForWidgetInfo: function getRendererPropsForWidgetInfo(widgetInfo, problemNum) {
var type = widgetInfo.type;
var widgetExports = widgets[type];
if (widgetExports == null) {
// The widget is not a registered widget
// It shouldn't matter what we return here, but for consistency
// we return the untransformed options, as if the widget did
// not have a transform defined.
return widgetInfo.options;
}
var transform;
if (widgetInfo.static) {
// There aren't a lot of real places where we'll have to default to
// _.identity, but it's theoretically possile if someone changes
// the JSON manually / we have to back out static support for a
// widget.
transform = this.getStaticTransform(type) || _.identity;
} else {
transform = widgetExports.transform || _.identity;
}
// widgetInfo.options are the widgetEditor's props:
return transform(widgetInfo.options, problemNum);
},
traverseChildWidgets: function traverseChildWidgets(widgetInfo, traverseRenderer) {
if (!traverseRenderer) {
throw new Error("traverseRenderer must be provided, but was not");
}
if (!widgetInfo || !widgetInfo.type || !widgets[widgetInfo.type]) {
return widgetInfo;
}
var widgetExports = widgets[widgetInfo.type];
var props = widgetInfo.options;
if (widgetExports.traverseChildWidgets && props) {
var newProps = widgetExports.traverseChildWidgets(props, traverseRenderer);
return _.extend({}, widgetInfo, { options: newProps });
} else {
return widgetInfo;
}
},
/**
* Handling for the optional alignments for widgets
* See widget-container.jsx for details on how alignments are implemented.
*/
/**
* Returns the list of supported alignments for the given (string) widget
* type. This is used primarily at editing time to display the choices
* for the user.
*
* Supported alignments are given as an array of strings in the exports of
* a widget's module.
*/
getSupportedAlignments: function getSupportedAlignments(type) {
var widgetInfo = widgets[type];
return widgetInfo && widgetInfo.supportedAlignments || DEFAULT_SUPPORTED_ALIGNMENTS;
},
/**
* For the given (string) widget type, determine the default alignment for
* the widget. This is used at rendering time to go from "default" alignment
* to the actual alignment displayed on the screen.
*
* The default alignment is given either as a string (called
* `defaultAlignment`) or a function (called `getDefaultAlignment`) on
* the exports of a widget's module.
*/
getDefaultAlignment: function getDefaultAlignment(type) {
var widgetInfo = widgets[type];
var alignment;
if (!widgetInfo) {
return DEFAULT_ALIGNMENT;
}
if (widgetInfo.getDefaultAlignment) {
alignment = widgetInfo.getDefaultAlignment();
} else {
alignment = widgetInfo.defaultAlignment;
}
return alignment || DEFAULT_ALIGNMENT;
},
validAlignments: ["block", "inline-block", "inline", "float-left", "float-right", "full-width"],
/**
* Used at startup to fail fast if an alignment given by a widget is
* invalid.
*/
// TODO(alex): Change this to run as a testcase (vs. being run at runtime)
validateAlignments: function validateAlignments() {
_.each(widgets, function (widgetInfo) {
if (widgetInfo.defaultAlignment && !_.contains(Widgets.validAlignments, widgetInfo.defaultAlignment)) {
throw new Error("Widget '" + widgetInfo.displayName + "' has an invalid defaultAlignment value: " + widgetInfo.defaultAlignment);
}
if (widgetInfo.supportedAlignments) {
var unknownAlignments = _.difference(widgetInfo.supportedAlignments, Widgets.validAlignments);
if (unknownAlignments.length) {
throw new Error("Widget '" + widgetInfo.displayName + "' has an invalid value for supportedAlignments: " + unknownAlignments.join(" "));
}
}
});
},
/**
* Handling for static mode for widgets that support it.
*/
/**
* Returns true iff the widget supports static mode.
* A widget implicitly supports static mode if it exports a
* staticTransform function.
*/
supportsStaticMode: function supportsStaticMode(type) {
var widgetInfo = widgets[type];
return widgetInfo && widgetInfo.staticTransform != null;
},
/**
* Return the staticTransform function used to convert the editorProps to
* the rendered widget state.
*/
getStaticTransform: function getStaticTransform(type) {
var widgetInfo = widgets[type];
return widgetInfo && widgetInfo.staticTransform;
},
/**
* Returns the tracking option for the widget. The default is "",
* which means simply to track interactions once. The other available
* option is "all" which means to track all interactions.
*/
getTracking: function getTracking(type) {
var widgetInfo = widgets[type];
return widgetInfo && widgetInfo.tracking || DEFAULT_TRACKING;
},
/**
* Returns true if this widget can include lintable markdown text
* and supports a highlightLint prop, or false otherwise.
*/
isLintable: function isLintable(type) {
var widgetInfo = widgets[type];
return widgetInfo && widgetInfo.isLintable || DEFAULT_LINTABLE;
}
};
module.exports = Widgets;
/***/ },
/* 32 */
/***/ function(module, exports, __webpack_require__) {
/* globals true */
// As new widgets get added here, please also make sure they get added in
// webapp perseus/traversal.py so they can be properly translated.
module.exports = [[__webpack_require__(68), true && __webpack_require__(75)], [__webpack_require__(69), true && __webpack_require__(70)], [__webpack_require__(71), true && __webpack_require__(72)], [__webpack_require__(73), true && __webpack_require__(74)]];
/***/ },
/* 33 */
/***/ function(module, exports, __webpack_require__) {
/**
* This should be called by all clients, specifying whether extra widgets are
* needed via `loadExtraWidgets`. It is idempotent, so it's not a problem to
* call it multiple times.
*
* skipMathJax:
* if false/undefined, MathJax will be configured, and the
* promise will wait for MathJax to load (if it hasn't already).
* loadExtraWidgets:
* if true, `extra-widgets` will be required. The client must have already
* loaded the file, either by using the full perseus bundle
* `/build/perseus.js`, or by loading `/build/perseus-extras.js` prior to
* calling `Perseus.init()`.
*/
var init = function init(options) {
// Pass skipMathJax: true if MathJax is already loaded and configured.
var skipMathJax = options.skipMathJax;
var widgetsDeferred = $.Deferred();
// HACK(charlie): To maintain backwards compatibility, only exclude the
// extra widgets if the parameter is explicitly falsey (rather than merely
// undefined). We should probably bump the Perseus major version number
// (since this is a breaking change in the API) but this is a more
// lightweight fix that will get exercises working in our mobile apps
// immediately.
if (options.loadExtraWidgets === undefined || options.loadExtraWidgets) {
var Widgets = __webpack_require__(31);
__webpack_require__.e/*nsure*/(1, function (require) {
var extraWidgets = __webpack_require__(51);
Widgets.registerMany(extraWidgets);
widgetsDeferred.resolve();
}, 0);
} else {
widgetsDeferred.resolve();
}
var mathJaxDeferred = $.Deferred();
if (skipMathJax) {
mathJaxDeferred.resolve();
} else {
MathJax.Hub.Config({
messageStyle: "none",
skipStartupTypeset: "none",
"HTML-CSS": {
availableFonts: ["TeX"],
imageFont: null,
scale: 100,
showMathMenu: false
}
});
MathJax.Hub.Configured();
MathJax.Hub.Queue(mathJaxDeferred.resolve);
}
return widgetsDeferred.then(function () {
return mathJaxDeferred;
});
};
module.exports = init;
/***/ },
/* 34 */
/***/ function(module, exports, __webpack_require__) {
"use strict";
/**
* An article renderer. Articles are long-form pieces of content,
* composed of multiple (Renderer) sections concatenated together.
*/
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var classNames = __webpack_require__(86);
var Util = __webpack_require__(17);
var ApiOptions = __webpack_require__(12).Options;
var ApiClassNames = __webpack_require__(12).ClassNames;
var Renderer = __webpack_require__(37);
var ProvideKeypad = __webpack_require__(65);
var Gorgon = __webpack_require__(41);
var _require = __webpack_require__(52),
linterContextProps = _require.linterContextProps,
linterContextDefault = _require.linterContextDefault;
var rendererProps = React.PropTypes.shape({
content: React.PropTypes.string,
widgets: React.PropTypes.object,
images: React.PropTypes.object
});
var ArticleRenderer = React.createClass({
displayName: "ArticleRenderer",
propTypes: _extends({}, ProvideKeypad.propTypes, {
apiOptions: React.PropTypes.shape({
onFocusChange: React.PropTypes.func,
isMobile: React.PropTypes.bool
}),
json: React.PropTypes.oneOfType([rendererProps, React.PropTypes.arrayOf(rendererProps)]).isRequired,
// Whether to use the new Bibliotron styles for articles
useNewStyles: React.PropTypes.bool,
linterContext: linterContextProps,
legacyPerseusLint: React.PropTypes.arrayOf(React.PropTypes.string)
}),
getDefaultProps: function getDefaultProps() {
return {
apiOptions: {},
useNewStyles: false,
linterContext: linterContextDefault
};
},
getInitialState: function getInitialState() {
return ProvideKeypad.getInitialState();
},
componentDidMount: function componentDidMount() {
ProvideKeypad.componentDidMount.call(this);
this._currentFocus = null;
},
shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
return nextProps !== this.props || nextState !== this.state;
},
componentWillUnmount: function componentWillUnmount() {
ProvideKeypad.componentWillUnmount.call(this);
},
keypadElement: function keypadElement() {
return ProvideKeypad.keypadElement.call(this);
},
_handleFocusChange: function _handleFocusChange(newFocusPath, oldFocusPath) {
// TODO(charlie): DRY this up--some of this logic is repeated in
// ItemRenderer.
if (newFocusPath) {
this._setCurrentFocus(newFocusPath);
} else {
this._onRendererBlur(oldFocusPath);
}
},
_setCurrentFocus: function _setCurrentFocus(newFocusPath) {
var keypadElement = this.keypadElement();
var prevFocusPath = this._currentFocus;
this._currentFocus = newFocusPath;
// Use the section prefix to extract the relevant Renderer's input
// paths, so as to check whether the focused path represents an
// input.
var didFocusInput = false;
if (this._currentFocus) {
var _currentFocus = this._currentFocus,
sectionRef = _currentFocus[0],
focusPath = _currentFocus.slice(1);
var inputPaths = this.refs[sectionRef].getInputPaths();
didFocusInput = inputPaths.some(function (inputPath) {
return Util.inputPathsEqual(inputPath, focusPath);
});
}
if (this.props.apiOptions.onFocusChange != null) {
this.props.apiOptions.onFocusChange(this._currentFocus, prevFocusPath, didFocusInput && keypadElement && ReactDOM.findDOMNode(keypadElement));
}
if (keypadElement) {
if (didFocusInput) {
keypadElement.activate();
} else {
keypadElement.dismiss();
}
}
},
_onRendererBlur: function _onRendererBlur(blurPath) {
var _this = this;
var blurringFocusPath = this._currentFocus;
// Failsafe: abort if ID is different, because focus probably happened
// before blur.
if (!Util.inputPathsEqual(blurPath, blurringFocusPath)) {
return;
}
// Wait until after any new focus events fire this tick before declaring
// that nothing is focused, since if there were a focus change across
// sections, we could receive the blur before the focus.
setTimeout(function () {
if (Util.inputPathsEqual(_this._currentFocus, blurringFocusPath)) {
_this._setCurrentFocus(null);
}
});
},
blur: function blur() {
if (this._currentFocus) {
var _currentFocus2 = this._currentFocus,
sectionRef = _currentFocus2[0],
inputPath = _currentFocus2.slice(1);
this.refs[sectionRef].blurPath(inputPath);
}
},
_sections: function _sections() {
return Array.isArray(this.props.json) ? this.props.json : [this.props.json];
},
render: function render() {
var _classNames,
_this2 = this;
var apiOptions = _extends({}, ApiOptions.defaults, this.props.apiOptions, {
isArticle: true
});
var classes = classNames((_classNames = {
"framework-perseus": true,
"perseus-article": true,
"bibliotron-article": this.props.useNewStyles
}, _classNames[ApiClassNames.MOBILE] = apiOptions.isMobile, _classNames));
// TODO(alex): Add mobile api functions and pass them down here
var sections = this._sections().map(function (section, i) {
var refForSection = "section-" + i;
return React.createElement(
"div",
{ key: i, className: "clearfix" },
React.createElement(Renderer, _extends({}, section, {
ref: refForSection,
key: i,
key_: i,
keypadElement: _this2.keypadElement(),
apiOptions: _extends({}, apiOptions, {
onFocusChange: function onFocusChange(newFocusPath, oldFocusPath) {
// Prefix the paths with the relevant section,
// so as to allow us to distinguish between
// equivalently-named inputs across Renderers.
_this2._handleFocusChange(newFocusPath && [refForSection].concat(newFocusPath), oldFocusPath && [refForSection].concat(oldFocusPath));
}
}),
linterContext: Gorgon.pushContextStack(_this2.props.linterContext, "article"),
legacyPerseusLint: _this2.props.legacyPerseusLint
}))
);
});
return React.createElement(
"div",
{ className: classes },
sections
);
}
});
module.exports = ArticleRenderer;
/***/ },
/* 35 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/**
* A copy of the ItemRenderer which renders its question renderer and hints
* renderer normally instead of ReactDOM.render()ing them into elements in the
* DOM.
*
* This allows this component to be used in server-rendering of a perseus
* exercise.
*/
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var _ = __webpack_require__(56);
var _require = __webpack_require__(79),
StyleSheet = _require.StyleSheet,
css = _require.css;
var ApiOptions = __webpack_require__(12).Options;
var HintsRenderer = __webpack_require__(36);
var ProvideKeypad = __webpack_require__(65);
var Renderer = __webpack_require__(37);
var Util = __webpack_require__(17);
var _require2 = __webpack_require__(80),
mapObject = _require2.mapObject;
var RP = React.PropTypes;
var ItemRenderer = React.createClass({
displayName: "ItemRenderer",
propTypes: _extends({}, ProvideKeypad.propTypes, {
apiOptions: RP.any,
hintsVisible: RP.number,
item: RP.shape({
answerArea: RP.shape({
calculator: RP.bool,
chi2Table: RP.bool,
periodicTable: RP.bool,
tTable: RP.bool,
zTable: RP.bool
}),
hints: RP.arrayOf(RP.object),
question: RP.object
}).isRequired,
problemNum: RP.number,
reviewMode: RP.bool
}),
getDefaultProps: function getDefaultProps() {
return {
apiOptions: {} // a deep default is done in `this.update()`
};
},
getInitialState: function getInitialState() {
return _extends({}, ProvideKeypad.getInitialState(), {
questionCompleted: false,
questionHighlightedWidgets: []
});
},
componentDidMount: function componentDidMount() {
ProvideKeypad.componentDidMount.call(this);
this._currentFocus = null;
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
this.setState({
questionHighlightedWidgets: []
});
},
componentDidUpdate: function componentDidUpdate() {
if (this.props.apiOptions.answerableCallback) {
var isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
this.props.apiOptions.answerableCallback(isAnswerable);
}
},
componentWillUnmount: function componentWillUnmount() {
ProvideKeypad.componentWillUnmount.call(this);
},
keypadElement: function keypadElement() {
return ProvideKeypad.keypadElement.call(this);
},
_handleFocusChange: function _handleFocusChange(newFocus, oldFocus) {
if (newFocus != null) {
this._setCurrentFocus(newFocus);
} else {
this._onRendererBlur(oldFocus);
}
},
// Sets the current focus path and element and
// send an onChangeFocus event back to our parent.
_setCurrentFocus: function _setCurrentFocus(newFocus) {
var _this = this;
var keypadElement = this.keypadElement();
// By the time this happens, newFocus cannot be a prefix of
// prevFocused, since we must have either been called from
// an onFocusChange within a renderer, which is only called when
// this is not a prefix, or between the question and answer areas,
// which can never prefix each other.
var prevFocus = this._currentFocus;
this._currentFocus = newFocus;
// Determine whether the newly focused path represents an input.
var inputPaths = this.getInputPaths();
var didFocusInput = this._currentFocus && inputPaths.some(function (inputPath) {
return Util.inputPathsEqual(inputPath, _this._currentFocus);
});
if (this.props.apiOptions.onFocusChange != null) {
this.props.apiOptions.onFocusChange(this._currentFocus, prevFocus, didFocusInput && keypadElement && ReactDOM.findDOMNode(keypadElement));
}
if (keypadElement) {
if (didFocusInput) {
keypadElement.activate();
} else {
keypadElement.dismiss();
}
}
},
_onRendererBlur: function _onRendererBlur(blurPath) {
var _this2 = this;
var blurringFocusPath = this._currentFocus;
// Failsafe: abort if ID is different, because focus probably happened
// before blur
if (!_.isEqual(blurPath, blurringFocusPath)) {
return;
}
// Wait until after any new focus events fire this tick before
// declaring that nothing is focused.
// If a different widget was focused, we'll see an onBlur event
// now, but then an onFocus event on a different element before
// this callback is executed
_.defer(function () {
if (_.isEqual(_this2._currentFocus, blurringFocusPath)) {
_this2._setCurrentFocus(null);
}
});
},
/**
* Accepts a question area widgetId, or an answer area widgetId of
* the form "answer-input-number 1", or the string "answer-area"
* for the whole answer area (if the answer area is a single widget).
*/
_setWidgetProps: function _setWidgetProps(widgetId, newProps, callback) {
this.questionRenderer._setWidgetProps(widgetId, newProps, callback);
},
_handleAPICall: function _handleAPICall(functionName, path) {
// Get arguments to pass to function, including `path`
var functionArgs = _.rest(arguments);
var caller = this.questionRenderer;
return caller[functionName].apply(caller, functionArgs);
},
setInputValue: function setInputValue(path, newValue, focus) {
return this._handleAPICall("setInputValue", path, newValue, focus);
},
focusPath: function focusPath(path) {
return this._handleAPICall("focusPath", path);
},
blurPath: function blurPath(path) {
return this._handleAPICall("blurPath", path);
},
getDOMNodeForPath: function getDOMNodeForPath(path) {
return this._handleAPICall("getDOMNodeForPath", path);
},
getGrammarTypeForPath: function getGrammarTypeForPath(path) {
return this._handleAPICall("getGrammarTypeForPath", path);
},
getInputPaths: function getInputPaths() {
var questionAreaInputPaths = this.questionRenderer.getInputPaths();
return questionAreaInputPaths;
},
handleInteractWithWidget: function handleInteractWithWidget(widgetId) {
var withRemoved = _.difference(this.state.questionHighlightedWidgets, [widgetId]);
this.setState({
questionCompleted: false,
questionHighlightedWidgets: withRemoved
});
if (this.props.apiOptions.interactionCallback) {
this.props.apiOptions.interactionCallback();
}
},
focus: function focus() {
return this.questionRenderer.focus();
},
blur: function blur() {
if (this._currentFocus) {
this.blurPath(this._currentFocus);
}
},
getNumHints: function getNumHints() {
return this.props.item.hints.length;
},
/**
* Grades the item.
*
* Returns a KE-style score of {
* empty: bool,
* correct: bool,
* message: string|null,
* guess: Array
* }
*/
scoreInput: function scoreInput() {
var guessAndScore = this.questionRenderer.guessAndScore();
var guess = guessAndScore[0];
var score = guessAndScore[1];
// Continue to include an empty guess for the now defunct answer area.
// TODO(alex): Check whether we rely on the format here for
// analyzing ProblemLogs. If not, remove this layer.
var maxCompatGuess = [guess, []];
var keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
var emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
this.setState({
questionCompleted: keScore.correct,
questionHighlightedWidgets: emptyQuestionAreaWidgets
});
return keScore;
},
/**
* Returns an array of all widget IDs in the order they occur in
* the question content.
*/
getWidgetIds: function getWidgetIds() {
return this.questionRenderer.getWidgetIds();
},
/**
* Returns an object mapping from widget ID to KE-style score.
* The keys of this object are the values of the array returned
* from `getWidgetIds`.
*/
scoreWidgets: function scoreWidgets() {
var qScore = this.questionRenderer.scoreWidgets();
var qGuess = this.questionRenderer.getUserInputForWidgets();
var state = this.questionRenderer.getSerializedState();
return mapObject(qScore, function (score, id) {
return Util.keScoreFromPerseusScore(score, qGuess[id], state);
});
},
/**
* Get a representation of the current state of the item.
*/
getSerializedState: function getSerializedState() {
return {
question: this.questionRenderer.getSerializedState(),
hints: this.hintsRenderer.getSerializedState()
};
},
restoreSerializedState: function restoreSerializedState(state, callback) {
// We need to wait for both the question renderer and the hints
// renderer to finish restoring their states.
var numCallbacks = 2;
var fireCallback = function fireCallback() {
--numCallbacks;
if (callback && numCallbacks === 0) {
callback();
}
};
this.questionRenderer.restoreSerializedState(state.question, fireCallback);
this.hintsRenderer.restoreSerializedState(state.hints, fireCallback);
},
showRationalesForCurrentlySelectedChoices: function showRationalesForCurrentlySelectedChoices() {
this.questionRenderer.showRationalesForCurrentlySelectedChoices();
},
deselectIncorrectSelectedChoices: function deselectIncorrectSelectedChoices() {
this.questionRenderer.deselectIncorrectSelectedChoices();
},
render: function render() {
var _this3 = this;
var apiOptions = _extends({}, ApiOptions.defaults, this.props.apiOptions, {
onFocusChange: this._handleFocusChange
});
var questionRenderer = React.createElement(Renderer, _extends({
keypadElement: this.keypadElement(),
problemNum: this.props.problemNum,
onInteractWithWidget: this.handleInteractWithWidget,
highlightedWidgets: this.state.questionHighlightedWidgets,
apiOptions: apiOptions,
questionCompleted: this.state.questionCompleted,
reviewMode: this.props.reviewMode,
ref: function ref(elem) {
return _this3.questionRenderer = elem;
}
}, this.props.item.question));
var hintsRenderer = React.createElement(HintsRenderer, {
hints: this.props.item.hints,
hintsVisible: this.props.hintsVisible,
apiOptions: apiOptions,
ref: function ref(elem) {
return _this3.hintsRenderer = elem;
}
});
return React.createElement(
"div",
null,
React.createElement(
"div",
null,
questionRenderer
),
React.createElement(
"div",
{
className:
// Avoid adding any horizontal padding when applying the
// mobile hint styles, which are flush to the left.
// NOTE(charlie): We may still want to apply this
// padding for desktop exercises.
!apiOptions.isMobile && css(styles.hintsContainer)
},
hintsRenderer
)
);
}
});
var styles = StyleSheet.create({
hintsContainer: {
marginLeft: 50
}
});
module.exports = ItemRenderer;
/***/ },
/* 36 */
/***/ function(module, exports, __webpack_require__) {
var _mobileHintStylesHint, _mobileHintStylesGetA, _mobileHintStylesPlus;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var _require = __webpack_require__(79),
StyleSheet = _require.StyleSheet,
css = _require.css;
var classnames = __webpack_require__(86);
var _ = __webpack_require__(56);
var i18n = window.i18n;
var HintRenderer = __webpack_require__(38);
var SvgImage = __webpack_require__(67);
var ApiOptionsProps = __webpack_require__(66);
var mediaQueries = __webpack_require__(76);
var sharedStyles = __webpack_require__(78);
var _require2 = __webpack_require__(77),
baseUnitPx = _require2.baseUnitPx,
hintBorderWidth = _require2.hintBorderWidth,
kaGreen = _require2.kaGreen,
gray85 = _require2.gray85,
gray17 = _require2.gray17;
var Gorgon = __webpack_require__(41);
var _require3 = __webpack_require__(52),
linterContextProps = _require3.linterContextProps,
linterContextDefault = _require3.linterContextDefault;
var HintsRenderer = React.createClass({
displayName: "HintsRenderer",
propTypes: _extends({}, ApiOptionsProps.propTypes, {
className: React.PropTypes.string,
hints: React.PropTypes.arrayOf(React.PropTypes.any),
hintsVisible: React.PropTypes.number,
findExternalWidgets: React.PropTypes.func,
linterContext: linterContextProps
}),
getDefaultProps: function getDefaultProps() {
return {
linterContext: linterContextDefault
};
},
componentDidMount: function componentDidMount() {
this._cacheHintImages();
},
componentDidUpdate: function componentDidUpdate(prevProps, prevState) {
if (!_.isEqual(prevProps.hints, this.props.hints) || prevProps.hintsVisible !== this.props.hintsVisible) {
this._cacheHintImages();
}
// When a new hint is displayed we immediately focus it
if (prevProps.hintsVisible < this.props.hintsVisible) {
var pos = this.props.hintsVisible - 1;
ReactDOM.findDOMNode(this.refs["hintRenderer" + pos]).focus();
}
},
_hintsVisible: function _hintsVisible() {
if (this.props.hintsVisible == null || this.props.hintsVisible === -1) {
return this.props.hints.length;
} else {
return this.props.hintsVisible;
}
},
_cacheImagesInHint: function _cacheImagesInHint(hint) {
_.each(hint.images, function (data, src) {
var image = new Image();
image.src = SvgImage.getRealImageUrl(src);
});
},
_cacheHintImages: function _cacheHintImages() {
// Only cache images in the first hint at the start. When hints are
// taken, cache images in the rest of the hints
if (this._hintsVisible() > 0) {
_.each(this.props.hints, this._cacheImagesInHint);
} else if (this.props.hints.length > 0) {
this._cacheImagesInHint(this.props.hints[0]);
}
},
getApiOptions: function getApiOptions() {
return ApiOptionsProps.getApiOptions.call(this);
},
getSerializedState: function getSerializedState() {
var _this = this;
return _.times(this._hintsVisible(), function (i) {
return _this.refs["hintRenderer" + i].getSerializedState();
});
},
restoreSerializedState: function restoreSerializedState(state, callback) {
var _this2 = this;
// We need to wait until all the renderers are finished restoring their
// state before we fire our callback.
var numCallbacks = 1;
var fireCallback = function fireCallback() {
--numCallbacks;
if (callback && numCallbacks === 0) {
callback();
}
};
_.each(state, function (hintState, i) {
var hintRenderer = _this2.refs["hintRenderer" + i];
// This is not ideal in that it doesn't restore state
// if the hint isn't visible, but we can't exactly restore
// the state to an unmounted renderer, so...
// If you want to restore state to hints, make sure to
// have the appropriate number of hints visible already.
if (hintRenderer) {
++numCallbacks;
hintRenderer.restoreSerializedState(hintState, fireCallback);
}
});
// This makes sure that the callback is fired if there aren't any
// mounted renderers.
fireCallback();
},
render: function render() {
var _this3 = this;
var apiOptions = this.getApiOptions();
var hintsVisible = this._hintsVisible();
var hints = [];
this.props.hints.slice(0, hintsVisible).forEach(function (hint, i) {
var lastHint = i === _this3.props.hints.length - 1 && !/\*\*/.test(hint.content);
var lastRendered = i === hintsVisible - 1;
var renderer = React.createElement(HintRenderer, {
lastHint: lastHint,
lastRendered: lastRendered,
hint: hint,
pos: i,
totalHints: _this3.props.hints.length,
ref: "hintRenderer" + i,
key: "hintRenderer" + i,
apiOptions: apiOptions,
findExternalWidgets: _this3.props.findExternalWidgets,
linterContext: Gorgon.pushContextStack(_this3.props.linterContext, "hints[" + i + "]")
});
if (hint.replace && hints.length > 0) {
hints[hints.length - 1] = renderer;
} else {
hints.push(renderer);
}
});
var showGetAnotherHint = apiOptions.getAnotherHint && hintsVisible > 0 && hintsVisible < this.props.hints.length;
var hintRatioCopy = "(" + hintsVisible + "/" + this.props.hints.length + ")";
var classNames = classnames(this.props.className, apiOptions.isMobile && hintsVisible > 0 && css(styles.mobileHintStylesHintsRenderer));
return React.createElement(
"div",
{ className: classNames },
apiOptions.isMobile && hintsVisible > 0 && React.createElement(
"div",
{
className: css(styles.mobileHintStylesHintTitle, sharedStyles.responsiveLabel)
},
i18n._("Hints")
),
hints,
showGetAnotherHint && React.createElement(
"button",
{
rel: "button",
className: css(styles.linkButton, styles.getAnotherHintButton, apiOptions.isMobile && styles.mobileHintStylesGetAnotherHintButton),
onClick: function onClick(evt) {
evt.preventDefault();
evt.stopPropagation();
apiOptions.getAnotherHint();
}
},
React.createElement(
"span",
{
className: css(styles.plusText, apiOptions.isMobile && styles.mobileHintStylesPlusText)
},
"+"
),
React.createElement(
"span",
{ className: css(styles.getAnotherHintText) },
i18n._("Get another hint"),
" ",
hintRatioCopy
)
)
);
}
});
var hintIndentation = baseUnitPx + hintBorderWidth;
var styles = StyleSheet.create({
rendererMargins: {
marginTop: baseUnitPx
},
linkButton: {
cursor: "pointer",
border: "none",
backgroundColor: "transparent",
fontSize: "100%",
fontFamily: "inherit",
fontWeight: "bold",
color: kaGreen,
padding: 0,
position: "relative"
},
plusText: {
fontSize: 20,
position: "absolute",
top: -3,
left: 0
},
getAnotherHintText: {
marginLeft: 16
},
mobileHintStylesHintsRenderer: {
marginTop: 4 * baseUnitPx,
border: "solid " + gray85,
borderWidth: "1px 0 0 0",
position: "relative",
":before": {
content: '""',
display: "table",
clear: "both"
},
":after": {
content: '""',
display: "table",
clear: "both"
}
},
mobileHintStylesHintTitle: (_mobileHintStylesHint = {
fontFamily: "inherit",
fontStyle: "normal",
fontWeight: "bold",
color: gray17,
paddingTop: baseUnitPx,
paddingBottom: 1.5 * baseUnitPx
}, _mobileHintStylesHint[mediaQueries.lgOrSmaller] = {
paddingLeft: 0
}, _mobileHintStylesHint[mediaQueries.smOrSmaller] = {
// On phones, ensure that the button is aligned with the hint body
// content, which is inset at the standard `baseUnitPx`, plus an
// additional `hintBorderWidth`.
paddingLeft: hintIndentation
}, _mobileHintStylesHint),
getAnotherHintButton: {
marginTop: 1.5 * baseUnitPx
},
mobileHintStylesGetAnotherHintButton: (_mobileHintStylesGetA = {}, _mobileHintStylesGetA[mediaQueries.lgOrSmaller] = {
paddingLeft: 0
}, _mobileHintStylesGetA[mediaQueries.smOrSmaller] = {
// As with the title, on phones, ensure that the button is aligned
// with the hint body content.
paddingLeft: hintIndentation
}, _mobileHintStylesGetA),
mobileHintStylesPlusText: (_mobileHintStylesPlus = {}, _mobileHintStylesPlus[mediaQueries.lgOrSmaller] = {
left: 0
}, _mobileHintStylesPlus[mediaQueries.smOrSmaller] = {
left: hintIndentation
}, _mobileHintStylesPlus)
});
module.exports = HintsRenderer;
/***/ },
/* 37 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _notGorgon = __webpack_require__(201);
var _notGorgon2 = _interopRequireDefault(_notGorgon);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/* eslint-disable max-lines, no-var */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
/* globals KA */
var $ = __webpack_require__(169);
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var _ = __webpack_require__(56);
var classNames = __webpack_require__(86);
var JiptParagraphs = __webpack_require__(202);
var _require = __webpack_require__(29),
maybeUnescape = _require.maybeUnescape;
var PerseusMarkdown = __webpack_require__(49);
var QuestionParagraph = __webpack_require__(203);
var SvgImage = __webpack_require__(67);
var TeX = __webpack_require__(178);
var WidgetContainer = __webpack_require__(204);
var Widgets = __webpack_require__(31);
var Util = __webpack_require__(17);
var ApiOptionsProps = __webpack_require__(66);
var ApiClassNames = __webpack_require__(12).ClassNames;
var Zoomable = __webpack_require__(205);
var Deferred = __webpack_require__(206);
var preprocessTex = __webpack_require__(91);
var Gorgon = __webpack_require__(41); // The linter engine
var _require2 = __webpack_require__(52),
linterContextProps = _require2.linterContextProps,
linterContextDefault = _require2.linterContextDefault;
// The i18n linter
var keypadElementPropType = __webpack_require__(257).propTypes.keypadElementPropType;
var _require3 = __webpack_require__(80),
mapObject = _require3.mapObject,
mapObjectFromArray = _require3.mapObjectFromArray;
var rContainsNonWhitespace = /\S/;
var rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
var noopOnRender = function noopOnRender() {};
if (typeof KA !== "undefined" && KA.language === "en-pt") {
// When using crowdin's jipt (Just in place translation), we need to keep a
// registry of crowdinId's to component so that we can update the
// component's state as the translator enters their translation.
window.PerseusTranslationComponents = [];
if (!KA.jipt_dom_insert_checks) {
KA.jipt_dom_insert_checks = [];
}
// We add a function that will get called whenever jipt says the dom needs
// to be updated
KA.jipt_dom_insert_checks.push(function (text, node, attribute) {
var $node = $(node);
var index = $node.data("perseus-component-index");
var paragraphIndex = $node.data("perseus-paragraph-index");
// We only update if we had added an index onto the node's data.
if (node && typeof index !== "undefined") {
var component = window.PerseusTranslationComponents[index];
if (!component) {
// The component has disappeared, so we tell jipt not to try
// and insert anything
return false;
}
// Jipt sometimes sends down the escaped translation, so we need to
// unescape \\t to \t among other characters here
text = maybeUnescape(text);
component.replaceJiptContent(text, paragraphIndex);
// Return false to tell jipt not to insert anything into the DOM
// itself, otherwise it will mess up what React expects there to be
return false;
}
// The string updated wasn't part of perseus, so we tell jipt to just
// insert the translation as-is.
return text;
});
}
var SHOULD_CLEAR_WIDGETS_PROP_LIST = ["content", "problemNum", "widgets"];
// Check if one focus path / id path is a prefix of another
// The focus path null will never be a prefix of any non-null
// path, since it represents no focus.
// Otherwise, prefix is calculated by whether every array
// element in the prefix is present in the same position in the
// wholeArray path.
var isIdPathPrefix = function isIdPathPrefix(prefixArray, wholeArray) {
if (prefixArray === null || wholeArray === null) {
return prefixArray === wholeArray;
}
return _.every(prefixArray, function (elem, i) {
return _.isEqual(elem, wholeArray[i]);
});
};
/**
* Wrapper for the trackInteraction apiOption.
*
* @param trackApi Original API
* @param widgetType String name of the widget type
* @param widgetID String ID of the widget instance
* @param setting string setting for tracking (either "" for track once or
* "all")
*/
var InteractionTracker = function InteractionTracker(trackApi, widgetType, widgetID, setting) {
if (!trackApi) {
this.track = this._noop;
} else {
this._tracked = false;
this.trackApi = trackApi;
this.widgetType = widgetType;
this.widgetID = widgetID;
this.setting = setting;
this.track = this._track.bind(this);
}
};
/**
* Function that actually calls the API to mark the interaction. This is
* private. The public version is just `.track` and is bound to this object
* for easy use in other context.
*
* @param extraData Any extra data to track about the event.
* @private
*/
InteractionTracker.prototype._track = function (extraData) {
if (this._tracked && !this.setting) {
return;
}
this._tracked = true;
this.trackApi(_extends({
type: this.widgetType,
id: this.widgetID
}, extraData));
};
/**
* This alternate version of `.track` does nothing as an optimization.
*
* @private
*/
InteractionTracker.prototype._noop = function () {};
var Renderer = React.createClass({
displayName: "Renderer",
propTypes: _extends({}, ApiOptionsProps.propTypes, {
alwaysUpdate: React.PropTypes.bool,
findExternalWidgets: React.PropTypes.func,
highlightedWidgets: React.PropTypes.arrayOf(React.PropTypes.any),
ignoreMissingWidgets: React.PropTypes.bool,
images: React.PropTypes.any,
keypadElement: keypadElementPropType,
onInteractWithWidget: React.PropTypes.func,
onRender: React.PropTypes.func,
problemNum: React.PropTypes.number,
questionCompleted: React.PropTypes.bool,
reviewMode: React.PropTypes.bool,
serializedState: React.PropTypes.any,
// Callback which is called when serialized state changes with the new
// serialized state.
onSerializedStateUpdated: React.PropTypes.func,
// If linterContext.highlightLint is true, then content will be passed
// to the linter and any warnings will be highlighted in the rendered
// output.
linterContext: linterContextProps,
legacyPerseusLint: React.PropTypes.arrayOf(React.PropTypes.string)
}),
getDefaultProps: function getDefaultProps() {
return {
content: "",
widgets: {},
images: {},
// TODO(aria): Remove this now that it is true everywhere
// (here and in perseus-i18n)
ignoreMissingWidgets: true,
highlightedWidgets: [],
// onRender may be called multiple times per render, for example
// if there are multiple images or TeX pieces within `content`.
// It is a good idea to debounce any functions passed here.
questionCompleted: false,
onRender: noopOnRender,
onInteractWithWidget: function onInteractWithWidget() {},
findExternalWidgets: function findExternalWidgets() {
return [];
},
alwaysUpdate: false,
reviewMode: false,
serializedState: null,
onSerializedStateUpdated: function onSerializedStateUpdated() {},
linterContext: linterContextDefault
};
},
getInitialState: function getInitialState() {
return _.extend({
jiptContent: null,
// The i18n linter.
// TODO(joshuan): If this becomes an ES6 class, move to a
// member variable.
notGorgon: new _notGorgon2.default(),
// NotGorgon is async and currently does not contain a location.
// This is a list of error strings NotGorgon detected on its last
// run.
notGorgonLintErrors: []
}, this._getInitialWidgetState());
},
componentDidMount: function componentDidMount() {
this.handleRender({});
this._currentFocus = null;
this._rootNode = ReactDOM.findDOMNode(this);
this._isMounted = true;
// TODO(emily): actually make the serializedState prop work like a
// controlled prop, instead of manually calling .restoreSerializedState
// at the right times.
if (this.props.serializedState) {
this.restoreSerializedState(this.props.serializedState);
}
if (this.props.linterContext.highlightLint) {
// Get i18n lint errors asynchronously. If there are lint errors,
// this component will be rerendered.
this.state.notGorgon.runLinter(this.props.content, this.handleNotGorgonLintErrors);
}
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
if (!_.isEqual(_.pick(this.props, SHOULD_CLEAR_WIDGETS_PROP_LIST), _.pick(nextProps, SHOULD_CLEAR_WIDGETS_PROP_LIST))) {
this.setState(this._getInitialWidgetState(nextProps));
}
},
shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
if (this.props.alwaysUpdate) {
// TOTAL hacks so that findWidgets doesn't break
// when one widget updates without the other.
// See passage-refs inside radios, which was why
// this was introduced.
// I'm sorry!
// TODO(aria): cry
return true;
}
var stateChanged = !_.isEqual(this.state, nextState);
var propsChanged = !_.isEqual(this.props, nextProps);
return propsChanged || stateChanged;
},
componentWillUpdate: function componentWillUpdate(nextProps, nextState) {
var oldJipt = this.shouldRenderJiptPlaceholder(this.props, this.state);
var newJipt = this.shouldRenderJiptPlaceholder(nextProps, nextState);
var oldContent = this.getContent(this.props, this.state);
var newContent = this.getContent(nextProps, nextState);
var oldHighlightedWidgets = this.props.highlightedWidgets;
var newHighlightedWidgets = nextProps.highlightedWidgets;
// TODO(jared): This seems to be a perfect overlap with
// "shouldComponentUpdate" -- can we just remove this
// componentWillUpdate and the reuseMarkdown attr?
this.reuseMarkdown = !oldJipt && !newJipt && oldContent === newContent && _.isEqual(this.state.notGorgonLintErrors, nextState.notGorgonLintErrors),
// If we are running the linter then we need to know when
// widgets have changed because we need for force the linter to
// run when that happens. Note: don't do identity comparison here:
// it can cause frequent re-renders that break MathJax somehow
(!this.props.linterContext.highlightLint || _.isEqual(this.props.widgets, nextProps.widgets)) &&
// If the linter is turned on or off, we have to rerender
this.props.linterContext.highlightLint === nextProps.linterContext.highlightLint &&
// yes, this is identity array comparison, but these are passed
// in from state in the item-renderer, so they should be
// identity equal unless something changed, and it's expensive
// to loop through them to look for differences.
// Technically, we could reuse the markdown when this changes,
// but to do that we'd have to do more expensive checking of
// whether a widget should be highlighted in the common case
// where this array hasn't changed, so we just redo the whole
// render if this changed
oldHighlightedWidgets === newHighlightedWidgets;
},
componentDidUpdate: function componentDidUpdate(prevProps, prevState) {
var _this = this;
this.handleRender(prevProps);
// We even do this if we did reuse the markdown because
// we might need to update the widget props on this render,
// even though we have the same widgets.
// WidgetContainers don't update their widgets' props when
// they are re-rendered, so even if they've been
// re-rendered we need to call these methods on them.
_.each(this.widgetIds, function (id) {
var container = _this.refs["container:" + id];
container.replaceWidgetProps(_this.getWidgetProps(id));
});
if (this.props.serializedState && !_.isEqual(this.props.serializedState, this.getSerializedState())) {
this.restoreSerializedState(this.props.serializedState);
}
if (this.props.linterContext.highlightLint) {
// Get i18n lint errors asynchronously. If lint errors have changed
// since the last run, this component will be rerendered.
this.state.notGorgon.runLinter(this.props.content, this.handleNotGorgonLintErrors);
}
},
componentWillUnmount: function componentWillUnmount() {
// Clean out the list of widgetIds when unmounting, as this list is
// meant to be consistent with the refs controlled by the renderer, and
// refs are also cleared out during unmounting.
// (This may not be totally necessary, but mobile clients have been
// seeing JS errors due to an inconsistency between the list of
// widgetIds and the child refs of the renderer.
// See: https://phabricator.khanacademy.org/D32420.)
this.widgetIds = [];
if (this.translationIndex != null) {
window.PerseusTranslationComponents[this.translationIndex] = null;
}
this.state.notGorgon.destroy();
this._isMounted = false;
},
getApiOptions: function getApiOptions() {
return ApiOptionsProps.getApiOptions.call(this);
},
_getInitialWidgetState: function _getInitialWidgetState(props) {
props = props || this.props;
var allWidgetInfo = this._getAllWidgetsInfo(props);
return {
widgetInfo: allWidgetInfo,
widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
};
},
_getAllWidgetsInfo: function _getAllWidgetsInfo(props) {
props = props || this.props;
return mapObject(props.widgets, function (widgetInfo, widgetId) {
if (!widgetInfo.type || !widgetInfo.alignment) {
var newValues = {};
if (!widgetInfo.type) {
newValues.type = widgetId.split(" ")[0];
}
if (!widgetInfo.alignment) {
newValues.alignment = "default";
}
widgetInfo = _.extend({}, widgetInfo, newValues);
}
return Widgets.upgradeWidgetInfoToLatestVersion(widgetInfo);
});
},
_getAllWidgetsStartProps: function _getAllWidgetsStartProps(allWidgetInfo, props) {
return mapObject(allWidgetInfo, function (editorProps) {
return Widgets.getRendererPropsForWidgetInfo(editorProps, props.problemNum);
});
},
_getDefaultWidgetInfo: function _getDefaultWidgetInfo(widgetId) {
var widgetIdParts = Util.rTypeFromWidgetId.exec(widgetId);
if (widgetIdParts == null) {
return {};
}
return {
type: widgetIdParts[1],
graded: true,
options: {}
};
},
_getWidgetInfo: function _getWidgetInfo(widgetId) {
return this.state.widgetInfo[widgetId] || this._getDefaultWidgetInfo(widgetId);
},
renderWidget: function renderWidget(impliedType, id, state) {
var widgetInfo = this.state.widgetInfo[id];
if (widgetInfo && widgetInfo.alignment === "full-width") {
state.foundFullWidth = true;
}
if (widgetInfo || this.props.ignoreMissingWidgets) {
var type = widgetInfo && widgetInfo.type || impliedType;
var shouldHighlight = _.contains(this.props.highlightedWidgets, id);
// By this point we should have no duplicates, which are
// filtered out in this.render(), so we shouldn't have to
// worry about using this widget key and ref:
return React.createElement(WidgetContainer, {
ref: "container:" + id,
key: "container:" + id,
type: type,
initialProps: this.getWidgetProps(id),
shouldHighlight: shouldHighlight,
linterContext: Gorgon.pushContextStack(this.props.linterContext, "widget")
});
} else {
return null;
}
},
getWidgetProps: function getWidgetProps(id) {
var _this2 = this;
var apiOptions = this.getApiOptions();
var widgetProps = this.state.widgetProps[id] || {};
// The widget needs access to its "rubric" at all times when in review
// mode (which is really just part of its widget info).
var reviewModeRubric = null;
var widgetInfo = this.state.widgetInfo[id];
if (this.props.reviewMode && widgetInfo) {
reviewModeRubric = widgetInfo.options;
}
if (!this._interactionTrackers) {
this._interactionTrackers = {};
}
var interactionTracker = this._interactionTrackers[id];
if (!interactionTracker) {
interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, id, Widgets.getTracking(widgetInfo && widgetInfo.type));
}
return _extends({}, widgetProps, {
ref: id,
widgetId: id,
alignment: widgetInfo && widgetInfo.alignment,
static: widgetInfo && widgetInfo.static,
problemNum: this.props.problemNum,
apiOptions: this.getApiOptions(this.props),
keypadElement: this.props.keypadElement,
questionCompleted: this.props.questionCompleted,
onFocus: _.partial(this._onWidgetFocus, id),
onBlur: _.partial(this._onWidgetBlur, id),
findWidgets: this.findWidgets,
reviewModeRubric: reviewModeRubric,
onChange: function onChange(newProps, cb) {
var silent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
_this2._setWidgetProps(id, newProps, cb, silent);
},
trackInteraction: interactionTracker.track
});
},
/**
* Serializes the questions state so it can be recovered.
*
* The return value of this function can be sent to the
* `restoreSerializedState` method to restore this state.
*
* If an instance of widgetProps is passed in, it generates the serialized
* state from that instead of the current widget props.
*/
getSerializedState: function getSerializedState(widgetProps) {
var _this3 = this;
return mapObject(widgetProps || this.state.widgetProps, function (props, widgetId) {
var widget = _this3.getWidgetInstance(widgetId);
if (widget && widget.getSerializedState) {
return widget.getSerializedState();
} else {
return props;
}
});
},
restoreSerializedState: function restoreSerializedState(serializedState, callback) {
var _this4 = this;
// Do some basic validation on the serialized state (just make sure the
// widget IDs are what we expect).
var serializedWidgetIds = _.keys(serializedState);
var widgetPropIds = _.keys(this.state.widgetProps);
// If the two lists of IDs match (ignoring order)
if (serializedWidgetIds.length !== widgetPropIds.length || _.intersection(serializedWidgetIds, widgetPropIds).length !== serializedWidgetIds.length) {
// eslint-disable-next-line no-console
console.error("Refusing to restore bad serialized state:", serializedState, "Current props:", this.state.widgetProps);
return;
}
// We want to wait until any children widgets who have a
// restoreSerializedState function also call their own callbacks before
// we declare that the operation is finished.
var numCallbacks = 1;
var fireCallback = function fireCallback() {
--numCallbacks;
if (callback && numCallbacks === 0) {
callback();
}
};
this.setState({
widgetProps: mapObject(serializedState, function (props, widgetId) {
var widget = _this4.getWidgetInstance(widgetId);
if (widget && widget.restoreSerializedState) {
// Note that we probably can't call
// `this.change()/this.props.onChange()` in this
// function, so we take the return value and use
// that as props if necessary so that
// `restoreSerializedState` in a widget can
// change the props as well as state.
// If a widget has no props to change, it can
// safely return null.
++numCallbacks;
var restoreResult = widget.restoreSerializedState(props, fireCallback);
return _.extend({}, _this4.state.widgetProps[widgetId], restoreResult);
} else {
return props;
}
})
}, fireCallback);
},
/**
* Tell each of the radio widgets to show rationales for each of the
* currently selected choices inside of them. If the widget is correct, it
* shows rationales for all of the choices. This also disables interaction
* with the choices that we show rationales for.
*/
showRationalesForCurrentlySelectedChoices: function showRationalesForCurrentlySelectedChoices() {
var _this5 = this;
Object.keys(this.props.widgets).forEach(function (widgetId) {
var widget = _this5.getWidgetInstance(widgetId);
if (widget && widget.showRationalesForCurrentlySelectedChoices) {
widget.showRationalesForCurrentlySelectedChoices(_this5._getWidgetInfo(widgetId).options);
}
});
},
/**
* Tells each of the radio widgets to deselect any of the incorrect choices
* that are currently selected (leaving correct choices still selected).
*/
deselectIncorrectSelectedChoices: function deselectIncorrectSelectedChoices() {
var _this6 = this;
// TODO(emily): this has the exact same structure as
// showRationalesForCurrentlySelectedChoices above. Maybe DRY this up.
Object.keys(this.props.widgets).forEach(function (widgetId) {
var widget = _this6.getWidgetInstance(widgetId);
if (widget && widget.deselectIncorrectSelectedChoices) {
widget.deselectIncorrectSelectedChoices();
}
});
},
/**
* Allows inter-widget communication.
*
* This function yields this Renderer's own internal widgets, and it's used
* in two places.
*
* First, we expose our own internal widgets to each other by giving them
* a `findWidgets` function that, in turn, calls this function.
*
* Second, we expose our own internal widgets to this Renderer's parent,
* by allowing it to call this function directly. That way, it can hook us
* up to other Renderers on the page, by writing a `findExternalWidgets`
* prop that calls each other Renderer's `findInternalWidgets` function.
*
* Takes a `filterCriterion` on which widgets to return.
* `filterCriterion` can be one of:
* * A string widget id
* * A string widget type
* * a function from (id, widgetInfo, widgetComponent) to true or false
*
* Returns an array of the matching widget components.
*
* If you need to do logic with more than the components, it is possible
* to do such logic inside the filter, rather than on the result array.
*
* See the passage-ref widget for an example.
*
* "Remember: abilities are not inherently good or evil, it's how you use
* them." ~ Kyle Katarn
* Please use this one with caution.
*/
findInternalWidgets: function findInternalWidgets(filterCriterion) {
var _this7 = this;
var filterFunc;
// Convenience filters:
// "interactive-graph 3" will give you [[interactive-graph 3]]
// "interactive-graph" will give you all interactive-graphs
if (typeof filterCriterion === "string") {
if (filterCriterion.indexOf(" ") !== -1) {
var widgetId = filterCriterion;
filterFunc = function filterFunc(id, widgetInfo) {
return id === widgetId;
};
} else {
var widgetType = filterCriterion;
filterFunc = function filterFunc(id, widgetInfo) {
return widgetInfo.type === widgetType;
};
}
} else {
filterFunc = filterCriterion;
}
var results = this.widgetIds.filter(function (id) {
var widgetInfo = _this7._getWidgetInfo(id);
var widget = _this7.getWidgetInstance(id);
return filterFunc(id, widgetInfo, widget);
}).map(this.getWidgetInstance);
return results;
},
/**
* Allows inter-widget communication.
*
* Includes both widgets internal to this Renderer, and external widgets
* exposed by the `findExternalWidgets` prop.
*
* See `findInteralWidgets` for more information.
*/
findWidgets: function findWidgets(filterCriterion) {
return [].concat(this.findInternalWidgets(filterCriterion), this.props.findExternalWidgets(filterCriterion));
},
getWidgetInstance: function getWidgetInstance(id) {
var ref = this.refs["container:" + id];
if (!ref) {
return null;
}
return ref.getWidget();
},
_onWidgetFocus: function _onWidgetFocus(id, focusPath) {
if (focusPath === undefined) {
focusPath = [];
} else {
if (!_.isArray(focusPath)) {
throw new Error("widget props.onFocus focusPath must be an Array, " + "but was" + JSON.stringify(focusPath));
}
}
this._setCurrentFocus([id].concat(focusPath));
},
_onWidgetBlur: function _onWidgetBlur(id, blurPath) {
var _this8 = this;
var blurringFocusPath = this._currentFocus;
// Failsafe: abort if ID is different, because focus probably happened
// before blur
var fullPath = [id].concat(blurPath);
if (!_.isEqual(fullPath, blurringFocusPath)) {
return;
}
// Wait until after any new focus events fire this tick before
// declaring that nothing is focused.
// If a different widget was focused, we'll see an onBlur event
// now, but then an onFocus event on a different element before
// this callback is executed
_.defer(function () {
if (_.isEqual(_this8._currentFocus, blurringFocusPath)) {
_this8._setCurrentFocus(null);
}
});
},
getContent: function getContent(props, state) {
return state.jiptContent || props.content;
},
shouldRenderJiptPlaceholder: function shouldRenderJiptPlaceholder(props, state) {
// TODO(aria): Pass this in via webapp as an apiOption
return typeof KA !== "undefined" && KA.language === "en-pt" && state.jiptContent == null && props.content.indexOf("crwdns") !== -1;
},
replaceJiptContent: function replaceJiptContent(content, paragraphIndex) {
if (paragraphIndex == null) {
// we're not translating paragraph-wise; replace the whole content
// (we could also theoretically check for apiOptions.isArticle
// here, which is what causes paragraphIndex to not be null)
this.setState({
jiptContent: content
});
} else {
// This is the same regex we use in perseus/translate.py to find
// code blocks. We use it to count entire code blocks as
// paragraphs.
var codeFenceRegex = /^\s*(`{3,}|~{3,})\s*(\S+)?\s*\n([\s\S]+?)\s*\1\s*$/; // eslint-disable-line max-len
if (codeFenceRegex.test(content)) {
// If a paragraph is a code block, we're going to treat it as a
// single paragraph even if it has double-newlines in it, so
// skip the next two checks.
} else if (/\S\n\s*\n\S/.test(content)) {
// Our "render the exact same QuestionParagraphs each time"
// strategy will fail if we allow translating a paragraph
// to more than one paragraph. This hack renders as a single
// paragraph and lets the translator know to not use \n\n,
// hopefully. We can't wait for linting because we can't
// safely render the node.
// TODO(aria): Check for the max number of backticks or tildes
// in the content, and just render a red code block of the
// content here instead?
content = "$\\large{\\red{\\text{Please translate each " + "paragraph to a single paragraph.}}}$";
} else if (/^\s*$/.test(content)) {
// We similarly can't have an all-whitespace paragraph, or
// we will parse it as the closing of the previous paragraph
content = "$\\large{\\red{\\text{Translated paragraph is " + "currently empty}}}$";
}
// Split the paragraphs; we have to use getContent() in case
// nothing has been translated yet (in which case we just have
// this.props.content)
var allContent = this.getContent(this.props, this.state);
var paragraphs = JiptParagraphs.parseToArray(allContent);
paragraphs[paragraphIndex] = content;
this.setState({
jiptContent: JiptParagraphs.joinFromArray(paragraphs)
});
}
},
// wrap top-level elements in a QuestionParagraph, mostly
// for appropriate spacing and other css
outputMarkdown: function outputMarkdown(ast, state) {
if (_.isArray(ast)) {
// This is duplicated from simple-markdown
// TODO(aria): Don't duplicate this logic
var oldKey = state.key;
var result = [];
// map nestedOutput over the ast, except group any text
// nodes together into a single string output.
// NOTE(aria): These are never strings--always QuestionParagraphs
// TODO(aria): We probably don't need this string logic here.
var lastWasString = false;
for (var i = 0; i < ast.length; i++) {
state.key = i;
state.paragraphIndex = i;
var nodeOut = this.outputMarkdown(ast[i], state);
var isString = typeof nodeOut === "string";
if (isString && lastWasString) {
result[result.length - 1] += nodeOut;
} else {
result.push(nodeOut);
}
lastWasString = isString;
}
state.key = oldKey;
return result;
} else {
// !!! WARNING: Mutative hacks! mutates `this._foundTextNodes`:
// because I wrote a bad interface to simple-markdown.js' `output`
this._foundTextNodes = false;
state.foundFullWidth = false;
var output = this.outputNested(ast, state);
// In Jipt-land, we need to render the exact same outer
// QuestionParagraph nodes always. This means the number of
// paragraphs needs to stay the same, and we can't modify
// the classnames on the QuestionParagraphs or we'll destroy
// the crowdin classnames. So we just only use the
// 'paragraph' classname from the QuestionParagraph.
// If this becomes a problem it would be easy to fix by wrapping
// the nodes in an extra layer (hopefully only for jipt) that
// handles the jipt classnames, and let this layer handle the
// dynamic classnames.
// We can't render the classes the first time and leave them
// the same because we don't know at the time of the first
// render whether they are full-bleed or centered, since they
// only contain crowdin IDs like `crwdns:972384209:0...`
var className;
if (this.translationIndex != null) {
className = null;
} else {
className = classNames({
"perseus-paragraph-centered": !this._foundTextNodes,
// There is only one node being rendered,
// and it's a full-width widget.
"perseus-paragraph-full-width": state.foundFullWidth && ast.content.length === 1
});
}
return React.createElement(
QuestionParagraph,
{
key: state.key,
className: className,
translationIndex: this.translationIndex,
paragraphIndex: state.paragraphIndex
},
output
);
}
},
// output non-top-level nodes or arrays
outputNested: function outputNested(ast, state) {
if (_.isArray(ast)) {
// This is duplicated from simple-markdown
// TODO(aria): Don't duplicate this logic
var oldKey = state.key;
var result = [];
// map nestedOutput over the ast, except group any text
// nodes together into a single string output.
var lastWasString = false;
for (var i = 0; i < ast.length; i++) {
state.key = i;
var nodeOut = this.outputNested(ast[i], state);
var isString = typeof nodeOut === "string";
if (isString && lastWasString) {
result[result.length - 1] += nodeOut;
} else {
result.push(nodeOut);
}
lastWasString = isString;
}
state.key = oldKey;
return result;
} else {
return this.outputNode(ast, this.outputNested, state);
}
},
// output individual AST nodes [not arrays]
outputNode: function outputNode(node, nestedOutput, state) {
var _this9 = this;
var apiOptions = this.getApiOptions();
var imagePlaceholder = apiOptions.imagePlaceholder;
if (node.type === "widget") {
var widgetPlaceholder = apiOptions.widgetPlaceholder;
if (widgetPlaceholder) {
return widgetPlaceholder;
}
// Widgets can contain text nodes, so we don't center them with
// markdown magic here.
// Instead, we center them with css magic in articles.less
// /cry(aria)
this._foundTextNodes = true;
if (_.contains(this.widgetIds, node.id)) {
// We don't want to render a duplicate widget key/ref,
// as this causes problems with react (for obvious
// reasons). Instead we just notify the
// hopefully-content-creator that they need to change the
// widget id.
return React.createElement(
"span",
{ key: state.key, className: "renderer-widget-error" },
"Widget [[",
"\u2603",
" ",
node.id,
"]] already exists."
);
} else {
this.widgetIds.push(node.id);
return this.renderWidget(node.widgetType, node.id, state);
}
} else if (node.type === "blockMath") {
// We render math here instead of in perseus-markdown.jsx
// because we need to pass it our onRender callback.
var deferred = new Deferred();
var onRender = function onRender(node) {
_this9.props.onRender && _this9.props.onRender(node);
if (apiOptions.isMobile) {
// `onRender` only returns a node on the initial render.
if (node) {
var katex = node.querySelector(".katex");
// Though MathJax's visible elements should have been
// inserted into the DOM by now (and, thus, we should be
// able to query for .MathJax instead), we're not seeing
// that guarantee play out in practice. So we look for
// either .MathJax or the script tag that is inserted
// on initial render.
// TODO(charlie): This works, but feels very brittle.
// Figure out how we can call `onRender` only after the
// elements have been inserted into the DOM.
var mathjax = node.querySelector('script[type="math/tex"]') || node.querySelector(".MathJax");
if (katex) {
deferred.resolve();
} else if (mathjax) {
deferred.resolve();
} else {
throw new Error("No math present in Renderer");
}
}
}
};
var content = React.createElement(
TeX,
{ onRender: onRender, onResourceLoaded: onRender },
preprocessTex(node.content)
);
var innerStyle = {
// HACK(benkomalo): we only want horizontal scrolling, but
// overflowX: 'auto' causes a vertical scrolling scrollbar
// as well, despite the parent and child elements having
// the exact same height. Force it to not scroll by
// applying overflowY: 'hidden'
overflowX: "auto",
overflowY: "hidden",
// HACK(kevinb): overflowY: 'hidden' inadvertently clips the
// top and bottom of some fractions. We add padding to the
// top and bottom to avoid the clipping and then correct for
// the padding by adding equal but opposite margins.
paddingTop: 10,
paddingBottom: 10,
marginTop: -10,
marginBottom: -10
};
if (apiOptions.isMobile) {
// The style for the body of articles and exercises on mobile is
// to have a 16px margin. When a user taps to zoom math we'd
// like the math to extend all the way to the edge of the page/
// To achieve this affect we nest the Zoomable component in two
// nested divs. The outer div has a negative margin to
// counteract the margin on main perseus container. The inner
// div adds the margin back as padding so that when the math is
// scaled out it's inset from the edge of the page. When the
// TeX component is full size it will extend to the edge of the
// page if it's larger than the page.
//
// TODO(kevinb) automatically determine the margin size
var margin = 16;
var outerStyle = {
marginLeft: -margin,
marginRight: -margin
};
var horizontalPadding = {
paddingLeft: margin,
paddingRight: margin
};
var computeMathBounds = function computeMathBounds(parentNode, parentBounds) {
var textElement = parentNode.querySelector(".katex-html") || parentNode.querySelector(".MathJax");
var textBounds = {
width: textElement.offsetWidth,
height: textElement.offsetHeight
};
// HACK(benkomalo): when measuring math content, note that
// sometimes it actually peeks outside of the
// container in some cases. Just be conservative and use
// the maximum value of the text and the parent. :(
return {
width: Math.max(parentBounds.width, textBounds.width),
height: Math.max(parentBounds.height, textBounds.height)
};
};
return React.createElement(
"div",
{
key: state.key,
className: "perseus-block-math",
style: outerStyle
},
React.createElement(
"div",
{
className: "perseus-block-math-inner",
style: _extends({}, innerStyle, horizontalPadding)
},
React.createElement(
Zoomable,
{
readyToMeasureDeferred: deferred,
computeChildBounds: computeMathBounds
},
content
)
)
);
} else {
return React.createElement(
"div",
{ key: state.key, className: "perseus-block-math" },
React.createElement(
"div",
{
className: "perseus-block-math-inner",
style: innerStyle
},
content
)
);
}
} else if (node.type === "math") {
// Replace uses of \begin{align}...\end{align} which KaTeX doesn't
// support (yet) with \begin{aligned}...\end{aligned} which renders
// the same is supported by KaTeX. It does the same for align*.
// TODO(kevinb) update content to use aligned instead of align.
var tex = node.content.replace(/\{align[*]?\}/g, "{aligned}");
// We render math here instead of in perseus-markdown.jsx
// because we need to pass it our onRender callback.
return React.createElement(
"span",
{
key: state.key,
style: {
// If math is directly next to text, don't let it
// wrap to the next line
whiteSpace: "nowrap"
}
},
React.createElement("span", null),
React.createElement(
TeX,
{
onRender: this.props.onRender,
onResourceLoaded: this.props.onRender
},
tex
),
React.createElement("span", null)
);
} else if (node.type === "image") {
if (imagePlaceholder) {
return imagePlaceholder;
}
// We need to add width and height to images from our
// props.images mapping.
// We do a _.has check here to avoid weird things like
// 'toString' or '__proto__' as a url.
var extraAttrs = _.has(this.props.images, node.target) ? this.props.images[node.target] : null;
// The width of a table column is determined by the widest table
// cell within that column, but responsive images constrain
// themselves to the width of their parent containers. Thus,
// responsive images don't do very well within tables. To avoid
// haphazard sizing, simply make images within tables unresponsive.
// TODO(alex): Make tables themselves responsive.
var responsive = !state.inTable;
return React.createElement(SvgImage, _extends({
key: state.key,
src: PerseusMarkdown.sanitizeUrl(node.target),
alt: node.alt,
title: node.title,
responsive: responsive,
onUpdate: this.props.onRender,
zoomToFullSizeOnMobile: apiOptions.isMobile && apiOptions.isArticle
}, extraAttrs));
} else if (node.type === "columns") {
// Note that we have two columns. This is so we can put
// a className on the outer renderer content for SAT.
// TODO(aria): See if there is a better way we can do
// things like this
this._isTwoColumn = true;
// but then render normally:
return PerseusMarkdown.ruleOutput(node, nestedOutput, state);
} else if (node.type === "text") {
if (rContainsNonWhitespace.test(node.content)) {
this._foundTextNodes = true;
}
// Used by the translator portal to replace image URLs with
// placeholders, see preprocessWidgets in manticore-utils.js
// for more details.
if (imagePlaceholder && rImageURL.test(node.content)) {
return imagePlaceholder;
} else {
return node.content;
}
} else if (node.type === "table" || node.type === "titledTable") {
state.inTable = true;
var output = PerseusMarkdown.ruleOutput(node, nestedOutput, state);
state.inTable = false;
if (!apiOptions.isMobile) {
return output;
}
var _margin = 16;
var _outerStyle = {
marginLeft: -_margin,
marginRight: -_margin
};
var _innerStyle = {
paddingLeft: 0,
paddingRight: 0
};
var wrappedOutput = React.createElement(
"div",
{ style: _extends({}, _innerStyle, { overflowX: "auto" }) },
React.createElement(
Zoomable,
{ animateHeight: true },
output
)
);
// TODO(benkomalo): how should we deal with tappable items inside
// of tables?
return React.createElement(
"div",
{ style: _outerStyle },
wrappedOutput
);
} else {
// If it's a "normal" or "simple" markdown node, just
// output it using its output rule.
return PerseusMarkdown.ruleOutput(node, nestedOutput, state);
}
},
handleRender: function handleRender(prevProps) {
var onRender = this.props.onRender;
var oldOnRender = prevProps.onRender;
// In the common case of no callback specified, avoid this work.
if (onRender !== noopOnRender || oldOnRender !== noopOnRender) {
var $images = $(ReactDOM.findDOMNode(this)).find("img");
// Fire callback on image load...
if (oldOnRender !== noopOnRender) {
$images.off("load", oldOnRender);
}
if (onRender !== noopOnRender) {
$images.on("load", onRender);
}
}
// ...as well as right now (non-image, non-TeX or image from cache)
onRender();
},
// Sets the current focus path
// If the new focus path is not a prefix of the old focus path,
// we send an onChangeFocus event back to our parent.
_setCurrentFocus: function _setCurrentFocus(path) {
var apiOptions = this.getApiOptions();
// We don't do this when the new path is a prefix because
// that prefix is already focused (we're just in a more specific
// area of it). This makes it safe to call _setCurrentFocus
// whenever a widget is interacted with--we won't wipe out
// our focus state if we are already focused on a subpart
// of that widget (i.e. a transformation NumberInput inside
// of a transformer widget).
if (!isIdPathPrefix(path, this._currentFocus)) {
var prevFocus = this._currentFocus;
if (prevFocus) {
this.blurPath(prevFocus);
}
this._currentFocus = path;
if (apiOptions.onFocusChange != null) {
apiOptions.onFocusChange(this._currentFocus, prevFocus);
}
}
},
focus: function focus() {
var id;
var focusResult;
for (var i = 0; i < this.widgetIds.length; i++) {
var widgetId = this.widgetIds[i];
var widget = this.getWidgetInstance(widgetId);
var widgetFocusResult = widget && widget.focus && widget.focus();
if (widgetFocusResult) {
id = widgetId;
focusResult = widgetFocusResult;
break;
}
}
if (id) {
// reconstruct a {path, element} focus object
var path;
if (_.isObject(focusResult)) {
// The result of focus was a {path, id} object itself
path = [id].concat(focusResult.path || []);
} else {
// The result of focus was true or the like; just
// construct a root focus object
path = [id];
}
this._setCurrentFocus(path);
return true;
}
},
getDOMNodeForPath: function getDOMNodeForPath(path) {
var widgetId = _.first(path);
var interWidgetPath = _.rest(path);
// Widget handles parsing of the interWidgetPath. If the path is empty
// beyond the widgetID, as a special case we just return the widget's
// DOM node.
var widget = this.getWidgetInstance(widgetId);
var getNode = widget.getDOMNodeForPath;
if (getNode) {
return getNode(interWidgetPath);
} else if (interWidgetPath.length === 0) {
return ReactDOM.findDOMNode(widget);
}
},
getGrammarTypeForPath: function getGrammarTypeForPath(path) {
var widgetId = _.first(path);
var interWidgetPath = _.rest(path);
var widget = this.getWidgetInstance(widgetId);
return widget.getGrammarTypeForPath(interWidgetPath);
},
getInputPaths: function getInputPaths() {
var _this10 = this;
var inputPaths = [];
_.each(this.widgetIds, function (widgetId) {
var widget = _this10.getWidgetInstance(widgetId);
if (widget.getInputPaths) {
// Grab all input paths and add widgetID to the front
var widgetInputPaths = widget.getInputPaths();
// Prefix paths with their widgetID and add to collective
// list of paths.
_.each(widgetInputPaths, function (inputPath) {
var relativeInputPath = [widgetId].concat(inputPath);
inputPaths.push(relativeInputPath);
});
}
});
return inputPaths;
},
focusPath: function focusPath(path) {
// No need to focus if it's already focused
if (_.isEqual(this._currentFocus, path)) {
return;
} else if (this._currentFocus) {
// Unfocus old path, if exists
this.blurPath(this._currentFocus);
}
var widgetId = _.first(path);
var interWidgetPath = _.rest(path);
// Widget handles parsing of the interWidgetPath
var focusWidget = this.getWidgetInstance(widgetId).focusInputPath;
focusWidget && focusWidget(interWidgetPath);
},
blurPath: function blurPath(path) {
// No need to blur if it's not focused
if (!_.isEqual(this._currentFocus, path)) {
return;
}
var widgetId = _.first(path);
var interWidgetPath = _.rest(path);
var widget = this.getWidgetInstance(widgetId);
// We might be in the editor and blurring a widget that no
// longer exists, so only blur if we actually found the widget
if (widget) {
var blurWidget = this.getWidgetInstance(widgetId).blurInputPath;
// Widget handles parsing of the interWidgetPath
blurWidget && blurWidget(interWidgetPath);
}
},
blur: function blur() {
if (this._currentFocus) {
this.blurPath(this._currentFocus);
}
},
serialize: function serialize() {
var state = {};
_.each(this.state.widgetInfo, function (info, id) {
var widget = this.getWidgetInstance(id);
var s = widget.serialize();
if (!_.isEmpty(s)) {
state[id] = s;
}
}, this);
return state;
},
emptyWidgets: function emptyWidgets() {
var _this11 = this;
return _.filter(this.widgetIds, function (id) {
var widgetInfo = _this11._getWidgetInfo(id);
var score = _this11.getWidgetInstance(id).simpleValidate(widgetInfo.options, null);
return Util.scoreIsEmpty(score);
});
},
_setWidgetProps: function _setWidgetProps(id, newProps, cb,
// Widgets can call `onChange` with `silent` set to `true` to prevent
silent) {
var _this12 = this;
this.setState(function (prevState) {
var _extends2;
var widgetProps = _extends({}, prevState.widgetProps, (_extends2 = {}, _extends2[id] = _extends({}, prevState.widgetProps[id], newProps), _extends2));
if (!silent) {
_this12.props.onSerializedStateUpdated(_this12.getSerializedState(widgetProps));
}
return {
widgetProps: widgetProps
};
}, function () {
var cbResult = cb && cb();
if (!silent) {
_this12.props.onInteractWithWidget(id);
}
if (cbResult !== false) {
// TODO(jack): For some reason, some widgets don't always
// end up in refs here, which is repro-able if you make an
// [[ orderer 1 ]] and copy-paste this, then change it to
// be an [[ orderer 2 ]]. The resulting Renderer ends up
// with an "orderer 2" ref but not an "orderer 1" ref.
// @_@??
// TODO(jack): Figure out why this is happening and fix it
// As far as I can tell, this is only an issue in the
// editor-page, so doing this shouldn't break clients
// hopefully
_this12._setCurrentFocus([id]);
}
});
},
setInputValue: function setInputValue(path, newValue, focus) {
var widgetId = _.first(path);
var interWidgetPath = _.rest(path);
var widget = this.getWidgetInstance(widgetId);
// Widget handles parsing of the interWidgetPath.
widget.setInputValue(interWidgetPath, newValue, focus);
},
/**
* Returns an array of the widget `.getUserInput()` results
*/
getUserInput: function getUserInput() {
var _this13 = this;
return _.map(this.widgetIds, function (id) {
return _this13.getWidgetInstance(id).getUserInput();
});
},
/**
* Returns an array of all widget IDs in the order they occur in
* the content.
*/
getWidgetIds: function getWidgetIds() {
return this.widgetIds;
},
/**
* WARNING: This is an experimental/temporary API and should not be relied
* upon in production code. This function may change its behavior or
* disappear without notice.
*
* Returns a treelike structure containing all widget IDs (this will
* descend into group widgets as well).
*
* An example of what the structure looks like:
*
* [
* {id: "radio 1", children: []},
* {
* id: "group 1",
* children: [
* {id: "radio 1", children: []}
* {id: "radio 2", children: []}
* ]
* }
* ]
*
* Widgets will be listed in the order that they appear in their renderer.
*
* Note: If a group hasn't been rendered yet, though, then its children
* ids will not be returned.
* TODO(marcia): We should figure out a way to either return the widget ids
* without needing to render all-the-things, or we should probably have a
* better pattern for requesting widget ids so we are more likely to get
* one true answer.
*/
getAllWidgetIds: function getAllWidgetIds() {
var _this14 = this;
// Recursively builds our result
return _.map(this.getWidgetIds(), function (id) {
var groupPrefix = "group";
if (id.substring(0, groupPrefix.length) === groupPrefix && _this14.getWidgetInstance(id)) {
return {
id: id,
children: _this14.getWidgetInstance(id).getRenderer().getAllWidgetIds()
};
}
// This is our base case
return { id: id, children: [] };
});
},
/**
* Returns the result of `.getUserInput()` for each widget, in
* a map from widgetId to userInput.
*/
getUserInputForWidgets: function getUserInputForWidgets() {
var _this15 = this;
return mapObjectFromArray(this.widgetIds, function (id) {
return _this15.getWidgetInstance(id).getUserInput();
});
},
/**
* Returns an object mapping from widget ID to perseus-style score.
* The keys of this object are the values of the array returned
* from `getWidgetIds`.
*/
scoreWidgets: function scoreWidgets() {
var _this16 = this;
var widgetProps = this.state.widgetInfo;
var onInputError = this.getApiOptions().onInputError || function () {};
var gradedWidgetIds = _.filter(this.widgetIds, function (id) {
var props = widgetProps[id];
// props.graded is unset or true
return props.graded == null || props.graded;
});
var widgetScores = {};
_.each(gradedWidgetIds, function (id) {
var props = widgetProps[id];
var widget = _this16.getWidgetInstance(id);
if (!widget) {
// This can occur if the widget has not yet been rendered
return;
}
widgetScores[id] = widget.simpleValidate(props.options, onInputError);
});
return widgetScores;
},
/**
* Grades the content.
*
* Returns a perseus-style score of {
* type: "invalid"|"points",
* message: string,
* earned: undefined|number,
* total: undefined|number
* }
*/
score: function score() {
return _.reduce(this.scoreWidgets(), Util.combineScores, Util.noScore);
},
guessAndScore: function guessAndScore() {
var totalGuess = this.getUserInput();
var totalScore = this.score();
return [totalGuess, totalScore];
},
examples: function examples() {
var widgets = this.widgetIds;
var examples = _.compact(_.map(widgets, function (widget) {
return widget.examples ? widget.examples() : null;
}));
// no widgets with examples
if (!examples.length) {
return null;
}
var allEqual = _.all(examples, function (example) {
return _.isEqual(examples[0], example);
});
// some widgets have different examples
// TODO(alex): handle this better
if (!allEqual) {
return null;
}
return examples[0];
},
// NotGorgon callback
handleNotGorgonLintErrors: function handleNotGorgonLintErrors(lintErrors) {
if (!this._isMounted) {
return;
}
this.setState({
notGorgonLintErrors: lintErrors
});
},
render: function render() {
var _classNames;
var apiOptions = this.getApiOptions();
if (this.reuseMarkdown) {
return this.lastRenderedMarkdown;
}
var content = this.getContent(this.props, this.state);
// `this.widgetIds` is appended to in `this.outputMarkdown`:
this.widgetIds = [];
if (this.shouldRenderJiptPlaceholder(this.props, this.state)) {
// Crowdin's JIPT (Just in place translation) uses a fake language
// with language tag "en-pt" where the value of the translations
// look like: {crwdns2657085:0}{crwdne2657085:0} where it keeps the
// {crowdinId:ngettext variant}. We detect whether the current
// content matches this, so we can take over rendering of
// the perseus content as the translators interact with jipt.
// We search for only part of the tag that crowdin uses to guard
// against them changing the format on us. The full tag it looks
// for can be found in https://cdn.crowdin.net/jipt/jipt.js
// globalPhrase var.
// If we haven't already added this component to the registry do so
// now. showHints() may cause this component to be rerendered
// before jipt has a chance to replace its contents, so this check
// will keep us from adding the component to the registry a second
// time.
if (!this.translationIndex) {
this.translationIndex = window.PerseusTranslationComponents.push(this) - 1;
}
// For articles, we add jipt data to individual paragraphs. For
// exercises, we add it to the renderer and let translators
// translate the entire thing. For the article equivalent of
// this if block, search this file for where we render a
// QuestionParagraph, and see the `isJipt:` parameter sent to
// PerseusMarkdown.parse()
if (!apiOptions.isArticle) {
// We now need to output this tag, as jipt looks for it to be
// able to replace it with a translation that it runs an ajax
// call to get. We add a data attribute with the index to the
// Persues.TranslationComponent registry so that when jipt
// calls its before_dom_insert we can lookup this component by
// this attribute and render the text with markdown.
return React.createElement(
"div",
{ "data-perseus-component-index": this.translationIndex },
content
);
}
}
// Hacks:
// We use mutable state here to figure out whether the output
// had two columns.
// It is updated to true by `this.outputMarkdown` if a
// column break is found
// TODO(aria): We now have a state variable threaded through
// simple-markdown output. We should mutate it instead of
// state on this component to do this in a less hacky way.
this._isTwoColumn = false;
// Parse the string of markdown to a parse tree
var parsedMarkdown = PerseusMarkdown.parse(content, {
// Recognize crowdin IDs while translating articles
// (This should never be hit by exercises, though if you
// decide you want to add a check that this is an article,
// go for it.)
isJipt: this.translationIndex != null
});
// Optionally apply the linter to the parse tree
if (this.props.linterContext.highlightLint) {
// If highlightLint is true and lint is detected, this call
// will modify the parse tree by adding lint nodes that will
// serve to highlight the lint when rendered
var context = _extends({
content: this.props.content,
widgets: this.props.widgets
}, this.props.linterContext);
Gorgon.runLinter(parsedMarkdown, context, true);
// Apply the lint errors from the last NotGorgon run.
// TODO(joshuan): Support overlapping dots.
this.state.notGorgon.applyLintErrors(parsedMarkdown, [].concat(this.state.notGorgonLintErrors, this.props.legacyPerseusLint || []));
}
// Render the linted markdown parse tree with React components
var markdownContents = this.outputMarkdown(parsedMarkdown, {
baseElements: apiOptions.baseElements
});
var className = classNames((_classNames = {}, _classNames[ApiClassNames.RENDERER] = true, _classNames[ApiClassNames.RESPONSIVE_RENDERER] = true, _classNames[ApiClassNames.TWO_COLUMN_RENDERER] = this._isTwoColumn, _classNames));
this.lastRenderedMarkdown = React.createElement(
"div",
{ className: className },
markdownContents
);
return this.lastRenderedMarkdown;
}
});
module.exports = Renderer;
/***/ },
/* 38 */
/***/ function(module, exports, __webpack_require__) {
var _newHint;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var React = __webpack_require__(43);
var _require = __webpack_require__(79),
StyleSheet = _require.StyleSheet,
css = _require.css;
var classnames = __webpack_require__(86);
var i18n = window.i18n;
var Renderer = __webpack_require__(37);
var ApiOptions = __webpack_require__(12).Options;
var mediaQueries = __webpack_require__(76);
var _require2 = __webpack_require__(77),
baseUnitPx = _require2.baseUnitPx,
hintBorderWidth = _require2.hintBorderWidth,
kaGreen = _require2.kaGreen,
gray97 = _require2.gray97;
var Gorgon = __webpack_require__(41);
var _require3 = __webpack_require__(52),
linterContextProps = _require3.linterContextProps,
linterContextDefault = _require3.linterContextDefault;
/* Renders just a hint preview */
var HintRenderer = React.createClass({
displayName: "HintRenderer",
propTypes: {
apiOptions: ApiOptions.propTypes,
className: React.PropTypes.string,
hint: React.PropTypes.any,
lastHint: React.PropTypes.bool,
lastRendered: React.PropTypes.bool,
pos: React.PropTypes.number,
totalHints: React.PropTypes.number,
findExternalWidgets: React.PropTypes.func,
linterContext: linterContextProps
},
getDefaultProps: function getDefaultProps() {
return {
linterContext: linterContextDefault
};
},
getSerializedState: function getSerializedState() {
return this.refs.renderer.getSerializedState();
},
restoreSerializedState: function restoreSerializedState(state, callback) {
this.refs.renderer.restoreSerializedState(state, callback);
},
render: function render() {
var _props = this.props,
apiOptions = _props.apiOptions,
className = _props.className,
hint = _props.hint,
lastHint = _props.lastHint,
lastRendered = _props.lastRendered,
pos = _props.pos,
totalHints = _props.totalHints;
var isMobile = apiOptions.isMobile;
var classNames = classnames(!isMobile && "perseus-hint-renderer", isMobile && css(styles.newHint), isMobile && lastRendered && css(styles.lastRenderedNewHint), lastHint && "last-hint", lastRendered && "last-rendered", className);
// TODO(charlie): Allowing `staticRender` here would require that we
// extend `HintsRenderer` and `HintRenderer` to implement the full
// "input' API, so that clients could access the static inputs. Allowing
// `customKeypad` would require that we extend `ItemRenderer` to support
// nested inputs in the `HintsRenderer`. For now, we disable these
// options. Instead, clients will get standard elements, which
// aren't nice to use on mobile, but are at least usable.
var rendererApiOptions = _extends({}, apiOptions, {
customKeypad: false,
staticRender: false
});
return React.createElement(
"div",
{ className: classNames, tabIndex: "-1" },
!apiOptions.isMobile && React.createElement(
"span",
{ className: "perseus-sr-only" },
i18n._("Hint #%(pos)s", { pos: pos + 1 })
),
!apiOptions.isMobile && !apiOptions.satStyling && totalHints && pos != null && React.createElement(
"span",
{
className: "perseus-hint-label",
style: {
display: "block",
color: apiOptions.hintProgressColor
}
},
pos + 1 + " / " + totalHints
),
React.createElement(Renderer, {
ref: "renderer",
widgets: hint.widgets,
content: hint.content || "",
images: hint.images,
apiOptions: rendererApiOptions,
findExternalWidgets: this.props.findExternalWidgets,
linterContext: Gorgon.pushContextStack(this.props.linterContext, "hint")
})
);
}
});
var styles = StyleSheet.create({
newHint: (_newHint = {
marginBottom: 1.5 * baseUnitPx,
borderLeftColor: gray97,
borderLeftStyle: "solid",
borderLeftWidth: hintBorderWidth
}, _newHint[mediaQueries.lgOrSmaller] = {
paddingLeft: baseUnitPx
}, _newHint[mediaQueries.smOrSmaller] = {
paddingLeft: 0
}, _newHint[":focus"] = {
outline: "none"
}, _newHint),
lastRenderedNewHint: {
marginBottom: 0,
borderLeftColor: kaGreen
}
});
module.exports = HintRenderer;
/***/ },
/* 39 */,
/* 40 */,
/* 41 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _perseusMarkdown = __webpack_require__(49);
var _perseusMarkdown2 = _interopRequireDefault(_perseusMarkdown);
var _rule = __webpack_require__(84);
var _rule2 = _interopRequireDefault(_rule);
var _treeTransformer = __webpack_require__(85);
var _treeTransformer2 = _interopRequireDefault(_treeTransformer);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var allLintRules = __webpack_require__(87).filter(function (r) {
return r.severity < _rule2.default.Severity.BULK_WARNING;
});
//
// Run the Gorgon linter over the specified markdown parse tree,
// with the specified context object, and
// return a (possibly empty) array of lint warning objects. If the
// highlight argument is true, this function also modifies the parse
// tree to add "lint" nodes that can be visually rendered,
// highlighting the problems for the user. The optional rules argument
// is an array of Rule objects specifying which lint rules should be
// applied to this parse tree. When omitted, a default set of rules is used.
//
// The context object may have additional properties that some lint
// rules require:
//
// context.content is the source content string that was parsed to create
// the parse tree.
//
// context.widgets is the widgets object associated
// with the content string
//
// TODO: to make this even more general, allow the first argument to be
// a string and run the parser over it in that case? (but ignore highlight
// in that case). This would allow the one function to be used for both
// online linting and batch linting.
//
function runLinter(tree, context, highlight, rules) {
rules = rules || allLintRules;
var warnings = [];
var tt = new _treeTransformer2.default(tree);
// The markdown parser often outputs adjacent text nodes. We
// coalesce them before linting for efficiency and accuracy.
tt.traverse(function (node, state, content) {
if (_treeTransformer2.default.isTextNode(node)) {
var next = state.nextSibling();
while (_treeTransformer2.default.isTextNode(next)) {
node.content += next.content;
state.removeNextSibling();
next = state.nextSibling();
}
}
});
// HTML tables are complicated, and the CSS we use in
// ../components/lint.jsx to display lint does not work to
// correctly position the lint indicators in the margin when the
// lint is inside a table. So as a workaround we keep track of all
// the lint that appears within a table and move it up to the
// table element itself.
//
// It is not ideal to have to do this here,
// but it is cleaner here than fixing up the lint during rendering
// in perseus-markdown.jsx. If our lint display was simpler and
// did not require indicators in the margin, this wouldn't be a
// problem. Or, if we modified the lint display stuff so that
// indicator positioning and tooltip display were both handled
// with JavaScript (instead of pure CSS), then we could avoid this
// issue too. But using JavaScript has its own downsides: there is
// risk that the linter JavaScript would interfere with
// widget-related Javascript.
var tableWarnings = [];
var insideTable = false;
// Traverse through the nodes of the parse tree. At each node, loop
// through the array of lint rules and check whether there is a
// lint violation at that node.
tt.traverse(function (node, state, content) {
var nodeWarnings = [];
// If our rule is only designed to be tested against a particular
// content type and we're not in that content type, we don't need to
// consider that rule.
var applicableRules = rules.filter(function (r) {
return r.applies(context);
});
// Generate a stack so we can identify our position in the tree in
// lint rules
var stack = [].concat(context.stack);
stack.push(node.type);
var nodeContext = _extends({}, context, {
stack: stack.join('.')
});
applicableRules.forEach(function (rule) {
var warning = rule.check(node, state, content, nodeContext);
if (warning) {
// The start and end locations are relative to this
// particular node, and so are not generally very useful.
// TODO: When the markdown parser saves the node
// locations in the source string then we can add
// these numbers to that one and get and absolute
// character range that will be useful
if (warning.start || warning.end) {
warning.target = content.substring(warning.start, warning.end);
}
// Add the warning to the list of all lint we've found
warnings.push(warning);
// If we're going to be highlighting lint, then we also
// need to keep track of warnings specific to this node.
if (highlight) {
nodeWarnings.push(warning);
}
}
});
// If we're not highlighting lint in the tree, then we're done
// traversing this node.
if (!highlight) {
return;
}
// If the node we are currently at is a table, and there was lint
// inside the table, then we want to add that lint here
if (node.type === "table") {
if (tableWarnings.length) {
nodeWarnings.push.apply(nodeWarnings, tableWarnings);
}
// We're not in a table anymore, and don't have to remember
// the warnings for the table
insideTable = false;
tableWarnings = [];
} else if (!insideTable) {
// Otherwise, if we are not already inside a table, check
// to see if we've entered one. Because this is a post-order
// traversal we'll see the table contents before the table itself.
// Note that once we're inside the table, we don't have to
// do this check each time... We can just wait until we ascend
// up to the table, then we'll know we're out of it.
insideTable = state.ancestors().some(function (n) {
return n.type === "table";
});
}
// If we are inside a table and there were any warnings on
// this node, then we need to save the warnings for display
// on the table itself
if (insideTable && nodeWarnings.length) {
var _tableWarnings;
(_tableWarnings = tableWarnings).push.apply(_tableWarnings, nodeWarnings);
}
// If there were any warnings on this node, and if we're highlighting
// lint, then reparent the node so we can highlight it. Note that
// a single node can have multiple warnings. If this happends we
// concatenate the warnings and newline separate them. (The lint.jsx
// component that displays the warnings may want to convert the
// newlines into tags.) We also provide a lint rule name
// so that lint.jsx can link to a document that provides more details
// on that particular lint rule. If there is more than one warning
// we only link to the first rule, however.
//
// Note that even if we're inside a table, we still reparent the
// linty node so that it can be highlighted. We just make a note
// of whether this lint is inside a table or not.
if (nodeWarnings.length) {
nodeWarnings.sort(function (a, b) {
return a.severity - b.severity;
});
if (node.type !== "text" || nodeWarnings.length > 1) {
// If the linty node is not a text node, or if there is more
// than one warning on a text node, then reparent the entire
// node under a new lint node and put the warnings there.
state.replace({
type: "lint",
content: node,
message: nodeWarnings.map(function (w) {
return w.message;
}).join("\n\n"),
ruleName: nodeWarnings[0].rule,
insideTable: insideTable,
severity: nodeWarnings[0].severity
});
} else {
//
// Otherwise, it is a single warning on a text node, and we
// only want to highlight the actual linty part of that string
// of text. So we want to replace the text node with (in the
// general case) three nodes:
//
// 1) A new text node that holds the non-linty prefix
//
// 2) A lint node that is the parent of a new text node
// that holds the linty part
//
// 3) A new text node that holds the non-linty suffix
//
// If the lint begins and/or ends at the boundaries of the
// original text node, then nodes 1 and/or 3 won't exist, of
// course.
//
// Note that we could generalize this to work with multple
// warnings on a text node as long as the warnings are
// non-overlapping. Hopefully, though, multiple warnings in a
// single text node will be rare in practice. Also, we don't
// have a good way to display multiple lint indicators on a
// single line, so keeping them combined in that case might
// be the best thing, anyway.
//
var _content = node.content; // Text nodes have content
var warning = nodeWarnings[0]; // There is only one warning.
// These are the lint boundaries within the content
var start = warning.start || 0;
var end = warning.end || _content.length;
var prefix = _content.substring(0, start);
var lint = _content.substring(start, end);
var suffix = _content.substring(end);
var replacements = []; // What we'll replace the node with
// The prefix text node, if there is one
if (prefix) {
replacements.push({
type: "text",
content: prefix
});
}
// The lint node wrapped around the linty text
replacements.push({
type: "lint",
content: {
type: "text",
content: lint
},
message: warning.message,
ruleName: warning.rule,
insideTable: insideTable,
severity: warning.severity
});
// The suffix node, if there is one
if (suffix) {
replacements.push({
type: "text",
content: suffix
});
}
// Now replace the lint text node with the one to three
// nodes in the replacement array
state.replace.apply(state, replacements);
}
}
});
return warnings;
}
function pushContextStack(context, name) {
var stack = context.stack || [];
return _extends({}, context, {
stack: stack.concat(name)
});
}
//
// TODO(davidflanagan):
// Revisit these exports once we've got gorgon integrated into Perseus.
// Do we really need to export all of these things, or can we export a
// smaller set of functionality to enable both bulk linting by tools/gorgon.js
// and online linting?
//
// TODO(davidflanagan): switch from require to import
//
module.exports = {
runLinter: runLinter,
parse: _perseusMarkdown2.default.parse,
pushContextStack: pushContextStack,
rules: allLintRules
};
/***/ },
/* 42 */
/***/ function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_RESULT__;// From: hammerjs.github.io/touch-emulator/
(function(window, document, exportName, undefined) {
"use strict";
var isMultiTouch = false;
var multiTouchStartPos;
var eventTarget;
var touchElements = {};
// polyfills
if(!document.createTouch) {
document.createTouch = function(view, target, identifier, pageX, pageY, screenX, screenY, clientX, clientY) {
// auto set
if(clientX == undefined || clientY == undefined) {
clientX = pageX - window.pageXOffset;
clientY = pageY - window.pageYOffset;
}
return new Touch(target, identifier, {
pageX: pageX,
pageY: pageY,
screenX: screenX,
screenY: screenY,
clientX: clientX,
clientY: clientY
});
};
}
if(!document.createTouchList) {
document.createTouchList = function() {
var touchList = new TouchList();
for (var i = 0; i < arguments.length; i++) {
touchList[i] = arguments[i];
}
touchList.length = arguments.length;
return touchList;
};
}
/**
* create an touch point
* @constructor
* @param target
* @param identifier
* @param pos
* @param deltaX
* @param deltaY
* @returns {Object} touchPoint
*/
function Touch(target, identifier, pos, deltaX, deltaY) {
deltaX = deltaX || 0;
deltaY = deltaY || 0;
this.identifier = identifier;
this.target = target;
this.clientX = pos.clientX + deltaX;
this.clientY = pos.clientY + deltaY;
this.screenX = pos.screenX + deltaX;
this.screenY = pos.screenY + deltaY;
this.pageX = pos.pageX + deltaX;
this.pageY = pos.pageY + deltaY;
}
/**
* create empty touchlist with the methods
* @constructor
* @returns touchList
*/
function TouchList() {
var touchList = [];
touchList.item = function(index) {
return this[index] || null;
};
// specified by Mozilla
touchList.identifiedTouch = function(id) {
return this[id + 1] || null;
};
return touchList;
}
/**
* Simple trick to fake touch event support
* this is enough for most libraries like Modernizr and Hammer
*/
function fakeTouchSupport() {
var objs = [window, document.documentElement];
var props = ['ontouchstart', 'ontouchmove', 'ontouchcancel', 'ontouchend'];
for(var o=0; o 2; // pointer events
}
/**
* disable mouseevents on the page
* @param ev
*/
function preventMouseEvents(ev) {
ev.preventDefault();
ev.stopPropagation();
}
/**
* only trigger touches when the left mousebutton has been pressed
* @param touchType
* @returns {Function}
*/
function onMouse(touchType) {
return function(ev) {
// prevent mouse events
preventMouseEvents(ev);
if (ev.which !== 1) {
return;
}
// The EventTarget on which the touch point started when it was first placed on the surface,
// even if the touch point has since moved outside the interactive area of that element.
// also, when the target doesnt exist anymore, we update it
if (ev.type == 'mousedown' || !eventTarget || (eventTarget && !eventTarget.dispatchEvent)) {
eventTarget = ev.target;
}
// shiftKey has been lost, so trigger a touchend
if (isMultiTouch && !ev.shiftKey) {
triggerTouch('touchend', ev);
isMultiTouch = false;
}
triggerTouch(touchType, ev);
// we're entering the multi-touch mode!
if (!isMultiTouch && ev.shiftKey) {
isMultiTouch = true;
multiTouchStartPos = {
pageX: ev.pageX,
pageY: ev.pageY,
clientX: ev.clientX,
clientY: ev.clientY,
screenX: ev.screenX,
screenY: ev.screenY
};
triggerTouch('touchstart', ev);
}
// reset
if (ev.type == 'mouseup') {
multiTouchStartPos = null;
isMultiTouch = false;
eventTarget = null;
}
}
}
/**
* trigger a touch event
* @param eventName
* @param mouseEv
*/
function triggerTouch(eventName, mouseEv) {
var touchEvent = document.createEvent('Event');
touchEvent.initEvent(eventName, true, true);
touchEvent.altKey = mouseEv.altKey;
touchEvent.ctrlKey = mouseEv.ctrlKey;
touchEvent.metaKey = mouseEv.metaKey;
touchEvent.shiftKey = mouseEv.shiftKey;
touchEvent.which = 0;
touchEvent.touches = getActiveTouches(mouseEv, eventName);
touchEvent.targetTouches = getActiveTouches(mouseEv, eventName);
touchEvent.changedTouches = getChangedTouches(mouseEv, eventName);
eventTarget.dispatchEvent(touchEvent);
}
/**
* create a touchList based on the mouse event
* @param mouseEv
* @returns {TouchList}
*/
function createTouchList(mouseEv) {
var touchList = new TouchList();
if (isMultiTouch) {
var f = TouchEmulator.multiTouchOffset;
var deltaX = multiTouchStartPos.pageX - mouseEv.pageX;
var deltaY = multiTouchStartPos.pageY - mouseEv.pageY;
touchList.push(new Touch(eventTarget, 1, multiTouchStartPos, (deltaX*-1) - f, (deltaY*-1) + f));
touchList.push(new Touch(eventTarget, 2, multiTouchStartPos, deltaX+f, deltaY-f));
} else {
touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0));
}
return touchList;
}
/**
* receive all active touches
* @param mouseEv
* @returns {TouchList}
*/
function getActiveTouches(mouseEv, eventName) {
// empty list
if (mouseEv.type == 'mouseup') {
return new TouchList();
}
var touchList = createTouchList(mouseEv);
if(isMultiTouch && mouseEv.type != 'mouseup' && eventName == 'touchend') {
touchList.splice(1, 1);
}
return touchList;
}
/**
* receive a filtered set of touches with only the changed pointers
* @param mouseEv
* @param eventName
* @returns {TouchList}
*/
function getChangedTouches(mouseEv, eventName) {
var touchList = createTouchList(mouseEv);
// we only want to return the added/removed item on multitouch
// which is the second pointer, so remove the first pointer from the touchList
//
// but when the mouseEv.type is mouseup, we want to send all touches because then
// no new input will be possible
if(isMultiTouch && mouseEv.type != 'mouseup' &&
(eventName == 'touchstart' || eventName == 'touchend')) {
touchList.splice(0, 1);
}
return touchList;
}
/**
* show the touchpoints on the screen
*/
function showTouches(ev) {
var touch, i, el, styles;
// first all visible touches
for(i = 0; i < ev.touches.length; i++) {
touch = ev.touches[i];
el = touchElements[touch.identifier];
if(!el) {
el = touchElements[touch.identifier] = document.createElement("div");
document.body.appendChild(el);
}
styles = TouchEmulator.template(touch);
for(var prop in styles) {
el.style[prop] = styles[prop];
}
}
// remove all ended touches
if(ev.type == 'touchend' || ev.type == 'touchcancel') {
for(i = 0; i < ev.changedTouches.length; i++) {
touch = ev.changedTouches[i];
el = touchElements[touch.identifier];
if(el) {
el.parentNode.removeChild(el);
delete touchElements[touch.identifier];
}
}
}
}
/**
* TouchEmulator initializer
*/
function TouchEmulator() {
if (hasTouchSupport()) {
return;
}
fakeTouchSupport();
window.addEventListener("mousedown", onMouse('touchstart'), true);
window.addEventListener("mousemove", onMouse('touchmove'), true);
window.addEventListener("mouseup", onMouse('touchend'), true);
window.addEventListener("mouseenter", preventMouseEvents, true);
window.addEventListener("mouseleave", preventMouseEvents, true);
window.addEventListener("mouseout", preventMouseEvents, true);
window.addEventListener("mouseover", preventMouseEvents, true);
// it uses itself!
window.addEventListener("touchstart", showTouches, false);
window.addEventListener("touchmove", showTouches, false);
window.addEventListener("touchend", showTouches, false);
window.addEventListener("touchcancel", showTouches, false);
}
// start distance when entering the multitouch mode
TouchEmulator.multiTouchOffset = 75;
/**
* css template for the touch rendering
* @param touch
* @returns object
*/
TouchEmulator.template = function(touch) {
var size = 30;
var transform = 'translate('+ (touch.clientX-(size/2)) +'px, '+ (touch.clientY-(size/2)) +'px)';
return {
position: 'fixed',
left: 0,
top: 0,
background: '#fff',
border: 'solid 1px #999',
opacity: .6,
borderRadius: '100%',
height: size + 'px',
width: size + 'px',
padding: 0,
margin: 0,
display: 'block',
overflow: 'hidden',
pointerEvents: 'none',
webkitUserSelect: 'none',
mozUserSelect: 'none',
userSelect: 'none',
webkitTransform: transform,
mozTransform: transform,
transform: transform,
}
};
// export
if (true) {
!(__WEBPACK_AMD_DEFINE_RESULT__ = function() {
return TouchEmulator;
}.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
} else if (typeof module != "undefined" && module.exports) {
module.exports = TouchEmulator;
} else {
window[exportName] = TouchEmulator;
}
})(window, document, "TouchEmulator");
/***/ },
/* 43 */
/***/ function(module, exports, __webpack_require__) {
/* This note applies to jquery, react, and underscore.
*
* We're faking a node module for this package by just exporting the global.
* There are a few complications which led us to this solution as a temporary
* fix.
*
* - Browserify can slow down a lot when you include the other packages (and
* their dependency graphs). We were also battling general browserify
* slowness at this time - browserify 3.4.0 is "good" but later versions
* (3.53 if I remember correctly) are terribly slow (on the order of 20x
* slower).
*
* - I'm not clear on the details of packaging this so we don't duplicate
* dependencies anywhere. For instance when packaging perseus for webapp we
* need to be careful not to include packages like underscore from our
* dependencies or from the packages we depend on. (note: this is a very good
* opportunity to either explain how existing tools solve the problem or
* create a new tool to solve it)
*
* - Joel (and Jack)
*/
module.exports = window.React;
/***/ },
/* 44 */
/***/ function(module, exports, __webpack_require__) {
// This funky setup is dictated by how we build React in webapp currently. See
// the khan/react-build repo.
module.exports = window.React.__internalReactDOM;
/***/ },
/* 45 */,
/* 46 */,
/* 47 */
/***/ function(module, exports, __webpack_require__) {
/**
* Icon paths to be used with `inline-icon.jsx`.
*
* These paths are taken directly from webapp's `icon-paths.js`. Unlike the
* webapp equivalent, these can be directly required within Perseus files since
* this is all bundled together anyway.
*/
/* eslint-disable max-len */
module.exports = {
iconCheck: {
path: "M8.70710678,12.2928932 C8.31658249,11.9023689 7.68341751,11.9023689 7.29289322,12.2928932 C6.90236893,12.6834175 6.90236893,13.3165825 7.29289322,13.7071068 L9.82842712,16.2426407 C10.2207367,16.6349502 10.8574274,16.6328935 11.2471942,16.2380576 L16.7116603,10.7025237 C17.0996535,10.3094846 17.0955629,9.67633279 16.7025237,9.28833966 C16.3094846,8.90034653 15.6763328,8.90443714 15.2883397,9.29747629 L10.5309507,14.1167372 L8.70710678,12.2928932 Z",
width: 24,
height: 24
},
iconChevronDown: {
path: "M99.669 13.048q0 3.36-2.352 5.712l-41.664 41.664q-2.408 2.408-5.88 2.408t-5.712-2.408l-41.664-41.664q-2.408-2.24-2.408-5.712t2.408-5.88l4.76-4.816q2.52-2.352 5.88-2.352t5.656 2.352l31.136 31.136 31.08-31.136q2.352-2.352 5.712-2.352t5.88 2.352l4.816 4.816q2.352 2.52 2.352 5.88z",
width: 100,
height: 63.034
},
iconChevronRight: {
path: "M62.808 49.728q0 3.36-2.352 5.88l-41.72 41.664q-2.352 2.408-5.768 2.408t-5.768-2.408l-4.872-4.76q-2.352-2.52-2.352-5.88t2.352-5.712l31.08-31.136-31.08-31.024q-2.352-2.52-2.352-5.88t2.352-5.712l4.872-4.76q2.296-2.408 5.768-2.408t5.768 2.408l41.72 41.664q2.352 2.296 2.352 5.656z",
width: 63.034,
height: 100
},
iconCircle: {
path: "M100.035 50.046q.057 13.623-6.669 25.137t-18.24 18.183-25.08 6.669-25.137-6.726q-11.514-6.726-18.183-18.183-6.726-11.571-6.726-25.137t6.726-25.08 18.24-18.24 25.08-6.669q13.566 0 25.08 6.726 11.514 6.669 18.24 18.183t6.669 25.137z",
width: 100,
height: 100
},
iconCircleArrowDown: {
path: "M50.046 83.676q1.767 0 2.907-1.14l29.526-29.526q1.197-1.197 1.197-2.907t-1.197-2.964l-5.928-5.928q-1.197-1.14-2.964-1.14t-2.907 1.14l-12.312 12.312l0-32.661q0-1.71-1.254-2.964t-2.907-1.254l-8.322 0q-1.71 0-2.964 1.254t-1.254 2.964l0 32.661l-12.312-12.312q-1.197-1.254-2.907-1.254t-2.907 1.254l-5.928 5.928q-1.197 1.197-1.197 2.964t1.197 2.907l29.469 29.526q1.197 1.14 2.964 1.14zm49.989-33.63q.057 13.623-6.669 25.137t-18.24 18.183-25.08 6.669-25.137-6.726q-11.514-6.726-18.183-18.183-6.726-11.571-6.726-25.137t6.726-25.08 18.24-18.24 25.08-6.669q13.566 0 25.08 6.726 11.514 6.669 18.24 18.183t6.669 25.137z",
width: 100,
height: 100
},
iconCircleArrowUp: {
path: "M54.207 83.391q1.653 0 2.907-1.254t1.254-2.907l0-32.718l12.312 12.312q1.254 1.254 2.964 1.254t2.907-1.254l5.928-5.928q1.197-1.197 1.14-2.964 0-1.767-1.14-2.907l-29.526-29.526q-1.197-1.14-2.907-1.14t-2.964 1.14l-29.469 29.526q-1.197 1.254-1.197 2.964t1.197 2.907l5.928 5.928q1.197 1.197 2.907 1.197t2.907-1.197l12.312-12.312l0 32.718q0 1.653 1.254 2.907t2.964 1.254l8.322 0zm45.828-33.345q.057 13.623-6.669 25.137t-18.24 18.183-25.08 6.669-25.137-6.726q-11.514-6.726-18.183-18.183-6.726-11.571-6.726-25.137t6.726-25.08 18.24-18.24 25.08-6.669q13.566 0 25.08 6.726 11.514 6.669 18.24 18.183t6.669 25.137z",
width: 100,
height: 100
},
iconCircleThin: {
path: "M50.046 8.322q-8.493 0-16.188 3.306-15.561 6.669-22.173 22.23-3.363 7.695-3.363 16.188t3.306 16.188 8.949 13.281q5.586 5.586 13.281 8.892t16.188 3.306 16.188-3.306 13.281-8.892 8.892-13.281 3.306-16.188-3.306-16.188-8.892-13.281-13.281-8.949q-7.695-3.306-16.188-3.306zm0 91.713q-13.623 0-25.137-6.726t-18.183-18.183q-6.726-11.571-6.726-25.137t6.726-25.08 18.24-18.24 25.08-6.669q13.566 0 25.08 6.726 11.514 6.669 18.24 18.183t6.726 25.137-6.726 25.137-18.24 18.126q-11.514 6.726-25.08 6.726z",
width: 100,
height: 99.944
},
iconDesktop: {
path: "M94.208 52.119l0-43.746q0-.69-.506-1.15t-1.196-.506l-84.088 0q-.69 0-1.196.506t-.506 1.15l0 43.746q0 .69.506 1.196t1.196.506l84.088 0q.69 0 1.196-.506t.506-1.196zm6.716-43.746l0 57.224q0 3.45-2.484 5.934t-5.934 2.484l-28.566 0q0 3.128 2.53 7.774.828 1.61.828 2.622t-1.012 2.07q-1.012 1.012-2.346.966l-26.91 0q-1.38 0-2.392-1.012t-1.012-2.024q0-1.058 1.656-4.14t1.748-6.256l-28.612 0q-3.45 0-5.934-2.484t-2.484-5.934l0-57.224q0-3.45 2.484-5.934t5.934-2.438l84.088 0q3.45 0 5.98 2.438 2.438 2.484 2.438 5.934z",
width: 100,
height: 86.648
},
iconDropdownArrow: {
path: "M9 9.8c0 .5.7 1.7 1.5 2.8 1.5 1.9 1.5 1.9 3 0C15.7 9.7 15.4 9 12 9c-1.6 0-3 .4-3 .8z",
width: 24,
height: 24
},
iconExclamationSign: {
path: "M58.368 81.225l0-12.369q0-.912-.57-1.539t-1.425-.627l-12.54 0q-.855-.057-1.482.627t-.684 1.539l0 12.369q-.057.855.627 1.482t1.539.684l12.54 0q.855 0 1.425-.627t.57-1.539zm1.026-62.871q0-1.596-2.223-1.71l-14.307 0q-2.109 0-2.223 1.71l1.14 40.47q0 .627.627 1.14t1.539.456l12.084 0q.912-.057 1.539-.513t.684-1.083zm-9.348-18.354q13.566 0 25.08 6.726 11.514 6.669 18.24 18.183t6.726 25.137-6.726 25.137-18.24 18.183-25.08 6.669-25.137-6.726q-11.514-6.726-18.183-18.183-6.726-11.571-6.726-25.137t6.726-25.08 18.24-18.24 25.08-6.669z",
width: 100,
height: 99.944
},
// Grabbed from https://github.com/encharm/Font-Awesome-SVG-PNG
iconGear: {
path: "M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z",
width: 1792,
height: 1792
},
iconMobilePhone: {
path: "M36.04 89.557q0-2.584-1.836-4.42t-4.42-1.836-4.352 1.836q-1.836 1.836-1.836 4.42t1.836 4.352 4.42 1.836q2.652-.068 4.42-1.836t1.768-4.352zm16.184-12.444l0-54.74q0-1.088-.748-1.768t-1.768-.68l-39.78 0q-1.088 0-1.768.748t-.68 1.7l0 54.74q0 1.02.748 1.768t1.7.68l39.78 0q1.02-.068 1.768-.748t.748-1.7zm-14.892-65.892q0-1.224-1.292-1.292l-12.444 0q-1.224.068-1.224 1.292t1.224 1.224l12.444 0q1.292 0 1.292-1.224zm22.372-1.292l0 79.628q0 3.944-2.992 6.936t-7.004 2.992l-39.78 0q-4.012 0-7.004-2.924-2.924-2.924-2.924-7.004l0-79.628q0-4.012 2.924-6.936t7.004-2.992l39.78 0q4.012-.068 7.004 2.924t2.992 7.004z",
width: 60.013,
height: 100
},
iconOk: {
path: "M37.964 76.048q-2.576 0-4.368-1.792l-31.864-31.864q-1.792-1.792-1.792-4.368t1.792-4.368l8.736-8.68q1.792-1.792 4.368-1.792t4.312 1.792l18.816 18.872 42-42.056q1.792-1.792 4.368-1.792t4.312 1.792l8.736 8.736q1.792 1.792 1.792 4.368t-1.792 4.312l-55.048 55.048q-1.792 1.792-4.368 1.792z",
width: 100,
height: 76.637
},
iconPlus: {
path: "M99.758 43.09l0 13.578q0 2.852-1.984 4.836t-4.836 1.984l-29.45 0l0 29.45q0 2.852-1.984 4.836t-4.836 1.984l-13.578 0q-2.852 0-4.836-1.984t-1.984-4.836l0-29.45l-29.45 0q-2.852 0-4.836-1.984t-1.984-4.836l0-13.578q0-2.852 1.984-4.836t4.836-1.984l29.45 0l0-29.45q0-2.852 1.984-4.836t4.836-1.984l13.578 0q2.852 0 4.836 1.984t1.984 4.836l0 29.45l29.45 0q2.852 0 4.836 1.984t1.984 4.836z",
width: 100,
height: 100
},
iconRemove: {
path: "M100.464 80.808q0 3.404-2.368 5.772l-11.47 11.544q-2.368 2.368-5.772 2.368t-5.698-2.368l-24.864-24.864-24.864 24.864q-2.368 2.368-5.772 2.368t-5.772-2.368l-11.47-11.544q-2.368-2.368-2.368-5.772t2.368-5.698l24.864-24.864-24.864-24.864q-2.368-2.368-2.368-5.772t2.368-5.772l11.47-11.47q2.368-2.368 5.772-2.368t5.772 2.368l24.864 24.864 24.864-24.864q2.294-2.368 5.698-2.368t5.772 2.368l11.47 11.47q2.368 2.368 2.368 5.772t-2.368 5.772l-24.864 24.864 24.864 24.864q2.368 2.294 2.368 5.698z",
width: 100,
height: 100
},
iconStar: {
path: "M15.1052249,9.55978547 L22.0028147,9.55978545 C23.6568673,9.55978545 23.9349557,10.3753626 22.6181351,11.3858845 L16.9943688,15.7015366 L19.2518801,22.8294455 C19.7526645,24.4106317 19.0984455,24.8825885 17.769353,23.8673293 L12.0490577,19.4977438 L6.5116497,23.8422153 C5.20921411,24.8640642 4.53299569,24.4067544 5.00266927,22.8160582 L7.10332364,15.7015366 L1.42794544,11.3634306 C0.110226041,10.3562014 0.383967283,9.54239221 2.0409646,9.54574013 L8.9924676,9.55978547 L11.1485117,2.72669438 C11.6458693,1.15043244 12.4548928,1.15900049 12.9494787,2.72669438 L15.1052249,9.55978547 Z",
width: 24,
height: 24
},
iconTryAgain: {
path: "M3.74890556,17.9799506 C2.19251241,16.1970909 1.10103636,13.4971457 1.13090903,11.1491783 C1.17160478,7.95052637 4.01704076,0.865059407 11.7028044,0.865059407 C19.388568,0.865059407 22.3026521,7.35203035 22.3026521,11.5879453 C22.3026521,15.8238603 19.386629,20.5574509 13.6832464,21.7131548 L13.6757539,17.3722171 C17.0812986,16.2190517 18.331158,14.1944123 18.3311578,11.5879451 C18.3311574,8.16554692 15.6664205,5.03476549 11.7028048,5.20494205 C7.73918903,5.37511861 5.59244567,8.66930079 5.59244567,11.1491783 C5.59244567,12.9090077 6.11128139,14.1753512 6.93640437,15.3053215 L8.14052356,14.2949456 C8.98559348,13.5858477 9.6994861,13.9070448 9.73489556,15.0076413 L9.91284941,20.5388014 C9.94832683,21.6415103 9.09967118,22.3514475 8.02194403,22.1254594 L2.60571602,20.9897332 C1.5259204,20.7633114 1.34338662,19.9984207 2.18070755,19.295825 L3.74890556,17.9799506 Z",
width: 23,
height: 23
},
iconTablet: {
path: "M45.322 90.706q0-1.86-1.302-3.224-1.364-1.364-3.224-1.364t-3.224 1.364-1.302 3.224q0 1.86 1.364 3.224 1.302 1.364 3.162 1.302 1.86.062 3.224-1.302t1.302-3.224zm27.218-11.346l0-68.014q0-.93-.682-1.612t-1.55-.682l-58.962 0q-.93 0-1.612.682t-.682 1.612l0 68.014q0 .93.682 1.612t1.612.62l58.962 0q.992-.062 1.612-.682t.62-1.55zm9.114-68.014l0 77.066q0 4.65-3.348 7.998t-7.998 3.348l-58.962 0q-4.65 0-7.998-3.348t-3.348-7.998l0-77.066q0-4.65 3.348-7.998t7.998-3.348l58.962 0q4.65 0 7.998 3.348t3.348 7.998z",
width: 81.852,
height: 100
},
iconTrash: {
path: "M31.293 37.506q2.052 0 2.052 2.109l0 37.506q0 1.995-2.052 2.109l-4.218 0q-.912-.057-1.482-.627t-.57-1.482l0-37.506q0-2.109 2.052-2.109l4.218 0zm18.753 2.109l0 37.506q0 .912-.57 1.482t-1.539.627l-4.161 0q-1.995 0-2.109-2.109l0-37.506q.057-.912.627-1.482t1.482-.627l4.161 0q.969.057 1.539.627t.57 1.482zm14.592-2.109q2.052 0 2.052 2.109l0 37.506q0 1.995-2.052 2.109l-4.161 0q-.969-.057-1.539-.627t-.57-1.482l0-37.506q0-2.109 2.109-2.109l4.161 0zm10.431 49.248l0-61.731l-58.368 0l0 61.731q.057 2.679.969 3.819t1.083 1.14l54.207 0q.171 0 1.14-1.083t.969-3.876zm-43.776-70.11l29.184 0l-3.135-7.581q-.456-.57-1.14-.741l-20.634 0q-.627.114-1.083.741zm-31.293 2.109q0-1.995 2.109-2.109l20.121 0l4.56-10.83q.969-2.394 3.477-4.104 2.565-1.71 5.187-1.71l20.805 0q2.622 0 5.187 1.71t3.477 4.104l4.56 10.83l20.178 0q.912.057 1.482.627t.57 1.482l0 4.161q0 1.995-2.052 2.109l-6.27 0l0 61.731q0 5.415-3.078 9.348t-7.353 3.933l-54.207 0q-4.275 0-7.353-3.819t-3.078-9.177l0-62.016l-6.213 0q-.969 0-1.539-.57t-.57-1.539l0-4.161z",
width: 91.681,
height: 100
},
iconUndo: {
path: "M10,6.6C10,7.2,9.8,8,9.3,9.1c0,0,0,0.1-0.1,0.1S9.2,9.3,9.2,9.4c0,0,0,0.1-0.1,0.1C9,9.6,9,9.6,8.9,9.6 c-0.1,0-0.1,0-0.1-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1s0-0.1,0-0.1c0-0.3,0-0.5,0-0.7c0-0.4,0-0.7-0.1-1C8.6,7.1,8.5,6.9,8.4,6.7S8.2,6.3,8,6.1C7.8,5.9,7.6,5.8,7.4,5.7S6.9,5.5,6.7,5.5S6.1,5.4,5.8,5.4c-0.3,0-0.6,0-1,0H3.6v1.4c0,0.1,0,0.2-0.1,0.3C3.4,7.1,3.3,7.1,3.2,7.1C3.1,7.1,3,7.1,3,7L0.1,4.1C0,4.1,0,4,0,3.9s0-0.2,0.1-0.3L3,0.8C3,0.7,3.1,0.7,3.2,0.7c0.1,0,0.2,0,0.3,0.1C3.5,0.9,3.6,0.9,3.6,1v1.4h1.2c2.6,0,4.3,0.7,4.9,2.2C9.9,5.2,10,5.8,10,6.6z",
width: 10,
height: 10
},
iconMinus: {
path: "M8,13 L16,13 C16.5522847,13 17,12.5522847 17,12 C17,11.4477153 16.5522847,11 16,11 L8,11 C7.44771525,11 7,11.4477153 7,12 C7,12.5522847 7.44771525,13 8,13 Z",
width: 24,
height: 24
}
};
/***/ },
/* 48 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable react/forbid-prop-types */
/**
* A stripped version of Icon.jsx from webapp. Takes an SVG icon and renders it
* inline like Font Awesome did.
*
* If you are looking for an icon that we've used before you should look in
* webapp's `icon-paths.js` which is a reference file for all the SVG paths
* that we've used. You'll need to copy the object from that file into
* whichever file you're using the icon and explicitly pass it in to the
* React component.
*
* We assume that the viewBox is cropped and aligned to (0, 0), but icons can
* be defined differently. At some point we might want to add these attributes
* to `icon-paths.js`, but for now this is a fairly safe assumption.
*
* Sample usage:
*
* const editIcon = {
* path: "M41.209 53.753l5.39 0l0 5.39l3.136 0l6.468-6.517-8.477-8.526-6.517 6.517l0 3.136zm33.173-34.937q-.882-.882-1.862.049l-19.6 19.6q-.931.98-.049 1.862t1.862-.049l19.6-19.6q.931-.98.049-1.862zm-38.563 45.668l0-16.121l37.632-37.632 16.17 16.121-37.632 37.632l-16.17 0zm43.022-12.397l0 10.633q-.049 6.713-4.753 11.417t-11.368 4.704l-46.599 0q-6.713 0-11.417-4.753t-4.704-11.368l0-46.599q0-6.664 4.753-11.417t11.368-4.704l46.599 0q3.528 0 6.566 1.372.833.392.98 1.323t-.49 1.617l-2.744 2.744q-.784.784-1.96.441t-2.352-.343l-46.599 0q-3.675 0-6.321 2.646t-2.646 6.321l0 46.599q0 3.675 2.646 6.321t6.321 2.646l46.599 0q3.675 0 6.321-2.646t2.646-6.321l0-7.056q0-.735.49-1.225l3.577-3.577q.833-.833 1.96-.392t1.127 1.617zm7.203-51.646q2.254 0 3.773 1.568l8.526 8.526q1.568 1.568 1.568 3.822t-1.568 3.773l-5.145 5.145-16.121-16.121 5.145-5.145q1.568-1.568 3.822-1.568z", // @Nolint
* width: 100,
* height: 78.912,
* };
*
*
*/
var React = __webpack_require__(43);
var InlineIcon = function InlineIcon(_ref) {
var path = _ref.path,
width = _ref.width,
height = _ref.height,
_ref$style = _ref.style,
style = _ref$style === undefined ? {} : _ref$style,
title = _ref.title;
return React.createElement(
"svg",
{
role: "img",
"aria-hidden": !title,
style: _extends({ verticalAlign: "middle" }, style),
width: width / height + "em",
height: "1em",
viewBox: "0 0 " + width + " " + height
},
!!title && React.createElement(
"title",
null,
title
),
React.createElement("path", { d: path, fill: "currentColor" })
);
};
InlineIcon.propTypes = {
// An SVG path to render.
path: React.PropTypes.string.isRequired,
// The path's viewBox dimensions.
// We set the viewport height to 1em and scale the width accordingly.
height: React.PropTypes.number.isRequired,
width: React.PropTypes.number.isRequired,
style: React.PropTypes.object,
// A11y description for this icon. If absent, icon is marked
// aria-hidden=true
title: React.PropTypes.string
};
/* eslint-enable react/jsx-sort-prop-types */
module.exports = InlineIcon;
/***/ },
/* 49 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable no-var, object-curly-spacing */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
/* globals KA */
var _ = __webpack_require__(56);
var SimpleMarkdown = __webpack_require__(260);
var TeX = __webpack_require__(178);
var Util = __webpack_require__(17);
var Lint = __webpack_require__(179);
/**
* This match function matches math in `$`s, such as:
*
* $y = x + 1$
*
* It functions roughly like the following regex:
* /\$([^\$]*)\$/
*
* Unfortunately, math may have other `$`s inside it, as
* long as they are inside `{` braces `}`, mostly for
* `\text{ $math$ }`.
*
* To parse this, we can't use a regex, since we
* should support arbitrary nesting (even though
* MathJax actually only supports two levels of nesting
* here, which we *could* parse with a regex).
*
* Non-regex matchers like this are now a first-class
* concept in simple-markdown. Yay!
*
* This can also match block-math, which is math alone in a paragraph.
*/
var mathMatcher = function mathMatcher(source, state, isBlock) {
var length = source.length;
var index = 0;
// When looking for blocks, skip over leading spaces
if (isBlock) {
if (state.inline) {
return null;
}
while (index < length && source[index] === " ") {
index++;
}
}
// Our source must start with a "$"
if (!(index < length && source[index] === "$")) {
return null;
}
index++;
var startIndex = index;
var braceLevel = 0;
// Loop through the source, looking for a closing '$'
// closing '$'s only count if they are not escaped with
// a `\`, and we are not in nested `{}` braces.
while (index < length) {
var character = source[index];
if (character === "\\") {
// Consume both the `\` and the escaped char as a single
// token.
// This is so that the second `$` in `$\\$` closes
// the math expression, since the first `\` is escaping
// the second `\`, but the second `\` is not escaping
// the second `$`.
// This also handles the case of escaping `$`s or
// braces `\{`
index++;
} else if (braceLevel <= 0 && character === "$") {
var endIndex = index + 1;
if (isBlock) {
// Look for two trailing newlines after the closing `$`
var match = /^(?: *\n){2,}/.exec(source.slice(endIndex));
endIndex = match ? endIndex + match[0].length : null;
}
// Return an array that looks like the results of a
// regex's .exec function:
// capture[0] is the whole string
// capture[1] is the first "paren" match, which is the
// content of the math here, as if we wrote the regex
// /\$([^\$]*)\$/
if (endIndex) {
return [source.substring(0, endIndex), source.substring(startIndex, index)];
}
return null;
} else if (character === "{") {
braceLevel++;
} else if (character === "}") {
braceLevel--;
} else if (character === "\n" && source[index - 1] === "\n") {
// This is a weird case we supported in the old
// math implementation--double newlines break
// math. I'm preserving it for now because content
// creators might have questions with single '$'s
// in paragraphs...
return null;
}
index++;
}
// we didn't find a closing `$`
return null;
};
var mathMatch = function mathMatch(source, state) {
return mathMatcher(source, state, false);
};
var blockMathMatch = function blockMathMatch(source, state) {
return mathMatcher(source, state, true);
};
var TITLED_TABLE_REGEX = new RegExp("^\\|\\| +(.*) +\\|\\| *\\n" + "(" +
// The simple-markdown nptable regex, without
// the leading `^`
SimpleMarkdown.defaultRules.nptable.match.regex.source.substring(1) + ")");
var crowdinJiptMatcher = SimpleMarkdown.blockRegex(/^(crwdns.*)\n\s*\n/);
var rules = _.extend({}, SimpleMarkdown.defaultRules, {
// NOTE: basically ignored by JIPT. wraps everything at the outer layer
columns: {
order: -2,
match: SimpleMarkdown.blockRegex(/^([\s\S]*\n\n)={5,}\n\n([\s\S]*)/),
parse: function parse(capture, _parse, state) {
return {
col1: _parse(capture[1], state),
col2: _parse(capture[2], state)
};
},
react: function react(node, output, state) {
return React.createElement(
"div",
{ className: "perseus-two-columns", key: state.key },
React.createElement(
"div",
{ className: "perseus-column" },
React.createElement(
"div",
{ className: "perseus-column-content" },
output(node.col1, state)
)
),
React.createElement(
"div",
{ className: "perseus-column" },
React.createElement("div", { className: "sat-header-grafting-area" }),
React.createElement(
"div",
{ className: "perseus-column-content" },
React.createElement("div", { className: "sat-skill-subscore-grafting-area" }),
output(node.col2, state),
React.createElement("div", { className: "sat-grafting-area" })
)
)
);
}
},
// Match paragraphs consisting solely of crowdin IDs
// (they look roughly like crwdns9238932:0), which means that
// crowdin is going to take the DOM node that ID is rendered into
// and count it as the top-level translation node. They mutate this
// node, so we need to make sure it is an outer node, not an inner
// span. So here we parse this separately and just output the
// raw string, which becomes the body of the
// created by the Renderer.
// This currently (2015-09-01) affects only articles, since
// for exercises the renderer just renders the crowdin id to the
// renderer div.
crowdinId: {
order: -1,
match: function match(source, state, prevCapture) {
// Only match on the just-in-place translation site
if (state.isJipt) {
return crowdinJiptMatcher(source, state, prevCapture);
} else {
return null;
}
},
parse: function parse(capture, _parse2, state) {
return { id: capture[1] };
},
react: function react(node, output, state) {
return node.id;
}
},
// This is pretty much horrible, but we have a regex here to capture an
// entire table + a title. capture[1] is the title. capture[2] of the
// regex is a copy of the simple-markdown nptable regex. Then we turn
// our capture[2] into tableCapture[0], and any further captures in
// our table regex into tableCapture[1..], and we pass tableCapture to
// our nptable regex
titledTable: {
// process immediately before nptables
order: SimpleMarkdown.defaultRules.nptable.order - 0.5,
match: SimpleMarkdown.blockRegex(TITLED_TABLE_REGEX),
parse: function parse(capture, _parse3, state) {
var title = SimpleMarkdown.parseInline(_parse3, capture[1], state);
// Remove our [0] and [1] captures, and pass the rest to
// the nptable parser
var tableCapture = _.rest(capture, 2);
var table = SimpleMarkdown.defaultRules.nptable.parse(tableCapture, _parse3, state);
return {
title: title,
table: table
};
},
react: function react(node, output, state) {
var contents = void 0;
if (!node.table) {
contents = "//invalid table//";
} else {
var tableOutput = SimpleMarkdown.defaultRules.table.react(node.table, output, state);
var caption = React.createElement(
"caption",
{ key: "caption", className: "perseus-table-title" },
output(node.title, state)
);
// Splice the caption into the table's children with the
// caption as the first child.
contents = React.cloneElement(tableOutput, null, [caption].concat(tableOutput.props.children));
}
// Note: if the DOM structure changes, edit the Zoomable wrapper
// in src/renderer.jsx.
return React.createElement(
"div",
{ className: "perseus-titled-table", key: state.key },
contents
);
}
},
widget: {
order: SimpleMarkdown.defaultRules.link.order - 0.75,
match: SimpleMarkdown.inlineRegex(Util.rWidgetRule),
parse: function parse(capture, _parse4, state) {
return {
id: capture[1],
widgetType: capture[2]
};
},
react: function react(node, output, state) {
// The actual output is handled in the renderer, where
// we know the current widget props/state. This is
// just a stub for testing.
return React.createElement(
"em",
{ key: state.key },
"[Widget: ",
node.id,
"]"
);
}
},
blockMath: {
order: SimpleMarkdown.defaultRules.codeBlock.order + 0.5,
match: blockMathMatch,
parse: function parse(capture, _parse5, state) {
return {
content: capture[1]
};
},
react: function react(node, output, state) {
// The actual output is handled in the renderer, because
// it needs to pass in an `onRender` callback prop. This
// is just a stub for testing.
return React.createElement(
TeX,
{ key: state.key },
node.content
);
}
},
math: {
order: SimpleMarkdown.defaultRules.link.order - 0.25,
match: mathMatch,
parse: function parse(capture, _parse6, state) {
return {
content: capture[1]
};
},
react: function react(node, output, state) {
// The actual output is handled in the renderer, because
// it needs to pass in an `onRender` callback prop. This
// is just a stub for testing.
return React.createElement(
TeX,
{ key: state.key },
node.content
);
}
},
unescapedDollar: {
order: SimpleMarkdown.defaultRules.link.order - 0.24,
match: SimpleMarkdown.inlineRegex(/^(?!\\)\$/),
parse: function parse(capture, _parse7, state) {
return {};
},
react: function react(node, output, state) {
// Unescaped dollar signs render correctly, but result in
// untranslatable text after the i18n python linter flags it
return "$";
}
},
fence: _.extend({}, SimpleMarkdown.defaultRules.fence, {
parse: function parse(capture, _parse8, state) {
var node = SimpleMarkdown.defaultRules.fence.parse(capture, _parse8, state);
// support screenreader-only text with ```alt
if (node.lang === "alt") {
return {
type: "codeBlock",
lang: "alt",
// default codeBlock parsing doesn't parse the contents.
// We need to parse the contents for things like table
// support :).
// The \n\n is because the inside of the codeblock might
// not end in double newlines for block rules, because
// ordinarily we don't parse this :).
content: _parse8(node.content + "\n\n", state)
};
} else {
return node;
}
}
}),
// Extend the SimpleMarkdown link parser to make the link
// zero-rating-friendly if necessary. No changes will be made for
// non-zero-rated requests, but zero-rated requests will be re-pointed at
// either the zero-rated version of khanacademy.org or the external link
// warning interstitial. We also replace the default tag with a custom
// element, if necessary.
link: _.extend({}, SimpleMarkdown.defaultRules.link, {
react: function react(node, output, state) {
var link = SimpleMarkdown.defaultRules.link.react(node, output, state);
var href = link.props.href;
// TODO(charlie): Move this logic out of Perseus and into webapp via
// the component that is now injected as a dependency.
if (typeof KA !== "undefined" && KA.isZeroRated) {
if (href.match(/https?:\/\/[^\/]*khanacademy.org/)) {
href = href.replace("khanacademy.org", "zero.khanacademy.org");
} else {
href = "/zero/external-link?url=" + encodeURIComponent(href);
}
}
var newProps = _extends({}, link.props, { href: href });
if (state.baseElements && state.baseElements.Link) {
return state.baseElements.Link(newProps);
} else {
return React.cloneElement(link, newProps);
}
}
}),
codeBlock: _.extend({}, SimpleMarkdown.defaultRules.codeBlock, {
react: function react(node, output, state) {
// ideally this should be a different rule, with only an
// output function, but right now that breaks the parser.
if (node.lang === "alt") {
return React.createElement(
"div",
{
key: state.key,
className: "perseus-markdown-alt perseus-sr-only"
},
output(node.content, state)
);
} else {
return SimpleMarkdown.defaultRules.codeBlock.react(node, output, state);
}
}
}),
list: _.extend({}, SimpleMarkdown.defaultRules.list, {
match: function match(source, state, prevCapture) {
// Since lists can contain double newlines and we have special
// handling of double newlines while parsing jipt content, just
// disable the list parser.
if (state.isJipt) {
return null;
} else {
return SimpleMarkdown.defaultRules.list.match(source, state, prevCapture);
}
}
}),
// The lint rule never actually matches anything.
// We check for lint after parsing, and, if we find any, we
// transform the tree to add lint nodes. This rule is here
// just for the react() function
lint: {
order: 1000,
match: function match(s) {
return null;
},
parse: function parse(capture, _parse9, state) {
return {};
},
react: function react(node, output, state) {
return React.createElement(
Lint,
{
message: node.message,
ruleName: node.ruleName,
inline: isInline(node.content),
insideTable: node.insideTable,
severity: node.severity
},
output(node.content, state)
);
}
}
});
// Return true if the specified parse tree node represents inline content
// and false otherwise. We need this so that lint nodes can figure out whether
// they should behave as an inline wrapper or a block wrapper
function isInline(node) {
return !!(node && node.type && inlineNodeTypes.hasOwnProperty(node.type));
}
var inlineNodeTypes = {
text: true,
math: true,
unescapedDollar: true,
link: true,
img: true,
strong: true,
u: true,
em: true,
del: true,
code: true
};
var builtParser = SimpleMarkdown.parserFor(rules);
var parse = function parse(source, state) {
var paragraphedSource = source + "\n\n";
return builtParser(paragraphedSource, _.extend({ inline: false }, state));
};
var inlineParser = function inlineParser(source, state) {
return builtParser(source, _.extend({ inline: true }, state));
};
/**
* Traverse all of the nodes in the Perseus Markdown AST. The callback is
* called for each node in the AST.
*/
var traverseContent = function traverseContent(ast, cb) {
if (_.isArray(ast)) {
_.each(ast, function (node) {
return traverseContent(node, cb);
});
} else if (_.isObject(ast)) {
cb(ast);
if (ast.type === "table") {
traverseContent(ast.header, cb);
traverseContent(ast.cells, cb);
} else if (ast.type === "list") {
traverseContent(ast.items, cb);
} else if (ast.type === "titledTable") {
traverseContent(ast.table, cb);
} else if (ast.type === "columns") {
traverseContent(ast.col1, cb);
traverseContent(ast.col2, cb);
} else if (_.isArray(ast.content)) {
traverseContent(ast.content, cb);
}
}
};
/**
* Pull out text content from a Perseus Markdown AST.
* Returns an array of strings.
*/
var getContent = function getContent(ast) {
// Simplify logic by dealing with a single AST node at a time
if (_.isArray(ast)) {
return _.flatten(_.map(ast, getContent));
}
// Base case: This is where we actually extract text content
if (ast.content && _.isString(ast.content)) {
// Collapse whitespace within content unless it is code
if (ast.type.toLowerCase().indexOf("code") !== -1) {
// In case this is the sole child of a paragraph,
// prevent whitespace from being trimmed later
return ["", ast.content, ""];
} else {
return [ast.content.replace(/\s+/g, " ")];
}
}
// Recurse through child AST nodes
// Assumptions made:
// 1) Child AST nodes are either direct properties or inside
// arbitrarily nested lists that are direct properties.
// 2) Only AST nodes have a 'type' property.
var children = _.chain(ast).values().flatten().filter(function (object) {
return object != null && _.has(object, "type");
}).value();
if (!children.length) {
return [];
} else {
var nestedContent = getContent(children);
if (ast.type === "paragraph" && nestedContent.length) {
// Trim whitespace before or after a paragraph
nestedContent[0] = nestedContent[0].replace(/^\s+/, "");
var last = nestedContent.length - 1;
nestedContent[last] = nestedContent[last].replace(/\s+$/, "");
}
return nestedContent;
}
};
/**
* Count the number of characters in Perseus Markdown source.
* Markdown markup and widget references are ignored.
*/
var characterCount = function characterCount(source) {
var ast = parse(source);
var content = getContent(ast).join("");
return content.length;
};
module.exports = {
characterCount: characterCount,
traverseContent: traverseContent,
parse: parse,
parseInline: inlineParser,
reactFor: SimpleMarkdown.reactFor,
ruleOutput: SimpleMarkdown.ruleOutput(rules, "react"),
basicOutput: SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules, "react")),
sanitizeUrl: SimpleMarkdown.sanitizeUrl
};
/***/ },
/* 50 */
/***/ function(module, exports, __webpack_require__) {
module.exports = {"apiVersion":{"major":10,"minor":2},"itemDataVersion":{"major":0,"minor":1}}
/***/ },
/* 51 */,
/* 52 */
/***/ function(module, exports, __webpack_require__) {
Object.defineProperty(exports, "__esModule", {
value: true
});
// Define the shape of the linter context object that is passed through the
// tree with additional information about what we are checking.
var React = __webpack_require__(43);
var linterContextProps = exports.linterContextProps = React.PropTypes.shape({
contentType: React.PropTypes.string,
highlightLint: React.PropTypes.bool,
paths: React.PropTypes.arrayOf(React.PropTypes.string),
stack: React.PropTypes.arrayOf(React.PropTypes.string)
});
var linterContextDefault = exports.linterContextDefault = {
contentType: '',
highlightLint: false,
paths: [],
stack: []
};
/***/ },
/* 53 */,
/* 54 */,
/* 55 */
/***/ function(module, exports, __webpack_require__) {
/**
* Stub Tag Editor.
*
* This is stupidly used by Perseus Zero because I didn't implement
* the for Perseus Zero (since everyone wants me to
* delete it anyways).
*
* This is a small wrapper for a TextListEditor that allows us to
* edit raw Tag ID strings in perseus zero (please don't use this).
*
* It also gives a nicer interface for the group metadata editor
* in local demo mode.
*/
var React = __webpack_require__(43);
var TextListEditor = __webpack_require__(164);
var EMPTY_ARRAY = [];
var StubTagEditor = React.createClass({
displayName: "StubTagEditor",
propTypes: {
value: React.PropTypes.arrayOf(React.PropTypes.string),
onChange: React.PropTypes.func.isRequired,
showTitle: React.PropTypes.bool.isRequired
},
getDefaultProps: function getDefaultProps() {
return {
value: EMPTY_ARRAY,
showTitle: true
};
},
render: function render() {
return React.createElement(
"div",
null,
this.props.showTitle && React.createElement(
"div",
{ style: { fontSize: 14 } },
"Tags:"
),
React.createElement(TextListEditor, {
options: this.props.value || EMPTY_ARRAY,
layout: "vertical",
onChange: this.props.onChange
})
);
}
});
module.exports = StubTagEditor;
/***/ },
/* 56 */
/***/ function(module, exports, __webpack_require__) {
/* This note applies to jquery, react, and underscore.
*
* We're faking a node module for this package by just exporting the global.
* There are a few complications which led us to this solution as a temporary
* fix.
*
* - Browserify can slow down a lot when you include the other packages (and
* their dependency graphs). We were also battling general browserify
* slowness at this time - browserify 3.4.0 is "good" but later versions
* (3.53 if I remember correctly) are terribly slow (on the order of 20x
* slower).
*
* - I'm not clear on the details of packaging this so we don't duplicate
* dependencies anywhere. For instance when packaging perseus for webapp we
* need to be careful not to include packages like underscore from our
* dependencies or from the packages we depend on. (note: this is a very good
* opportunity to either explain how existing tools solve the problem or
* create a new tool to solve it)
*
* - Joel (and Jack)
*/
module.exports = window._;
/***/ },
/* 57 */,
/* 58 */,
/* 59 */,
/* 60 */,
/* 61 */
/***/ function(module, exports, __webpack_require__) {
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var babelPluginFlowReactPropTypes_proptype_ItemObjectNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_ItemObjectNode || __webpack_require__(43).PropTypes.any;
/**
* Utility functions for constructing and manipulating multi-items.
*
* These functions apply *specifically* to Items and ItemTrees - things that
* actually semantically *are* multi-items. For more general functions for
* traversing and manipulating *anything* shaped like a multi-item (like a
* renderer tree or a score tree or, well, a multi-item), see trees.js.
*/
var babelPluginFlowReactPropTypes_proptype_ItemArrayNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_ItemArrayNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_TagsNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_TagsNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_HintNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_HintNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ContentNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_ContentNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ItemTree = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_ItemTree || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Item = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_Item || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Shape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_Shape || __webpack_require__(43).PropTypes.any;
var _require = __webpack_require__(172),
buildMapper = _require.buildMapper;
var shapes = __webpack_require__(64);
/**
* Return a semantically empty ItemTree that conforms to the given shape.
*
* - An empty content node has an empty content string and no widgets/images.
* - An empty hint node has an empty content string and no widgets/images.
* - An empty array node has no elements.
* - An empty object node has a semantically empty node for each of its keys.
* (That is, we recursively call buildEmptyItemTreeForShape for each key.)
*/
function buildEmptyItemTreeForShape(shape) {
if (shape.type === "content") {
return {
"__type": "content",
"content": "",
"images": {},
"widgets": {}
};
} else if (shape.type === "hint") {
return {
"__type": "hint",
"replace": false,
"content": "",
"images": {},
"widgets": {}
};
} else if (shape.type === "tags") {
return [];
} else if (shape.type === "array") {
return [];
} else if (shape.type === "object") {
var valueShapes = shape.shape;
var object = {};
Object.keys(valueShapes).forEach(function (key) {
object[key] = buildEmptyItemTreeForShape(valueShapes[key]);
});
return object;
} else {
throw new Error("unexpected shape type " + shape.type);
}
}
/**
* Return a semantically empty Item that conforms to the given shape.
*
* - An empty content node has an empty content string and no widgets/images.
* - An empty hint node has an empty content string and no widgets/images.
* - An empty array node has no elements.
* - An empty object node has a semantically empty node for each of its keys.
* (That is, we recursively call buildEmptyItemTreeForShape for each key.)
*/
function buildEmptyItemForShape(shape) {
return treeToItem(buildEmptyItemTreeForShape(shape));
}
/**
* Given an Item and its Shape, yield all of its content nodes to the callback.
*/
function findContentNodesInItem(item, shape, callback) {
var itemTree = itemToTree(item);
buildMapper().setContentMapper(callback).mapTree(itemTree, shape);
}
/**
* Given an Item and its Shape, yield all of its hint nodes to the callback.
*/
function findHintNodesInItem(item, shape, callback) {
var itemTree = itemToTree(item);
buildMapper().setHintMapper(callback).mapTree(itemTree, shape);
}
/**
* Given an ItemTree, return a Shape that it conforms to.
*
* The Shape might not be complete or correct Shape that this Item was designed
* for. If you have access to the intended Shape, use that instead.
*/
function inferItemShape(item) {
var itemTree = itemToTree(item);
return inferItemTreeShape(itemTree);
}
function inferItemTreeShape(node) {
if (Array.isArray(node)) {
if (node.length) {
if (typeof node[0] === "string") {
// There's no ItemTree that can manifest as a string.
// So, an array of strings must be a TagsNode, not ArrayNode.
return shapes.tags;
} else {
// Otherwise, assume that this is a valid ArrayNode, and
// therefore the shape of the first element applies to all
// elements in the array.
return shapes.arrayOf(inferItemTreeShape(node[0]));
}
} else {
// The array is empty, so we arbitrarily guess that it's a content
// array. As discussed in the docstring, this might be incorrect,
// and you shouldn't depend on it.
return shapes.arrayOf(shapes.content);
}
} else if (
// TODO(mdr): Remove #LegacyContentNode support.
(typeof node === "undefined" ? "undefined" : _typeof(node)) === "object" && (node.__type === "content" || node.__type === "item")) {
return shapes.content;
} else if ((typeof node === "undefined" ? "undefined" : _typeof(node)) === "object" && node.__type === "hint") {
return shapes.hint;
} else if ((typeof node === "undefined" ? "undefined" : _typeof(node)) === "object") {
var valueShapes = {};
Object.keys(node).forEach(function (key) {
// $FlowFixMe: Not sure why this property deref is an error.
valueShapes[key] = inferItemTreeShape(node[key]);
});
return shapes.shape(valueShapes);
} else {
throw new Error("unexpected multi-item node " + JSON.stringify(node));
}
}
/**
* Convert the given ItemTree to an Item, by wrapping it in the `_multi` key.
*/
function itemToTree(item) {
return item._multi;
}
/**
* Convert the given Item to an ItemTree, by unwrapping the `_multi` key.
*/
function treeToItem(node) {
return { _multi: node };
}
module.exports = {
buildEmptyItemTreeForShape: buildEmptyItemTreeForShape,
buildEmptyItemForShape: buildEmptyItemForShape,
findContentNodesInItem: findContentNodesInItem,
findHintNodesInItem: findHintNodesInItem,
inferItemShape: inferItemShape,
itemToTree: itemToTree,
treeToItem: treeToItem
};
/***/ },
/* 62 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var babelPluginFlowReactPropTypes_proptype_TagsNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_TagsNode || __webpack_require__(43).PropTypes.any;
/**
* Main entry point to the MultiRenderer render portion.
*
* This file exposes the `MultiRenderer` component which performs
* multi-rendering. To multi-render a question, pass in the content of the item
* to the `MultiRenderer` component as a props. Then, pass in a function which
* takes an object of renderers (in the same structure as the content), and
* return a render tree. The `MultiRenderer` component will allow you to
* combine scores, serialized state, etc. without having to manually call on
* each of the functions. It also handles inter-widgets requests between the
* different renderers.
*
* Example:
*
* item = {_multi: {
* left: ,
* right: [, ],
* }}
* shape = shapes.shape({
* left: shapes.content,
* right: shapes.arrayOf(shapes.content),
* })
*
*
* {({renderers}) =>
*
*
{renderers.left}
*
* {renderers.right.map(r =>
{r}
)}
*
*
* }
*
*/
var babelPluginFlowReactPropTypes_proptype_HintNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_HintNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ContentNode = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_ContentNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Item = __webpack_require__(170).babelPluginFlowReactPropTypes_proptype_Item || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ArrayShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_ArrayShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Shape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_Shape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Tree = __webpack_require__(188).babelPluginFlowReactPropTypes_proptype_Tree || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Path = __webpack_require__(172).babelPluginFlowReactPropTypes_proptype_Path || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_HintMapper = __webpack_require__(172).babelPluginFlowReactPropTypes_proptype_HintMapper || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ContentMapper = __webpack_require__(172).babelPluginFlowReactPropTypes_proptype_ContentMapper || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_TreeMapper = __webpack_require__(172).babelPluginFlowReactPropTypes_proptype_TreeMapper || __webpack_require__(43).PropTypes.any;
var _require = __webpack_require__(79),
StyleSheet = _require.StyleSheet,
css = _require.css;
var lens = __webpack_require__(180);
var React = __webpack_require__(43);
var _require2 = __webpack_require__(61),
itemToTree = _require2.itemToTree;
var HintsRenderer = __webpack_require__(36);
var Renderer = __webpack_require__(37);
var _require3 = __webpack_require__(172),
buildMapper = _require3.buildMapper;
var Util = __webpack_require__(17); // TODO(mdr)
// TODO(mdr)
// TODO(mdr)
// TODO(mdr)
// TODO(mdr)
// TODO(mdr)
var MultiRenderer = function (_React$Component) {
_inherits(MultiRenderer, _React$Component);
/* eslint-disable react/sort-comp */
// TODO(mdr): Update the linter to allow property type declarations here.
function MultiRenderer(props) {
_classCallCheck(this, MultiRenderer);
var _this = _possibleConstructorReturn(this, _React$Component.call(this, props));
_this._handleSerializedStateUpdated = function (path, newState) {
var onSerializedStateUpdated = _this.props.onSerializedStateUpdated;
if (onSerializedStateUpdated) {
var oldState = _this._getSerializedState(_this.props.serializedState);
onSerializedStateUpdated(lens(oldState).set(path, newState).freeze());
}
};
_this.rendererDataTreeMapper = buildMapper().setContentMapper(function (c, _, p) {
return _this._makeContentRendererData(c, p);
}).setHintMapper(function (h) {
return _this._makeHintRendererData(h);
}).setTagsMapper(function (t) {
return null;
});
_this.getRenderersMapper = buildMapper().setContentMapper(function (c) {
return c.makeRenderer();
}).setHintMapper(function (h) {
return h.makeRenderer();
}).setArrayMapper(_this._annotateRendererArray.bind(_this));
// Keep state in sync with props.
_this.state = _this._tryMakeRendererState(_this.props);
return _this;
}
/* eslint-enable react/sort-comp */
MultiRenderer.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
// Keep state in sync with props.
if (nextProps.item !== this.props.item) {
this.setState(this._tryMakeRendererState(nextProps));
}
};
/**
* Attempt to build a State that includes a renderer tree corresponding to
* the item provided in props. On error, return a state with `renderError`
* set instead.
*/
MultiRenderer.prototype._tryMakeRendererState = function _tryMakeRendererState(props) {
try {
return {
rendererDataTree: this._makeRendererDataTree(props.item, props.shape),
renderError: null
};
} catch (e) {
// NOTE(mdr): It's appropriate to log an error traceback in a
// caught error condition, and console.error is supported in
// all target browsers. Just do it, linter.
// eslint-disable-next-line no-console
console.error(e);
return {
rendererDataTree: null,
renderError: e
};
}
};
/**
* Props that aren't directly used by the MultiRenderer are delegated to
* the underlying Renderers.
*/
MultiRenderer.prototype._getRendererProps = function _getRendererProps() {
/* eslint-disable no-unused-vars */
// eslint is complaining that `item` and `children` are unused. I'm
// explicitly pulling them out of `this.props` so I don't pass them to
// ``. I'm not sure how else to do this.
var _props = this.props,
item = _props.item,
children = _props.children,
shape = _props.shape,
otherProps = _objectWithoutProperties(_props, ["item", "children", "shape"]);
/* eslint-enable no-unused-vars */
return otherProps;
};
/**
* Construct a Renderer and a ref placeholder for the given ContentNode.
*/
MultiRenderer.prototype._makeContentRendererData = function _makeContentRendererData(content, path) {
var _this2 = this;
// NOTE(emily): The `findExternalWidgets` function here is computed
// inline and thus changes each time we run this function. If it
// were to change every render, it would cause the Renderer to
// re-render a lot more than is necessary. Don't re-compute this
// element unless it is necessary!
// HACK(mdr): Flow can't prove that this is a ContentRendererData,
// because of how we awkwardly construct it in order to obtain a
var data = { ref: null, makeRenderer: null };
var refFunc = function refFunc(e) {
return data.ref = e;
};
var findExternalWidgets = function findExternalWidgets(criterion) {
return _this2._findWidgets(data, criterion);
};
var handleSerializedState = function handleSerializedState(state) {
return _this2._handleSerializedStateUpdated(path, state);
};
data.makeRenderer = function () {
return React.createElement(Renderer, _extends({}, _this2._getRendererProps(), content, {
ref: refFunc,
findExternalWidgets: findExternalWidgets,
serializedState: _this2.props.serializedState ? lens(_this2.props.serializedState).get(path) : null,
onSerializedStateUpdated: handleSerializedState
}));
};
return data;
};
/**
* Construct a Renderer for the given HintNode, and keep track of the hint
* itself for future use, too.
*/
MultiRenderer.prototype._makeHintRendererData = function _makeHintRendererData(hint) {
var _this3 = this;
// TODO(mdr): Once HintsRenderer supports inter-widget communication,
// give it a ref. Until then, leave the ref null forever, to avoid
// confusing the findWidgets functions.
//
// NOTE(davidflanagan): As a partial step toward inter-widget
// communication we're going to pass a findExternalWidgets function
// (using a dummy data object). This allows passage-ref widgets in
// hints to use findWidget() to find the passage widgets they reference.
// Note that this is one-way only, however. It does not allow
// widgets in the question to find widgets in the hints, for example.
var findExternalWidgets = function findExternalWidgets(criterion) {
return _this3._findWidgets({}, criterion);
};
return {
hint: hint,
findExternalWidgets: findExternalWidgets, // _annotateRendererArray() needs this
ref: null,
makeRenderer: function makeRenderer() {
return React.createElement(HintsRenderer, _extends({}, _this3._getRendererProps(), {
findExternalWidgets: findExternalWidgets,
hints: [hint]
}));
}
};
};
/**
* Construct a tree of interconnected RendererDatas, corresponding to the
* given item. Called in `_tryMakeRendererState`, in order to store this
* tree in the component state.
*/
MultiRenderer.prototype._makeRendererDataTree = function _makeRendererDataTree(item, shape) {
var itemTree = itemToTree(item);
return this.rendererDataTreeMapper.mapTree(itemTree, shape);
};
/**
* Return all widgets that meet the given criterion, from all Renderers
* except the Renderer that triggered this call.
*
* This function is provided to each Renderer's `findExternalWidgets` prop,
* which enables widgets in different Renderers to discover each other and
* communicate.
*/
MultiRenderer.prototype._findWidgets = function _findWidgets(callingData, filterCriterion) {
var results = [];
this._mapRenderers(function (data) {
if (callingData !== data && data.ref) {
results.push.apply(results, data.ref.findInternalWidgets(filterCriterion));
}
});
return results;
};
/**
* Copy the renderer tree, apply the given transformation to the leaf nodes
* and the optional given transformation to the array nodes, and return the
* result.
*
* Used to provide structured data to the call site (the Renderer tree on
* `render`, the Score tree on `getScores`, etc.), and to traverse the
* renderer tree even when we disregard the output (like in
* `_findWidgets`).
*/
MultiRenderer.prototype._mapRenderers = function _mapRenderers(leafMapper) {
var rendererDataTree = this.state.rendererDataTree;
if (!rendererDataTree) {
return null;
}
var mapper = buildMapper().setContentMapper(leafMapper).setHintMapper(leafMapper);
return mapper.mapTree(rendererDataTree, this.props.shape);
};
MultiRenderer.prototype._scoreFromRef = function _scoreFromRef(ref) {
if (!ref) {
return null;
}
var _ref$guessAndScore = ref.guessAndScore(),
guess = _ref$guessAndScore[0],
score = _ref$guessAndScore[1];
var state = void 0;
if (ref.getSerializedState) {
state = ref.getSerializedState();
}
return Util.keScoreFromPerseusScore(score, guess, state);
};
/**
* Return a tree in the shape of the multi-item, with scores at each of
* the content nodes and `null` at the other leaf nodes.
*/
MultiRenderer.prototype.getScores = function getScores() {
var _this4 = this;
return this._mapRenderers(function (data) {
return _this4._scoreFromRef(data.ref);
});
};
/**
* Return a single composite score for all rendered content nodes.
* The `guess` is a tree in the shape of the multi-item, with an individual
* guess at each content node and `null` at the other leaf nodes.
*/
MultiRenderer.prototype.score = function score() {
var scores = [];
var state = [];
var guess = this._mapRenderers(function (data) {
if (!data.ref) {
return null;
}
if (data.ref.getSerializedState) {
state.push(data.ref.getSerializedState());
}
scores.push(data.ref.score());
return data.ref.getUserInput();
});
var combinedScore = scores.reduce(Util.combineScores);
return Util.keScoreFromPerseusScore(combinedScore, guess, state);
};
/**
* Return a tree in the shape of the multi-item, with serialized state at
* each of the content nodes and `null` at the other leaf nodes.
*
* If the lastSerializedState argument is supplied, this function will fill
* in the state of not-currently-rendered content and hint nodes with the
* values from the previous serialized state. If no lastSerializedState is
* supplied, `null` will be returned for not-currently-rendered content and
* hint nodes.
*/
MultiRenderer.prototype._getSerializedState = function _getSerializedState(lastSerializedState) {
return this._mapRenderers(function (data, _, path) {
if (data.ref) {
return data.ref.getSerializedState();
} else if (lastSerializedState) {
return lens(lastSerializedState).get(path);
} else {
return null;
}
});
};
/**
* Given a tree in the shape of the multi-item, with serialized state at
* each of the content nodes, restore each state to the corresponding
* renderer if currently mounted.
*/
MultiRenderer.prototype.restoreSerializedState = function restoreSerializedState(serializedState, callback) {
// We want to call our async callback only once all of the childrens'
// callbacks have run. We add one to this counter before we call out to
// each renderer and decrement it when it runs our callback.
var numCallbacks = 0;
var countCallback = function countCallback() {
numCallbacks--;
if (callback && numCallbacks === 0) {
callback();
}
};
this._mapRenderers(function (data, _, path) {
if (!data.ref) {
return;
}
var state = lens(serializedState).get(path);
if (!state) {
return;
}
numCallbacks++;
data.ref.restoreSerializedState(state, countCallback);
});
};
/**
* Given an array of renderers, if it happens to be an array of *hint*
* renderers, then attach a `firstN` method to the array, which allows the
* layout to render the hints together in one HintsRenderer.
*/
MultiRenderer.prototype._annotateRendererArray = function _annotateRendererArray(renderers, rendererDatas, shape) {
var _this5 = this;
if (shape.elementShape.type === "hint") {
// The shape says that these are HintRendererDatas, even though
var hintRendererDatas = rendererDatas;
renderers = [].concat(renderers);
renderers.firstN = function (n) {
return React.createElement(HintsRenderer, _extends({}, _this5._getRendererProps(), {
findExternalWidgets: hintRendererDatas[0] ? hintRendererDatas[0].findExternalWidgets : undefined,
hints: hintRendererDatas.map(function (d) {
return d.hint;
}),
hintsVisible: n
}));
};
}
return renderers;
};
/**
* Return a tree in the shape of the multi-item, with a Renderer at each
* content node and a HintRenderer at each hint node.
*
* This is generated by running each of the `makeRenderer` functions at the
* leaf nodes.
*/
MultiRenderer.prototype._getRenderers = function _getRenderers() {
return this.getRenderersMapper.mapTree(this.state.rendererDataTree, this.props.shape);
};
MultiRenderer.prototype.render = function render() {
if (this.state.renderError) {
return React.createElement(
"div",
{ className: css(styles.error) },
"Error rendering: ",
String(this.state.renderError)
);
}
// Pass the renderer tree to the `children` function, which will
// determine the actual content of this component.
return this.props.children({
renderers: this._getRenderers()
});
};
return MultiRenderer;
}(React.Component);
MultiRenderer.propTypes = {
item: babelPluginFlowReactPropTypes_proptype_Item,
shape: babelPluginFlowReactPropTypes_proptype_Shape,
children: __webpack_require__(43).PropTypes.func.isRequired,
serializedState: babelPluginFlowReactPropTypes_proptype_Tree,
onSerializedStateUpdated: __webpack_require__(43).PropTypes.func
};
var styles = StyleSheet.create({
error: {
color: "red"
}
});
module.exports = MultiRenderer;
/***/ },
/* 63 */
/***/ function(module, exports, __webpack_require__) {
/**
* Utility functions to build React PropTypes for multi-items and shapes.
*
* If you're writing new components, though, consider using the Item and Shape
* Flow types instead.
*/
var React = __webpack_require__(43);
/**
* A recursive PropType that accepts Shape objects, and rejects other objects.
*
* Usage: `propTypes: {shape: shapePropType}`.
*/
var babelPluginFlowReactPropTypes_proptype_Shape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_Shape || __webpack_require__(43).PropTypes.any;
function shapePropType() {
var itemShape = React.PropTypes.oneOfType([React.PropTypes.shape({
type: React.PropTypes.oneOf(["content"]).isRequired
}).isRequired, React.PropTypes.shape({
type: React.PropTypes.oneOf(["hint"]).isRequired
}).isRequired, React.PropTypes.shape({
type: React.PropTypes.oneOf(["tags"]).isRequired
}).isRequired, React.PropTypes.shape({
type: React.PropTypes.oneOf(["object"]).isRequired,
shape: React.PropTypes.objectOf(shapePropType)
}).isRequired, React.PropTypes.shape({
type: React.PropTypes.oneOf(["array"]).isRequired,
elementShape: shapePropType
}).isRequired]);
return itemShape.apply(undefined, arguments);
}
/**
* Return a PropType that accepts Items of the given shape, and rejects other
* objects.
*
* Usage: `propTypes: {item: buildPropTypeForShape(myShape)}`
*/
function buildPropTypeForShape(shape) {
return React.PropTypes.oneOfType([React.PropTypes.shape({
_multi: buildTreePropTypeForShape(shape)
}), React.PropTypes.oneOf([null, undefined])]);
}
/**
* Return a PropType that accepts ItemTrees of the given shape, and rejects
* other objects.
*/
function buildTreePropTypeForShape(shape) {
if (shape.type === "content") {
return React.PropTypes.shape({
// TODO(mdr): Remove #LegacyContentNode support.
__type: React.PropTypes.oneOf(["content", "item"]).isRequired,
content: React.PropTypes.string,
images: React.PropTypes.objectOf(React.PropTypes.any),
widgets: React.PropTypes.objectOf(React.PropTypes.any)
});
} else if (shape.type === "hint") {
return React.PropTypes.shape({
__type: React.PropTypes.oneOf(["hint"]).isRequired,
content: React.PropTypes.string,
images: React.PropTypes.objectOf(React.PropTypes.any),
widgets: React.PropTypes.objectOf(React.PropTypes.any),
replace: React.PropTypes.bool
});
} else if (shape.type === "tags") {
return React.PropTypes.arrayOf(React.PropTypes.string.isRequired);
} else if (shape.type === "array") {
var elementPropType = buildTreePropTypeForShape(shape.elementShape);
return React.PropTypes.arrayOf(elementPropType.isRequired);
} else if (shape.type === "object") {
var valueShapes = shape.shape;
var propTypeShape = {};
Object.keys(valueShapes).forEach(function (key) {
propTypeShape[key] = buildTreePropTypeForShape(valueShapes[key]).isRequired;
});
return React.PropTypes.shape(propTypeShape);
} else {
throw new Error("unexpected shape type " + shape.type);
}
}
module.exports = {
shapePropType: shapePropType,
buildPropTypeForShape: buildPropTypeForShape
};
/***/ },
/* 64 */
/***/ function(module, exports, __webpack_require__) {
/**
* These tools allow you to construct arbirtary shapes, by combining simple
* leaf shapes like `content` and `hint` into composite shapes like
* `arrayOf(shape({question: content, hints: arrayOf(hint)}))`.
*/
var babelPluginFlowReactPropTypes_proptype_ObjectShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_ObjectShape || __webpack_require__(43).PropTypes.any;
/**
* Utility functions for constructing and inferring multi-item shapes.
*
* A shape is an object that serves as a runtime type declaration: it specifies
* a tree structure for a particular class of multi-item. See shape-types.js
* for further discussion.
*
* This module allows you to construct arbitrary Shape trees, by combining
* leaf node shapes like `content` and `hint` into composite shapes like
* `arrayOf(shape({foo: content, bar: hint}))`.
*/
var babelPluginFlowReactPropTypes_proptype_ArrayShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_ArrayShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_TagsShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_TagsShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_HintShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_HintShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ContentShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_ContentShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Shape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_Shape || __webpack_require__(43).PropTypes.any;
var contentShape = {
type: "content"
};
var hintShape = {
type: "hint"
};
var tagsShape = {
type: "tags"
};
var buildArrayShape = function buildArrayShape(elementShape) {
return {
type: "array",
elementShape: elementShape
};
};
var buildObjectShape = function buildObjectShape(shape) {
return {
type: "object",
shape: shape
};
};
var hintsShape = buildArrayShape(hintShape);
module.exports = {
content: contentShape,
hint: hintShape,
hints: hintsShape,
tags: tagsShape,
arrayOf: buildArrayShape,
shape: buildObjectShape
};
/***/ },
/* 65 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable object-curly-spacing */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
/**
* A mixin that renders a custom software keypad in additional to the base
* component. The base component will receive blur events when the keypad is
* dismissed and can access the keypad element itself so as to manage its
* activation and dismissal.
*
* TODO(charlie): This would make a nicer higher-order component, except that
* we need to expose methods on the base component (i.e., `ItemRenderer`). When
* `ItemRenderer` and friends are written as ES6 Classes, we can have them
* extend a `ProvideKeypad` component instead of using this mixin.
*/
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var Keypad = __webpack_require__(257).components.Keypad;
var ProvideKeypad = {
propTypes: {
apiOptions: React.PropTypes.shape({
customKeypad: React.PropTypes.bool
}),
// An Aphrodite style object, to be applied to the keypad container.
// Note that, given our awkward structure of injecting the keypad, this
// style won't be applied or updated dynamically. Rather, it will only
// be applied in `componentDidMount`.
keypadStyle: React.PropTypes.any
},
getInitialState: function getInitialState() {
return {
keypadElement: null
};
},
componentDidMount: function componentDidMount() {
var _this = this;
if (this.props.apiOptions && this.props.apiOptions.customKeypad) {
// TODO(charlie): Render this and the wrapped component in the same
// React tree. We may also want to add this keypad asynchronously or
// on-demand in the future.
this._keypadContainer = document.createElement('div');
document.body.appendChild(this._keypadContainer);
ReactDOM.render(React.createElement(Keypad, {
onElementMounted: function onElementMounted(element) {
_this.setState({
keypadElement: element
});
},
onDismiss: function onDismiss() {
_this.blur && _this.blur();
},
style: this.props.keypadStyle
}), this._keypadContainer);
}
},
componentWillUnmount: function componentWillUnmount() {
if (this._keypadContainer) {
ReactDOM.unmountComponentAtNode(this._keypadContainer);
if (this._keypadContainer.parentNode) {
// Note ChildNode.remove() isn't available in older Android
// webviews.
this._keypadContainer.parentNode.removeChild(this._keypadContainer);
}
this._keypadContainer = null;
}
},
keypadElement: function keypadElement() {
return this.state.keypadElement;
}
};
module.exports = ProvideKeypad;
/***/ },
/* 66 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/**
* A mixin that accepts the `apiOptions` prop, and populates any missing values
* with defaults.
*/
var React = __webpack_require__(43);
var ApiOptions = __webpack_require__(12).Options;
var ApiOptionsProps = {
propTypes: {
// TODO(mdr): Should this actually be objectOf(any)?
apiOptions: React.PropTypes.any
},
getDefaultProps: function getDefaultProps() {
return { apiOptions: {} };
},
getApiOptions: function getApiOptions() {
return _extends({}, ApiOptions.defaults, this.props.apiOptions);
}
};
module.exports = ApiOptionsProps;
/***/ },
/* 67 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* globals KA */
var classNames = __webpack_require__(86);
var React = __webpack_require__(43);
var $ = __webpack_require__(169);
var _ = __webpack_require__(56);
var FixedToResponsive = __webpack_require__(189);
var Graphie = __webpack_require__(190);
var ImageLoader = __webpack_require__(191);
var _require = __webpack_require__(29),
maybeUnescape = _require.maybeUnescape;
var Util = __webpack_require__(17);
var Zoom = __webpack_require__(192);
// Minimum image width to make an image appear as zoomable.
var ZOOMABLE_THRESHOLD = 700;
// The global cache of label data. Its format is:
// {
// hash (e.g. "c21435944d2cf0c8f39d9059cb35836aa701d04a"): {
// loaded: a boolean of whether the data has been loaded or not
// dataCallbacks: a list of callbacks to call with the data when the data
// is loaded
// data: the other data for this hash
// },
// ...
// }
var labelDataCache = {};
// Write our own JSONP handler because all the other ones don't do things we
// need.
var doJSONP = function doJSONP(url, options) {
options = _extends({
callbackName: "callback",
success: $.noop,
error: $.noop
}, options);
// Create the script
var script = document.createElement("script");
script.setAttribute("async", "");
script.setAttribute("src", url);
// A cleanup function to run when we're done.
function cleanup() {
document.head.removeChild(script);
delete window[options.callbackName];
}
// Add the global callback.
window[options.callbackName] = function () {
cleanup();
options.success.apply(null, arguments);
};
// Add the error handler.
script.addEventListener("error", function () {
cleanup();
options.error.apply(null, arguments);
});
// Insert the script to start the download.
document.head.appendChild(script);
};
var svgLabelsRegex = /^web\+graphie\:/;
var hashRegex = /\/([^/]+)$/;
function isLabeledSVG(url) {
return svgLabelsRegex.test(url);
}
function isImageProbablyPhotograph(imageUrl) {
// TODO(david): Do an inventory to refine this heuristic. For example, what
// % of .png images are illustrations?
return (/\.(jpg|jpeg)$/i.test(imageUrl)
);
}
// For each svg+labels, there are two urls we need to download from. This gets
// the base url without the suffix, and `getSvgUrl` and `getDataUrl` apply
// appropriate suffixes to get the image and other data
function getBaseUrl(url) {
// Force HTTPS connection unless we're on HTTP, so that IE works.
var protocol = window.location.protocol === "http:" ? "http:" : "https:";
return url.replace(svgLabelsRegex, protocol);
}
function getSvgUrl(url) {
return getBaseUrl(url) + ".svg";
}
function getDataUrl(url) {
return getBaseUrl(url) + "-data.json";
}
function shouldUseLocalizedData() {
// TODO(emily): Remove this depenency on `KA` and pass it down with
// Perseus' initialization. (Also used in renderer.jsx)
return typeof KA !== "undefined" && KA.language !== "en";
}
function shouldRenderJipt() {
return typeof KA !== "undefined" && KA.language === "en-pt";
}
var jiptLabels = [];
if (shouldRenderJipt()) {
if (!KA.jipt_dom_insert_checks) {
KA.jipt_dom_insert_checks = [];
}
KA.jipt_dom_insert_checks.push(function (text, node, attribute) {
var index = $(node).data("jipt-label-index");
if (node && typeof index !== "undefined") {
var _jiptLabels$index = jiptLabels[index],
label = _jiptLabels$index.label,
useMath = _jiptLabels$index.useMath;
label.text("");
text = maybeUnescape(text);
if (useMath) {
var mathRegex = /^\$(.*)\$$/;
var match = text.match(mathRegex);
var mathText = match ? match[1] : "\\color{red}{\\text{Invalid Math}}";
label.processMath(mathText, true);
} else {
label.processText(text);
}
return false;
}
return text;
});
}
// A regex to split at the last / of a URL, separating the base part from the
// hash. This is used to create the localized label data URLs.
var splitHashRegex = /\/(?=[^/]+$)/;
function getLocalizedDataUrl(url) {
if (typeof KA !== "undefined") {
// Parse out the hash and base so that we can insert the locale
var _getBaseUrl$split = getBaseUrl(url).split(splitHashRegex),
base = _getBaseUrl$split[0],
hash = _getBaseUrl$split[1];
return base + "/" + KA.language + "/" + hash + "-data.json";
} else {
return getDataUrl(url);
}
}
// Get the hash from the url, which is just the filename
function getUrlHash(url) {
var match = url.match(hashRegex);
return match && match[1];
}
function defaultPreloader() {
return React.DOM.span({
style: {
background: "url(" + PERSEUS_PREFIX + "stylesheets/www.khanacademy.org/images/spinner.gif) no-repeat",
backgroundPosition: "center",
width: "100%",
height: "100%",
position: "absolute",
minWidth: "20px"
}
});
}
var SvgImage = React.createClass({
displayName: "SvgImage",
propTypes: {
allowFullBleed: React.PropTypes.bool,
alt: React.PropTypes.string,
constrainHeight: React.PropTypes.bool,
extraGraphie: React.PropTypes.shape({
box: React.PropTypes.array.isRequired,
range: React.PropTypes.array.isRequired,
labels: React.PropTypes.array.isRequired
}),
height: React.PropTypes.number,
// When the DOM updates to replace the preloader with the image, or
// vice-versa, we trigger this callback.
onUpdate: React.PropTypes.func,
preloader: React.PropTypes.func,
// By default, this component attempts to be responsive whenever
// possible (specifically, when width and height are passed in).
// You can expliclty force unresponsive behavior by *either*
// not passing in width/height *or* setting this prop to false.
// The difference is that forcing via this prop will result in
// explicit width and height styles being set on the rendered
// component.
responsive: React.PropTypes.bool,
scale: React.PropTypes.number,
src: React.PropTypes.string.isRequired,
title: React.PropTypes.string,
trackInteraction: React.PropTypes.func,
width: React.PropTypes.number,
// Whether clicking this image will allow it to be fully zoomed in to
// its original size on click, and allow the user to scroll in that
// state. This also does some hacky viewport meta tag changing to
// ensure this works on mobile devices, so I (david@) don't recommend
// enabling this on desktop yet.
zoomToFullSizeOnMobile: React.PropTypes.bool
},
statics: {
// Sometimes other components want to download the actual image e.g. to
// determine its size. Here, we transform an .svg-labels url into the
// correct image url, and leave normal image urls alone
getRealImageUrl: function getRealImageUrl(url) {
if (isLabeledSVG(url)) {
return getSvgUrl(url);
} else {
return url;
}
}
},
getDefaultProps: function getDefaultProps() {
return {
constrainHeight: false,
onUpdate: function onUpdate() {},
responsive: true,
src: "",
scale: 1,
zoomToFullSizeOnMobile: false
};
},
getInitialState: function getInitialState() {
return {
imageLoaded: false,
imageDimensions: null,
dataLoaded: false,
labelDataIsLocalized: false,
labels: [],
range: [[0, 0], [0, 0]]
};
},
componentDidMount: function componentDidMount() {
if (isLabeledSVG(this.props.src)) {
this.loadResources();
}
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
if (this.props.src !== nextProps.src) {
this.setState({
imageLoaded: false,
dataLoaded: false
});
}
},
shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
// If the props changed, we definitely need to update
if (!_.isEqual(this.props, nextProps)) {
return true;
}
if (!isLabeledSVG(nextProps.src)) {
return false;
}
var wasLoaded = this.isLoadedInState(this.state);
var nextLoaded = this.isLoadedInState(nextState);
return wasLoaded !== nextLoaded;
},
componentDidUpdate: function componentDidUpdate() {
if (isLabeledSVG(this.props.src) && !this.isLoadedInState(this.state)) {
this.loadResources();
}
},
// Check if all of the resources are loaded in a given state
isLoadedInState: function isLoadedInState(state) {
return state.imageLoaded && state.dataLoaded;
},
loadResources: function loadResources() {
var _this = this;
var hash = getUrlHash(this.props.src);
// We can't make multiple jsonp calls to the same file because their
// callbacks will collide with each other. Instead, we cache the data
// and only make the jsonp calls once.
if (labelDataCache[hash]) {
if (labelDataCache[hash].loaded) {
var _labelDataCache$hash = labelDataCache[hash],
data = _labelDataCache$hash.data,
localized = _labelDataCache$hash.localized;
this.onDataLoaded(data, localized);
} else {
labelDataCache[hash].dataCallbacks.push(this.onDataLoaded);
}
} else {
var cacheData = {
loaded: false,
dataCallbacks: [this.onDataLoaded],
data: null,
localized: shouldUseLocalizedData()
};
labelDataCache[hash] = cacheData;
var retrieveData = function retrieveData(url, errorCallback) {
doJSONP(url, {
callbackName: "svgData" + hash,
success: function success(data) {
cacheData.data = data;
cacheData.loaded = true;
_.each(cacheData.dataCallbacks, function (callback) {
callback(cacheData.data, cacheData.localized);
});
},
error: errorCallback
});
};
if (shouldUseLocalizedData()) {
retrieveData(getLocalizedDataUrl(this.props.src), function (x, status, error) {
cacheData.localized = false;
// If there is isn't any localized data, fall back to
// the original, unlocalized data
retrieveData(getDataUrl(_this.props.src), function (x, status, error) {
// eslint-disable-next-line no-console
console.error("Data load failed:", getDataUrl(_this.props.src), error);
});
});
} else {
retrieveData(getDataUrl(this.props.src), function (x, status, error) {
// eslint-disable-next-line no-console
console.error("Data load failed:", getDataUrl(_this.props.src), error);
});
}
}
},
onDataLoaded: function onDataLoaded(data, localized) {
if (this.isMounted() && data.labels && data.range) {
this.setState({
dataLoaded: true,
labelDataIsLocalized: localized,
labels: data.labels,
range: data.range
});
}
},
sizeProvided: function sizeProvided() {
return this.props.width != null && this.props.height != null;
},
onImageLoad: function onImageLoad() {
var _this2 = this;
// Only need to do this if rendering a Graphie
if (this.sizeProvided()) {
// If width and height are provided, we don't need to calculate the
// size ourselves
this.setState({
imageLoaded: true
});
} else {
Util.getImageSize(this.props.src, function (width, height) {
if (_this2.isMounted()) {
_this2.setState({
imageLoaded: true,
imageDimensions: [width, height]
});
}
});
}
},
setupGraphie: function setupGraphie(graphie, options) {
var _this3 = this;
_.map(options.labels, function (labelData) {
if (shouldRenderJipt() && _this3.state.labelDataIsLocalized) {
// If we're using JIPT translation and we got proper JIPT tags,
// render the labels as plain text (so JIPT can find them) and
// add some extra properties to the element so we can properly
// re-render the label once it is replaced with translated
// text.
var elem = graphie.label(labelData.coordinates, labelData.content, labelData.alignment, false);
$(elem).data("jipt-label-index", jiptLabels.length);
jiptLabels.push({
label: elem,
useMath: labelData.typesetAsMath
});
} else if (labelData.coordinates) {
// Create labels from the data
// TODO(charlie): Some erroneous labels are being sent down
// without coordinates. They don't seem to have any content, so
// it seems fine to just ignore them (rather than error), but
// we should figure out why this is happening.
var label = graphie.label(labelData.coordinates, labelData.content, labelData.alignment, labelData.typesetAsMath, { "font-size": 100 * _this3.props.scale + "%" });
// Convert absolute positioning css from pixels to percentages
// TODO(alex): Dynamically resize font-size as well. This
// almost certainly means listening to throttled window.resize
// events.
var labelStyle = label[0].style;
var labelTop = _this3._tryGetPixels(labelStyle.top);
var labelLeft = _this3._tryGetPixels(labelStyle.left);
if (labelTop === null || labelLeft === null) {
// Graphie labels are supposed to have an explicit position,
// but to be on the safe side, let's fall back to using
// jQuery's position(). The reason we're not always using
// this is that in the presence of CSS transforms, it will
// give the rendered position, which may be scaled and
// not equal to the explicitly specified one.
var labelPosition = label.position();
labelTop = labelPosition.top;
labelLeft = labelPosition.left;
}
var svgHeight = _this3.props.height * _this3.props.scale;
var svgWidth = _this3.props.width * _this3.props.scale;
label.css({
top: labelTop / svgHeight * 100 + "%",
left: labelLeft / svgWidth * 100 + "%"
});
// Add back the styles to each of the labels
_.each(labelData.style, function (styleValue, styleName) {
label.css(styleName, styleValue);
});
}
});
},
// Try to parse a CSS value as pixels. Returns null if the parameter string
// does not contain a number followed by "px".
_tryGetPixels: function _tryGetPixels(value) {
value = value || "";
// While this doesn't check that there are no other alphabetical
// characters prior to "px", that should be taken care of by the DOM,
// which won't accept invalid units.
if (!value.endsWith("px")) {
return null;
}
// parseFloat() ignores trailing non-numerical characters.
return parseFloat(value) || null;
},
_handleZoomClick: function _handleZoomClick(e) {
var $image = $(e.target);
// It's possible that the image is already displayed at its
// full size, but we don't really know that until we get a chance
// to measure it (just now, after the user clicks). We only zoom
// if there's more image to be shown.
//
// TODO(kevindangoor) If the window is narrow and the image is
// already displayed as wide as possible, we may want to do
// nothing in that case as well. Figuring this out correctly
// likely required accounting for the image alignment and margins.
if ($image.width() < this.props.width || this.props.zoomToFullSizeOnMobile) {
Zoom.ZoomService.handleZoomClick(e, this.props.zoomToFullSizeOnMobile);
}
this.props.trackInteraction && this.props.trackInteraction();
},
render: function render() {
var imageSrc = this.props.src;
// Props to send to all images
var imageProps = {
alt: this.props.alt,
title: this.props.title
};
var width = this.props.width && this.props.width * this.props.scale;
var height = this.props.height && this.props.height * this.props.scale;
var dimensions = {
width: width,
height: height
};
// To make an image responsive, we need to know what its width and
// height are in advance (before inserting it into the DOM) so that we
// can ensure it doesn't grow past those limits. We don't always have
// this information, especially in places where is used
// to render inline Markdown images within a widget. See Radio, Sorter,
// Matcher, etc.
// TODO(alex): Make all of those image rendering locations aware of
// width+height so that they too can render responsively.
var responsive = this.props.responsive && !!(width && height);
// An additional may be inserted after the image/graphie
// pair. Only used by the image widget, for its legacy labels support.
// Note that since the image widget always provides width and height
// data, extraGraphie can be ignored for unresponsive images.
// TODO(alex): Convert all existing uses of that to web+graphie. This
// is tricky because web+graphie doesn't support labels on non-graphie
// images.
var extraGraphie = void 0;
if (this.props.extraGraphie && this.props.extraGraphie.labels.length) {
extraGraphie = React.createElement(Graphie, {
box: this.props.extraGraphie.box,
range: this.props.extraGraphie.range,
options: { labels: this.props.extraGraphie.labels },
responsive: true,
addMouseLayer: false,
setup: this.setupGraphie
});
}
// If preloader is undefined, we use the default. If it's
// null, there will be no preloader in use.
var preloaderBaseFunc = this.props.preloader === undefined ? defaultPreloader : this.props.preloader;
var preloader = preloaderBaseFunc ? function () {
return preloaderBaseFunc(dimensions);
} : null;
// Just use a normal image if a normal image is provided
if (!isLabeledSVG(imageSrc)) {
if (responsive) {
var wrapperClasses = classNames({
zoomable: width > ZOOMABLE_THRESHOLD,
"svg-image": true
});
imageProps.onClick = this._handleZoomClick;
return React.createElement(
FixedToResponsive,
{
className: wrapperClasses,
width: width,
height: height,
constrainHeight: this.props.constrainHeight,
allowFullBleed: this.props.allowFullBleed && isImageProbablyPhotograph(imageSrc)
},
React.createElement(ImageLoader, {
src: imageSrc,
imgProps: imageProps,
preloader: preloader,
onUpdate: this.props.onUpdate
}),
extraGraphie
);
} else {
imageProps.style = dimensions;
return React.createElement(ImageLoader, {
src: imageSrc,
preloader: preloader,
imgProps: imageProps,
onUpdate: this.props.onUpdate
});
}
}
var imageUrl = getSvgUrl(imageSrc);
var graphie = void 0;
// Since we only want to do the graphie setup once, we only render the
// graphie once everything is loaded
if (this.isLoadedInState(this.state)) {
// Use the provided width and height to size the graphie if
// possible, otherwise use our own calculated size
var box = void 0;
if (this.sizeProvided()) {
box = [width, height];
} else {
box = [this.state.imageDimensions[0] * this.props.scale, this.state.imageDimensions[1] * this.props.scale];
}
var scale = [40 * this.props.scale, 40 * this.props.scale];
graphie = React.createElement(Graphie, {
ref: "graphie",
box: box,
scale: scale,
range: this.state.range,
options: _.pick(this.state, "labels"),
responsive: responsive,
addMouseLayer: false,
setup: this.setupGraphie
});
}
if (responsive) {
return React.createElement(
FixedToResponsive,
{
className: "svg-image",
width: width,
height: height,
constrainHeight: this.props.constrainHeight
},
React.createElement(ImageLoader, {
src: imageUrl,
onLoad: this.onImageLoad,
onUpdate: this.props.onUpdate,
preloader: preloader,
imgProps: imageProps
}),
graphie,
extraGraphie
);
} else {
imageProps.style = dimensions;
return React.createElement(
"div",
{ className: "unresponsive-svg-image", style: dimensions },
React.createElement(ImageLoader, {
src: imageUrl,
onLoad: this.onImageLoad,
onUpdate: this.props.onUpdate,
preloader: preloader,
imgProps: imageProps
}),
graphie
);
}
}
});
module.exports = SvgImage;
/***/ },
/* 68 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable no-var, object-curly-spacing */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var _2 = __webpack_require__(56);
var shuffle = __webpack_require__(17).shuffle;
var Radio = __webpack_require__(173);
var _choiceTransform = function _choiceTransform(editorProps, problemNum) {
var _maybeRandomize = function _maybeRandomize(array) {
return editorProps.randomize ? shuffle(array, problemNum) : array;
};
var _addNoneOfAbove = function _addNoneOfAbove(choices) {
var noneOfTheAbove = null;
var newChoices = _2.reject(choices, function (choice, index) {
if (choice.isNoneOfTheAbove) {
noneOfTheAbove = choice;
return true;
}
});
// Place the "None of the above" options last
if (noneOfTheAbove) {
newChoices.push(noneOfTheAbove);
}
return newChoices;
};
// Add meta-information to choices
var choices = editorProps.choices.slice();
choices = _2.map(choices, function (choice, i) {
return _2.extend({}, choice, {
originalIndex: i,
correct: Boolean(choice.correct)
});
});
// Randomize and add 'None of the above'
return _addNoneOfAbove(_maybeRandomize(choices));
};
var transform = function transform(editorProps, problemNum) {
var choices = _choiceTransform(editorProps, problemNum);
var numCorrect = _2.reduce(editorProps.choices, function (memo, choice) {
return choice.correct ? memo + 1 : memo;
}, 0);
var hasNoneOfTheAbove = editorProps.hasNoneOfTheAbove,
multipleSelect = editorProps.multipleSelect,
countChoices = editorProps.countChoices,
correctAnswer = editorProps.correctAnswer,
deselectEnabled = editorProps.deselectEnabled;
return {
numCorrect: numCorrect,
hasNoneOfTheAbove: hasNoneOfTheAbove,
multipleSelect: multipleSelect,
countChoices: countChoices,
correctAnswer: correctAnswer,
deselectEnabled: deselectEnabled,
choices: choices,
selectedChoices: _2.pluck(choices, "correct")
};
};
var propUpgrades = {
1: function _(v0props) {
var choices;
var hasNoneOfTheAbove;
if (!v0props.noneOfTheAbove) {
choices = v0props.choices;
hasNoneOfTheAbove = false;
} else {
choices = _2.clone(v0props.choices);
var noneOfTheAboveIndex = _2.random(0, v0props.choices.length - 1);
var noneChoice = _2.extend({}, v0props.choices[noneOfTheAboveIndex], {
isNoneOfTheAbove: true
});
choices.splice(noneOfTheAboveIndex, 1);
choices.push(noneChoice);
hasNoneOfTheAbove = true;
}
return _2.extend(_2.omit(v0props, "noneOfTheAbove"), {
choices: choices,
hasNoneOfTheAbove: hasNoneOfTheAbove
});
}
};
module.exports = {
name: "radio",
displayName: "Multiple choice",
accessible: true,
widget: Radio,
transform: transform,
staticTransform: transform,
version: { major: 1, minor: 0 },
propUpgrades: propUpgrades,
isLintable: true
};
/***/ },
/* 69 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable brace-style, comma-dangle, no-undef, no-var, object-curly-spacing, react/forbid-prop-types, react/prop-types, react/sort-comp */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var classNames = __webpack_require__(86);
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
var InputWithExamples = __webpack_require__(181);
var SimpleKeypadInput = __webpack_require__(182);
var ParseTex = __webpack_require__(183).parseTex;
var PossibleAnswers = __webpack_require__(184);
var KhanAnswerTypes = __webpack_require__(82);
var keypadElementPropType = __webpack_require__(257).propTypes.keypadElementPropType;
var _require = __webpack_require__(52),
linterContextProps = _require.linterContextProps,
linterContextDefault = _require.linterContextDefault;
var ApiClassNames = __webpack_require__(12).ClassNames;
var ApiOptions = __webpack_require__(12).Options;
var answerTypes = {
number: {
name: "Numbers",
forms: "integer, decimal, proper, improper, mixed"
},
decimal: {
name: "Decimals",
forms: "decimal"
},
integer: {
name: "Integers",
forms: "integer"
},
rational: {
name: "Fractions and mixed numbers",
forms: "integer, proper, improper, mixed"
},
improper: {
name: "Improper numbers (no mixed)",
forms: "integer, proper, improper"
},
mixed: {
name: "Mixed numbers (no improper)",
forms: "integer, proper, mixed"
},
percent: {
name: "Numbers or percents",
forms: "integer, decimal, proper, improper, mixed, percent"
},
pi: {
name: "Numbers with pi",
forms: "pi"
}
};
var formExamples = {
integer: function integer(options) {
return i18n._("an integer, like $6$");
},
proper: function proper(options) {
if (options.simplify === "optional") {
return i18n._("a *proper* fraction, like $1/2$ or $6/10$");
} else {
return i18n._("a *simplified proper* fraction, like $3/5$");
}
},
improper: function improper(options) {
if (options.simplify === "optional") {
return i18n._("an *improper* fraction, like $10/7$ or $14/8$");
} else {
return i18n._("a *simplified improper* fraction, like $7/4$");
}
},
mixed: function mixed(options) {
return i18n._("a mixed number, like $1\\ 3/4$");
},
decimal: function decimal(options) {
return i18n._("an *exact* decimal, like $0.75$");
},
percent: function percent(options) {
return i18n._("a percent, like $12.34\\%$");
},
pi: function pi(options) {
return i18n._("a multiple of pi, like $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$");
}
};
var InputNumber = React.createClass({
displayName: "InputNumber",
propTypes: {
answerType: React.PropTypes.oneOf(Object.keys(answerTypes)),
currentValue: React.PropTypes.string,
keypadElement: keypadElementPropType,
reviewModeRubric: React.PropTypes.object,
widgetId: React.PropTypes.string.isRequired,
linterContext: linterContextProps
},
getDefaultProps: function getDefaultProps() {
return {
currentValue: "",
size: "normal",
answerType: "number",
apiOptions: ApiOptions.defaults,
linterContext: linterContextDefault
};
},
shouldShowExamples: function shouldShowExamples() {
return this.props.answerType !== "number" && !this.props.apiOptions.staticRender;
},
render: function render() {
if (this.props.apiOptions.customKeypad) {
// TODO(charlie): Support "Review Mode".
return React.createElement(SimpleKeypadInput, {
ref: "input",
value: this.props.currentValue,
keypadElement: this.props.keypadElement,
onChange: this.handleChange,
onFocus: this._handleFocus,
onBlur: this._handleBlur
});
} else {
// HACK(johnsullivan): Create a function with shared logic between
// this and NumericInput.
var rubric = this.props.reviewModeRubric;
var correct = null;
var answerBlurb = null;
if (this.props.apiOptions.satStyling && rubric) {
var score = this.simpleValidate(rubric);
correct = score.type === "points" && score.earned === score.total;
if (!correct) {
// TODO(johnsullivan): Make this a little more
// human-friendly.
var answerString = String(rubric.value);
if (rubric.inexact && rubric.maxError) {
answerString += " \xB1 " + rubric.maxError;
}
var answerStrings = [answerString];
answerBlurb = React.createElement(PossibleAnswers, { answers: answerStrings });
}
}
var classes = {};
classes["perseus-input-size-" + this.props.size] = true;
classes[ApiClassNames.CORRECT] = rubric && correct && this.props.currentValue;
classes[ApiClassNames.INCORRECT] = rubric && !correct && this.props.currentValue;
classes[ApiClassNames.UNANSWERED] = rubric && !this.props.currentValue;
var input = React.createElement(InputWithExamples, {
ref: "input",
value: this.props.currentValue,
onChange: this.handleChange,
className: classNames(classes),
type: this._getInputType(),
examples: this.examples(),
shouldShowExamples: this.shouldShowExamples(),
onFocus: this._handleFocus,
onBlur: this._handleBlur,
id: this.props.widgetId,
disabled: this.props.apiOptions.readOnly,
linterContext: this.props.linterContext
});
if (answerBlurb) {
return React.createElement(
"span",
{ className: "perseus-input-with-answer-blurb" },
input,
answerBlurb
);
} else {
return input;
}
}
},
handleChange: function handleChange(newValue, cb) {
this.props.onChange({ currentValue: newValue }, cb);
},
_getInputType: function _getInputType() {
if (this.props.apiOptions.staticRender) {
return "tex";
} else {
return "text";
}
},
_handleFocus: function _handleFocus() {
this.props.onFocus([]);
// HACK(kevinb): We want to dismiss the feedback popover that webapp
// displays as soon as a user clicks in in the input field so we call
// interactionCallback directly.
var interactionCallback = this.props.apiOptions.interactionCallback;
if (interactionCallback) {
interactionCallback();
}
},
_handleBlur: function _handleBlur() {
this.props.onBlur([]);
},
focus: function focus() {
this.refs.input.focus();
return true;
},
focusInputPath: function focusInputPath(inputPath) {
this.refs.input.focus();
},
blurInputPath: function blurInputPath(inputPath) {
this.refs.input.blur();
},
getInputPaths: function getInputPaths() {
// The widget itself is an input, so we return a single empty list to
// indicate this.
return [[]];
},
getGrammarTypeForPath: function getGrammarTypeForPath(path) {
return "number";
},
setInputValue: function setInputValue(path, newValue, cb) {
this.props.onChange({
currentValue: newValue
}, cb);
},
getUserInput: function getUserInput() {
return {
currentValue: this.props.currentValue
};
},
simpleValidate: function simpleValidate(rubric, onInputError) {
onInputError = onInputError || function () {};
return InputNumber.validate(this.getUserInput(), rubric, onInputError);
},
examples: function examples() {
var type = this.props.answerType;
var forms = answerTypes[type].forms.split(/\s*,\s*/);
var examples = _.map(forms, function (form) {
return formExamples[form](this.props);
}, this);
return [i18n._("**Your answer should be** ")].concat(examples);
}
});
_.extend(InputNumber, {
validate: function validate(state, rubric, onInputError) {
if (rubric.answerType == null) {
rubric.answerType = "number";
}
var val = KhanAnswerTypes.number.createValidatorFunctional(rubric.value, {
simplify: rubric.simplify,
inexact: rubric.inexact || undefined,
maxError: rubric.maxError,
forms: answerTypes[rubric.answerType].forms
});
// We may have received TeX; try to parse it before grading.
// If `currentValue` is not TeX, this should be a no-op.
var currentValue = ParseTex(state.currentValue);
var result = val(currentValue);
// TODO(eater): Seems silly to translate result to this invalid/points
// thing and immediately translate it back in ItemRenderer.scoreInput()
if (result.empty) {
var apiResult = onInputError(null, // reserved for some widget identifier
state.currentValue, result.message);
return {
type: "invalid",
message: apiResult === false ? null : result.message
};
} else {
return {
type: "points",
earned: result.correct ? 1 : 0,
total: 1,
message: result.message
};
}
}
});
var propTransform = function propTransform(editorProps) {
var simplify = editorProps.simplify,
size = editorProps.size,
answerType = editorProps.answerType;
return {
simplify: simplify,
size: size,
answerType: answerType
};
};
module.exports = {
name: "input-number",
displayName: "Number text box (old)",
defaultAlignment: "inline-block",
hidden: true,
widget: InputNumber,
transform: propTransform,
isLintable: true
};
/***/ },
/* 70 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable comma-dangle, no-var, react/jsx-closing-bracket-location, react/jsx-indent-props, react/prop-types, react/sort-comp */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var _ = __webpack_require__(56);
var Util = __webpack_require__(17);
var BlurInput = __webpack_require__(185);
var InfoTip = __webpack_require__(176);
var answerTypes = {
number: {
name: "Numbers",
forms: "integer, decimal, proper, improper, mixed"
},
decimal: {
name: "Decimals",
forms: "decimal"
},
integer: {
name: "Integers",
forms: "integer"
},
rational: {
name: "Fractions and mixed numbers",
forms: "integer, proper, improper, mixed"
},
improper: {
name: "Improper numbers (no mixed)",
forms: "integer, proper, improper"
},
mixed: {
name: "Mixed numbers (no improper)",
forms: "integer, proper, mixed"
},
percent: {
name: "Numbers or percents",
forms: "integer, decimal, proper, improper, mixed, percent"
},
pi: {
name: "Numbers with pi",
forms: "pi"
}
};
var InputNumberEditor = React.createClass({
displayName: "InputNumberEditor",
propTypes: {
value: React.PropTypes.number,
simplify: React.PropTypes.oneOf(["required", "optional", "enforced"]),
size: React.PropTypes.oneOf(["normal", "small"]),
inexact: React.PropTypes.bool,
maxError: React.PropTypes.number,
answerType: React.PropTypes.string
},
getDefaultProps: function getDefaultProps() {
return {
value: 0,
simplify: "required",
size: "normal",
inexact: false,
maxError: 0.1,
answerType: "number"
};
},
handleAnswerChange: function handleAnswerChange(str) {
var value = Util.firstNumericalParse(str) || 0;
this.props.onChange({ value: value });
},
render: function render() {
var _this = this;
var answerTypeOptions = _.map(answerTypes, function (v, k) {
return React.createElement(
"option",
{ value: k, key: k },
v.name
);
}, this);
return React.createElement(
"div",
null,
React.createElement(
"div",
null,
React.createElement(
"label",
null,
"Correct answer:",
" ",
React.createElement(BlurInput, {
value: "" + this.props.value,
onChange: this.handleAnswerChange,
ref: "input"
})
)
),
React.createElement(
"div",
null,
React.createElement(
"label",
null,
"Unsimplified answers",
" ",
React.createElement(
"select",
{
value: this.props.simplify,
onChange: function onChange(e) {
_this.props.onChange({
simplify: e.target.value
});
}
},
React.createElement(
"option",
{ value: "required" },
"will not be graded"
),
React.createElement(
"option",
{ value: "optional" },
"will be accepted"
),
React.createElement(
"option",
{ value: "enforced" },
"will be marked wrong"
)
)
),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Normally select \"will not be graded\". This will give the user a message saying the answer is correct but not simplified. The user will then have to simplify it and re-enter, but will not be penalized. (5th grade and anything after)"
),
React.createElement(
"p",
null,
"Select \"will be accepted\" only if the user is not expected to know how to simplify fractions yet. (Anything prior to 5th grade)"
),
React.createElement(
"p",
null,
"Select \"will be marked wrong\" only if we are specifically assessing the ability to simplify."
)
)
),
React.createElement(
"div",
null,
React.createElement(
"label",
null,
React.createElement("input", {
type: "checkbox",
checked: this.props.inexact,
onChange: function onChange(e) {
_this.props.onChange({
inexact: e.target.checked
});
}
}),
" ",
"Allow inexact answers"
),
React.createElement(
"label",
null,
React.createElement("input", { /* TODO(emily): don't use a hidden checkbox
for alignment */
type: "checkbox",
style: { visibility: "hidden" }
}),
"Max error:",
" ",
React.createElement("input", {
type: "text",
disabled: !this.props.inexact,
defaultValue: this.props.maxError,
onBlur: function onBlur(e) {
var ans = "" + (Util.firstNumericalParse(e.target.value) || 0);
e.target.value = ans;
_this.props.onChange({ maxError: ans });
}
})
)
),
React.createElement(
"div",
null,
"Answer type:",
" ",
React.createElement(
"select",
{
value: this.props.answerType,
onChange: function onChange(e) {
_this.props.onChange({ answerType: e.target.value });
}
},
answerTypeOptions
),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Use the default \"Numbers\" unless the answer must be in a specific form (e.g., question is about converting decimals to fractions)."
)
)
),
React.createElement(
"div",
null,
React.createElement(
"label",
null,
"Width",
" ",
React.createElement(
"select",
{
value: this.props.size,
onChange: function onChange(e) {
_this.props.onChange({ size: e.target.value });
}
},
React.createElement(
"option",
{ value: "normal" },
"Normal (80px)"
),
React.createElement(
"option",
{ value: "small" },
"Small (40px)"
)
)
),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Use size \"Normal\" for all text boxes, unless there are multiple text boxes in one line and the answer area is too narrow to fit them."
)
)
)
);
},
focus: function focus() {
ReactDOM.findDOMNode(this.refs.input).focus();
return true;
},
serialize: function serialize() {
return _.pick(this.props, "value", "simplify", "size", "inexact", "maxError", "answerType");
}
});
module.exports = InputNumberEditor;
/***/ },
/* 71 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable brace-style, comma-dangle, indent, no-undef, no-var, object-curly-spacing, react/forbid-prop-types, react/prop-types, react/sort-comp */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var classNames = __webpack_require__(86);
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
var _require = __webpack_require__(79),
StyleSheet = _require.StyleSheet,
css = _require.css;
var styleConstants = __webpack_require__(77);
var InputWithExamples = __webpack_require__(181);
var SimpleKeypadInput = __webpack_require__(182);
var ParseTex = __webpack_require__(183).parseTex;
var PossibleAnswers = __webpack_require__(184);
var ApiClassNames = __webpack_require__(12).ClassNames;
var ApiOptions = __webpack_require__(12).Options;
var KhanAnswerTypes = __webpack_require__(82);
var KhanMath = __webpack_require__(208);
var keypadElementPropType = __webpack_require__(257).propTypes.keypadElementPropType;
var _require2 = __webpack_require__(52),
linterContextProps = _require2.linterContextProps,
linterContextDefault = _require2.linterContextDefault;
var _require3 = __webpack_require__(47),
iconDropdownArrow = _require3.iconDropdownArrow;
var InlineIcon = __webpack_require__(48);
var answerFormButtons = [{ title: "Integers", value: "integer", content: "6" }, { title: "Decimals", value: "decimal", content: "0.75" }, { title: "Proper fractions", value: "proper", content: "\u2157" }, {
title: "Improper fractions",
value: "improper",
content: "\u2077\u2044\u2084"
}, { title: "Mixed numbers", value: "mixed", content: "1\xBE" }, { title: "Numbers with \u03C0", value: "pi", content: "\u03C0" }];
var formExamples = {
integer: function integer() {
return i18n._("an integer, like $6$");
},
proper: function proper(form) {
return form.simplify === "optional" ? i18n._("a *proper* fraction, like $1/2$ or $6/10$") : i18n._("a *simplified proper* fraction, like $3/5$");
},
improper: function improper(form) {
return form.simplify === "optional" ? i18n._("an *improper* fraction, like $10/7$ or $14/8$") : i18n._("a *simplified improper* fraction, like $7/4$");
},
mixed: function mixed() {
return i18n._("a mixed number, like $1\\ 3/4$");
},
decimal: function decimal() {
return i18n._("an *exact* decimal, like $0.75$");
},
pi: function pi() {
return i18n._("a multiple of pi, like $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$");
}
};
var NumericInput = React.createClass({
displayName: "NumericInput",
propTypes: {
currentValue: React.PropTypes.string,
currentMultipleValues: React.PropTypes.arrayOf(React.PropTypes.string),
size: React.PropTypes.oneOf(["normal", "small"]),
apiOptions: ApiOptions.propTypes,
coefficient: React.PropTypes.bool,
answerForms: React.PropTypes.arrayOf(React.PropTypes.shape({
name: React.PropTypes.string.isRequired,
simplify: React.PropTypes.oneOf(["required", "optional"]).isRequired
})),
keypadElement: keypadElementPropType,
labelText: React.PropTypes.string,
reviewModeRubric: React.PropTypes.object,
trackInteraction: React.PropTypes.func.isRequired,
widgetId: React.PropTypes.string.isRequired,
linterContext: linterContextProps,
multipleNumberInput: React.PropTypes.bool
},
getDefaultProps: function getDefaultProps() {
return {
currentValue: "",
// currentMultipleValues has an empty string, because if finite
// solutions is chosen, there must be at least 1 answer
currentMultipleValues: [""],
size: "normal",
apiOptions: ApiOptions.defaults,
coefficient: false,
answerForms: [],
labelText: "",
linterContext: linterContextDefault,
multipleNumberInput: false
};
},
getInitialState: function getInitialState() {
return {
// dropdown option: either no-solutions or finite-solutions
numSolutions: "no-solutions",
// keeps track of the other set of values when switching
// between 0 and finite solutions
previousValues: [""]
};
},
getAnswerBlurb: function getAnswerBlurb(rubric) {
var correct;
var answerBlurb;
if (this.props.apiOptions.satStyling && rubric) {
var score = this.simpleValidate(rubric);
correct = score.type === "points" && score.earned === score.total;
if (!correct) {
var correctAnswers = _.filter(rubric.answers, function (answer) {
return answer.status === "correct";
});
var answerStrings = _.map(correctAnswers, function (answer) {
// Figure out how this answer is supposed to be
// displayed
var format = "decimal";
if (answer.answerForms && answer.answerForms[0]) {
// NOTE(johnsullivan): This isn't exactly ideal, but
// it does behave well for all the currently known
// problems. See D14742 for some discussion on
// alternate strategies.
format = answer.answerForms[0];
}
var answerString = KhanMath.toNumericString(answer.value, format);
if (answer.maxError) {
answerString += " \xB1 " + KhanMath.toNumericString(answer.maxError, format);
}
return answerString;
});
answerBlurb = React.createElement(PossibleAnswers, { answers: answerStrings });
}
}
return [answerBlurb, correct];
},
getClasses: function getClasses(correct, rubric) {
var classes = {};
classes["perseus-input-size-" + this.props.size] = true;
classes[ApiClassNames.CORRECT] = rubric && correct && this.props.currentValue;
classes[ApiClassNames.INCORRECT] = rubric && !correct && this.props.currentValue;
classes[ApiClassNames.UNANSWERED] = rubric && !this.props.currentValue;
return classes;
},
render: function render() {
var _this = this;
var rubric = this.props.reviewModeRubric;
var answers = this.getAnswerBlurb(rubric);
var answerBlurb = answers[0];
var correct = answers[1];
var classes = this.getClasses(correct, rubric);
var labelText = this.props.labelText;
if (labelText == null || labelText === "") {
labelText = i18n._("Your answer:");
}
var selectClasses = classNames({
"perseus-widget-dropdown": true
});
var dropdown = React.createElement(
"div",
null,
React.createElement(
"select",
{
onChange: this.handleNumSolutionsChange,
className: selectClasses + " " + css(styles.dropdown) + " " + ApiClassNames.INTERACTIVE,
value: this.state.numSolutions
},
React.createElement(
"option",
{ value: "no-solutions" },
i18n._("0 solutions")
),
React.createElement(
"option",
{ value: "finite-solutions" },
i18n._("Finite solutions")
)
),
React.createElement(InlineIcon, _extends({}, iconDropdownArrow, {
style: {
marginLeft: -24,
height: 24,
width: 24
}
}))
);
var input;
if (this.props.multipleNumberInput) {
if (this.state.numSolutions === "no-solutions") {
return dropdown;
} else {
var addInput = React.createElement(
"div",
{
className: css(styles.addInputButton),
onClick: this._addInput
},
"+"
);
input = React.createElement(
"div",
null,
this.props.currentMultipleValues.map(function (item, i) {
return React.createElement(
"div",
{
key: i,
className: css(styles.numberInputContainer)
},
i > 0 && React.createElement(
"div",
{
className: css(styles.removeInputButton),
onClick: function onClick(evt) {
return _this._removeInput(i, evt);
},
"aria-label": "Remove this answer"
},
"-"
),
_this.props.apiOptions.customKeypad ? React.createElement(SimpleKeypadInput, {
ref: "input",
value: _this.props.currentMultipleValues[i],
keypadElement: _this.props.keypadElement,
onChange: function onChange(e) {
return _this.handleMultipleInputChange(i, e);
},
onFocus: _this._handleFocus,
onBlur: _this._handleBlur
}) : React.createElement(InputWithExamples, {
ref: "input",
value: _this.props.currentMultipleValues[i],
onChange: function onChange(e) {
return _this.handleMultipleInputChange(i, e);
},
className: classNames(classes, css(styles.numberInput)),
labelText: labelText,
type: _this._getInputType(),
examples: _this.examples(),
shouldShowExamples: _this.shouldShowExamples(),
onFocus: _this._handleFocus,
onBlur: _this._handleBlur,
id: _this.props.widgetId,
disabled: _this.props.apiOptions.readOnly,
highlightLint: _this.props.highlightLint
})
);
}),
addInput,
"Add answer"
);
}
} else {
if (this.props.apiOptions.customKeypad) {
// TODO(charlie): Support "Review Mode".
return React.createElement(SimpleKeypadInput, {
ref: "input",
value: this.props.currentValue,
keypadElement: this.props.keypadElement,
onChange: this.handleChange,
onFocus: this._handleFocus,
onBlur: this._handleBlur
});
} else {
input = React.createElement(InputWithExamples, {
ref: "input",
value: this.props.currentValue,
onChange: this.handleChange,
className: classNames(classes),
labelText: labelText,
type: this._getInputType(),
examples: this.examples(),
shouldShowExamples: this.shouldShowExamples(),
onFocus: this._handleFocus,
onBlur: this._handleBlur,
id: this.props.widgetId,
disabled: this.props.apiOptions.readOnly,
highlightLint: this.props.highlightLint
});
}
}
if (answerBlurb) {
return React.createElement(
"span",
{ className: "perseus-input-with-answer-blurb" },
this.props.multipleNumberInput && dropdown,
input,
answerBlurb
);
} else if (this.props.apiOptions.satStyling) {
// NOTE(amy): the input widgets themselves already have
// a default aria label of "Your Answer", so we hide this
// redundant label from screen-readers.
return React.createElement(
"label",
{
className: "perseus-input-with-label",
"aria-hidden": "true"
},
React.createElement(
"span",
{ className: "perseus-input-label" },
i18n.i18nDoNotTranslate("Answer:")
),
this.props.multipleNumberInput && dropdown,
input
);
} else {
return React.createElement(
"div",
null,
this.props.multipleNumberInput && dropdown,
input
);
}
},
handleChange: function handleChange(newValue, cb) {
this.props.onChange({ currentValue: newValue }, cb);
this.props.trackInteraction();
},
handleMultipleInputChange: function handleMultipleInputChange(index, newValue) {
var newValues = this.props.currentMultipleValues.slice();
newValues[index] = newValue;
this.props.onChange({
currentMultipleValues: newValues
});
this.props.trackInteraction();
},
handleNumSolutionsChange: function handleNumSolutionsChange(event) {
var newValue = event.target.value;
this.setState({ numSolutions: newValue });
// Saves the values the user entered when switching between no
// solutions and finite solutions, however, we also correctly update
// the answer that is to be graded
if (newValue === "no-solutions") {
this.setState({
previousValues: this.props.currentMultipleValues
});
this.props.onChange({
currentMultipleValues: []
});
} else {
this.props.onChange({
currentMultipleValues: this.state.previousValues
});
this.setState({
previousValues: []
});
}
},
_getInputType: function _getInputType() {
if (this.props.apiOptions.staticRender) {
return "tex";
} else {
return "text";
}
},
_handleFocus: function _handleFocus() {
this.props.onFocus([]);
// HACK(kevinb): We want to dismiss the feedback popover that webapp
// displays as soon as a user clicks in in the input field so we call
// interactionCallback directly.
var interactionCallback = this.props.apiOptions.interactionCallback;
if (interactionCallback) {
interactionCallback();
}
},
_handleBlur: function _handleBlur() {
this.props.onBlur([]);
},
_addInput: function _addInput() {
// Add a new blank value to the list of current values
this.props.onChange({
currentMultipleValues: this.props.currentMultipleValues.concat([""])
});
},
_removeInput: function _removeInput(i, event) {
var length = this.props.currentMultipleValues.length;
var newValues = this.props.currentMultipleValues.slice(0, i).concat(this.props.currentMultipleValues.slice(i + 1, length));
this.props.onChange({
currentMultipleValues: newValues
});
},
focus: function focus() {
this.refs.input.focus();
return true;
},
focusInputPath: function focusInputPath(inputPath) {
this.refs.input.focus();
},
blurInputPath: function blurInputPath(inputPath) {
this.refs.input.blur();
},
getInputPaths: function getInputPaths() {
// The widget itself is an input, so we return a single empty list to
// indicate this.
return [[]];
},
getGrammarTypeForPath: function getGrammarTypeForPath(inputPath) {
return "number";
},
setInputValue: function setInputValue(path, newValue, cb) {
this.props.onChange({
currentValue: newValue
}, cb);
},
getUserInput: function getUserInput() {
var multiple = this.props.multipleNumberInput;
return {
multInput: multiple,
currentValue: multiple ? this.props.currentMultipleValues : this.props.currentValue
};
},
simpleValidate: function simpleValidate(rubric) {
return NumericInput.validate(this.getUserInput(), rubric);
},
shouldShowExamples: function shouldShowExamples() {
var noFormsAccepted = this.props.answerForms.length === 0;
// To check if all answer forms are accepted, we must first
// find the *names* of all accepted forms, and see if they are
// all present, ignoring duplicates
var answerFormNames = _.uniq(this.props.answerForms.map(function (form) {
return form.name;
}));
var allFormsAccepted = answerFormNames.length >= _.size(formExamples);
return !noFormsAccepted && !allFormsAccepted;
},
examples: function examples() {
// if the set of specified forms are empty, allow all forms
var forms = this.props.answerForms.length !== 0 ? this.props.answerForms : _.map(_.keys(formExamples), function (name) {
return {
name: name,
simplify: "required"
};
});
var examples = _.map(forms, function (form) {
return formExamples[form.name](form);
});
// Ensure no duplicate tooltip text from simplified and unsimplified
// versions of the same format
examples = _.uniq(examples);
return [i18n._("**Your answer should be** ")].concat(examples);
}
});
_.extend(NumericInput, {
validate: function validate(state, rubric) {
var allAnswerForms = _.pluck(answerFormButtons, "value");
var createValidator = function createValidator(answer) {
return KhanAnswerTypes.number.createValidatorFunctional(answer.value, {
message: answer.message,
simplify: answer.status === "correct" ? answer.simplify : "optional",
inexact: true, // TODO(merlob) backfill / delete
maxError: answer.maxError,
forms: answer.strict && answer.answerForms && answer.answerForms.length !== 0 ? answer.answerForms : allAnswerForms
});
};
// We may have received TeX; try to parse it before grading.
// If `currentValue` is not TeX, this should be a no-op.
var currentValue = ParseTex(state.currentValue);
var correctAnswers = _.where(rubric.answers, { status: "correct" });
if (state.multInput) {
// sort the answers and the solutions so they can be compared
var sortedInputs = currentValue.split(",").sort();
correctAnswers.sort(function (a, b) {
return a.value > b.value ? 1 : -1;
});
// If the number of correct answers and user answers do not match
// return early that the answer is wrong
if (sortedInputs.length !== correctAnswers.length) {
return {
type: "points",
earned: 0,
total: 1,
message: "Incorrect number of answers"
};
}
// Look through all correct answers and make sure there is
// the correct user answer for each
var correct = true;
var message;
correctAnswers.forEach(function (answer, i) {
var localValue = sortedInputs[i];
if (rubric.coefficient) {
if (!localValue) {
localValue = 1;
} else if (localValue === "-") {
localValue = -1;
}
}
var validate = createValidator(answer);
var status = validate(localValue);
correct = correct && status.correct;
if (status.message) {
message = status.message;
}
});
return {
type: "points",
earned: correct ? 1 : 0,
total: 1,
message: message
};
} else {
// Look through all correct answers for one that matches either
// precisely or approximately and return the appropriate message:
// - if precise, return the message that the answer came with
// - if it needs to be simplified, etc., show that message
var result = _.find(_.map(correctAnswers, function (answer) {
// The coefficient is an attribute of the widget
var localValue = currentValue;
if (rubric.coefficient) {
if (!localValue) {
localValue = 1;
} else if (localValue === "-") {
localValue = -1;
}
}
var validate = createValidator(answer);
return validate(localValue);
}), function (match) {
return match.correct || match.empty;
});
if (!result) {
// Otherwise, if the guess is not correct
var otherAnswers = [].concat(_.where(rubric.answers, { status: "ungraded" }), _.where(rubric.answers, { status: "wrong" }));
// Look through all other answers and if one matches either
// precisely or approximately return the answer's message
var match = _.find(otherAnswers, function (answer) {
var validate = createValidator(answer);
return validate(currentValue).correct;
});
result = {
empty: match ? match.status === "ungraded" : false,
correct: match ? match.status === "correct" : false,
message: match ? match.message : null,
guess: currentValue
};
}
// TODO(eater): Seems silly to translate result to this
// invalid/points thing and immediately translate it
// back in ItemRenderer.scoreInput()
if (result.empty) {
return {
type: "invalid",
message: result.message
};
} else {
return {
type: "points",
earned: result.correct ? 1 : 0,
total: 1,
message: result.message
};
}
}
}
});
// TODO(thomas): Currently we receive a list of lists of acceptable answer types
// and union them down into a single set. It's worth considering whether it
// wouldn't make more sense to have a single set of acceptable answer types for
// a given *problem* rather than for each possible [correct/wrong] *answer*.
// When should two answers to a problem take different answer types?
// See D27790 for more discussion.
var unionAnswerForms = function unionAnswerForms(answerFormsList) {
// Takes a list of lists of answer forms, and returns a list of the forms
// in each of these lists in the same order that they're listed in the
// `formExamples` forms from above.
// uniqueBy takes a list of elements and a function which compares whether
// two elements are equal, and returns a list of unique elements. This is
// just a helper function here, but works generally.
var uniqueBy = function uniqueBy(list, iteratee) {
return _.reduce(list, function (uniqueList, element) {
// For each element, decide whether it's already in the list of
// unique items.
var inList = _.find(uniqueList, iteratee.bind(null, element));
if (inList) {
return uniqueList;
} else {
return uniqueList.concat([element]);
}
}, []);
};
// Pull out all of the forms from the different lists.
var allForms = _.flatten(answerFormsList);
// Pull out the unique forms using uniqueBy.
var uniqueForms = uniqueBy(allForms, _.isEqual);
// Sort them by the order they appear in the `formExamples` list.
return _.sortBy(uniqueForms, function (form) {
return _.keys(formExamples).indexOf(form.name);
});
};
var propsTransform = function propsTransform(editorProps) {
var rendererProps = _.extend(_.omit(editorProps, "answers"), {
answerForms: unionAnswerForms(
// Pull out the name of each form and whether that form has
// required simplification.
_.map(editorProps.answers, function (answer) {
return _.map(answer.answerForms, function (form) {
return {
simplify: answer.simplify,
name: form
};
});
}))
});
return rendererProps;
};
var styles = StyleSheet.create({
addInputButton: {
cursor: "pointer",
display: "inline-block",
border: "2px solid " + styleConstants.kaGreen,
backgroundColor: styleConstants.kaGreen,
color: styleConstants.white,
fontSize: 20,
borderRadius: 15,
width: 18,
height: 18,
marginBottom: 7,
marginRight: 8,
marginTop: 3,
textAlign: "center",
paddingTop: 1
},
removeInputButton: {
cursor: "pointer",
display: "inline-block",
border: "2px solid " + styleConstants.red,
backgroundColor: styleConstants.red,
color: styleConstants.white,
fontSize: 20,
borderRadius: 15,
width: 18,
height: 18,
marginBottom: 7,
marginRight: 8,
marginTop: 4,
textAlign: "center"
},
dropdown: {
width: 250,
marginBottom: 10,
appearance: 'none',
backgroundColor: 'transparent',
border: "1px solid " + styleConstants.gray76,
borderRadius: 4,
boxShadow: 'none',
fontFamily: styleConstants.baseFontFamily,
padding: "9px 25px 9px 9px",
':focus': {
outline: 'none',
border: "2px solid " + styleConstants.kaGreen,
padding: "8px 25px 8px 8px"
},
':focus + svg': {
color: "" + styleConstants.kaGreen
},
':disabled': {
color: styleConstants.gray68
},
':disabled + svg': {
color: styleConstants.gray68
}
},
numberInput: {
float: "right",
width: 170,
marginBottom: 10,
border: "1px solid " + styleConstants.gray76,
borderRadius: 4,
padding: "9px 25px 9px 9px",
':focus': {
outline: 'none',
border: "2px solid " + styleConstants.kaGreen,
padding: "8px 25px 8px 8px"
}
},
numberInputContainer: {
display: "flex"
}
});
module.exports = {
name: "numeric-input",
displayName: "Number text box",
defaultAlignment: "inline-block",
accessible: true,
widget: NumericInput,
transform: propsTransform,
isLintable: true
};
/***/ },
/* 72 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable comma-dangle, no-redeclare, no-var, react/jsx-closing-bracket-location, react/jsx-indent-props, react/sort-comp, space-infix-ops */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
var Changeable = __webpack_require__(187);
var EditorJsonify = __webpack_require__(197);
var ButtonGroup = __webpack_require__(83);
var Editor = __webpack_require__(25);
var _require = __webpack_require__(47),
iconGear = _require.iconGear,
iconTrash = _require.iconTrash;
var InfoTip = __webpack_require__(176);
var InlineIcon = __webpack_require__(48);
var MultiButtonGroup = __webpack_require__(198);
var NumberInput = __webpack_require__(199);
var PropCheckBox = __webpack_require__(90);
var TextInput = __webpack_require__(200);
var firstNumericalParse = __webpack_require__(17).firstNumericalParse;
var answerFormButtons = [{ title: "Integers", value: "integer", content: "6" }, { title: "Decimals", value: "decimal", content: "0.75" }, { title: "Proper fractions", value: "proper", content: "\u2157" }, {
title: "Improper fractions",
value: "improper",
content: "\u2077\u2044\u2084"
}, { title: "Mixed numbers", value: "mixed", content: "1\xBE" }, { title: "Numbers with \u03C0", value: "pi", content: "\u03C0" }];
var initAnswer = function initAnswer(status) {
return {
value: null,
status: status,
message: "",
simplify: "required",
answerForms: [],
strict: false,
maxError: null
};
};
var NumericInputEditor = React.createClass({
displayName: "NumericInputEditor",
propTypes: _extends({}, Changeable.propTypes),
getDefaultProps: function getDefaultProps() {
return {
answers: [initAnswer("correct")],
size: "normal",
coefficient: false,
labelText: "",
multipleNumberInput: false
};
},
getInitialState: function getInitialState() {
return {
lastStatus: "wrong",
showOptions: _.map(this.props.answers, function () {
return false;
})
};
},
render: function render() {
var _this = this;
var answers = this.props.answers;
var unsimplifiedAnswers = function unsimplifiedAnswers(i) {
return React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(
"label",
null,
"Unsimplified answers are"
),
React.createElement(ButtonGroup, {
value: answers[i]["simplify"],
allowEmpty: false,
buttons: [{ value: "required", content: "ungraded" }, { value: "optional", content: "accepted" }, { value: "enforced", content: "wrong" }],
onChange: _this.updateAnswer(i, "simplify")
}),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Normally select \"ungraded\". This will give the user a message saying the answer is correct but not simplified. The user will then have to simplify it and re-enter, but will not be penalized. (5th grade and after)"
),
React.createElement(
"p",
null,
"Select \"accepted\" only if the user is not expected to know how to simplify fractions yet. (Anything prior to 5th grade)"
),
React.createElement(
"p",
null,
"Select \"wrong\" ",
React.createElement(
"em",
null,
"only"
),
" if we are specifically assessing the ability to simplify."
)
)
);
};
var suggestedAnswerTypes = function suggestedAnswerTypes(i) {
return React.createElement(
"div",
null,
React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(
"label",
null,
"Choose the suggested answer formats"
),
React.createElement(MultiButtonGroup, {
buttons: answerFormButtons,
values: answers[i]["answerForms"],
onChange: _this.updateAnswer(i, "answerForms")
}),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Formats will be autoselected for you based on the given answer; to show no suggested formats and accept all types, simply have a decimal/integer be the answer. Values with \u03C0 will have format \"pi\", and values that are fractions will have some subset (mixed will be \"mixed\" and \"proper\"; improper/proper will both be \"improper\" and \"proper\"). If you would like to specify that it is only a proper fraction (or only a mixed/improper fraction), deselect the other format. Except for specific cases, you should not need to change the autoselected formats."
),
React.createElement(
"p",
null,
"To restrict the answer to ",
React.createElement(
"em",
null,
"only"
),
" an improper fraction (i.e. 7/4), select the improper fraction and toggle \"strict\" to true. This ",
React.createElement(
"b",
null,
"will not"
),
" ",
" accept 1.75 as an answer.",
" "
),
React.createElement(
"p",
null,
"Unless you are testing that specific skill, please do not restrict the answer format."
)
)
),
React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(PropCheckBox, {
label: "Strictly match only these formats",
strict: answers[i]["strict"],
onChange: _this.updateAnswer.bind(_this, i)
})
)
);
};
var maxError = function maxError(i) {
return React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(
"label",
null,
"Max error",
" ",
React.createElement(NumberInput, {
className: "max-error",
value: answers[i]["maxError"],
onChange: _this.updateAnswer(i, "maxError"),
placeholder: "0"
})
)
);
};
var inputSize = React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(
"label",
null,
"Width: "
),
React.createElement(ButtonGroup, {
value: this.props.size,
allowEmpty: false,
buttons: [{ value: "normal", content: "Normal (80px)" }, { value: "small", content: "Small (40px)" }],
onChange: this.change("size")
}),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Use size \"Normal\" for all text boxes, unless there are multiple text boxes in one line and the answer area is too narrow to fit them."
)
)
);
var labelText = React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(
"label",
null,
"Label text:",
" ",
React.createElement(TextInput, {
value: this.props.labelText,
onChange: this.change("labelText")
})
),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Text to describe this input. This will be shown to users using screenreaders."
)
)
);
var coefficientCheck = React.createElement(
"div",
null,
React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(PropCheckBox, {
label: "Coefficient",
coefficient: this.props.coefficient,
onChange: this.props.onChange
}),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"A coefficient style number allows the student to use - for -1 and an empty string to mean 1."
)
)
)
);
var addAnswerButton = React.createElement(
"div",
null,
React.createElement(
"a",
{
href: "javascript:void(0)",
className: "simple-button orange",
onClick: function onClick() {
return _this.addAnswer();
},
onKeyDown: function onKeyDown(e) {
return _this.onSpace(e, _this.addAnswer);
}
},
React.createElement(
"span",
null,
"Add new answer"
)
)
);
var instructions = {
wrong: "(address the mistake/misconception)",
ungraded: "(explain in detail to avoid confusion)",
correct: "(reinforce the user's understanding)"
};
var generateInputAnswerEditors = function generateInputAnswerEditors() {
return answers.map(function (answer, i) {
var editor = React.createElement(Editor, {
apiOptions: _this.props.apiOptions,
content: answer.message || "",
placeholder: "Why is this answer " + answer.status + "?\t" + instructions[answer.status],
widgetEnabled: false,
onChange: function onChange(newProps) {
if ("content" in newProps) {
_this.updateAnswer(i, {
message: newProps.content
});
}
}
});
return React.createElement(
"div",
{ className: "perseus-widget-row", key: i },
React.createElement(
"div",
{
className: "input-answer-editor-value-container" + (answer.maxError ? " with-max-error" : "")
},
React.createElement(NumberInput, {
value: answer.value,
className: "numeric-input-value",
placeholder: "answer",
format: _.last(answer.answerForms),
onFormatChange: function onFormatChange(newValue, format) {
// NOTE(charlie): The mobile web expression
// editor relies on this automatic answer
// form resolution for determining when to
// show the Pi symbol. If we get rid of it,
// we should also disable Pi for
// NumericInput and require problems that
// use Pi to build on Expression.
// Alternatively, we could store answers
// as plaintext and parse them to determine
// whether or not to reveal Pi on the
// keypad (right now, answers are stored as
// resolved values, like '0.125' rather
// than '1/8').
var forms;
if (format === "pi") {
forms = ["pi"];
} else if (format === "mixed") {
forms = ["proper", "mixed"];
} else if (format === "proper" || format === "improper") {
forms = ["proper", "improper"];
}
_this.updateAnswer(i, {
value: firstNumericalParse(newValue),
answerForms: forms
});
},
onChange: function onChange(newValue) {
_this.updateAnswer(i, {
value: firstNumericalParse(newValue)
});
}
}),
answer.strict && React.createElement(
"div",
{
className: "is-strict-indicator",
title: "strictly equivalent to"
},
"\u2261"
),
answer.simplify !== "required" && answer.status === "correct" && React.createElement(
"div",
{
className: "simplify-indicator " + answer.simplify,
title: "accepts unsimplified answers"
},
"\u2030"
),
answer.maxError ? React.createElement(
"div",
{ className: "max-error-container" },
React.createElement(
"div",
{ className: "max-error-plusmn" },
"\xB1"
),
React.createElement(NumberInput, {
placeholder: 0,
value: answers[i]["maxError"],
format: _.last(answer.answerForms),
onChange: _this.updateAnswer(i, "maxError")
})
) : null,
React.createElement("div", { className: "value-divider" }),
React.createElement(
"a",
{
href: "javascript:void(0)",
className: "answer-status " + answer.status,
onClick: function onClick() {
return _this.onStatusChange(i);
},
onKeyDown: function onKeyDown(e) {
return _this.onSpace(e, _this.onStatusChange, i);
}
},
answer.status
),
React.createElement(
"a",
{
href: "javascript:void(0)",
className: "answer-trash",
onClick: function onClick() {
return _this.onTrashAnswer(i);
},
onKeyDown: function onKeyDown(e) {
return _this.onSpace(e, _this.onTrashAnswer, i);
}
},
React.createElement(InlineIcon, iconTrash)
),
React.createElement(
"a",
{
href: "javascript:void(0)",
className: "options-toggle",
onClick: function onClick() {
return _this.onToggleOptions(i);
},
onKeyDown: function onKeyDown(e) {
return _this.onSpace(e, _this.onToggleOptions, i);
}
},
React.createElement(InlineIcon, iconGear)
)
),
React.createElement(
"div",
{ className: "input-answer-editor-message" },
editor
),
_this.state.showOptions[i] && React.createElement(
"div",
{ className: "options-container" },
maxError(i),
answer.status === "correct" && unsimplifiedAnswers(i),
suggestedAnswerTypes(i)
)
);
});
};
return React.createElement(
"div",
{ className: "perseus-input-number-editor" },
React.createElement(
"div",
{ ref: function ref(e) {
return _this.multInputOption = e;
} },
React.createElement(
"select",
{ onChange: this.onMultipleInputChange },
React.createElement(
"option",
{ value: "simple-numeric-input" },
"Ask for one correct solution"
),
React.createElement(
"option",
{ value: "multiple-numeric-input" },
"Ask for all correct solutions"
)
)
),
React.createElement(
"div",
{ className: "ui-title" },
"User input"
),
React.createElement(
"div",
{ className: "msg-title" },
"Message shown to user on attempt"
),
generateInputAnswerEditors(),
addAnswerButton,
inputSize,
coefficientCheck,
labelText
);
},
change: function change() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return Changeable.change.apply(this, args);
},
onToggleOptions: function onToggleOptions(choiceIndex) {
var showOptions = this.state.showOptions.slice();
showOptions[choiceIndex] = !showOptions[choiceIndex];
this.setState({ showOptions: showOptions });
},
onTrashAnswer: function onTrashAnswer(choiceIndex) {
if (choiceIndex >= 0 && choiceIndex < this.props.answers.length) {
var answers = this.props.answers.slice(0);
answers.splice(choiceIndex, 1);
this.props.onChange({ answers: answers });
}
},
onSpace: function onSpace(e, callback) {
if (e.key === " ") {
e.preventDefault(); // prevent page shifting
var args = _.toArray(arguments).slice(2);
callback.apply(this, args);
}
},
onStatusChange: function onStatusChange(choiceIndex) {
var statuses = ["wrong", "ungraded", "correct"];
var answers = this.props.answers;
var i = _.indexOf(statuses, answers[choiceIndex].status);
var newStatus = statuses[(i + 1) % statuses.length];
this.updateAnswer(choiceIndex, {
status: newStatus,
simplify: newStatus === "correct" ? "required" : "accepted"
});
},
onMultipleInputChange: function onMultipleInputChange(event) {
var newOption = event.target.value;
if (newOption === "multiple-numeric-input") {
this.props.onChange({ multipleNumberInput: true });
} else {
this.props.onChange({ multipleNumberInput: false });
}
},
updateAnswer: function updateAnswer(choiceIndex, update) {
var _this2 = this;
if (!_.isObject(update)) {
return _.partial(function (choiceIndex, key, value) {
var update = {};
update[key] = value;
_this2.updateAnswer(choiceIndex, update);
}, choiceIndex, update);
}
var answers = _.clone(this.props.answers);
// Don't bother to make a new answer box unless we are editing the last
// one.
// TODO(oliver): This might not be necessary anymore.
if (choiceIndex === answers.length) {
var lastAnswer = initAnswer(this.state.lastStatus);
var answers = answers.concat(lastAnswer);
}
answers[choiceIndex] = _.extend({}, answers[choiceIndex], update);
this.props.onChange({ answers: answers });
},
addAnswer: function addAnswer() {
var lastAnswer = initAnswer(this.state.lastStatus);
var answers = this.props.answers.concat(lastAnswer);
this.props.onChange({ answers: answers });
},
getSaveWarnings: function getSaveWarnings() {
// Filter out all the empty answers
var warnings = [];
// TODO(emily): This doesn't actually work, because the value is either
// null or undefined when undefined, probably.
if (_.contains(_.pluck(this.props.answers, "value"), "")) {
warnings.push("One or more answers is empty");
}
this.props.answers.forEach(function (answer, i) {
var formatError = answer.strict && (!answer.answerForms || answer.answerForms.length === 0);
if (formatError) {
warnings.push("Answer " + (i + 1) + " is set to string format " + "matching, but no format was selected");
}
});
return warnings;
},
serialize: function serialize() {
return EditorJsonify.serialize.call(this);
}
});
module.exports = NumericInputEditor;
/***/ },
/* 73 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable comma-dangle, indent, no-redeclare, no-undef, no-unused-vars, no-var, object-curly-spacing, react/jsx-closing-bracket-location, react/jsx-indent-props, react/sort-comp */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var classNames = __webpack_require__(86);
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var Tooltip = __webpack_require__(193);
var _ = __webpack_require__(56);
var ApiOptions = __webpack_require__(12).Options;
var Changeable = __webpack_require__(187);
var ApiOptions = __webpack_require__(12).Options;
var ApiClassNames = __webpack_require__(12).ClassNames;
var KhanAnswerTypes = __webpack_require__(82);
var InlineIcon = __webpack_require__(48);
var InputWithExamples = __webpack_require__(181);
var MathInput = __webpack_require__(194);
var TexButtons = __webpack_require__(195);
var KeypadInput = __webpack_require__(257).components.KeypadInput;
var _require$propTypes = __webpack_require__(257).propTypes,
keypadConfigurationPropType = _require$propTypes.keypadConfigurationPropType,
keypadElementPropType = _require$propTypes.keypadElementPropType;
var KeypadTypes = __webpack_require__(257).consts.KeypadTypes;
var _require = __webpack_require__(52),
linterContextProps = _require.linterContextProps,
linterContextDefault = _require.linterContextDefault;
var _require2 = __webpack_require__(47),
iconExclamationSign = _require2.iconExclamationSign;
var lens = __webpack_require__(180);
var ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
// TODON'T(emily): Don't delete these.
var NO_ANSWERS_WARNING = ["An expression without an answer", "is no expression to me.", "Who can learn from an input", "like the one that I see?", "Put something in there", "won't you please?", "A few digits will do -", "might I suggest some threes?"].join("\n");
var NO_CORRECT_ANSWERS_WARNING = "This question is probably going to be too " + "hard because the expression has no correct answer.";
var SIMPLIFY_WARNING = function SIMPLIFY_WARNING(str) {
return "\"" + str + "\" is required to be simplified but is not considered " + "simplified by our fancy computer algebra system. This will be " + "graded as incorrect.";
};
var PARSE_WARNING = function PARSE_WARNING(str) {
return "\"" + str + "\" <- you sure that's math?";
};
var NOT_SPECIFIED_WARNING = function NOT_SPECIFIED_WARNING(ix) {
return "mind filling in answer " + ix + "? (the blank one)";
};
var insertBraces = function insertBraces(value) {
// HACK(alex): Make sure that all LaTeX super/subscripts are wrapped
// in curly braces to avoid the mismatch between KAS and LaTeX sup/sub
// parsing.
//
// What exactly is this mismatch? Due to its heritage of parsing plain
// text math from , KAS parses "x^12" as x^(12).
// This is both generally what the user expects to happen, and is
// consistent with other computer algebra systems. It is NOT
// consistent with LaTeX however, where x^12 is equivalent to x^{1}2.
//
// Since the only LaTeX we parse comes from MathQuill, this wouldn't
// be a problem if MathQuill just always gave us the latter version
// (with explicit braces). However, instead it always gives the former.
// This behavior is baked in pretty deep; my naive attempts at changing
// it triggered all sorts of confusing errors. So instead we just make
// sure to add in any missing braces before grading MathQuill input.
//
// TODO(alex): Properly hack MathQuill to always use explicit braces.
return value.replace(/([_^])([^{])/g, "$1{$2}");
};
// The new, MathQuill input expression widget
var Expression = React.createClass({
displayName: "Expression",
propTypes: _extends({}, Changeable.propTypes, {
apiOptions: ApiOptions.propTypes,
buttonSets: TexButtons.buttonSetsType,
buttonsVisible: React.PropTypes.oneOf(["always", "never", "focused"]),
functions: React.PropTypes.arrayOf(React.PropTypes.string),
keypadConfiguration: keypadConfigurationPropType,
keypadElement: keypadElementPropType,
times: React.PropTypes.bool,
trackInteraction: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
widgetId: React.PropTypes.string.isRequired,
linterContext: linterContextProps
}),
getDefaultProps: function getDefaultProps() {
return {
value: "",
times: false,
functions: [],
buttonSets: ["basic", "trig", "prealgebra", "logarithms"],
onFocus: function onFocus() {},
onBlur: function onBlur() {},
apiOptions: ApiOptions.defaults,
linterContext: linterContextDefault
};
},
getInitialState: function getInitialState() {
return {
showErrorTooltip: false,
showErrorText: false
};
},
change: function change() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return Changeable.change.apply(this, args);
},
parse: function parse(value, props) {
// TODO(jack): Disable icu for content creators here, or
// make it so that solution answers with ','s or '.'s work
var options = _.pick(props || this.props, "functions");
if (window.icu && window.icu.getDecimalFormatSymbols) {
_.extend(options, window.icu.getDecimalFormatSymbols());
}
return KAS.parse(insertBraces(value), options);
},
render: function render() {
var _this = this;
if (this.props.apiOptions.customKeypad) {
return React.createElement(KeypadInput, {
ref: "input",
value: this.props.value,
keypadElement: this.props.keypadElement,
onChange: this.changeAndTrack,
onFocus: function onFocus() {
_this.props.keypadElement.configure(_this.props.keypadConfiguration, function () {
if (_this.isMounted()) {
_this._handleFocus();
}
});
},
onBlur: this._handleBlur
});
} else if (this.props.apiOptions.staticRender) {
// To make things slightly easier, we just use an InputWithExamples
// component to handle the static rendering, which is the same
// component used by InputNumber and NumericInput
return React.createElement(InputWithExamples, {
ref: "input",
value: this.props.value,
type: "tex",
examples: [],
shouldShowExamples: false,
onChange: this.changeAndTrack,
onFocus: this._handleFocus,
onBlur: this._handleBlur,
id: this.props.widgetId,
linterContext: this.props.linterContext
});
} else {
// TODO(alex): Style this tooltip to be more consistent with other
// tooltips on the site; align to left middle (once possible)
var errorTooltip = React.createElement(
"span",
{ className: "error-tooltip" },
React.createElement(
Tooltip,
{
className: "error-text-container",
horizontalPosition: "right",
horizontalAlign: "left",
verticalPosition: "top",
arrowSize: 10,
borderColor: "#fcc335",
show: this.state.showErrorText
},
React.createElement(
"span",
{
className: "error-icon",
onMouseEnter: function onMouseEnter() {
_this.setState({ showErrorText: true });
},
onMouseLeave: function onMouseLeave() {
_this.setState({ showErrorText: false });
},
onClick: function onClick() {
// TODO(alex): Better error feedback for mobile
_this.setState({
showErrorText: !_this.state.showErrorText
});
}
},
React.createElement(InlineIcon, iconExclamationSign)
),
React.createElement(
"div",
{ className: "error-text" },
ERROR_MESSAGE
)
)
);
var className = classNames({
"perseus-widget-expression": true,
"show-error-tooltip": this.state.showErrorTooltip
});
return React.createElement(
"span",
{ className: className },
React.createElement(MathInput, {
ref: "input",
className: ApiClassNames.INTERACTIVE,
value: this.props.value,
onChange: this.changeAndTrack,
convertDotToTimes: this.props.times,
buttonsVisible: this.props.buttonsVisible || "focused",
buttonSets: this.props.buttonSets,
onFocus: this._handleFocus,
onBlur: this._handleBlur
}),
this.state.showErrorTooltip && errorTooltip
);
}
},
changeAndTrack: function changeAndTrack(e, cb) {
this.change("value", e, cb);
this.props.trackInteraction();
},
_handleFocus: function _handleFocus() {
this.props.onFocus([]);
},
_handleBlur: function _handleBlur() {
this.props.onBlur([]);
},
errorTimeout: null,
// Whenever the input value changes, attempt to parse it.
//
// Clear any errors if this parse succeeds, show an error within a second
// if it fails.
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
var _this2 = this;
if (!_.isEqual(this.props.value, nextProps.value) || !_.isEqual(this.props.functions, nextProps.functions)) {
clearTimeout(this.errorTimeout);
if (this.parse(nextProps.value, nextProps).parsed) {
this.setState({ showErrorTooltip: false });
} else {
// Store timeout ID so that we can clear it above
this.errorTimeout = setTimeout(function () {
var apiResult = _this2.props.apiOptions.onInputError(null, // reserved for some widget identifier
_this2.props.value, ERROR_MESSAGE);
if (apiResult !== false) {
_this2.setState({ showErrorTooltip: true });
}
}, 2000);
}
}
},
componentWillUnmount: function componentWillUnmount() {
clearTimeout(this.errorTimeout);
},
focus: function focus() {
if (this.props.apiOptions.customKeypad) {
this.refs.input.focus();
} else {
// The buttons are often on top of text you're trying to read, so
// don't focus the editor automatically.
}
return true;
},
focusInputPath: function focusInputPath(inputPath) {
this.refs.input.focus();
},
blurInputPath: function blurInputPath(inputPath) {
this.refs.input.blur();
},
// HACK(joel)
insert: function insert(text) {
if (!this.props.apiOptions.staticRender) {
this.refs.input.insert(text);
}
},
getInputPaths: function getInputPaths() {
// The widget itself is an input, so we return a single empty list to
// indicate this.
return [[]];
},
getGrammarTypeForPath: function getGrammarTypeForPath(inputPath) {
return "expression";
},
setInputValue: function setInputValue(path, newValue, cb) {
this.props.onChange({
value: newValue
}, cb);
},
getAcceptableFormatsForInputPath: function getAcceptableFormatsForInputPath() {
// TODO(charlie): What format does the mobile team want this in?
return null;
},
getUserInput: function getUserInput() {
return insertBraces(this.props.value);
},
simpleValidate: function simpleValidate(rubric, onInputError) {
onInputError = onInputError || function () {};
return Expression.validate(this.getUserInput(), rubric, onInputError);
}
});
/* Content creators input a list of answers which are matched from top to
* bottom. The intent is that they can include spcific solutions which should
* be graded as correct or incorrect (or ungraded!) first, then get more
* general.
*
* We iterate through each answer, trying to match it with the user's input
* using the following angorithm:
* - Try to parse the user's input. If it doesn't parse then return "not
* graded".
* - For each answer:
* ~ Try to validate the user's input against the answer. The answer is
* expected to parse.
* ~ If the user's input validates (the validator judges it "correct"), we've
* matched and can stop considering answers.
* - If there were no matches or the matching answer is considered "ungraded",
* show the user an error. TODO(joel) - what error?
* - Otherwise, pass through the resulting points and message.
*/
_.extend(Expression, {
validate: function validate(state, rubric, onInputError) {
var options = _.clone(rubric);
if (window.icu && window.icu.getDecimalFormatSymbols) {
_.extend(options, window.icu.getDecimalFormatSymbols());
}
var createValidator = function createValidator(answer) {
return KhanAnswerTypes.expression.createValidatorFunctional(
// We don't give options to KAS.parse here because that is
// parsing the solution answer, not the student answer, and we
// don't want a solution to work if the student is using a
// different language but not in english.
KAS.parse(answer.value, rubric).expr, _({}).extend(options, {
simplify: answer.simplify,
form: answer.form
}));
};
// find the first result to match the user's input
var result;
var matchingAnswer;
var allEmpty = true;
var foundMatch = !!_(rubric.answerForms).find(function (answer) {
var validate = createValidator(answer);
// save these because they'll be needed if this answer matches
result = validate(state);
matchingAnswer = answer;
allEmpty = allEmpty && result.empty;
// short-circuit as soon as an answer matches
return result.correct;
});
var message = "" || result && result.message;
// now check to see whether it's considered correct, incorrect, or
// ungraded
if (!foundMatch) {
if (allEmpty) {
// If everything graded as empty, it's invalid.
return {
type: "invalid",
message: null
};
} else {
// We fell through all the possibilities and we're not empty,
// so the answer is considered incorrect.
return {
type: "points",
earned: 0,
total: 1
};
}
// we matched an ungraded answer - return "invalid"
} else if (matchingAnswer.considered === "ungraded") {
var apiResult = onInputError(null, // reserved for some widget identifier
state, message);
return {
type: "invalid",
message: apiResult === false ? null : message
};
// The user's input matched one of the answers - is it correct or
// incorrect?
} else {
// TODO(eater): Seems silly to translate result to this
// invalid/points thing and immediately translate it back in
// ItemRenderer.scoreInput()
return {
type: "points",
earned: matchingAnswer.considered === "correct" ? 1 : 0,
total: 1,
message: message
};
}
}
});
/**
* Determine the keypad configuration parameters for the input, based on the
* provided properties.
*
* There are two configuration parameters to be passed to the keypad:
* (1) The keypad type. For the Expression widget, we always use the
* Expression keypad.
* (2) The extra keys; namely, any variables or constants (like Pi) that need
* to be included as keys on the keypad. These are scraped from the answer
* forms.
*/
var keypadConfigurationForProps = function keypadConfigurationForProps(props) {
// Always use the Expression keypad, regardless of the button sets that have
// been enabled.
var keypadType = KeypadTypes.EXPRESSION;
// Extract any and all variables and constants from the answer forms.
var uniqueExtraVariables = {};
var uniqueExtraConstants = {};
for (var _iterator = props.answerForms, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) {
var _ref;
if (_isArray) {
if (_i >= _iterator.length) break;
_ref = _iterator[_i++];
} else {
_i = _iterator.next();
if (_i.done) break;
_ref = _i.value;
}
var answerForm = _ref;
var maybeExpr = KAS.parse(answerForm.value, props);
if (maybeExpr.parsed) {
(function () {
var expr = maybeExpr.expr;
// The keypad expects Greek letters to be capitalized (e.g., it
// requires `PI` instead of `pi`). Right now, it only supports Pi
// and Theta, so we special-case.
var isGreek = function isGreek(symbol) {
return symbol === "pi" || symbol === "theta";
};
var toKey = function toKey(symbol) {
return isGreek(symbol) ? symbol.toUpperCase() : symbol;
};
for (var _iterator2 = expr.getVars(), _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) {
var _ref2;
if (_isArray2) {
if (_i2 >= _iterator2.length) break;
_ref2 = _iterator2[_i2++];
} else {
_i2 = _iterator2.next();
if (_i2.done) break;
_ref2 = _i2.value;
}
var variable = _ref2;
uniqueExtraVariables[toKey(variable)] = true;
}
for (var _iterator3 = expr.getConsts(), _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) {
var _ref3;
if (_isArray3) {
if (_i3 >= _iterator3.length) break;
_ref3 = _iterator3[_i3++];
} else {
_i3 = _iterator3.next();
if (_i3.done) break;
_ref3 = _i3.value;
}
var constant = _ref3;
uniqueExtraConstants[toKey(constant)] = true;
}
})();
}
}
// TODO(charlie): Alert the keypad as to which of these symbols should be
// treated as functions.
var extraVariables = Object.keys(uniqueExtraVariables);
extraVariables.sort();
var extraConstants = Object.keys(uniqueExtraConstants);
extraConstants.sort();
var extraKeys = [].concat(extraVariables, extraConstants);
if (!extraKeys.length) {
// If there are no extra symbols available, we include Pi anyway, so
// that the "extra symbols" button doesn't appear empty.
extraKeys.push("PI");
}
return { keypadType: keypadType, extraKeys: extraKeys };
};
/*
* v0 props follow this schema:
*
* times: bool
* buttonSets: [string]
* functions: [string]
* buttonsVisible: "always" | "focused" | "never"
*
* value: string
* form: bool
* simplify: bool
*
* v1 props follow this schema:
*
* times: bool
* buttonSets: [string]
* functions: [string]
* buttonsVisible: "always" | "focused" | "never"
*
* answerForms: [{
* considered: "correct" | "ungraded" | "incorrect"
* form: bool
* simplify: bool
* value: string
* }]
*/
var propUpgrades = {
1: function _(v0props) {
return {
times: v0props.times,
buttonSets: v0props.buttonSets,
functions: v0props.functions,
buttonsVisible: v0props.buttonsVisible,
answerForms: [{
considered: "correct",
form: v0props.form,
simplify: v0props.simplify,
value: v0props.value,
key: 0
}]
};
}
};
module.exports = {
name: "expression",
displayName: "Expression / Equation",
defaultAlignment: "inline-block",
widget: Expression,
transform: function transform(editorProps) {
var times = editorProps.times,
functions = editorProps.functions,
buttonSets = editorProps.buttonSets,
buttonsVisible = editorProps.buttonsVisible;
return {
keypadConfiguration: keypadConfigurationForProps(editorProps),
times: times,
functions: functions,
buttonSets: buttonSets,
buttonsVisible: buttonsVisible
};
},
version: { major: 1, minor: 0 },
propUpgrades: propUpgrades,
// For use by the editor
Expression: Expression,
isLintable: true
};
/***/ },
/* 74 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable comma-dangle, indent, no-var, object-curly-spacing, one-var, react/forbid-prop-types, react/jsx-closing-bracket-location, react/jsx-indent-props, react/sort-comp, space-infix-ops */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
var lens = __webpack_require__(180);
var Changeable = __webpack_require__(187);
var InfoTip = __webpack_require__(176);
var PropCheckBox = __webpack_require__(90);
var SortableArea = __webpack_require__(196);
var TeX = __webpack_require__(178); // OldExpression only
var TexButtons = __webpack_require__(195);
var Expression = __webpack_require__(73).Expression;
// An answer can be considered correct, wrong, or ungraded.
var CONSIDERED = ["correct", "wrong", "ungraded"];
var answerFormType = React.PropTypes.shape({
considered: React.PropTypes.oneOf(CONSIDERED).isRequired,
value: React.PropTypes.string.isRequired,
form: React.PropTypes.bool.isRequired,
simplify: React.PropTypes.bool.isRequired
});
// Pick a key that isn't currently used by an answer in answerForms
var _makeNewKey = function _makeNewKey(answerForms) {
// first note all the currently used keys in an array, used like a map :3
// note that this automatically updates the array's length property to
// be one past the largest key.
var usedKeys = [];
answerForms.forEach(function (ans) {
usedKeys[ans.key] = true;
});
// then scan through the array to find the first unused (undefined) key
for (var i = 0; i < usedKeys.length; i++) {
if (!usedKeys[i]) {
return i;
}
}
// if we didn't find a key, make one bigger than all the other keys,
// since that's how the length property is defined to work on arrays
return usedKeys.length;
};
var ExpressionEditor = React.createClass({
displayName: "ExpressionEditor",
propTypes: _extends({}, Changeable.propTypes, {
answerForms: React.PropTypes.arrayOf(answerFormType),
times: React.PropTypes.bool,
buttonSets: TexButtons.buttonSetsType,
functions: React.PropTypes.arrayOf(React.PropTypes.string)
}),
getDefaultProps: function getDefaultProps() {
return {
answerForms: [],
times: false,
buttonSets: ["basic"],
functions: ["f", "g", "h"]
};
},
getInitialState: function getInitialState() {
// Is the format of `value` TeX or plain text?
// TODO(alex): Remove after backfilling everything to TeX
// TODO(joel) - sucks if you edit some expression without
// backslashes or curly braces, then come back to the question and
// it's surprisingly not TeX anymore.
var isTex;
// default to TeX if new;
if (this.props.answerForms.length === 0) {
isTex = true;
} else {
isTex = _(this.props.answerForms).any(function (form) {
var value = form.value;
// only TeX has backslashes and curly braces
return _.indexOf(value, "\\") !== -1 || _.indexOf(value, "{") !== -1;
});
}
return { isTex: isTex };
},
change: function change() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return Changeable.change.apply(this, args);
},
render: function render() {
var _this = this;
var answerOptions = this.props.answerForms.map(function (obj, ix) {
var expressionProps = {
// note we're using
// *this.props*.{times,functions,buttonSets} since each
// answer area has the same settings for those
times: _this.props.times,
functions: _this.props.functions,
buttonSets: _this.props.buttonSets,
buttonsVisible: "focused",
form: obj.form,
simplify: obj.simplify,
value: obj.value,
onChange: function onChange(props) {
return _this.updateForm(ix, props);
},
trackInteraction: function trackInteraction() {},
widgetId: _this.props.widgetId + "-" + ix
};
return lens(obj).merge([], {
draggable: true,
onChange: function onChange(props) {
return _this.updateForm(ix, props);
},
onDelete: function onDelete() {
return _this.handleRemoveForm(ix);
},
expressionProps: expressionProps
}).freeze();
}).map(function (obj) {
return React.createElement(AnswerOption, obj);
});
var sortable = React.createElement(SortableArea, {
components: answerOptions,
onReorder: this.handleReorder,
className: "answer-options-list"
});
// checkboxes to choose which sets of input buttons are shown
var buttonSetChoices = _(TexButtons.buttonSets).map(function (set, name) {
// The first one gets special cased to always be checked, disabled,
// and float left.
var isFirst = name === "basic";
var checked = _.contains(_this.props.buttonSets, name) || isFirst;
var className = isFirst ? "button-set-label-float" : "button-set-label";
return React.createElement(
"label",
{ className: className, key: name },
React.createElement("input", {
type: "checkbox",
checked: checked,
disabled: isFirst,
onChange: function onChange() {
return _this.handleButtonSet(name);
}
}),
name
);
});
buttonSetChoices.splice(1, 1, React.createElement(
"label",
{ key: "show-div" },
React.createElement("input", { type: "checkbox", onChange: this.handleToggleDiv }),
React.createElement(
"span",
{ className: "show-div-button" },
"show ",
React.createElement(
TeX,
null,
"\\div"
),
" button"
)
));
return React.createElement(
"div",
{ className: "perseus-widget-expression-editor" },
React.createElement(
"h3",
{ className: "expression-editor-h3" },
"Global Options"
),
React.createElement(
"div",
null,
React.createElement(PropCheckBox, {
times: this.props.times,
onChange: this.props.onChange,
labelAlignment: "right",
label: "Use \xD7 for rendering multiplication instead of a center dot."
}),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"For pre-algebra problems this option displays multiplication as \\times instead of \\cdot in both the rendered output and the acceptable formats examples."
)
)
),
React.createElement(
"div",
null,
React.createElement(
"label",
null,
"Function variables: ",
React.createElement("input", {
type: "text",
defaultValue: this.props.functions.join(" "),
onChange: this.handleFunctions
})
),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"Single-letter variables listed here will be interpreted as functions. This let us know that f(x) means \"f of x\" and not \"f times x\"."
)
)
),
React.createElement(
"div",
null,
React.createElement(
"div",
null,
"Button sets:"
),
buttonSetChoices
),
this.state.isTex && React.createElement(TexButtons, {
className: "math-input-buttons",
sets: this.props.buttonSets,
convertDotToTimes: this.props.times,
onInsert: this.handleTexInsert
}),
React.createElement(
"h3",
{ className: "expression-editor-h3" },
"Answers"
),
React.createElement(
"p",
{ style: { margin: "4px 0" } },
"student responses area matched against these from top to bottom"
),
sortable,
React.createElement(
"div",
null,
React.createElement(
"button",
{
className: "simple-button orange",
style: { fontSize: 13 },
onClick: this.newAnswer,
type: "button"
},
"Add new answer"
)
)
);
},
serialize: function serialize() {
var formSerializables = ["value", "form", "simplify", "considered",
// it's a little weird to serialize the react key, but saves some
// effort reconstructing them when this item is loaded later.
"key"];
var serializables = ["answerForms", "buttonSets", "functions", "times"];
var answerForms = this.props.answerForms.map(function (form) {
return _(form).pick(formSerializables);
});
return lens(this.props).set(["answerForms"], answerForms).mod([], function (props) {
return _(props).pick(serializables);
}).freeze();
},
getSaveWarnings: function getSaveWarnings() {
var _this2 = this;
var issues = [];
if (this.props.answerForms.length === 0) {
issues.push("No answers specified");
} else {
var hasCorrect = !!_(this.props.answerForms).find(function (form) {
return form.considered === "correct";
});
if (!hasCorrect) {
issues.push("No correct answer specified");
}
_(this.props.answerForms).each(function (form, ix) {
if (_this2.props.value === "") {
issues.push("Answer " + (ix + 1) + " is empty");
} else {
// note we're not using icu for content creators
var expression = KAS.parse(form.value);
if (!expression.parsed) {
issues.push("Couldn't parse " + form.value);
} else if (form.simplify && !expression.expr.isSimplified()) {
issues.push(form.value + " isn't simplified, but is required\" +\n \" to be");
}
}
});
// TODO(joel) - warn about:
// - unreachable answers (how??)
// - specific answers following unspecific answers
// - incorrect answers as the final form
}
return issues;
},
_newEmptyAnswerForm: function _newEmptyAnswerForm() {
return {
considered: "correct",
form: false,
// note: the key means "n-th form created" - not "form in
// position n" and will stay the same for the life of this form
key: _makeNewKey(this.props.answerForms),
simplify: false,
value: ""
};
},
newAnswer: function newAnswer() {
var answerForms = this.props.answerForms.slice();
answerForms.push(this._newEmptyAnswerForm());
this.change({ answerForms: answerForms });
},
handleRemoveForm: function handleRemoveForm(i) {
var answerForms = this.props.answerForms.slice();
answerForms.splice(i, 1);
this.change({ answerForms: answerForms });
},
// called when the options (including the expression itself) to an answer
// form change
updateForm: function updateForm(i, props) {
var answerForms = lens(this.props.answerForms).merge([i], props).freeze();
this.change({ answerForms: answerForms });
},
handleReorder: function handleReorder(components) {
var answerForms = _(components).map(function (component) {
var form = _(component.props).pick("considered", "form", "simplify", "value");
form.key = component.key;
return form;
});
this.change({ answerForms: answerForms });
},
// called when the selected buttonset changes
handleButtonSet: function handleButtonSet(changingName) {
var _this3 = this;
var buttonSetNames = _(TexButtons.buttonSets).keys();
// Filter to preserve order - using .union and .difference would always
// move the last added button set to the end.
var buttonSets = _(buttonSetNames).filter(function (set) {
return _(_this3.props.buttonSets).contains(set) !== (set === changingName);
});
this.props.onChange({ buttonSets: buttonSets });
},
handleToggleDiv: function handleToggleDiv() {
// We always want buttonSets to contain exactly one of "basic" and
// "basic+div". Toggle between the two of them.
// If someone can think of a more elegant formulation of this (there
// must be one!) feel free to change it.
var keep, remove;
if (_(this.props.buttonSets).contains("basic+div")) {
keep = "basic";
remove = "basic+div";
} else {
keep = "basic+div";
remove = "basic";
}
var buttonSets = _(this.props.buttonSets).reject(function (set) {
return set === remove;
}).concat(keep);
this.change("buttonSets", buttonSets);
},
// called when the correct answer changes
handleTexInsert: function handleTexInsert(str) {
this.refs.expression.insert(str);
},
// called when the function variables change
handleFunctions: function handleFunctions(e) {
var newProps = {};
newProps.functions = _.compact(e.target.value.split(/[ ,]+/));
this.props.onChange(newProps);
}
});
// Find the next element in arr after val, wrapping around to the first.
var findNextIn = function findNextIn(arr, val) {
var ix = _(arr).indexOf(val);
ix = (ix + 1) % arr.length;
return arr[ix];
};
var AnswerOption = React.createClass({
displayName: "AnswerOption",
propTypes: _extends({}, Changeable.propTypes, {
considered: React.PropTypes.oneOf(CONSIDERED).isRequired,
expressionProps: React.PropTypes.object.isRequired,
// Must the answer have the same form as this answer.
form: React.PropTypes.bool.isRequired,
// Must the answer be simplified.
simplify: React.PropTypes.bool.isRequired,
onChange: React.PropTypes.func.isRequired,
onDelete: React.PropTypes.func.isRequired
}),
getInitialState: function getInitialState() {
return { deleteFocused: false };
},
handleDeleteBlur: function handleDeleteBlur() {
this.setState({ deleteFocused: false });
},
change: function change() {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return Changeable.change.apply(this, args);
},
render: function render() {
var removeButton = null;
if (this.state.deleteFocused) {
removeButton = React.createElement(
"button",
{
type: "button",
className: "simple-button orange",
onClick: this.handleImSure,
onBlur: this.handleDeleteBlur
},
"I'm sure!"
);
} else {
removeButton = React.createElement(
"button",
{
type: "button",
className: "simple-button orange",
onClick: this.handleDelete
},
"Delete"
);
}
return React.createElement(
"div",
{ className: "expression-answer-option" },
React.createElement("div", { className: "answer-handle" }),
React.createElement(
"div",
{ className: "answer-body" },
React.createElement(
"div",
{ className: "answer-considered" },
React.createElement(
"div",
{
onClick: this.toggleConsidered,
className: "answer-status " + this.props.considered
},
this.props.considered
),
React.createElement(
"div",
{ className: "answer-expression" },
React.createElement(Expression, this.props.expressionProps)
)
),
React.createElement(
"div",
{ className: "answer-option" },
React.createElement(PropCheckBox, {
form: this.props.form,
onChange: this.props.onChange,
labelAlignment: "right",
label: "Answer expression must have the same form."
}),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"The student's answer must be in the same form. Commutativity and excess negative signs are ignored."
)
)
),
React.createElement(
"div",
{ className: "answer-option" },
React.createElement(PropCheckBox, {
simplify: this.props.simplify,
onChange: this.props.onChange,
labelAlignment: "right",
label: "Answer expression must be fully expanded and simplified."
}),
React.createElement(
InfoTip,
null,
React.createElement(
"p",
null,
"The student's answer must be fully expanded and simplified. Answering this equation (x^2+2x+1) with this factored equation (x+1)^2 will render this response \"Your answer is not fully expanded and simplified.\""
)
)
),
React.createElement(
"div",
{ className: "remove-container" },
removeButton
)
)
);
},
handleImSure: function handleImSure() {
this.props.onDelete();
},
handleDelete: function handleDelete() {
this.setState({ deleteFocused: true });
},
toggleConsidered: function toggleConsidered() {
var newVal = findNextIn(CONSIDERED, this.props.considered);
this.change({ considered: newVal });
}
});
module.exports = ExpressionEditor;
/***/ },
/* 75 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable comma-dangle, indent, no-undef, no-var, object-curly-spacing, react/forbid-prop-types, react/jsx-closing-bracket-location, react/jsx-indent-props, react/sort-comp */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
var ApiOptions = __webpack_require__(12).Options;
var BaseRadio = __webpack_require__(186);
var Changeable = __webpack_require__(187);
var Editor = __webpack_require__(25);
var _require = __webpack_require__(47),
iconPlus = _require.iconPlus,
iconTrash = _require.iconTrash;
var InlineIcon = __webpack_require__(48);
var PropCheckBox = __webpack_require__(90);
var ChoiceEditor = React.createClass({
displayName: "ChoiceEditor",
propTypes: {
apiOptions: ApiOptions.propTypes,
choice: React.PropTypes.object,
showDelete: React.PropTypes.bool,
onClueChange: React.PropTypes.func,
onContentChange: React.PropTypes.func,
onDelete: React.PropTypes.func
},
render: function render() {
var checkedClass = this.props.choice.correct ? "correct" : "incorrect";
var placeholder = "Type a choice here...";
if (this.props.choice.isNoneOfTheAbove) {
placeholder = this.props.choice.correct ? "Type the answer to reveal to the user..." : "None of the above";
}
var editor = React.createElement(Editor, {
ref: "content-editor",
apiOptions: this.props.apiOptions,
content: this.props.choice.content || "",
widgetEnabled: false,
placeholder: placeholder,
disabled: this.props.choice.isNoneOfTheAbove && !this.props.choice.correct,
onChange: this.props.onContentChange
});
var clueEditor = React.createElement(Editor, {
ref: "clue-editor",
apiOptions: this.props.apiOptions,
content: this.props.choice.clue || "",
widgetEnabled: false,
placeholder: i18n._("Why is this choice " + checkedClass + "?"),
onChange: this.props.onClueChange
});
var deleteLink = React.createElement(
"a",
{
className: "simple-button orange delete-choice",
href: "#",
onClick: this.props.onDelete,
title: "Remove this choice"
},
React.createElement(InlineIcon, iconTrash)
);
return React.createElement(
"div",
{ className: "choice-clue-editors" },
React.createElement(
"div",
{ className: "choice-editor " + checkedClass },
editor
),
React.createElement(
"div",
{ className: "clue-editor" },
clueEditor
),
this.props.showDelete && deleteLink
);
}
});
var RadioEditor = React.createClass({
displayName: "RadioEditor",
propTypes: _extends({}, Changeable.propTypes, {
apiOptions: ApiOptions.propTypes,
choices: React.PropTypes.arrayOf(React.PropTypes.shape({
content: React.PropTypes.string,
clue: React.PropTypes.string,
correct: React.PropTypes.bool
})),
displayCount: React.PropTypes.number,
randomize: React.PropTypes.bool,
hasNoneOfTheAbove: React.PropTypes.bool,
multipleSelect: React.PropTypes.bool,
countChoices: React.PropTypes.bool,
// TODO(kevinb): DEPRECATED: This is be used to force deselectEnabled
// behavior on mobile but not on desktop. When enabled, the user can
// deselect a radio input by tapping on it again.
deselectEnabled: React.PropTypes.bool,
static: React.PropTypes.bool
}),
getDefaultProps: function getDefaultProps() {
return {
choices: [{}, {}],
displayCount: null,
randomize: false,
hasNoneOfTheAbove: false,
multipleSelect: false,
countChoices: false,
deselectEnabled: false
};
},
render: function render() {
var numCorrect = _.reduce(this.props.choices, function (memo, choice) {
return choice.correct ? memo + 1 : memo;
}, 0);
return React.createElement(
"div",
null,
React.createElement(
"div",
{ className: "perseus-widget-row" },
React.createElement(
"a",
{
href: "https://docs.google.com/document/d/1frZf7yrWVWb1n4tVjqlzqVUiv1pn4cZXbxgP62-JDBY/edit#heading=h.8ng1isya19nu",
target: "_blank"
},
"Multiple choice style guide"
),
React.createElement("br", null),
React.createElement(
"div",
{ className: "perseus-widget-left-col" },
React.createElement(PropCheckBox, {
label: "Multiple selections",
labelAlignment: "right",
multipleSelect: this.props.multipleSelect,
onChange: this.onMultipleSelectChange
})
),
React.createElement(
"div",
{ className: "perseus-widget-right-col" },
React.createElement(PropCheckBox, {
label: "Randomize order",
labelAlignment: "right",
randomize: this.props.randomize,
onChange: this.props.onChange
})
),
this.props.multipleSelect && React.createElement(
"div",
{ className: "perseus-widget-left-col" },
React.createElement(PropCheckBox, {
label: "Specify number correct",
labelAlignment: "right",
countChoices: this.props.countChoices,
onChange: this.onCountChoicesChange
})
)
),
React.createElement(BaseRadio, {
ref: "baseRadio",
multipleSelect: this.props.multipleSelect,
countChoices: this.props.countChoices,
numCorrect: numCorrect,
editMode: true,
labelWrap: false,
apiOptions: this.props.apiOptions,
choices: this.props.choices.map(function (choice, i) {
var _this = this;
return {
content: React.createElement(ChoiceEditor, {
ref: "choice-editor" + i,
apiOptions: this.props.apiOptions,
choice: choice,
onContentChange: function onContentChange(newProps) {
if ("content" in newProps) {
_this.onContentChange(i, newProps.content);
}
},
onClueChange: function onClueChange(newProps) {
if ("content" in newProps) {
_this.onClueChange(i, newProps.content);
}
},
onDelete: this.onDelete.bind(this, i),
showDelete: this.props.choices.length >= 2
}),
isNoneOfTheAbove: choice.isNoneOfTheAbove,
checked: choice.correct
};
}, this),
onChange: this.onChange
}),
React.createElement(
"div",
{ className: "add-choice-container" },
React.createElement(
"a",
{
className: "simple-button orange",
href: "#",
onClick: this.addChoice.bind(this, false)
},
React.createElement(InlineIcon, iconPlus),
" Add a choice",
" "
),
!this.props.hasNoneOfTheAbove && React.createElement(
"a",
{
className: "simple-button",
href: "#",
onClick: this.addChoice.bind(this, true)
},
React.createElement(InlineIcon, iconPlus),
" None of the above",
" "
)
)
);
},
change: function change() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return Changeable.change.apply(this, args);
},
onMultipleSelectChange: function onMultipleSelectChange(allowMultiple) {
allowMultiple = allowMultiple.multipleSelect;
var numCorrect = _.reduce(this.props.choices, function (memo, choice) {
return choice.correct ? memo + 1 : memo;
}, 0);
if (!allowMultiple && numCorrect > 1) {
var choices = _.map(this.props.choices, function (choice) {
return _.defaults({
correct: false
}, choice);
});
this.props.onChange({
multipleSelect: allowMultiple,
choices: choices
});
} else {
this.props.onChange({
multipleSelect: allowMultiple
});
}
},
onCountChoicesChange: function onCountChoicesChange(count) {
count = count.countChoices;
this.props.onChange({ countChoices: count });
},
onChange: function onChange(_ref) {
var checked = _ref.checked;
var choices = _.map(this.props.choices, function (choice, i) {
return _.extend({}, choice, {
correct: checked[i],
content: choice.isNoneOfTheAbove && !checked[i] ? "" : choice.content
});
});
this.props.onChange({ choices: choices });
},
onContentChange: function onContentChange(choiceIndex, newContent) {
var choices = this.props.choices.slice();
choices[choiceIndex] = _.extend({}, choices[choiceIndex], {
content: newContent
});
this.props.onChange({ choices: choices });
},
onClueChange: function onClueChange(choiceIndex, newClue) {
var choices = this.props.choices.slice();
choices[choiceIndex] = _.extend({}, choices[choiceIndex], {
clue: newClue
});
if (newClue === "") {
delete choices[choiceIndex].clue;
}
this.props.onChange({ choices: choices });
},
onDelete: function onDelete(choiceIndex, e) {
e.preventDefault();
var choices = this.props.choices.slice();
var deleted = choices[choiceIndex];
choices.splice(choiceIndex, 1);
this.props.onChange({
choices: choices,
hasNoneOfTheAbove: this.props.hasNoneOfTheAbove && !deleted.isNoneOfTheAbove
});
},
addChoice: function addChoice(noneOfTheAbove, e) {
var _this2 = this;
e.preventDefault();
var choices = this.props.choices.slice();
var newChoice = { isNoneOfTheAbove: noneOfTheAbove };
var addIndex = choices.length - (this.props.hasNoneOfTheAbove ? 1 : 0);
choices.splice(addIndex, 0, newChoice);
this.props.onChange({
choices: choices,
hasNoneOfTheAbove: noneOfTheAbove || this.props.hasNoneOfTheAbove
}, function () {
_this2.refs["choice-editor" + addIndex].refs["content-editor"].focus();
});
},
setDisplayCount: function setDisplayCount(num) {
this.props.onChange({ displayCount: num });
},
focus: function focus() {
this.refs["choice-editor0"].refs["content-editor"].focus();
return true;
},
getSaveWarnings: function getSaveWarnings() {
if (!_.some(_.pluck(this.props.choices, "correct"))) {
return ["No choice is marked as correct."];
}
return [];
},
serialize: function serialize() {
return _.pick(this.props, "choices", "randomize", "multipleSelect", "countChoices", "displayCount", "hasNoneOfTheAbove", "deselectEnabled");
}
});
module.exports = RadioEditor;
/***/ },
/* 76 */
/***/ function(module, exports, __webpack_require__) {
/**
* A default set of media queries to use for different screen sizes. Based on
* the breakpoints from purecss.
*
* Use like:
* StyleSheet.create({
* blah: {
* [mediaQueries.xs]: {
*
* },
* },
* });
*/
var _require = __webpack_require__(77),
pureXsMax = _require.pureXsMax,
pureSmMin = _require.pureSmMin,
pureSmMax = _require.pureSmMax,
pureMdMin = _require.pureMdMin,
pureMdMax = _require.pureMdMax,
pureLgMin = _require.pureLgMin,
pureLgMax = _require.pureLgMax,
pureXlMin = _require.pureXlMin;
module.exports = {
xs: "@media screen and (max-width: " + pureXsMax + ")",
sm: "@media screen and (min-width: " + pureSmMin + ") and " + ("(max-width: " + pureSmMax + ")"),
md: "@media screen and (min-width: " + pureMdMin + ") and " + ("(max-width: " + pureMdMax + ")"),
lg: "@media screen and (min-width: " + pureLgMin + ") and " + ("(max-width: " + pureLgMax + ")"),
xl: "@media screen and (min-width: " + pureXlMin + ")",
smOrSmaller: "@media screen and (max-width: " + pureSmMax + ")",
mdOrSmaller: "@media screen and (max-width: " + pureMdMax + ")",
lgOrSmaller: "@media screen and (max-width: " + pureLgMax + ")",
smOrLarger: "@media screen and (min-width: " + pureSmMin + ")",
mdOrLarger: "@media screen and (min-width: " + pureMdMin + ")",
lgOrLarger: "@media screen and (min-width: " + pureLgMin + ")"
};
/***/ },
/* 77 */
/***/ function(module, exports, __webpack_require__) {
// Generated by running:
// `node less-to-js.js 'stylesheets/exercise-content-package/variables.less'`
module.exports = {
// @baseFontFamily: "Proxima Nova", sans-serif;
baseFontFamily: "'Proxima Nova',sans-serif",
// @boldFontFamily: "Proxima Nova Semibold", sans-serif;
boldFontFamily: "'Proxima Nova Semibold',sans-serif",
// @green: #76a005;
green: "#76A005",
// @kaGreen: #71b307;
kaGreen: "#71B307",
// @blue: #1c758a;
blue: "#1C758A",
// @gray: #aaa;
gray: "#AAAAAA",
// @red: #ffbaba;
red: "#FFBABA",
// @questionWidth: 480px;
questionWidth: "480px",
// @grayLight: #aaa;
grayLight: "#AAAAAA",
// @grayLighter: #ddd;
grayLighter: "#DDDDDD",
// @learnstormBlue: #4898fc;
learnstormBlue: "#4898FC",
white: '#FFFFFF',
gray98: '#FAFAFA',
gray97: '#F6F7F7',
gray95: '#F0F1F2',
gray90: '#E3E5E6',
gray85: '#D6D8DA',
gray76: '#BABEC2',
gray68: '#888D93',
gray41: '#626569',
gray25: '#3B3e40',
gray17: '#21242c',
black: '#000000',
warning1: '#F86700',
warning3: '#C75300',
// @pure-sm-min: 568px;
pureSmMin: "568px",
// @pure-md-min: 768px;
pureMdMin: "768px",
// @pure-lg-min: 1024px;
pureLgMin: "1024px",
// @pure-xl-min: 1280px;
pureXlMin: "1280px",
// @pure-xs-max: (@pure-sm-min - 1);
pureXsMax: "567px",
// @pure-sm-max: (@pure-md-min - 1);
pureSmMax: "767px",
// @pure-md-max: (@pure-lg-min - 1);
pureMdMax: "1023px",
// @pure-lg-max: (@pure-xl-min - 1);
pureLgMax: "1279px",
// @tableBackgroundAccent: #f9f9f9; // for striping
tableBackgroundAccent: "#F9F9F9",
gtpBlue: "#4c00ff",
gtpIncorrectColor: "#babec2",
gtpCorrectColor: "#ffbe26",
// @satBlue: #0084ce;
satBlue: "#0084CE",
// @satSelectedBackgroundColor: #e4f3f9;
satSelectedBackgroundColor: "#E4F3F9",
// @satActiveBackgroundColor: #d0edf4;
satActiveBackgroundColor: "#D0EDF4",
// @satCorrectColor: #009900;
satCorrectColor: "#009900",
// @satCorrectBorderColor: #00cc00;
satCorrectBorderColor: "#00CC00",
// @satCorrectBackgroundColor: #e4f7e4;
satCorrectBackgroundColor: "#E4F7E4",
// @satIncorrectColor: #990000;
satIncorrectColor: "#990000",
// @satIncorrectBorderColor: #cc5252;
satIncorrectBorderColor: "#CC5252",
// @satIncorrectBackgroundColor: #f2ebeb;
satIncorrectBackgroundColor: "#F2EBEB",
// @zIndexScratchPad: 1;
zIndexScratchPad: "1",
// @zIndexAboveScratchpad: @zIndexScratchPad + 1;
zIndexAboveScratchpad: "2",
// @zIndexInteractiveComponent: @zIndexAboveScratchpad + 1;
zIndexInteractiveComponent: "3",
// @zIndexCurrentlyDragging: @zIndexInteractiveComponent + 1;
zIndexCurrentlyDragging: "4",
// @zIndexCalculator: @zIndexCurrentlyDragging + 1;
zIndexCalculator: "5",
// @phoneMargin: 16px;
phoneMargin: 16,
negativePhoneMargin: -16,
hintBorderWidth: 4,
// The 'base unit' -- our new typography and layout styles are defined in
// terms of multiples of the 'base unit'.
baseUnitPx: 16,
interactiveSizes: {
defaultBoxSize: 400,
defaultBoxSizeSmall: 288
},
circleSize: 24,
radioMarginWidth: 2,
warningColor: "#f86700",
warningColorHover: "#df5c00",
warningColorActive: "#c75300",
publishBlockingErrorColor: "#be2612"
};
module.exports.radioBorderColor = module.exports.gray76;
module.exports.checkedColor = module.exports.kaGreen;
/***/ },
/* 78 */
/***/ function(module, exports, __webpack_require__) {
var _responsiveLabel;
/* eslint-disable object-curly-spacing */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
var _require = __webpack_require__(79),
StyleSheet = _require.StyleSheet;
var mediaQueries = __webpack_require__(76);
var _require2 = __webpack_require__(77),
zIndexAboveScratchpad = _require2.zIndexAboveScratchpad,
zIndexInteractiveComponent = _require2.zIndexInteractiveComponent,
radioBorderColor = _require2.radioBorderColor,
checkedColor = _require2.checkedColor,
circleSize = _require2.circleSize,
radioMarginWidth = _require2.radioMarginWidth;
module.exports = StyleSheet.create({
perseusInteractive: {
zIndex: zIndexInteractiveComponent,
position: "relative"
},
aboveScratchpad: {
position: "relative",
zIndex: zIndexAboveScratchpad
},
blankBackground: {
// TODO(emily): Use KhanUtil._BACKGROUND?
backgroundColor: "#FDFDFD"
},
perseusSrOnly: {
border: 0,
clip: "rect(0,0,0,0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
width: 1
},
responsiveLabel: (_responsiveLabel = {}, _responsiveLabel[mediaQueries.smOrSmaller] = {
fontSize: 14,
lineHeight: 1.3
}, _responsiveLabel[mediaQueries.md] = {
fontSize: 17,
lineHeight: 1.4
}, _responsiveLabel[mediaQueries.lgOrLarger] = {
fontSize: 20,
lineHeight: 1.4
}, _responsiveLabel),
responsiveInput: {
display: "inline-block",
WebkitAppearance: "none",
appearance: "none",
"::-ms-check": {
display: "none"
},
backgroundColor: "#fff",
border: "2px solid #fff",
boxShadow: "0 0px 0px 1px " + radioBorderColor,
outline: "none",
boxSizing: "border-box",
flexShrink: 0,
marginBottom: 1,
marginLeft: 1,
marginRight: 1,
marginTop: 1,
height: circleSize - 2,
width: circleSize - 2
},
responsiveRadioInput: {
borderRadius: "50%",
":checked": {
backgroundColor: checkedColor,
border: "none",
borderRadius: "50%",
boxShadow: "inset 0px 0px 0px 2px white, " + ("0 0px 0px 2px " + checkedColor),
marginTop: radioMarginWidth,
marginBottom: radioMarginWidth,
marginLeft: radioMarginWidth,
marginRight: radioMarginWidth,
height: circleSize - 2 * radioMarginWidth,
width: circleSize - 2 * radioMarginWidth
}
},
responsiveRadioInputActive: {
backgroundColor: "#fff",
border: "2px solid #fff",
borderRadius: "50%",
boxShadow: "0 0px 0px 2px " + checkedColor,
marginTop: radioMarginWidth,
marginBottom: radioMarginWidth,
marginLeft: radioMarginWidth,
marginRight: radioMarginWidth,
height: circleSize - 2 * radioMarginWidth,
width: circleSize - 2 * radioMarginWidth,
":checked": {
backgroundColor: "#fff"
}
},
disableTextSelection: {
userSelect: 'none'
}
});
/***/ },
/* 79 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
var _util = __webpack_require__(174);
var _inject = __webpack_require__(175);
var StyleSheet = {
create: function create(sheetDefinition) {
return (0, _util.mapObj)(sheetDefinition, function (_ref) {
var _ref2 = _slicedToArray(_ref, 2);
var key = _ref2[0];
var val = _ref2[1];
return [key, {
// TODO(emily): Make a 'production' mode which doesn't prepend
// the class name here, to make the generated CSS smaller.
_name: key + '_' + (0, _util.hashObject)(val),
_definition: val
}];
});
},
rehydrate: function rehydrate() {
var renderedClassNames = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0];
(0, _inject.addRenderedClassNames)(renderedClassNames);
}
};
var StyleSheetServer = {
renderStatic: function renderStatic(renderFunc) {
(0, _inject.reset)();
(0, _inject.startBuffering)();
var html = renderFunc();
var cssContent = (0, _inject.flushToString)();
return {
html: html,
css: {
content: cssContent,
renderedClassNames: (0, _inject.getRenderedClassNames)()
}
};
}
};
var css = function css() {
for (var _len = arguments.length, styleDefinitions = Array(_len), _key = 0; _key < _len; _key++) {
styleDefinitions[_key] = arguments[_key];
}
// Filter out falsy values from the input, to allow for
// `css(a, test && c)`
var validDefinitions = styleDefinitions.filter(function (def) {
return def;
});
// Break if there aren't any valid styles.
if (validDefinitions.length === 0) {
return "";
}
var className = validDefinitions.map(function (s) {
return s._name;
}).join("-o_O-");
(0, _inject.injectStyleOnce)(className, '.' + className, validDefinitions.map(function (d) {
return d._definition;
}));
return className;
};
exports['default'] = {
StyleSheet: StyleSheet,
StyleSheetServer: StyleSheetServer,
css: css
};
module.exports = exports['default'];
/***/ },
/* 80 */
/***/ function(module, exports, __webpack_require__) {
/**
* A work-in-progress of _ methods for objects.
* That is, they take an object as a parameter,
* and return an object instead of an array.
*
* TODO(aria): Move this out of interactive2
*/
var _ = __webpack_require__(56);
/**
* Does a pluck on keys inside objects in an object
*
* Ex:
* tools = {
* translation: {
* enabled: true
* },
* rotation: {
* enabled: false
* }
* };
* pluckObject(tools, "enabled") returns {
* translation: true
* rotation: false
* }
*/
var pluck = function pluck(table, subKey) {
return _.object(_.map(table, function (value, key) {
return [key, value[subKey]];
}));
};
/**
* Maps an object to an object
*
* > mapObject({a: '1', b: '2'}, (value, key) => {
* return value + 1;
* });
* {a: 2, b: 3}
*/
var mapObject = function mapObject(obj, lambda) {
var result = {};
_.each(_.keys(obj), function (key) {
result[key] = lambda(obj[key], key);
});
return result;
};
/**
* Maps an array to an object
*
* > mapObjectFromArray(['a', 'b'], function(elem) {
* return elem + elem;
* });
* {a: 'aa', b: 'bb'}
*/
var mapObjectFromArray = function mapObjectFromArray(arr, lambda) {
var result = {};
_.each(arr, function (elem) {
result[elem] = lambda(elem);
});
return result;
};
module.exports = {
pluck: pluck,
mapObject: mapObject,
mapObjectFromArray: mapObjectFromArray
};
/***/ },
/* 81 */,
/* 82 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable object-curly-spacing */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
/* global i18n:false */
var $ = __webpack_require__(169);
var _ = __webpack_require__(56);
var KhanMath = __webpack_require__(208);
var MAXERROR_EPSILON = Math.pow(2, -42);
/*
* Answer types
*
* Utility for creating answerable questions displayed in exercises
*
* Different answer types produce different kinds of input displays, and do
* different kinds of checking on the solutions.
*
* Each of the objects contain two functions, setup and createValidator.
*
* The setup function takes a solutionarea and solution, and performs setup
* within the solutionarea, and then returns an object which contains:
*
* answer: a function which, when called, will retrieve the current answer from
* the solutionarea, which can then be validated using the validator
* function
* validator: a function returned from the createValidator function (defined
* below)
* solution: the correct answer to the problem
* showGuess: a function which, when given a guess, shows the guess within the
* provided solutionarea
* showGuessCustom: a function which displays parts of a guess that are not
* within the solutionarea; currently only used for custom
* answers
*
* The createValidator function only takes a solution, and it returns a
* function which can be used to validate an answer.
*
* The resulting validator function returns:
* - true: if the answer is fully correct
* - false: if the answer is incorrect
* - "" (the empty string): if no answer has been provided (e.g. the answer box
* is left unfilled)
* - a string: if there is some slight error
*
* In most cases, setup and createValidator don't really need the solution DOM
* element so we have setupFunctional and createValidatorFunctional for them
* which take only $solution.text() and $solution.data(). This makes it easier
* to reuse specific answer types.
*
* TODO(alpert): Think of a less-absurd name for createValidatorFunctional.
*
*/
var KhanAnswerTypes = {
/*
* predicate answer type
*
* performs simple predicate-based checking of a numeric solution, with
* different kinds of number formats
*
* Uses the data-forms option on the solution to choose which number formats
* are acceptable. Available data-forms:
*
* - integer: 3
* - proper: 3/5
* - improper: 5/3
* - pi: 3 pi
* - log: log(5)
* - percent: 15%
* - mixed: 1 1/3
* - decimal: 1.7
*
* The solution should be a predicate of the form:
*
* function(guess, maxError) {
* return abs(guess - 3) < maxError;
* }
*
*/
predicate: {
defaultForms: "integer, proper, improper, mixed, decimal",
createValidatorFunctional: function createValidatorFunctional(predicate, options) {
// Extract the options from the given solution object
options = _.extend({
simplify: "required",
ratio: false,
forms: KhanAnswerTypes.predicate.defaultForms
}, options);
var acceptableForms = void 0;
// this is maintaining backwards compatibility
// TODO(merlob) fix all places that depend on this, then delete
if (!_.isArray(options.forms)) {
acceptableForms = options.forms.split(/\s*,\s*/);
} else {
acceptableForms = options.forms;
}
// TODO(jack): remove options.inexact in favor of options.maxError
if (options.inexact === undefined) {
// If we aren't allowing inexact, ensure that we don't have a
// large maxError as well.
options.maxError = 0;
}
// Allow a small tolerance on maxError, to avoid numerical
// representation issues (2.3 should be correct for a solution of
// 2.45 with maxError=0.15).
options.maxError = +options.maxError + MAXERROR_EPSILON;
// If percent is an acceptable form, make sure it's the last one
// in the list so we don't prematurely complain about not having
// a percent sign when the user entered the correct answer in a
// different form (such as a decimal or fraction)
if (_.contains(acceptableForms, "percent")) {
acceptableForms = _.without(acceptableForms, "percent");
acceptableForms.push("percent");
}
// Take text looking like a fraction, and turn it into a number
var fractionTransformer = function fractionTransformer(text) {
text = text
// Replace unicode minus sign with hyphen
.replace(/\u2212/, "-")
// Remove space after +, -
.replace(/([+-])\s+/g, "$1")
// Remove leading/trailing whitespace
.replace(/(^\s*)|(\s*$)/gi, "");
// Extract numerator and denominator
var match = text.match(/^([+-]?\d+)\s*\/\s*([+-]?\d+)$/);
var parsedInt = parseInt(text, 10);
if (match) {
var num = parseFloat(match[1]);
var denom = parseFloat(match[2]);
var simplified = denom > 0 && (options.ratio || match[2] !== "1") && KhanMath.getGCD(num, denom) === 1;
return [{
value: num / denom,
exact: simplified
}];
} else if (!isNaN(parsedInt) && "" + parsedInt === text) {
return [{
value: parsedInt,
exact: true
}];
}
return [];
};
/*
* Different forms of numbers
*
* Each function returns a list of objects of the form:
*
* {
* value: numerical value,
* exact: true/false
* }
*/
var forms = {
// integer, which is encompassed by decimal
integer: function integer(text) {
// Compare the decimal form to the decimal form rounded to
// an integer. Only accept if the user actually entered an
// integer.
var decimal = forms.decimal(text);
var rounded = forms.decimal(text, 1);
if (decimal[0].value != null && decimal[0].value === rounded[0].value || decimal[1].value != null && decimal[1].value === rounded[1].value) {
return decimal;
}
return [];
},
// A proper fraction
proper: function proper(text) {
return $.map(fractionTransformer(text), function (o) {
// All fractions that are less than 1
if (Math.abs(o.value) < 1) {
return [o];
} else {
return [];
}
});
},
// an improper fraction
improper: function improper(text) {
return $.map(fractionTransformer(text), function (o) {
// All fractions that are greater than 1
if (Math.abs(o.value) >= 1) {
return [o];
} else {
return [];
}
});
},
// pi-like numbers
pi: function pi(text) {
var match = void 0;
var possibilities = [];
// Replace unicode minus sign with hyphen
text = text.replace(/\u2212/, "-");
// - pi
// (Note: we also support \pi (for TeX), p, tau (and \tau,
// and t), pau.)
if (match = text.match(/^([+-]?)\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)) {
possibilities = [{
value: parseFloat(match[1] + "1"),
exact: true
}];
// 5 / 6 pi
} else if (match = text.match(/^([+-]?\s*\d+\s*(?:\/\s*[+-]?\s*\d+)?)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i // eslint-disable-line max-len
)) {
possibilities = fractionTransformer(match[1]);
// 4 5 / 6 pi
} else if (match = text.match(/^([+-]?)\s*(\d+)\s*([+-]?\d+)\s*\/\s*([+-]?\d+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i // eslint-disable-line max-len
)) {
var sign = parseFloat(match[1] + "1");
var integ = parseFloat(match[2]);
var num = parseFloat(match[3]);
var denom = parseFloat(match[4]);
var simplified = num < denom && KhanMath.getGCD(num, denom) === 1;
possibilities = [{
value: sign * (integ + num / denom),
exact: simplified
}];
// 5 pi / 6
} else if (match = text.match(/^([+-]?\s*\d+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)\s*(?:\/\s*([+-]?\s*\d+))?$/i // eslint-disable-line max-len
)) {
possibilities = fractionTransformer(match[1] + "/" + match[3]);
// - pi / 4
} else if (match = text.match(/^([+-]?)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)\s*(?:\/\s*([+-]?\d+))?$/i // eslint-disable-line max-len
)) {
possibilities = fractionTransformer(match[1] + "1/" + match[3]);
// 0
} else if (text === "0") {
possibilities = [{ value: 0, exact: true }];
// 0.5 pi (fallback)
} else if (match = text.match(/^(.+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i // eslint-disable-line max-len
)) {
possibilities = forms.decimal(match[1]);
} else {
possibilities = _.reduce(KhanAnswerTypes.predicate.defaultForms.split(/\s*,\s*/), function (memo, form) {
return memo.concat(forms[form](text));
}, []);
// If the answer is a floating point number that's
// near a multiple of pi, mark is as being possibly
// an approximation of pi. We actually check if
// it's a plausible approximation of pi/12, since
// sometimes the correct answer is like pi/3 or pi/4.
// We also say it's a pi-approximation if it involves
// x/7 (since 22/7 is an approximation of pi.)
// Never mark an integer as being an approximation
// of pi.
var approximatesPi = false;
var number = parseFloat(text);
if (!isNaN(number) && number !== parseInt(text)) {
var piMult = Math.PI / 12;
var roundedNumber = piMult * Math.round(number / piMult);
if (Math.abs(number - roundedNumber) < 0.01) {
approximatesPi = true;
}
} else if (text.match(/\/\s*7/)) {
approximatesPi = true;
}
if (approximatesPi) {
_.each(possibilities, function (possibility) {
possibility.piApprox = true;
});
}
return possibilities;
}
var multiplier = Math.PI;
if (text.match(/\\?tau|t|\u03c4/)) {
multiplier = Math.PI * 2;
}
// We're taking an early stand along side xkcd in the
// inevitable ti vs. pau debate... http://xkcd.com/1292
if (text.match(/pau/)) {
multiplier = Math.PI * 1.5;
}
$.each(possibilities, function (ix, possibility) {
possibility.value *= multiplier;
});
return possibilities;
},
// Converts '' to 1 and '-' to -1 so you can write "[___] x"
// and accept sane things
coefficient: function coefficient(text) {
var possibilities = [];
// Replace unicode minus sign with hyphen
text = text.replace(/\u2212/, "-");
if (text === "") {
possibilities = [{ value: 1, exact: true }];
} else if (text === "-") {
possibilities = [{ value: -1, exact: true }];
}
return possibilities;
},
// simple log(c) form
log: function log(text) {
var match = void 0;
var possibilities = [];
// Replace unicode minus sign with hyphen
text = text.replace(/\u2212/, "-");
text = text.replace(/[ \(\)]/g, "");
if (match = text.match(/^log\s*(\S+)\s*$/i)) {
possibilities = forms.decimal(match[1]);
} else if (text === "0") {
possibilities = [{ value: 0, exact: true }];
}
return possibilities;
},
// Numbers with percent signs
percent: function percent(text) {
text = $.trim(text);
// store whether or not there is a percent sign
var hasPercentSign = false;
if (text.indexOf("%") === text.length - 1) {
text = $.trim(text.substring(0, text.length - 1));
hasPercentSign = true;
}
var transformed = forms.decimal(text);
$.each(transformed, function (ix, t) {
t.exact = hasPercentSign;
t.value = t.value / 100;
});
return transformed;
},
// Mixed numbers, like 1 3/4
mixed: function mixed(text) {
var match = text
// Replace unicode minus sign with hyphen
.replace(/\u2212/, "-")
// Remove space after +, -
.replace(/([+-])\s+/g, "$1")
// Extract integer, numerator and denominator
.match(/^([+-]?)(\d+)\s+(\d+)\s*\/\s*(\d+)$/);
if (match) {
var sign = parseFloat(match[1] + "1");
var integ = parseFloat(match[2]);
var num = parseFloat(match[3]);
var denom = parseFloat(match[4]);
var simplified = num < denom && KhanMath.getGCD(num, denom) === 1;
return [{
value: sign * (integ + num / denom),
exact: simplified
}];
}
return [];
},
// Decimal numbers -- compare entered text rounded to
// 'precision' Reciprical of the precision against the correct
// answer. We round to 1/1e10 by default, which is healthily
// less than machine epsilon but should be more than any real
// decimal answer would use. (The 'integer' answer type uses
// precision == 1.)
decimal: function decimal(text, precision) {
if (precision == null) {
precision = 1e10;
}
var normal = function normal(text) {
text = $.trim(text);
var match = text
// Replace unicode minus sign with hyphen
.replace(/\u2212/, "-")
// Remove space after +, -
.replace(/([+-])\s+/g, "$1")
// Extract integer, numerator and denominator. If
// commas or spaces are used, they must be in the
// "correct" places
.match(/^([+-]?(?:\d{1,3}(?:[, ]?\d{3})*\.?|\d{0,3}(?:[, ]?\d{3})*\.(?:\d{3}[, ]?)*\d{1,3}))$/ // eslint-disable-line max-len
);
// You can't start a number with `0,`, to prevent us
// interpeting '0.342' as correct for '342'
var badLeadingZero = text.match(/^0[0,]*,/);
if (match && !badLeadingZero) {
var x = parseFloat(match[1].replace(/[, ]/g, ""));
if (options.inexact === undefined) {
x = Math.round(x * precision) / precision;
}
return x;
}
};
var commas = function commas(text) {
text = text.replace(/([\.,])/g, function (_, c) {
return c === "." ? "," : ".";
});
return normal(text);
};
return [{ value: normal(text), exact: true }, { value: commas(text), exact: true }];
}
};
// validator function
return function (guess) {
// The fallback variable is used in place of the answer, if no
// answer is provided (i.e. the field is left blank)
var fallback = options.fallback != null ? "" + options.fallback : "";
guess = $.trim(guess) || fallback;
var score = {
empty: guess === "",
correct: false,
message: null,
guess: guess
};
// iterate over all the acceptable forms, and if one of the
// answers is correct, return true
$.each(acceptableForms, function (i, form) {
var transformed = forms[form](guess);
for (var j = 0, l = transformed.length; j < l; j++) {
var val = transformed[j].value;
var exact = transformed[j].exact;
var piApprox = transformed[j].piApprox;
// If a string was returned, and it exactly matches,
// return true
if (predicate(val, options.maxError)) {
// If the exact correct number was returned,
// return true
if (exact || options.simplify === "optional") {
score.correct = true;
score.message = options.message || null;
// If the answer is correct, don't say it's
// empty. This happens, for example, with the
// coefficient type where guess === "" but is
// interpreted as "1" which is correct.
score.empty = false;
} else if (form === "percent") {
// Otherwise, an error was returned
score.empty = true;
score.message = i18n._("Your answer is almost correct, " + "but it is missing a " + "\\% at the end.");
} else {
if (options.simplify !== "enforced") {
score.empty = true;
}
score.message = i18n._("Your answer is almost correct, " + "but it needs to be simplified.");
}
return false;
} else if (piApprox && predicate(val, Math.abs(val * 0.001))) {
score.empty = true;
score.message = i18n._("Your answer is close, but you may " + "have approximated pi. Enter your " + "answer as a multiple of pi, like " + "12\\ \\text{pi} or " + "2/3\\ \\text{pi}");
}
}
});
if (score.correct === false) {
var interpretedGuess = false;
_.each(forms, function (form) {
var anyAreNaN = _.any(form(guess), function (t) {
return t.value != null && !_.isNaN(t.value);
});
if (anyAreNaN) {
interpretedGuess = true;
}
});
if (!interpretedGuess) {
score.empty = true;
score.message = i18n._("We could not understand your " + "answer. Please check your answer for extra " + "text or symbols.");
return score;
}
}
return score;
};
}
},
/*
* number answer type
*
* wraps the predicate answer type to performs simple number-based checking
* of a solution
*/
number: {
convertToPredicate: function convertToPredicate(correct, options) {
// TODO(alpert): Don't think this $.trim is necessary
var correctFloat = parseFloat($.trim(correct));
return [function (guess, maxError) {
return Math.abs(guess - correctFloat) < maxError;
}, $.extend({}, options, { type: "predicate" })];
},
createValidatorFunctional: function createValidatorFunctional(correct, options) {
var _KhanAnswerTypes$pred;
return (_KhanAnswerTypes$pred = KhanAnswerTypes.predicate).createValidatorFunctional.apply(_KhanAnswerTypes$pred, KhanAnswerTypes.number.convertToPredicate(correct, options));
}
},
/*
* The expression answer type parses a given expression or equation
* and semantically compares it to the solution. In addition, instant
* feedback is provided by rendering the last answer that fully parsed.
*
* Parsing options:
* functions (e.g. data-functions="f g h")
* A space or comma separated list of single-letter variables that
* should be interpreted as functions. Case sensitive. "e" and "i"
* are reserved.
*
* no functions specified: f(x+y) == fx + fy
* with "f" as a function: f(x+y) != fx + fy
*
* Comparison options:
* same-form (e.g. data-same-form)
* If present, the answer must match the solution's structure in
* addition to evaluating the same. Commutativity and excess negation
* are ignored, but all other changes will trigger a rejection. Useful
* for requiring a particular form of an equation, or if the answer
* must be factored.
*
* example question: Factor x^2 + x - 2
* example solution: (x-1)(x+2)
* accepted answers: (x-1)(x+2), (x+2)(x-1), ---(-x-2)(-1+x), etc.
* rejected answers: x^2+x-2, x*x+x-2, x(x+1)-2, (x-1)(x+2)^1, etc.
* rejection message: Your answer is not in the correct form
*
* simplify (e.g. data-simplify)
* If present, the answer must be fully expanded and simplified. Use
* carefully - simplification is hard and there may be bugs, or you
* might not agree on the definition of "simplified" used. You will
* get an error if the provided solution is not itself fully expanded
* and simplified.
*
* example question: Simplify ((n*x^5)^5) / (n^(-2)*x^2)^-3
* example solution: x^31 / n
* accepted answers: x^31 / n, x^31 / n^1, x^31 * n^(-1), etc.
* rejected answers: (x^25 * n^5) / (x^(-6) * n^6), etc.
* rejection message: Your answer is not fully expanded and simplified
*
* Rendering options:
* times (e.g. data-times)
* If present, explicit multiplication (such as between numbers) will
* be rendered with a cross/x symbol (TeX: \times) instead of the usual
* center dot (TeX: \cdot).
*
* normal rendering: 2 * 3^x -> 2 \cdot 3^{x}
* but with "times": 2 * 3^x -> 2 \times 3^{x}
*/
expression: {
parseSolution: function parseSolution(solutionString, options) {
var solution = KAS.parse(solutionString, options);
if (!solution.parsed) {
throw new Error("The provided solution (" + solutionString + ") didn't parse.");
} else if (options.simplified && !solution.expr.isSimplified()) {
throw new Error("The provided solution (" + solutionString + ") isn't fully expanded and simplified.");
} else {
solution = solution.expr;
}
return solution;
},
createValidatorFunctional: function createValidatorFunctional(solution, options) {
return function (guess) {
var score = {
empty: false,
correct: false,
message: null,
guess: guess
};
// Don't bother parsing an empty input
if (!guess) {
score.empty = true;
return score;
}
var answer = KAS.parse(guess, options);
// An unsuccessful parse doesn't count as wrong
if (!answer.parsed) {
score.empty = true;
return score;
}
// Solution will need to be parsed again if we're creating
// this from a multiple question type
if (typeof solution === "string") {
solution = KhanAnswerTypes.expression.parseSolution(solution, options);
}
var result = KAS.compare(answer.expr, solution, options);
if (result.equal) {
// Correct answer
score.correct = true;
} else if (result.message) {
// Nearly correct answer
score.message = result.message;
} else {
// Replace x with * and see if it would have been correct
var answerX = KAS.parse(guess.replace(/[xX]/g, "*"), options);
if (answerX.parsed) {
var resultX = KAS.compare(answerX.expr, solution, options);
if (resultX.equal) {
score.empty = true;
score.message = "I'm a computer. I only " + "understand multiplication if you use an " + "asterisk (*) as the multiplication sign.";
} else if (resultX.message) {
score.message = resultX.message + " Also, " + "I'm a computer. I only " + "understand multiplication if you use an " + "asterisk (*) as the multiplication sign.";
}
}
}
return score;
};
}
}
};
module.exports = KhanAnswerTypes;
/***/ },
/* 83 */
/***/ function(module, exports, __webpack_require__) {
/* ButtonGroup is an aesthetically pleasing group of buttons.
*
* The class requires these properties:
* buttons - an array of objects with keys:
* "value": this is the value returned when the button is selected
* "content": this is the JSX shown within the button, typically a string
* that gets rendered as the button's display text
* "title": this is the title-text shown on hover
* onChange - a function that is provided with the updated value
* (which it then is responsible for updating)
*
* The class has these optional properties:
* value - the initial value of the button selected, defaults to null.
* allowEmpty - if false, exactly one button _must_ be selected; otherwise
* it defaults to true and _at most_ one button (0 or 1) may be selected.
*
* Requires stylesheets/perseus-admin-package/editor.less to look nice.
*/
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var styles = __webpack_require__(177);
var css = __webpack_require__(79).css;
var ButtonGroup = React.createClass({
displayName: 'ButtonGroup',
propTypes: {
value: React.PropTypes.any,
buttons: React.PropTypes.arrayOf(React.PropTypes.shape({
value: React.PropTypes.any.isRequired,
content: React.PropTypes.node,
title: React.PropTypes.string
})).isRequired,
onChange: React.PropTypes.func.isRequired,
allowEmpty: React.PropTypes.bool
},
getDefaultProps: function getDefaultProps() {
return {
value: null,
allowEmpty: true
};
},
focus: function focus() {
ReactDOM.findDOMNode(this).focus();
return true;
},
toggleSelect: function toggleSelect(newValue) {
var value = this.props.value;
if (this.props.allowEmpty) {
// Select the new button or unselect if it's already selected
this.props.onChange(value !== newValue ? newValue : null);
} else {
this.props.onChange(newValue);
}
},
render: function render() {
var _this = this;
var value = this.props.value;
var buttons = this.props.buttons.map(function (button, i) {
return React.createElement(
'button',
{ title: button.title,
type: 'button',
id: "" + i,
ref: "button" + i,
key: "" + i,
className: css(styles.button.buttonStyle, button.value === value && styles.button.selectedStyle),
onClick: _this.toggleSelect.bind(_this, button.value)
},
button.content || "" + button.value
);
});
var outerStyle = {
display: 'inline-block'
};
return React.createElement(
'div',
{ style: outerStyle },
buttons
);
}
});
module.exports = ButtonGroup;
/***/ },
/* 84 */
/***/ function(module, exports, __webpack_require__) {
Object.defineProperty(exports, "__esModule", {
value: true
});
var _selector = __webpack_require__(207);
var _selector2 = _interopRequireDefault(_selector);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /**
* The Rule class represents a Gorgon lint rule. A Rule instance has a check()
* method that takes the same (node, state, content) arguments that a
* TreeTransformer traversal callback function does. Call the check() method
* during a tree traversal to determine whether the current node of the tree
* violates the rule. If there is no violation, then check() returns
* null. Otherwise, it returns an object that includes the name of the rule,
* an error message, and the start and end positions within the node's content
* string of the lint.
*
* A Gorgon lint rule consists of a name, a severity, a selector, a pattern
* (RegExp) and two functions. The check() method uses the selector, pattern,
* and functions as follows:
*
* - First, when determining which rules to apply to a particular piece of
* content, each rule can specify an optional function provided in the fifth
* parameter to evaluate whether or not we should be applying this rule.
* If the function returns false, we don't use the rule on this content.
*
* - Next, check() tests whether the node currently being traversed matches
* the selector. If it does not, then the rule does not apply at this node
* and there is no lint and check() returns null.
*
* - If the selector matched, then check() tests the text content of the node
* (and its children) against the pattern. If the pattern does not match,
* then there is no lint, and check() returns null.
*
* - If both the selector and pattern match, then check() calls the function
* passing the TraversalState object, the content string for the node, the
* array of nodes returned by the selector match, and the array of strings
* returned by the pattern match. This function can use these arguments to
* implement any kind of lint detection logic it wants. If it determines
* that there is no lint, then it should return null. Otherwise, it should
* return an error message as a string, or an object with `message`, `start`
* and `end` properties. The start and end properties are numbers that mark
* the beginning and end of the problematic content. Note that these numbers
* are relative to the content string passed to the traversal callback, not
* to the entire string that was used to generate the parse tree in the
* first place. TODO(davidflanagan): modify the simple-markdown library to
* have an option to add the text offset of each node to the parse
* tree. This will allows us to pinpoint lint errors within a long string
* of markdown text.
*
* - If the function returns null, then check() returns null. Otherwise,
* check() returns an object with `rule`, `message`, `start` and `end`
* properties. The value of the `rule` property is the name of the rule,
* which is useful for error reporting purposes.
*
* The name, severity, selector, pattern and function arguments to the Rule()
* constructor are optional, but you may not omit both the selector and the
* pattern. If you do not specify a selector, a default selector that matches
* any node of type "text" will be used. If you do not specify a pattern, then
* any node that matches the selector will be assumed to match the pattern as
* well. If you don't pass a function as the fourth argument to the Rule()
* constructor, then you must pass an error message string instead. If you do
* this, you'll get a default function that unconditionally returns an object
* that includes the error message and the start and end indexes of the
* portion of the content string that matched the pattern. If you don't pass a
* function in the fifth parameter, the rule will be applied in any context.
*
* One of the design goals of this Rule class is to allow simple lint rules to
* be described in JSON files without any JavaScript code. So in addition to
* the Rule() constructor, the class also defines a Rule.makeRule() factory
* method. This method takes a single object as its argument and expects the
* object to have four string properties. The `name` property is passed as the
* first argument to the Rule() construtctor. The optional `selector`
* property, if specified, is passed to Selector.parse() and the resulting
* Selector object is used as the second argument to Rule(). The optional
* `pattern` property is converted to a RegExp before being passed as the
* third argument to Rule(). (See Rule.makePattern() for details on the string
* to RegExp conversion). Finally, the `message` property specifies an error
* message that is passed as the final argument to Rule(). You can also use a
* real RegExp as the value of the `pattern` property or define a custom lint
* function on the `lint` property instead of setting the `message`
* property. Doing either of these things means that your rule description can
* no longer be saved in a JSON file, however.
*
* For example, here are two lint rules defined with Rule.makeRule():
*
* let nestedLists = Rule.makeRule({
* name: "nested-lists",
* selector: "list list",
* message: `Nested lists:
* nested lists are hard to read on mobile devices;
* do not use additional indentation.`,
* });
*
* let longParagraph = Rule.makeRule({
* name: "long-paragraph",
* selector: "paragraph",
* pattern: /^.{501,}/,
* lint: function(state, content, nodes, match) {
* return `Paragraph too long:
* This paragraph is ${content.length} characters long.
* Shorten it to 500 characters or fewer.`;
* },
* });
*
* Certain advanced lint rules need additional information about the content
* being linted in order to detect lint. For example, a rule to check for
* whitespace at the start and end of the URL for an image can't use the
* information in the node or content arguments because the markdown parser
* strips leading and trailing whitespace when parsing. (Nevertheless, these
* spaces have been a practical problem for our content translation process so
* in order to check for them, a lint rule needs access to the original
* unparsed source text. Similarly, there are various lint rules that check
* widget usage. For example, it is easy to write a lint rule to ensure that
* images have alt text for images encoded in markdown. But when images are
* added to our content via an image widget we also want to be able to check
* for alt text. In order to do this, the lint rule needs to be able to look
* widgets up by name in the widgets object associated with the parse tree.
*
* In order to support advanced linting rules like these, the check() method
* takes a context object as its optional fourth argument, and passes this
* object on to the lint function of each rule. Rules that require extra
* context should not assume that they will always get it, and should verify
* that the necessary context has been supplied before using it. Currently the
* "content" property of the context object is the unparsed source text if
* available, and the "widgets" property of the context object is the widget
* object associated with that content string in the JSON object that defines
* the Perseus article or exercise that is being linted.
*/
// This represents the type returned by String.match(). It is an
// array of strings, but also has index:number and input:string properties.
// Flow doesn't handle it well, so we punt and just use any.
var babelPluginFlowReactPropTypes_proptype_TreeNode = __webpack_require__(85).babelPluginFlowReactPropTypes_proptype_TreeNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_TraversalState = __webpack_require__(85).babelPluginFlowReactPropTypes_proptype_TraversalState || __webpack_require__(43).PropTypes.any;
// This is the return type of the check() method of a Rule object
// This is the return type of the lint detection function passed as the 4th
// argument to the Rule() constructor. It can return null or a string or an
// object containing a string and two numbers.
// prettier-ignore
// (prettier formats this in a way that ka-lint does not like)
// This is the type of the lint detection function that the Rule() constructor
// expects as its fourth argument. It is passed the TraversalState object and
// content string that were passed to check(), and is also passed the array of
// nodes returned by the selector match and the array of strings returned by
// the pattern match. It should return null if no lint is detected or an
// error message or an object contining an error message.
// An optional check to verify whether or not a particular rule should
// be checked by context. For example, some rules only apply in exercises,
// and should never be applied to articles. Defaults to true, so if we
// omit the applies function in a rule, it'll be tested everywhere.
/**
* A Rule object describes a Gorgon lint rule. See the comment at the top of
* this file for detailed description.
*/
var Rule = function () {
// The comment at the top of this file has detailed docs for
// this constructor and its arguments
// Checks to see if we should apply a rule or not
// A regular expression if one was specified
// The severity of the rule
function Rule(name, severity, selector, pattern, lint, applies) {
_classCallCheck(this, Rule);
if (!selector && !pattern) {
throw new Error("Lint rules must have a selector or pattern");
}
this.name = name || "unnamed rule";
this.severity = severity || Rule.Severity.BULK_WARNING;
this.selector = selector || Rule.DEFAULT_SELECTOR;
this.pattern = pattern || null;
// If we're called with an error message instead of a function then
// use a default function that will return the message.
if (typeof lint === "function") {
this.lint = lint;
this.message = null;
} else {
this.lint = this._defaultLintFunction;
this.message = lint;
}
this.applies = applies || function () {
return true;
};
}
// A factory method for use with rules described in JSON files
// See the documentation at the start of this file for details.
// The error message for use with the default function
// The lint-testing function or a default
// The specified selector or the DEFAULT_SELECTOR
// The name of the rule
Rule.makeRule = function makeRule(options) {
return new Rule(options.name, options.severity, options.selector ? _selector2.default.parse(options.selector) : null, Rule.makePattern(options.pattern), options.lint || options.message, options.applies);
};
// Check the node n to see if it violates this lint rule. A return value
// of false means there is no lint. A returned object indicates a lint
// error. See the documentation at the top of this file for details.
Rule.prototype.check = function check(node, traversalState, content, context) {
// First, see if we match the selector.
// If no selector was passed to the constructor, we use a
// default selector that matches text nodes.
var selectorMatch = this.selector.match(traversalState);
// If the selector did not match, then we're done
if (!selectorMatch) {
return null;
}
// If the selector matched, then see if the pattern matches
var patternMatch = void 0;
if (this.pattern) {
patternMatch = content.match(this.pattern);
} else {
// If there is no pattern, then just match all of the content.
// Use a fake RegExp match object to represent this default match.
patternMatch = Rule.FakePatternMatch(content, content, 0);
}
// If there was a pattern and it didn't match, then we're done
if (!patternMatch) {
return null;
}
try {
// If we get here, then the selector and pattern have matched
// so now we call the lint function to see if there is lint.
var error = this.lint(traversalState, content, selectorMatch, patternMatch, context);
if (!error) {
return null; // No lint; we're done
} else if (typeof error === "string") {
// If the lint function returned a string we assume it
// applies to the entire content of the node and return it.
return {
rule: this.name,
severity: this.severity,
message: error,
start: 0,
end: content.length
};
} else {
// If the lint function returned an object, then we just
// add the rule name to the message, start and end.
return {
rule: this.name,
severity: this.severity,
message: error.message,
start: error.start,
end: error.end
};
}
} catch (e) {
// If the lint function threw an exception we handle that as
// a special type of lint. We want the user to see the lint
// warning in this case (even though it is out of their control)
// so that the bug gets reported. Otherwise we'd never know that
// a rule was failing.
return {
rule: "lint-rule-failure",
message: "Exception in rule " + this.name + ": " + e.message + "\nStack trace:\n" + e.stack,
start: 0,
end: content.length
};
}
};
// This internal method is the default lint function that we use when a
// rule is defined without a function. This is useful for rules where the
// selector and/or pattern match are enough to indicate lint. This
// function unconditionally returns the error message that was passed in
// place of a function, but also adds start and end properties that
// specify which particular portion of the node content matched the
// pattern.
Rule.prototype._defaultLintFunction = function _defaultLintFunction(state, content, selectorMatch, patternMatch) {
return {
message: this.message || "",
start: patternMatch.index,
end: patternMatch.index + patternMatch[0].length
};
};
// The makeRule() factory function uses this static method to turn its
// argument into a RegExp. If the argument is already a RegExp, we just
// return it. Otherwise, we compile it into a RegExp and return that.
// The reason this is necessary is that Rule.makeRule() is designed for
// use with data from JSON files and JSON files can't include RegExp
// literals. Strings passed to this function do not need to be delimited
// with / characters unless you want to include flags for the RegExp.
//
// Examples:
//
// input "" ==> output null
// input /foo/ ==> output /foo/
// input "foo" ==> output /foo/
// input "/foo/i" ==> output /foo/i
//
Rule.makePattern = function makePattern(pattern) {
if (!pattern) {
return null;
} else if (pattern instanceof RegExp) {
return pattern;
} else if (pattern[0] === "/") {
var lastSlash = pattern.lastIndexOf("/");
var expression = pattern.substring(1, lastSlash);
var flags = pattern.substring(lastSlash + 1);
return new RegExp(expression, flags);
} else {
return new RegExp(pattern);
}
};
// This static method returns an string array with index and input
// properties added, in order to simulate the return value of the
// String.match() method. We use it when a Rule has no pattern and we
// want to simulate a match on the entire content string.
Rule.FakePatternMatch = function FakePatternMatch(input, match, index) {
var result = [match];
result.index = index;
result.input = input;
return result;
};
return Rule;
}();
Rule.Severity = {
ERROR: 1,
WARNING: 2,
GUIDELINE: 3,
BULK_WARNING: 4
};
exports.default = Rule;
Rule.DEFAULT_SELECTOR = _selector2.default.parse("text");
/***/ },
/* 85 */
/***/ function(module, exports, __webpack_require__) {
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// TraversalCallback is the type of the callback function passed to the
// traverse() method. It is invoked with node, state, and content arguments
// and is expected to return nothing.
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_TreeNode", __webpack_require__(43).PropTypes.shape({
type: __webpack_require__(43).PropTypes.string.isRequired
})); /**
* TreeTransformer is a class for traversing and transforming trees. Create a
* TreeTransformer by passing the root node of the tree to the
* constructor. Then traverse that tree by calling the traverse() method. The
* argument to traverse() is a callback function that will be called once for
* each node in the tree. This is a post-order depth-first traversal: the
* callback is not called on the a way down, but on the way back up. That is,
* the children of a node are traversed before the node itself is.
*
* The traversal callback function is passed three arguments, the node being
* traversed, a TraversalState object, and the concatentated text content of
* the node and all of its descendants. The TraversalState object is the most
* most interesting argument: it has methods for querying the ancestors and
* siblings of the node, and for deleting or replacing the node. These
* transformation methods are why this class is a tree transformer and not
* just a tree traverser.
*
* A typical tree traversal looks like this:
*
* new TreeTransformer(root).traverse((node, state, content) => {
* let parent = state.parent();
* let previous = state.previousSibling();
* // etc.
* });
*
* The traverse() method descends through nodes and arrays of nodes and calls
* the traverse callback on each node on the way back up to the root of the
* tree. (Note that it only calls the callback on the nodes themselves, not
* any arrays that contain nodes.) A node is loosely defined as any object
* with a string-valued `type` property. Objects that do not have a type
* property are assumed to not be part of the tree and are not traversed. When
* traversing an array, all elements of the array are examined, and any that
* are nodes or arrays are recursively traversed. When traversing a node, all
* properties of the object are examined and any node or array values are
* recursively traversed. In typical parse trees, the children of a node are
* in a `children` or `content` array, but this class is designed to handle
* more general trees. The Perseus markdown parser, for example, produces
* nodes of type "table" that have children in the `header` and `cells`
* properties.
*
* CAUTION: the traverse() method does not make any attempt to detect
* cycles. If you call it on a cyclic graph instead of a tree, it will cause
* infinite recursion (or, more likely, a stack overflow).
*
* TODO(davidflanagan): it probably wouldn't be hard to detect cycles: when
* pushing a new node onto the containers stack we could just check that it
* isn't already there.
*
* If a node has a text-valued `content` property, it is taken to be the
* plain-text content of the node. The traverse() method concatenates these
* content strings and passes them to the traversal callback for each
* node. This means that the callback has access the full text content of its
* node and all of the nodes descendants.
*
* See the TraversalState class for more information on what information and
* methods are available to the traversal callback.
**/
// TreeNode is the type of a node in a parse tree. The only real requirement is
// that every node has a string-valued `type` property
// This is the TreeTransformer class described in detail at the
// top of this file.
var TreeTransformer = function () {
// To create a tree transformer, just pass the root node of the tree
function TreeTransformer(root) {
_classCallCheck(this, TreeTransformer);
this.root = root;
}
// A utility function for determing whether an arbitrary value is a node
TreeTransformer.isNode = function isNode(n) {
return n && (typeof n === "undefined" ? "undefined" : _typeof(n)) === "object" && typeof n.type === "string";
};
// Determines whether a value is a node with type "text" and has
// a text-valued `content` property.
TreeTransformer.isTextNode = function isTextNode(n) {
return TreeTransformer.isNode(n) && n.type === "text" && typeof n.content === "string";
};
// This is the main entry point for the traverse() method. See the comment
// at the top of this file for a detailed description. Note that this
// method just creates a new TraversalState object to use for this
// traversal and then invokes the internal _traverse() method to begin the
// recursion.
TreeTransformer.prototype.traverse = function traverse(f) {
this._traverse(this.root, new TraversalState(this.root), f);
};
// Do a post-order traversal of node and its descendants, invoking the
// callback function f() once for each node and returning the concatenated
// text content of the node and its descendants. f() is passed three
// arguments: the current node, a TraversalState object representing the
// current state of the traversal, and a string that holds the
// concatenated text of the node and its descendants.
//
// This private method holds all the traversal logic and implementation
// details. Note that this method uses the TraversalState object to store
// information about the structure of the tree.
TreeTransformer.prototype._traverse = function _traverse(n, state, f) {
var _this = this;
var content = "";
if (TreeTransformer.isNode(n)) {
// If we were called on a node object, then we handle it
// this way.
var _node = n; // safe cast; we just tested
// Put the node on the stack before recursing on its children
state._containers.push(_node);
state._ancestors.push(_node);
// Record the node's text content if it has any.
// Usually this is for nodes with a type property of "text",
// but other nodes types like "math" may also have content.
if (typeof _node.content === "string") {
content = _node.content;
}
// Recurse on the node. If there was content above, then there
// probably won't be any children to recurse on, but we check
// anyway.
//
// If we wanted to make the traversal completely specific to the
// actual Perseus parse trees that we'll be dealing with we could
// put a switch statement here to dispatch on the node type
// property with specific recursion steps for each known type of
// node.
var keys = Object.keys(_node);
keys.forEach(function (key) {
// Never recurse on the type property
if (key === "type") {
return;
}
// Ignore properties that are null or primitive and only
// recurse on objects and arrays. Note that we don't do a
// isNode() check here. That is done in the recursive call to
// _traverse(). Note that the recursive call on each child
// returns the text content of the child and we add that
// content to the content for this node. Also note that we
// push the name of the property we're recursing over onto a
// TraversalState stack.
var value = _node[key];
if (value && (typeof value === "undefined" ? "undefined" : _typeof(value)) === "object") {
state._indexes.push(key);
content += _this._traverse(value, state, f);
state._indexes.pop();
}
});
// Restore the stacks after recursing on the children
state._currentNode = state._ancestors.pop();
state._containers.pop();
// And finally call the traversal callback for this node. Note
// that this is post-order traversal. We call the callback on the
// way back up the tree, not on the way down. That way we already
// know all the content contained within the node.
f(_node, state, content);
} else if (Array.isArray(n)) {
// If we were called on an array instead of a node, then
var nodes = n;
// Push the array onto the stack. This will allow the
// TraversalState object to locate siblings of this node.
state._containers.push(nodes);
// Now loop through this array and recurse on each element in it.
// Before recursing on an element, we push its array index on a
// TraversalState stack so that the TraversalState sibling methods
// can work. Note that TraversalState methods can alter the length
// of the array, and change the index of the current node, so we
// are careful here to test the array length on each iteration and
// to reset the index when we pop the stack. Also note that we
// concatentate the text content of the children.
var index = 0;
while (index < nodes.length) {
state._indexes.push(index);
content += this._traverse(nodes[index], state, f);
// Casting to convince Flow that this is a number
index = state._indexes.pop() + 1;
}
// Pop the array off the stack. Note, however, that we do not call
// the traversal callback on the array. That function is only
// called for nodes, not arrays of nodes.
state._containers.pop();
}
// The _traverse() method always returns the text content of
// this node and its children. This is the one piece of state that
// is not tracked in the TraversalState object.
return content;
};
return TreeTransformer;
}();
// An instance of this class is passed to the callback function for
// each node traversed. The class itself is not exported, but its
// methods define the API available to the traversal callback.
/**
* This class represents the state of a tree traversal. An instance is created
* by the traverse() method of the TreeTransformer class to maintain the state
* for that traversal, and the instance is passed to the traversal callback
* function for each node that is traversed. This class is not intended to be
* instantiated directly, but is exported so that its type can be used for
* Flow annotaions.
**/
exports.default = TreeTransformer;
var TraversalState = exports.TraversalState = function () {
// The constructor just stores the root node and creates empty stacks.
// These are internal state properties. Use the accessor methods defined
// below instead of using these properties directly. Note that the
// _containers and _indexes stacks can have two different types of
// elements, depending on whether we just recursed on an array or on a
// node. This is hard for Flow to deal with, so you'll see a number of
// Flow casts through the any type when working with these two properties.
function TraversalState(root) {
_classCallCheck(this, TraversalState);
this.root = root;
// When the callback is called, this property will hold the
// node that is currently being traversed.
this._currentNode = null;
// This is a stack of the objects and arrays that we've
// traversed through before reaching the currentNode.
// It is different than the ancestors array.
this._containers = new Stack();
// This stack has the same number of elements as the _containers
// stack. The last element of this._indexes[] is the index of
// the current node in the object or array that is the last element
// of this._containers[]. If the last element of this._containers[] is
// an array, then the last element of this stack will be a number.
// Otherwise if the last container is an object, then the last index
// will be a string property name.
this._indexes = new Stack();
// This is a stack of the ancestor nodes of the current one.
// It is different than the containers[] stack because it only
// includes nodes, not arrays.
this._ancestors = new Stack();
}
/**
* Return the current node in the traversal. Any time the traversal
* callback is called, this method will return the name value as the
* first argument to the callback.
*/
// The root node of the tree being traversed
TraversalState.prototype.currentNode = function currentNode() {
return this._currentNode || this.root;
};
/**
* Return the parent of the current node, if there is one, or null.
*/
TraversalState.prototype.parent = function parent() {
return this._ancestors.top();
};
/**
* Return an array of ancestor nodes. The first element of this array is
* the same as this.parent() and the last element is the root node. If we
* are currently at the root node, the the returned array will be empty.
* This method makes a copy of the internal state, so modifications to the
* returned array have no effect on the traversal.
*/
TraversalState.prototype.ancestors = function ancestors() {
return this._ancestors.values();
};
/**
* Return the next sibling of this node, if it has one, or null otherwise.
*/
TraversalState.prototype.nextSibling = function nextSibling() {
var siblings = this._containers.top();
// If we're at the root of the tree or if the parent is an
// object instead of an array, then there are no siblings.
if (!siblings || !Array.isArray(siblings)) {
return null;
}
// The top index is a number because the top container is an array
var index = this._indexes.top();
if (siblings.length > index + 1) {
return siblings[index + 1];
} else {
return null; // There is no next sibling
}
};
/**
* Return the previous sibling of this node, if it has one, or null
* otherwise.
*/
TraversalState.prototype.previousSibling = function previousSibling() {
var siblings = this._containers.top();
// If we're at the root of the tree or if the parent is an
// object instead of an array, then there are no siblings.
if (!siblings || !Array.isArray(siblings)) {
return null;
}
// The top index is a number because the top container is an array
var index = this._indexes.top();
if (index > 0) {
return siblings[index - 1];
} else {
return null; // There is no previous sibling
}
};
/**
* Remove the next sibling node (if there is one) from the tree. Returns
* the removed sibling or null. This method makes it easy to traverse a
* tree and concatenate adjacent text nodes into a single node.
*/
TraversalState.prototype.removeNextSibling = function removeNextSibling() {
var siblings = this._containers.top();
if (siblings && Array.isArray(siblings)) {
// top index is a number because top container is an array
var index = this._indexes.top();
if (siblings.length > index + 1) {
return siblings.splice(index + 1, 1)[0];
}
}
return null;
};
/**
* Replace the current node in the tree with the specified nodes. If no
* nodes are passed, this is a node deletion. If one node (or array) is
* passed, this is a 1-for-1 replacement. If more than one node is passed
* then this is a combination of deletion and insertion. The new node or
* nodes will not be traversed, so this method can safely be used to
* reparent the current node node beneath a new parent.
*
* This method throws an error if you attempt to replace the root node of
* the tree.
*/
TraversalState.prototype.replace = function replace() {
var parent = this._containers.top();
if (!parent) {
throw new Error("Can't replace the root of the tree");
}
// The top of the container stack is either an array or an object
// and the top of the indexes stack is a corresponding array index
// or object property. This is hard for Flow, so we have to do some
// unsafe casting and be careful when we use which cast version
var parentIsArray = Array.isArray(parent);
var array = parent;
var index = this._indexes.top();
var object = parent;
var property = this._indexes.top();
for (var _len = arguments.length, replacements = Array(_len), _key = 0; _key < _len; _key++) {
replacements[_key] = arguments[_key];
}
if (parentIsArray) {
// For an array parent we just splice the new nodes in
array.splice.apply(array, [index, 1].concat(replacements));
// Adjust the index to account for the changed array length.
// We don't want to traverse any of the newly inserted nodes.
this._indexes.pop();
this._indexes.push(index + replacements.length - 1);
} else {
// For an object parent we care how many new nodes there are
if (replacements.length === 0) {
// Deletion
delete object[property];
} else if (replacements.length === 1) {
// Replacement
object[property] = replacements[0];
} else {
// Replace one node with an array of nodes
object[property] = replacements;
}
}
};
/**
* Returns true if the current node has a previous sibling and false
* otherwise. If this method returns false, then previousSibling() will
* return null, and goToPreviousSibling() will throw an error.
*/
TraversalState.prototype.hasPreviousSibling = function hasPreviousSibling() {
return Array.isArray(this._containers.top()) && this._indexes.top() > 0;
};
/**
* Modify this traversal state object to have the state it would have had
* when visiting the previous sibling. Note that you may want to use
* clone() to make a copy before modifying the state object like this.
* This mutator method is not typically used during ordinary tree
* traversals, but is used by the Selector class for matching multi-node
* selectors.
*/
TraversalState.prototype.goToPreviousSibling = function goToPreviousSibling() {
if (!this.hasPreviousSibling()) {
throw new Error("goToPreviousSibling(): node has no previous sibling");
}
this._currentNode = this.previousSibling();
// Since we know that we have a previous sibling, we know that
// the value on top of the stack is a number, but we have to do
// this unsafe cast because Flow doesn't know that.
var index = this._indexes.pop();
this._indexes.push(index - 1);
};
/**
* Returns true if the current node has an ancestor and false otherwise.
* If this method returns false, then the parent() method will return
* null and goToParent() will throw an error
*/
TraversalState.prototype.hasParent = function hasParent() {
return this._ancestors.size() !== 0;
};
/**
* Modify this object to look like it will look when we (later) visit the
* parent node of this node. You should not modify the instance passed to
* the tree traversal callback. Instead, make a copy with the clone()
* method and modify that. This mutator method is not typically used
* during ordinary tree traversals, but is used by the Selector class for
* matching multi-node selectors that involve parent and ancestor
* selectors.
*/
TraversalState.prototype.goToParent = function goToParent() {
if (!this.hasParent()) {
throw new Error("goToParent(): node has no ancestor");
}
this._currentNode = this._ancestors.pop();
// We need to pop the containers and indexes stacks at least once
// and more as needed until we restore the invariant that
// this._containers.top()[this.indexes.top()] === this._currentNode
//
while (this._containers.size() &&
// This is safe, but easier to just disable flow than do casts
// $FlowFixMe
this._containers.top()[this._indexes.top()] !== this._currentNode) {
this._containers.pop();
this._indexes.pop();
}
};
/**
* Return a new TraversalState object that is a copy of this one.
* This method is useful in conjunction with the mutating methods
* goToParent() and goToPreviousSibling().
*/
TraversalState.prototype.clone = function clone() {
var clone = new TraversalState(this.root);
clone._currentNode = this._currentNode;
clone._containers = this._containers.clone();
clone._indexes = this._indexes.clone();
clone._ancestors = this._ancestors.clone();
return clone;
};
/**
* Returns true if this TraversalState object is equal to that
* TraversalState object, or false otherwise. This method exists
* primarily for use by our unit tests.
*/
TraversalState.prototype.equals = function equals(that) {
return this.root === that.root && this._currentNode === that._currentNode && this._containers.equals(that._containers) && this._indexes.equals(that._indexes) && this._ancestors.equals(that._ancestors);
};
return TraversalState;
}();
/**
* This class is an internal utility that just treats an array as a stack
* and gives us a top() method so we don't have to write expressions like
* `ancestors[ancestors.length-1]`. The values() method automatically
* copies the internal array so we don't have to worry about client code
* modifying our internal stacks. The use of this Stack abstraction makes
* the TraversalState class simpler in a number of places.
*/
var Stack = function () {
function Stack(array) {
_classCallCheck(this, Stack);
this.stack = array ? array.slice(0) : [];
}
/** Push a value onto the stack. */
Stack.prototype.push = function push(v) {
this.stack.push(v);
};
/** Pop a value off of the stack. */
Stack.prototype.pop = function pop() {
return this.stack.pop();
};
/** Return the top value of the stack without popping it. */
Stack.prototype.top = function top() {
return this.stack[this.stack.length - 1];
};
/** Return a copy of the stack as an array */
Stack.prototype.values = function values() {
return this.stack.slice(0);
};
/** Return the number of elements in the stack */
Stack.prototype.size = function size() {
return this.stack.length;
};
/** Return a string representation of the stack */
Stack.prototype.toString = function toString() {
return this.stack.toString();
};
/** Return a shallow copy of the stack */
Stack.prototype.clone = function clone() {
return new Stack(this.stack);
};
/**
* Compare this stack to another and return true if the contents of
* the two arrays are the same.
*/
Stack.prototype.equals = function equals(that) {
if (!that || !that.stack || that.stack.length !== this.stack.length) {
return false;
}
for (var i = 0; i < this.stack.length; i++) {
if (this.stack[i] !== that.stack[i]) {
return false;
}
}
return true;
};
return Stack;
}();
/***/ },
/* 86 */
/***/ function(module, exports, __webpack_require__) {
function classNames() {
var args = arguments;
var classes = [];
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (!arg) {
continue;
}
if ('string' === typeof arg || 'number' === typeof arg) {
classes.push(arg);
} else if ('object' === typeof arg) {
for (var key in arg) {
if (!arg.hasOwnProperty(key) || !arg[key]) {
continue;
}
classes.push(key);
}
}
}
return classes.join(' ');
}
// safely export classNames in case the script is included directly on a page
if (typeof module !== 'undefined' && module.exports) {
module.exports = classNames;
}
/***/ },
/* 87 */
/***/ function(module, exports, __webpack_require__) {
// TODO(davidflanagan):
// This should probably be converted to use import and to export
// and object that maps rule names to rules. Also, maybe this should
// be an auto-generated file with a script that updates it any time
// we add a new rule?
module.exports = [__webpack_require__(209), __webpack_require__(210), __webpack_require__(211), __webpack_require__(212), __webpack_require__(213), __webpack_require__(214), __webpack_require__(215), __webpack_require__(216), __webpack_require__(217), __webpack_require__(218), __webpack_require__(219), __webpack_require__(220), __webpack_require__(221), __webpack_require__(222), __webpack_require__(223), __webpack_require__(224), __webpack_require__(225), __webpack_require__(226), __webpack_require__(227), __webpack_require__(228), __webpack_require__(229), __webpack_require__(230), __webpack_require__(231), __webpack_require__(232), __webpack_require__(233), __webpack_require__(234), __webpack_require__(235), __webpack_require__(236), __webpack_require__(237), __webpack_require__(238), __webpack_require__(239)];
/***/ },
/* 88 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* This component makes its children a drag target. Example:
*
* Drag to me
*
* ...
*
* handleDrop: function(e) {
* this.addImages(e.nativeEvent.dataTransfer.files);
* }
*
* Now "Drag to me" will be a drag target - when something is dragged over it,
* the element will become partially transparent as a visual indicator that
* it's a target.
*/
// TODO(joel) - indicate before the hover is over the target that it's possible
// to drag into the target. This would (I think) require a high level handler -
// like on Perseus itself, waiting for onDragEnter, then passing down the
// event. Sounds like a pain. Possible workaround - create a div covering the
// entire page...
//
// Other extensions:
// * custom styles for global drag and dragOver
// * only respond to certain types of drags (only images for instance)!
var React = __webpack_require__(43);
var DragTarget = React.createClass({
displayName: "DragTarget",
propTypes: {
// All props not listed here are forwarded to the root element without
// modification.
onDrop: React.PropTypes.func.isRequired,
component: React.PropTypes.any, // component type
shouldDragHighlight: React.PropTypes.func,
style: React.PropTypes.any
},
getDefaultProps: function getDefaultProps() {
return {
component: "div",
shouldDragHighlight: function shouldDragHighlight() {
return true;
}
};
},
getInitialState: function getInitialState() {
return { dragHover: false };
},
handleDrop: function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
this.setState({ dragHover: false });
this.props.onDrop(e);
},
handleDragEnd: function handleDragEnd() {
this.setState({ dragHover: false });
},
handleDragOver: function handleDragOver(e) {
e.preventDefault();
},
handleDragLeave: function handleDragLeave() {
this.setState({ dragHover: false });
},
handleDragEnter: function handleDragEnter(e) {
this.setState({ dragHover: this.props.shouldDragHighlight(e) });
},
render: function render() {
var opacity = this.state.dragHover ? { "opacity": 0.3 } : {};
var Component = this.props.component;
var forwardProps = Object.assign({}, this.props);
delete forwardProps.component;
delete forwardProps.shouldDragHighlight;
return React.createElement(Component, _extends({}, forwardProps, {
style: Object.assign({}, this.props.style, opacity),
onDrop: this.handleDrop,
onDragEnd: this.handleDragEnd,
onDragOver: this.handleDragOver,
onDragEnter: this.handleDragEnter,
onDragLeave: this.handleDragLeave
}));
}
});
module.exports = DragTarget;
/***/ },
/* 89 */
/***/ function(module, exports, __webpack_require__) {
/**
* Displays a collapsable list of KaTeX rendering errors.
*/
var React = __webpack_require__(43);
var _require = __webpack_require__(79),
css = _require.css,
StyleSheet = _require.StyleSheet;
var KatexErrorView = React.createClass({
displayName: "KatexErrorView",
propTypes: {
errorList: React.PropTypes.arrayOf(React.PropTypes.shape({
math: React.PropTypes.string.isRequired,
message: React.PropTypes.string.isRequired
})).isRequired
},
getInitialState: function getInitialState() {
return {
showErrors: false
};
},
handleToggleKatexErrors: function handleToggleKatexErrors(e) {
this.setState({ showErrors: !this.state.showErrors });
},
render: function render() {
var errorList = this.props.errorList;
var showErrors = this.state.showErrors;
// TODO(riley) replace with SVG icons
var disclosureClass = showErrors ? "icon-chevron-down" : "icon-chevron-right";
return React.createElement(
"div",
{ className: css(styles.errorContainer) },
React.createElement(
"div",
{
className: css(styles.title),
onClick: this.handleToggleKatexErrors
},
React.createElement("i", { className: disclosureClass, style: { fontSize: 14 } }),
"\xA0 KaTeX Errors (",
errorList.length,
")"
),
showErrors && React.createElement(
"div",
{ className: css(styles.errorExplanation) },
"These errors will cause your LaTeX to load really slowly for the student. Please fix them if you can. If you can\u2019t because KaTeX doesn\u2019t support the feature you need, please message Cam."
),
showErrors && errorList.map(function (e, index) {
return React.createElement(
"div",
{ className: css(styles.error), key: index },
React.createElement(
"div",
{ style: { color: "red" } },
e.math
),
React.createElement(
"div",
null,
e.message
)
);
})
);
}
});
var styles = StyleSheet.create({
title: {
backgroundColor: "#eee",
fontSize: "1.25em",
padding: "4px 10px"
},
errorContainer: {
border: "1px solid #ddd",
borderTop: "none"
},
errorExplanation: {
padding: "4px 10px",
backgroundColor: "pink"
},
error: {
padding: "4px 10px"
}
});
module.exports = KatexErrorView;
/***/ },
/* 90 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable react/prop-types, react/sort-comp */
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
/* A checkbox that syncs its value to props using the
* renderer's onChange method, and gets the prop name
* dynamically from its props list
*/
var PropCheckBox = React.createClass({
displayName: "PropCheckBox",
propTypes: {
labelAlignment: React.PropTypes.oneOf(["left", "right"])
},
DEFAULT_PROPS: {
label: null,
onChange: null,
labelAlignment: "left"
},
getDefaultProps: function getDefaultProps() {
return this.DEFAULT_PROPS;
},
propName: function propName() {
var propName = _.find(_.keys(this.props), function (localPropName) {
return !_.has(this.DEFAULT_PROPS, localPropName);
}, this);
if (!propName) {
throw new Error("Attempted to create a PropCheckBox with no prop!");
}
return propName;
},
_labelAlignLeft: function _labelAlignLeft() {
return this.props.labelAlignment === "left";
},
render: function render() {
var propName = this.propName();
return React.createElement(
"label",
null,
this._labelAlignLeft() && this.props.label,
React.createElement("input", {
type: "checkbox",
checked: this.props[propName],
onChange: this.toggle
}),
!this._labelAlignLeft() && this.props.label
);
},
toggle: function toggle() {
var propName = this.propName();
var changes = {};
changes[propName] = !this.props[propName];
this.props.onChange(changes);
}
});
module.exports = PropCheckBox;
/***/ },
/* 91 */
/***/ function(module, exports, __webpack_require__) {
/**
* Preprocess TeX code to convert things that KaTeX doesn't know how to handle
* to things is does.
*/
module.exports = function (texCode) {
return texCode
// Replace uses of \begin{align}...\end{align} which KaTeX doesn't
// support (yet) with \begin{aligned}...\end{aligned} which renders
// the same is supported by KaTeX. It does the same for align*.
// TODO(kevinb) update content to use aligned instead of align.
.replace(/\{align[*]?\}/g, "{aligned}")
// Replace non-breaking spaces with regular spaces.
.replace(/[\u00a0]/g, " ");
};
/***/ },
/* 92 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/*
This is essentially a more advanced `textarea`, using Draft.js
https://facebook.github.io/draft-js/
The important Draft.js concepts needed to understand this file are:
- Everything is immutable, and inputs all result in a new `editorState`
object being passed to `handleChange`. All changes must be done by
constructing new objects. This means simply editing text involves
creating a new ContentState, which is used to create a new EditorState
- `EditorState` contains a `ContentState` property which contains the data
relevant to the text content
- `ContentState` is organized into individual "Blocks", which helps with
performance as updates only affect a single block
- Specific text in blocks can be denoted as "Entities", which can store
data relevant to its text. This is what allows backspacing a widget
to result in its deletion in Perseus
- Special styling is done using Decorators, which allow substituting text
content with a custom react element
- Modifier is a collection of helpful utilities for modification purposes
TODO(samiskin): Make tasks such as "addWidget" and "updateWidget" not functions
that you call on the PerseusEditor component (Can do once this
fully replacess the old editor).
*/
var React = __webpack_require__(43);
var _require = __webpack_require__(298),
CharacterMetadata = _require.CharacterMetadata,
Entity = _require.Entity,
Editor = _require.Editor,
EditorState = _require.EditorState,
CompositeDecorator = _require.CompositeDecorator,
ContentState = _require.ContentState,
Modifier = _require.Modifier,
genKey = _require.genKey,
getDefaultKeyBinding = _require.getDefaultKeyBinding,
KeyBindingUtil = _require.KeyBindingUtil;
var Widgets = __webpack_require__(31);
var DraftUtils = __webpack_require__(269);
// This controls the minimum time between when updates for the parent
// component are generated. The best time for this number sort of depends
// on the user's typing speed though, as if the time between each letter being
// typed is longer than the throttle, they would notice a freeze when the update
// is being calculated.
// TODO(samiskin): Figure out whats the best value for this number
// 100 is best for my typing speed, but may not work as well for slower typists
var UPDATE_PARENT_THROTTLE = 100;
var widgetPlaceholder = "[[\u2603 {id}]]";
var widgetRegExp = /\[\[\u2603 [a-z-]+ [0-9]+\]\]/g;
var widgetPartsRegExp = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]$/;
var widgetRegexForId = function widgetRegexForId(id) {
return new RegExp("(\\[\\[\u2603 " + id + "\\]\\])", "gm");
};
var partialWidgetRegex = /\[\[([a-z-]+)$/; // Used for autocompletion
var imageRegExp = /!\[[^\]]*?\]\([^\)].*?\)/g;
// Note: Nested decorators currently do not work, therefore this will not
// work when nesting bold/italics/underline. Hopefully this is
// fixed in future versions of Draft.js
var boldRegExp = /\*\*([\s\S]+?)\*\*(?!\*)/g;
var italicsRegExp = /\**(?:^|[^*])((\*|_)(\w+(\s\w+)*)\2)/g; // copied from https://github.com/ayberkt/RFMarkdownTextView/blob/387312e602f03b87f3ef82dc82c62df455d6fd30/RFMarkdownTextView/RFMarkdownSyntaxStorage.m
var boldItalicsRegExp = /(\*\*\*\w+(\s\w+)*\*\*\*)/g;
var underlineRegExp = /__([\s\S]+?)__(?!_)/g;
var headerRegExp = /^ *(#{1,6})([^\n]+)$/g;
/*
Styled ranges in Draft.js are done using a `CompositeDecorator`,
where a `strategy` is given to denote what ranges of text to style,
and a `component` is given to denote how that range should be rendered
*/
var entityStrategy = function entityStrategy(contentBlock, callback, type) {
return contentBlock.findEntityRanges(function (char) {
return char.getEntity() && Entity.get(char.getEntity()).type === type;
}, callback);
};
var styledBlock = function styledBlock(props, style) {
return React.createElement(
"span",
_extends({}, props, { style: style }),
props.children
);
};
styledBlock.propTypes = { children: React.PropTypes.any };
var highlightedBlock = function highlightedBlock(props, backgroundColor) {
return styledBlock(props, { backgroundColor: backgroundColor });
};
var entityColorDecorator = function entityColorDecorator(type, color) {
return {
strategy: function strategy() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return entityStrategy.apply(undefined, args.concat([type]));
},
component: function component(props) {
return highlightedBlock(props, color);
}
};
};
var regexColorDecorator = function regexColorDecorator(regex, color) {
return {
strategy: function strategy() {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return DraftUtils.regexStrategy.apply(DraftUtils, args.concat([regex]));
},
component: function component(props) {
return highlightedBlock(props, color);
}
};
};
var boldDecorator = {
strategy: function strategy() {
for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return DraftUtils.regexStrategy.apply(DraftUtils, args.concat([boldRegExp]));
},
component: function component(props) {
return styledBlock(props, { fontWeight: "bold" });
}
};
// The italics regex has a group that ensures that the *___* block
// does not include the * used to create a list. Since this results
// in match.index also including the first non-capturing group, we must
// use custom logic for this strategy
var italicsStrategy = function italicsStrategy() {
for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
return DraftUtils.regexStrategy.apply(DraftUtils, args.concat([italicsRegExp, function (matchArr) {
var start = matchArr.index + matchArr[0].length - matchArr[1].length;
var end = start + matchArr[1].length;
return { start: start, end: end };
}]));
};
var italicsDecorator = {
strategy: italicsStrategy,
component: function component(props) {
return styledBlock(props, { fontStyle: "italic" });
}
};
var underlineDecorator = {
strategy: function strategy() {
for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
args[_key5] = arguments[_key5];
}
return DraftUtils.regexStrategy.apply(DraftUtils, args.concat([underlineRegExp]));
},
component: function component(props) {
return styledBlock(props, { textDecoration: "underline" });
}
};
var boldItalicsDecorator = {
strategy: function strategy() {
for (var _len6 = arguments.length, args = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
args[_key6] = arguments[_key6];
}
return DraftUtils.regexStrategy.apply(DraftUtils, args.concat([boldItalicsRegExp]));
},
component: function component(props) {
return styledBlock(props, {
fontWeight: "bold",
fontStyle: "italic"
});
}
};
// TODO: Make the headers also able to scale with the rest of the text
// when changing the fontSize percentage
var headerComponent = function headerComponent(props) {
var text = props.decoratedText;
var headerSize = text.split(headerRegExp)[1].length;
var style = { marginBottom: 0 };
return React.createElement("h" + headerSize, { style: style }, props.children);
};
headerComponent.propTypes = {
decoratedText: React.PropTypes.string,
children: React.PropTypes.any
};
var headerDecorator = {
strategy: function strategy() {
for (var _len7 = arguments.length, args = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
args[_key7] = arguments[_key7];
}
return DraftUtils.regexStrategy.apply(DraftUtils, args.concat([headerRegExp]));
},
component: headerComponent
};
var decorator = new CompositeDecorator([entityColorDecorator("WIDGET", "#DFD"), entityColorDecorator("TEMP_IMAGE", "#fdffdd"), regexColorDecorator(imageRegExp, "#dffdfa"), boldItalicsDecorator, boldDecorator, underlineDecorator, italicsDecorator, headerDecorator]);
// Key bindings are handled by mapping events to strings
var keyBindings = function keyBindings(e) {
var isCommandPressed = KeyBindingUtil.hasCommandModifier(e);
if (isCommandPressed && e.keyCode === 66) {
// 66 = b
return "perseus-bold";
} else if (isCommandPressed && e.keyCode === 73) {
// 73 = i
return "perseus-italics";
} else if (isCommandPressed && e.keyCode === 85) {
// 85 = u
return "perseus-underline";
} else if (isCommandPressed && e.keyCode === 219) {
// 219 = [
return "perseus-decrease-font-size";
} else if (isCommandPressed && e.keyCode === 221) {
// 221 = ]
return "perseus-increase-font-size";
} else if (isCommandPressed && e.keyCode === 220) {
// 220 = ]
return "perseus-reset-font-size";
} else {
return getDefaultKeyBinding(e);
}
};
/*
This is the main Draft.js editor. It keeps track of its internal Draft.js
state, however what it exposes through its `onChange` is a simple string
as well as a list of the currently active widgets.
*/
var PerseusEditor = React.createClass({
displayName: "PerseusEditor",
propTypes: {
onChange: React.PropTypes.func,
content: React.PropTypes.string,
initialWidgets: React.PropTypes.any,
placeholder: React.PropTypes.string,
imageUploader: React.PropTypes.func,
widgetEnabled: React.PropTypes.bool
},
getDefaultProps: function getDefaultProps() {
return {
onChange: function onChange() {},
content: "",
initialWidgets: {},
widgetEnabled: true,
placeholder: "Type here"
};
},
getInitialState: function getInitialState() {
var _props = this.props,
content = _props.content,
initialWidgets = _props.initialWidgets,
widgetEnabled = _props.widgetEnabled;
var contentState = ContentState.createFromText(content);
var editorState = EditorState.createWithContent(contentState, decorator);
if (widgetEnabled) {
editorState = this._insertWidgetsAsEntities(editorState, initialWidgets);
}
return {
editorState: editorState,
widgets: initialWidgets,
fontSizePercentage: 100
};
},
// The editor can have its content changed completely by changing the
// content prop, however if the data this component sent to its parent
// using `this.props.onChange()` is being fed back in, ignore it
componentDidUpdate: function componentDidUpdate(prevProps) {
if (this.props.content !== this.lastContentUpdate) {
this.lastContentUpdate = this.props.content;
this.setState(this.getInitialState()); //eslint-disable-line
}
},
// By turning widgets into Entities, we allow for widgets to be considered
// "IMMUTABLE", that is, backspacing a widget will delete the entire text
// rather than just a "]" character. It also enables us to detect which
// widget id has been deleted, as metadata can be attached to entities
// TODO(samiskin): Turn this task of `applyEntities(pattern, createEntity)`
// into a DraftUtils function
_insertWidgetsAsEntities: function _insertWidgetsAsEntities(editorState, widgets) {
var content = editorState.getCurrentContent();
Object.keys(widgets).forEach(function (id) {
var selection = DraftUtils.findPattern(content, widgetRegexForId(id)); //eslint-disable-line max-len
if (selection) {
// Sometimes the widgets don't actually exist
var entity = Entity.create("WIDGET", "IMMUTABLE", { id: id });
content = Modifier.applyEntity(content, selection, entity);
}
});
// EditorState.set is used rather than push, because no state should
// be added to the undo stack.
var withEntity = EditorState.set(editorState, {
currentContent: content
});
return withEntity;
},
_getDraftData: function _getDraftData() {
var editorState = this.state.editorState;
var contentState = editorState.getCurrentContent();
var selection = editorState.getSelection();
return { editorState: editorState, contentState: contentState, selection: selection };
},
_getNextWidgetId: function _getNextWidgetId(type) {
var currWidgets = this.state.widgets;
return Object.keys(currWidgets).filter(function (id) {
return currWidgets[id].type === type;
}).map(function (id) {
return +id.split(" ")[1];
}) //ids are (([a-z-]+) ([0-9]+))
.reduce(function (maxId, currId) {
return Math.max(maxId, currId);
}, 0);
},
_createInitialWidget: function _createInitialWidget(widgetType) {
// Since widgets are given IDs, adding a new widget must ensure that a
// unique id is generated for it.
var widgetNum = this._getNextWidgetId(widgetType);
var id = widgetType + " " + (widgetNum + 1);
var widget = {
options: Widgets.getEditor(widgetType).defaultProps,
type: widgetType,
// Track widget version on creation, so that a widget editor
// without a valid version prop can only possibly refer to a
// pre-versioning creation time.
version: Widgets.getVersion(widgetType)
};
return [id, widget];
},
addWidget: function addWidget(type) {
var _this = this;
this.focus(function () {
return _this._handleChange(_this._insertNewWidget(type));
});
},
_insertNewWidget: function _insertNewWidget(type, draftDataParams) {
var _extends2;
var draftData = _extends({}, this._getDraftData(), draftDataParams);
var _createInitialWidget2 = this._createInitialWidget(type),
id = _createInitialWidget2[0],
widget = _createInitialWidget2[1];
var newWidgets = _extends({}, this.state.widgets, (_extends2 = {}, _extends2[id] = widget, _extends2));
var newDraftData = this._insertWidgetText(draftData, id);
return {
editorState: newDraftData.editorState,
widgets: newWidgets
};
},
_insertWidgetText: function _insertWidgetText(draftData, id) {
// Text for the widget is inserted, and an entity is assigned
var text = widgetPlaceholder.replace("{id}", id);
var entity = Entity.create("WIDGET", "IMMUTABLE", { id: id });
return DraftUtils.replaceSelection(draftData, text, entity);
},
updateWidget: function updateWidget(id, newProps) {
var _extends3;
this.setState({ widgets: _extends({}, this.state.widgets, (_extends3 = {}, _extends3[id] = newProps, _extends3)) });
},
// This function only removes the widget from the content, and then
// handleChange handles removing widgets from the state, as widgets
// can also be deleted by editor actions such as backspace and delete
removeWidget: function removeWidget(id) {
var _this2 = this;
this.focus(function () {
var _getDraftData2 = _this2._getDraftData(),
editorState = _getDraftData2.editorState,
contentState = _getDraftData2.contentState;
var selection = DraftUtils.findPattern(contentState, widgetRegexForId(id)); //eslint-disable-line max-len
var newDraftData = DraftUtils.deleteSelection({
editorState: editorState,
selection: selection
});
_this2._handleChange({ editorState: newDraftData.editorState });
});
},
addTemplate: function addTemplate(templateType) {
var _this3 = this;
this.focus(function () {
_this3._addTemplate(templateType);
});
},
_addTemplate: function _addTemplate(templateType) {
var _this4 = this;
var _getDraftData3 = this._getDraftData(),
editorState = _getDraftData3.editorState,
contentState = _getDraftData3.contentState,
selection = _getDraftData3.selection;
var widgets = _extends({}, this.state.widgets);
// Templates shouldn't interrupt a line if the cursor is not at the end
var currBlock = contentState.getBlockForKey(selection.getEndKey());
selection = DraftUtils.selectEnd(currBlock);
// Insert a new line at the beginning if there is content there, that
// way the template appears on a newline in the rendered markdown
if (currBlock.getText().length > 0) {
contentState = Modifier.splitBlock(contentState, selection);
selection = contentState.getSelectionAfter();
}
if (templateType === "allWidgets") {
var allTypes = Widgets.getAllWidgetTypes().sort();
// Insert a newline at the beginning
contentState = Modifier.splitBlock(contentState, selection);
contentState = allTypes.reduce(function (content, type) {
var _createInitialWidget3 = _this4._createInitialWidget(type),
id = _createInitialWidget3[0],
widget = _createInitialWidget3[1];
widgets[id] = widget;
content = _this4._insertWidgetText({
contentState: content,
selection: content.getSelectionAfter()
}, id).contentState;
return Modifier.splitBlock(
// Put each widget on a new line
content, content.getSelectionAfter());
}, contentState);
editorState = EditorState.push(editorState, contentState, "insert-fragment");
} else {
var template = "";
if (templateType === "table") {
template = "header 1 | header 2 | header 3\n" + "- | - | -\n" + "data 1 | data 2 | data 3\n" + "data 4 | data 5 | data 6\n" + "data 7 | data 8 | data 9";
} else if (templateType === "titledTable") {
template = "|| **Table title** ||\n" + "header 1 | header 2 | header 3\n" + "- | - | -\n" + "data 1 | data 2 | data 3\n" + "data 4 | data 5 | data 6\n" + "data 7 | data 8 | data 9";
} else if (templateType === "alignment") {
template = "$\\begin{align} x+5 &= 30 \\\\\n" + "x+5-5 &= 30-5 \\\\\n" + "x &= 25 \\end{align}$";
} else if (templateType === "piecewise") {
template = "$f(x) = \\begin{cases}\n" + "7 & \\text{if }x=1 \\\\\n" + "f(x-1)+5 & \\text{if }x > 1\n" + "\\end{cases}$";
}
editorState = DraftUtils.insertText({ editorState: editorState, contentState: contentState, selection: selection }, "\n" + template + "\n").editorState;
}
this._handleChange({ editorState: editorState, widgets: widgets });
},
_handleCopy: function _handleCopy() {
var _this5 = this;
var _getDraftData4 = this._getDraftData(),
contentState = _getDraftData4.contentState,
selection = _getDraftData4.selection;
var entities = DraftUtils.getEntities(contentState, selection);
var copiedWidgets = entities.reduce(function (map, entity) {
var id = entity.getData().id;
map[id] = _this5.state.widgets[id];
return map;
}, {});
localStorage.perseusLastCopiedWidgets = JSON.stringify(copiedWidgets);
return false;
},
// Widgets cannot have ID conflicts, therefore this function exists
// to return a mapping of { new id -> safe id }
_createSafeWidgetMapping: function _createSafeWidgetMapping(newWidgets, currentWidgets) {
// Create a mapping of { type -> largest id of that type }
var maxIds = Object.keys(currentWidgets).reduce(function (idMap, widget) {
var _widget$split = widget.split(" "),
type = _widget$split[0],
id = _widget$split[1];
idMap[type] = idMap[type] ? Math.max(idMap[type], +id) : +id;
return idMap;
}, {});
var safeWidgetMapping = Object.keys(newWidgets).reduce(function (safeMap, widget) {
var type = widget.split(" ")[0];
maxIds[type] = maxIds[type] ? maxIds[type] + 1 : 1;
safeMap[widget] = type + " " + maxIds[type];
return safeMap;
}, {});
return safeWidgetMapping;
},
// Pasting text from another Perseus editor instance should also copy over
// the widgets. To do this properly, we must parse the text, replace the
// widget ids with non-conflicting ones, store them, and also assign them
// proper entities. Sadly Draft.js only supports `handlePastedText` which
// happens prior to the new content state being generated (which is needed
// to add entities to). We therefore must reimplement the default Paste
// functionality, in order to add our custom steps afterwards
_handlePaste: function _handlePaste(pastedText, html, selection) {
// If no widgets are in localstorage, just use default behavior
var sourceWidgetsJSON = localStorage.perseusLastCopiedWidgets;
if (!sourceWidgetsJSON) {
return false;
}
var sourceWidgets = JSON.parse(sourceWidgetsJSON);
var widgets = _extends({}, this.state.widgets);
var safeWidgetMapping = this._createSafeWidgetMapping(sourceWidgets, widgets); //eslint-disable-line max-len
var charData = CharacterMetadata.create();
// insertText takes a sanitizer function which gets ran on every
// line. It is used here in order to fix the new widgets to not
// have conflicting IDs, as well as fill in the widget data
var sanitizeText = function sanitizeText(textLine) {
var sanitized = textLine.replace(new RegExp("\r", "g"), ""); //eslint-disable-line no-control-regex
var characterList = Array(sanitized.length).fill(charData);
var safeText = sanitized.replace(widgetRegExp, function (syntax, offset) {
//eslint-disable-line max-len
var match = widgetPartsRegExp.exec(syntax);
var fullText = match[0]; // The entire [[ widgetName id ]]
var widgetId = match[1]; // Just the "widgetName id" part
var newId = safeWidgetMapping[widgetId];
var newText = widgetPlaceholder.replace("{id}", newId);
// Create an entity for the new widget, and assign it to the
// characters that match up to the new widget text (splice)
var entity = Entity.create("WIDGET", "IMMUTABLE", {
id: newId
}); //eslint-disable-line max-len
var entityChar = CharacterMetadata.applyEntity(charData, entity); //eslint-disable-line max-len
var entityChars = Array(newText.length).fill(entityChar);
characterList.splice.apply(characterList, [offset, fullText.length].concat(entityChars));
widgets[newId] = sourceWidgets[widgetId];
return fullText.replace(widgetId, newId);
});
return { text: safeText, characterList: characterList };
};
var data = this._getDraftData();
data.selection = selection || data.selection;
var _DraftUtils$insertTex = DraftUtils.insertText(data, pastedText, sanitizeText),
editorState = _DraftUtils$insertTex.editorState;
this._handleChange({ editorState: editorState, widgets: widgets });
return true; // True means draft doesn't run its default behavior
},
_handleDrop: function _handleDrop(selection, dataTransfer) {
// All insertions are done to the end of the current block
var contentState = this.state.editorState.getCurrentContent();
var endKey = selection.getEndKey();
var endBlock = contentState.getBlockForKey(endKey);
var endSelection = DraftUtils.selectEnd(endBlock);
var imageUrl = dataTransfer.getLink();
if (imageUrl) {
// Adds new lines and collapses the selection
var _DraftUtils$insertTex2 = DraftUtils.insertText(_extends({}, this._getDraftData(), { selection: endSelection }), "\n"),
editorState = _DraftUtils$insertTex2.editorState;
this._handleChange({ editorState: editorState });
} else {
var text = dataTransfer.getText();
return this._handlePaste(text, null, endSelection);
}
return true; // Disable default draft drop handler
},
_handleDroppedFiles: function _handleDroppedFiles(selection, files) {
var _this6 = this;
var images = files.filter(function (file) {
return file.type.match("image.*");
});
var contentState = this.state.editorState.getCurrentContent();
images.forEach(function (image) {
// Insert placeholder text to show that the image is being uploaded
var text = "";
var id = genKey();
var entity = Entity.create("TEMP_IMAGE", "IMMUTABLE", { id: id });
var charData = CharacterMetadata.create().merge({ entity: entity });
var characterList = Array(text.length).fill(charData);
var sanitizer = function sanitizer(textLine) {
return textLine === text ? { text: text, characterList: characterList } : null;
};
var blockKey = selection.getEndKey();
var contentBlock = contentState.getBlockForKey(blockKey);
var endOfBlockSelection = DraftUtils.selectEnd(contentBlock);
contentState = DraftUtils.insertText({ contentState: contentState, selection: endOfBlockSelection }, "\n" + text + "\n", sanitizer).contentState;
// Begin uploading the image, and update the link once complete
_this6.props.imageUploader(image, function (url) {
var currEditor = _this6.state.editorState;
var currContent = currEditor.getCurrentContent();
var placeholderLocation = DraftUtils.findEntity(currContent, function (c) {
return c.getData().id === id;
});
var newDraftData = DraftUtils.replaceSelection({
editorState: currEditor,
contentState: currContent,
selection: placeholderLocation
}, "");
_this6._handleChange({ editorState: newDraftData.editorState });
});
});
var editorState = EditorState.push(this.state.editorState, contentState, "insert-fragment");
this._handleChange({ editorState: editorState });
return true; // Disable default draft drop handler
},
// This implements tab completion for widgets. When the user
// has typed [[d, then presses tab, we should replace [[d
// with the full [[ {emoji} dropdown 1 ]] text
_handleTab: function _handleTab(e) {
var _getDraftData5 = this._getDraftData(),
contentState = _getDraftData5.contentState,
selection = _getDraftData5.selection;
// isCollapsed means that there is no active selection, its just
// a blinking cursor. For the SelectionState object, this
// essentially means that anchorOffset === focusOffset
if (!selection.isCollapsed() || !this.props.widgetEnabled) {
return;
}
e.preventDefault();
var currBlock = contentState.getBlockForKey(selection.getEndKey());
var text = currBlock.getText().substring(0, selection.getEndOffset());
var match = text.match(partialWidgetRegex);
if (match) {
var partialName = match[1];
var allWidgets = Widgets.getAllWidgetTypes();
var matchingWidgets = allWidgets.filter(function (widget) {
return widget.substring(0, partialName.length) === partialName;
});
// If only one match is available, complete it
if (matchingWidgets.length === 1) {
var widgetType = matchingWidgets[0];
var replacementArea = selection.merge({
anchorOffset: match.index
});
this._handleChange(this._insertNewWidget(widgetType, {
selection: replacementArea
}));
}
}
return true; // Say that we've handled the event, no other work needed
},
_getDecorationForStyle: function _getDecorationForStyle(style) {
switch (style) {
case "perseus-bold":
return "**";
case "perseus-italics":
return "*";
case "perseus-underline":
return "__";
default:
return null;
}
},
_handleKeyCommand: function _handleKeyCommand(command) {
// Check if the font size should be changed
var fontSizePercentage = this.state.fontSizePercentage;
if (command === "perseus-increase-font-size") {
this.setState({ fontSizePercentage: fontSizePercentage + 10 });
return true;
} else if (command === "perseus-decrease-font-size") {
this.setState({ fontSizePercentage: fontSizePercentage - 10 });
return true;
} else if (command === "perseus-reset-font-size") {
this.setState({ fontSizePercentage: 100 });
return true;
}
// Check whether a style such as bold/italics/underline should be added
var decoration = this._getDecorationForStyle(command);
if (decoration !== null) {
var data = this._getDraftData();
var _DraftUtils$toggleDec = DraftUtils.toggleDecoration(data, decoration),
editorState = _DraftUtils$toggleDec.editorState;
this._handleChange({ editorState: editorState });
return true;
}
return false;
},
lastContentUpdate: "",
_updateParent: function _updateParent(content, widgets) {
// The parent component should know of only the active widgets,
// however the widgets are not deleted from this.state because a
// user undoing a widget deletion should also recover the
// widget's metadata
var currEntities = DraftUtils.getEntities(content);
var visibleWidgets = currEntities.reduce(function (map, entity) {
var id = entity.getData().id;
map[id] = widgets[id];
return map;
}, {});
this.lastContentUpdate = content.getPlainText("\n");
// Provide the parent component with the current text
// representation, as well as the current active widgets
this.props.onChange({
content: this.lastContentUpdate,
widgets: visibleWidgets
});
},
pastContentState: null,
lastIdleCallback: null,
_handleChange: function _handleChange(newState) {
var _this7 = this;
var state = _extends({}, this.state, newState);
var widgets = state.widgets;
var editorState = state.editorState;
// The cursor should not exist within an entity
editorState = DraftUtils.snapSelectionOutsideEntities({ editorState: editorState }, this.state.editorState.getSelection()).editorState;
var newContent = editorState.getCurrentContent();
// editorState contains more than just the content, such as the current
// cursor position. This means `handleChange` gets called for more than
// just content changes, so certain calculations aren't always needed.
if (newContent !== this.pastContentState) {
// This ensures that unless the content stops changing for a certain
// short duration, no processing will be done to update the parent.
// This allows the editing to remain performant for large files,
// as basic tasks only occur on individual ContentBlocks, while
// updating the parent involves iterating through them all
clearTimeout(this.lastIdleCallback);
this.lastIdleCallback = setTimeout(function () {
return _this7._updateParent(newContent, widgets);
}, UPDATE_PARENT_THROTTLE);
}
this.pastContentState = newContent;
this.setState({ editorState: editorState, widgets: widgets });
},
// HACK: There are currently serious Draft.js bugs related to mutating the
// editorState when it is not in focus, then pressing undo. This
// workaround uses a callback parameter to run code after the
// editorState has been updated to be in focus, that way functions
// such as addWidget will not bring up serious issues when undone
focus: function focus(callback) {
this.editor.focus();
var editorState = this.state.editorState;
editorState = EditorState.set(editorState, {
selection: editorState.getSelection().set("hasFocus", true),
forceSelection: true
});
this.setState({ editorState: editorState }, callback);
},
render: function render() {
var _this8 = this;
return React.createElement(
"div",
{
onCopy: this._handleCopy,
onCut: this._handleCopy,
onDragStart: this._handleCopy,
style: {
fontSize: this.state.fontSizePercentage + "%"
}
},
React.createElement(Editor, {
ref: function ref(e) {
return _this8.editor = e;
},
editorState: this.state.editorState,
onChange: function onChange(editorState) {
return _this8._handleChange({ editorState: editorState });
},
spellCheck: true,
stripPastedStyles: true,
placeholder: this.props.placeholder,
handlePastedText: this._handlePaste,
handleDroppedFiles: this._handleDroppedFiles,
handleDrop: this._handleDrop,
keyBindingFn: keyBindings,
handleKeyCommand: this._handleKeyCommand,
onTab: this._handleTab
})
);
}
});
module.exports = PerseusEditor;
/***/ },
/* 93 */
/***/ function(module, exports, __webpack_require__) {
/**
* These are things that widgets should exclude when serializing themselves.
*
* The use of this list needs to die. Basically, there are codepaths that
* blindly serialize the "props" of a widget so that it can pass around its
* info. Unfortunately, props aren't guaranteed to be serializable, and
* automatically serializing schemaless list of attributes causes issues (e.g.
* circular JSON structures sometimes).
*
* This blacklists things that we know don't need to be serialized.
*/
module.exports = [
// standard props "added" by react
// (technically the renderer still adds them)
"key", "ref",
// added by src/renderer.jsx
"containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "questionCompleted", "findWidgets",
// added by src/editor.jsx, for widgets removing themselves
// this is soooo not the right place for this, but alas.
"onRemove",
// also added by src/editor.jsx
"id",
// Callbacks and items for interaction handling
"onBlur", "onFocus", "trackInteraction", "keypadElement"];
/***/ },
/* 94 */,
/* 95 */,
/* 96 */,
/* 97 */,
/* 98 */,
/* 99 */,
/* 100 */,
/* 101 */,
/* 102 */,
/* 103 */,
/* 104 */,
/* 105 */,
/* 106 */,
/* 107 */,
/* 108 */,
/* 109 */,
/* 110 */,
/* 111 */,
/* 112 */,
/* 113 */,
/* 114 */,
/* 115 */,
/* 116 */,
/* 117 */,
/* 118 */,
/* 119 */,
/* 120 */,
/* 121 */,
/* 122 */,
/* 123 */,
/* 124 */,
/* 125 */,
/* 126 */,
/* 127 */,
/* 128 */,
/* 129 */,
/* 130 */,
/* 131 */,
/* 132 */,
/* 133 */,
/* 134 */,
/* 135 */,
/* 136 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* eslint-disable comma-dangle, no-var, react/sort-comp */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */
/* globals $_ */
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
var Changeable = __webpack_require__(187);
var PerseusMarkdown = __webpack_require__(49);
var WidgetJsonifyDeprecated = __webpack_require__(242);
var EN_DASH = "\u2013";
var PassageRef = React.createClass({
displayName: "PassageRef",
propTypes: _extends({}, Changeable.propTypes, {
passageNumber: React.PropTypes.number,
referenceNumber: React.PropTypes.number,
summaryText: React.PropTypes.string
}),
getDefaultProps: function getDefaultProps() {
return {
passageNumber: 1,
referenceNumber: 1,
summaryText: ""
};
},
getInitialState: function getInitialState() {
return {
lineRange: null,
content: null
};
},
shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
return !_.isEqual(this.props, nextProps) || !_.isEqual(this.state, nextState);
},
getUserInput: function getUserInput() {
return WidgetJsonifyDeprecated.getUserInput.call(this);
},
render: function render() {
var lineRange = this.state.lineRange;
var lineRangeOutput;
if (!lineRange) {
lineRangeOutput = $_({ lineRange: "?" + EN_DASH + "?" }, "lines %(lineRange)s");
} else if (lineRange[0] === lineRange[1]) {
lineRangeOutput = $_({ lineNumber: lineRange[0] }, "line %(lineNumber)s");
} else {
lineRangeOutput = $_({
lineRange: lineRange[0] + EN_DASH + lineRange[1]
}, "lines %(lineRange)s");
}
var summaryOutput;
if (this.props.summaryText) {
var summaryTree = PerseusMarkdown.parseInline(this.props.summaryText);
summaryOutput = React.createElement(
"span",
{ "aria-hidden": true },
" ",
"(\u201C",
PerseusMarkdown.basicOutput(summaryTree),
"\u201D)"
);
} else {
summaryOutput = null;
}
return React.createElement(
"span",
null,
lineRangeOutput,
summaryOutput,
lineRange && React.createElement(
"div",
{ className: "perseus-sr-only" },
this.state.content
)
);
},
change: function change() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return Changeable.change.apply(this, args);
},
componentDidMount: function componentDidMount() {
this._deferredUpdateRange();
this._throttledUpdateRange = _.throttle(this._deferredUpdateRange, 500);
window.addEventListener("resize", this._throttledUpdateRange);
},
componentDidUpdate: function componentDidUpdate() {
this._deferredUpdateRange();
},
componentWillUnmount: function componentWillUnmount() {
window.removeEventListener("resize", this._throttledUpdateRange);
},
_deferredUpdateRange: function _deferredUpdateRange() {
_.defer(this._updateRange);
},
_updateRange: function _updateRange() {
var passage = this.props.findWidgets("passage " + this.props.passageNumber)[0];
var refInfo = null;
if (passage) {
refInfo = passage.getReference(this.props.referenceNumber);
}
if (this.isMounted()) {
if (refInfo) {
this.setState({
lineRange: [refInfo.startLine, refInfo.endLine],
content: refInfo.content
});
} else {
this.setState({
lineRange: null,
content: null
});
}
}
},
simpleValidate: function simpleValidate(rubric) {
return PassageRef.validate(this.getUserInput(), rubric);
}
});
_.extend(PassageRef, {
validate: function validate(state, rubric) {
return {
type: "points",
earned: 0,
total: 0,
message: null
};
}
});
module.exports = {
name: "passage-ref",
displayName: "PassageRef (SAT only)",
defaultAlignment: "inline",
widget: PassageRef,
transform: function transform(editorProps) {
return _.pick(editorProps, "passageNumber", "referenceNumber", "summaryText");
},
version: { major: 0, minor: 1 }
};
/***/ },
/* 137 */,
/* 138 */,
/* 139 */,
/* 140 */,
/* 141 */,
/* 142 */,
/* 143 */,
/* 144 */,
/* 145 */,
/* 146 */,
/* 147 */,
/* 148 */,
/* 149 */,
/* 150 */,
/* 151 */,
/* 152 */,
/* 153 */,
/* 154 */,
/* 155 */,
/* 156 */,
/* 157 */,
/* 158 */,
/* 159 */,
/* 160 */,
/* 161 */,
/* 162 */,
/* 163 */,
/* 164 */
/***/ function(module, exports, __webpack_require__) {
/* eslint-disable react/forbid-prop-types, react/sort-comp */
var React = __webpack_require__(43);
var ReactDOM = __webpack_require__(44);
var _ = __webpack_require__(56);
var textWidthCache = {};
function getTextWidth(text) {
if (!textWidthCache[text]) {
// Hacky way to guess the width of an input box
var $test = $("").text(text).appendTo("body");
textWidthCache[text] = $test.width() + 5;
$test.remove();
}
return textWidthCache[text];
}
var TextListEditor = React.createClass({
displayName: "TextListEditor",
propTypes: {
options: React.PropTypes.array,
layout: React.PropTypes.string,
onChange: React.PropTypes.func.isRequired
},
getDefaultProps: function getDefaultProps() {
return {
options: [],
layout: "horizontal"
};
},
getInitialState: function getInitialState() {
return {
items: this.props.options.concat("")
};
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
this.setState({
items: nextProps.options.concat("")
});
},
render: function render() {
var className = ["perseus-text-list-editor", "perseus-clearfix", "layout-" + this.props.layout].join(" ");
var inputs = _.map(this.state.items, function (item, i) {
return React.createElement(
"li",
{ key: i },
React.createElement("input", {
ref: "input_" + i,
type: "text",
value: item,
onChange: this.onChange.bind(this, i),
onKeyDown: this.onKeyDown.bind(this, i),
style: { width: getTextWidth(item) }
})
);
}, this);
return React.createElement(
"ul",
{ className: className },
inputs
);
},
onChange: function onChange(index, event) {
var items = _.clone(this.state.items);
items[index] = event.target.value;
if (index === items.length - 1) {
items = items.concat("");
}
this.setState({ items: items });
this.props.onChange(_.compact(items));
},
onKeyDown: function onKeyDown(index, event) {
var which = event.nativeEvent.keyCode;
// Backspace deletes an empty input...
if (which === 8 /* backspace */ && this.state.items[index] === "") {
event.preventDefault();
var items = _.clone(this.state.items);
var focusIndex = index === 0 ? 0 : index - 1;
if (index === items.length - 1 && (index === 0 || items[focusIndex] !== "")) {
// ...except for the last one, iff it is the only empty
// input at the end.
ReactDOM.findDOMNode(this.refs["input_" + focusIndex]).focus();
} else {
items.splice(index, 1);
this.setState({ items: items }, function () {
ReactDOM.findDOMNode(this.refs["input_" + focusIndex]).focus();
});
}
// Deleting the last character in the second-to-last input
// removes it
} else if (which === 8 /* backspace */ && this.state.items[index].length === 1 && index === this.state.items.length - 2) {
event.preventDefault();
var _items = _.clone(this.state.items);
_items.splice(index, 1);
this.setState({ items: _items });
this.props.onChange(_.compact(_items));
// Enter adds an option below the current one...
} else if (which === 13 /* enter */) {
event.preventDefault();
var _items2 = _.clone(this.state.items);
var _focusIndex = index + 1;
if (index === _items2.length - 2) {
// ...unless the empty input is just below.
ReactDOM.findDOMNode(this.refs["input_" + _focusIndex]).focus();
} else {
_items2.splice(_focusIndex, 0, "");
this.setState({ items: _items2 }, function () {
ReactDOM.findDOMNode(this.refs["input_" + _focusIndex]).focus();
});
}
}
}
});
module.exports = TextListEditor;
/***/ },
/* 165 */,
/* 166 */,
/* 167 */,
/* 168 */
/***/ function(module, exports, __webpack_require__) {
/* NOTE: This mimics what we do in webapp and links to our custom version of
React -- this was not added with npm */
module.exports = __webpack_require__(43).__internalAddons.createFragment;
/***/ },
/* 169 */
/***/ function(module, exports, __webpack_require__) {
module.exports = window.$
/***/ },
/* 170 */
/***/ function(module, exports, __webpack_require__) {
var babelPluginFlowReactPropTypes_proptype_ObjectNode = __webpack_require__(188).babelPluginFlowReactPropTypes_proptype_ObjectNode || __webpack_require__(43).PropTypes.any;
/**
* Type definitions for multi-item types, including:
*
* - Item: A multi-item tree wrapped in a `_multi` key, to help us recognize it
* as a multi-item in other contexts and avoid misinterpreting its
* other properties.
* - ItemTree: A multi-item without the `_multi` key. Conforms to the Tree
* interface, so it's compatible with our tree traversal functions.
* - And the various types of nodes that compose a tree.
*/
var babelPluginFlowReactPropTypes_proptype_ArrayNode = __webpack_require__(188).babelPluginFlowReactPropTypes_proptype_ArrayNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Tree = __webpack_require__(188).babelPluginFlowReactPropTypes_proptype_Tree || __webpack_require__(43).PropTypes.any;
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_ContentNode", __webpack_require__(43).PropTypes.shape({
__type: __webpack_require__(43).PropTypes.oneOf(["content", "item"]).isRequired,
content: __webpack_require__(43).PropTypes.string,
images: __webpack_require__(43).PropTypes.shape({}),
widgets: __webpack_require__(43).PropTypes.shape({})
}));
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_HintNode", __webpack_require__(43).PropTypes.shape({
__type: __webpack_require__(43).PropTypes.oneOf(["hint"]).isRequired,
content: __webpack_require__(43).PropTypes.string,
images: __webpack_require__(43).PropTypes.shape({}),
widgets: __webpack_require__(43).PropTypes.shape({}),
replace: __webpack_require__(43).PropTypes.bool
}));
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_Item", __webpack_require__(43).PropTypes.shape({
_multi: __webpack_require__(43).PropTypes.any.isRequired
}));
/***/ },
/* 171 */
/***/ function(module, exports, __webpack_require__) {
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_ContentShape", __webpack_require__(43).PropTypes.shape({
type: __webpack_require__(43).PropTypes.oneOf(["content"]).isRequired
}));
/**
* Type definitions for multi-item shapes.
*
* A shape is an object that serves as a runtime type declaration: it specifies
* a tree structure for a particular class of multi-item.
*
* We use shapes instead static compile-time typing because the CMS needs to
* understand the shape of our content library's multi-items at runtime, and
* it's not always possible to infer the full shape from an example multi-item.
*
* Shapes also enable us to traverse a multi-item-shaped tree with confidence,
* even when we can't infer the shape from the tree alone.
*
* We *could* go all-in on a more general library to make certain Flow types
* runtime-inspectable, in order to DRY some things up, but that's probably a
* big ol' infrastructural magic mess, and the narrower scope of Shapes makes
* it easier to be confident that we've covered all cases rather than having to
* deal with all possible Javascript types.
*/
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_HintShape", __webpack_require__(43).PropTypes.shape({
type: __webpack_require__(43).PropTypes.oneOf(["hint"]).isRequired
}));
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_TagsShape", __webpack_require__(43).PropTypes.shape({
type: __webpack_require__(43).PropTypes.oneOf(["tags"]).isRequired
}));
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_ArrayShape", __webpack_require__(43).PropTypes.shape({
type: __webpack_require__(43).PropTypes.oneOf(["array"]).isRequired,
elementShape: __webpack_require__(43).PropTypes.any.isRequired
}));
Object.defineProperty(module.exports, "babelPluginFlowReactPropTypes_proptype_ObjectShape", __webpack_require__(43).PropTypes.shape({
type: __webpack_require__(43).PropTypes.oneOf(["object"]).isRequired,
shape: __webpack_require__(43).PropTypes.shape({}).isRequired
}));
/***/ },
/* 172 */
/***/ function(module, exports, __webpack_require__) {
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var babelPluginFlowReactPropTypes_proptype_ArrayShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_ArrayShape || __webpack_require__(43).PropTypes.any;
/**
* Utility functions for manipulating multi-item-shaped trees.
*
* Multi-items are trees! But we also often have other trees that are shaped
* like multi-items - for example, if we map a multi-item tree into a tree of
* renderer info and state, and then map that again into a tree of just the
* renderer nodes, like we do in MultiRenderer. See tree-types.js for further
* discussion.
*
* These functions enable us to manipulate generic multi-item-shaped trees,
* regardless of what type of data they contain at their leaves. You can use
* the mapper functions to transform a tree into another tree of the same
* shape, or to discover all the nodes of a particular type.
*
* We expose two simple mapper functions (mapContentNodes and mapHintNodes),
* and also a more complex interface for creating a mapping over all of a
* tree's node types simultaneously:
*
* `buildMapper()` returns a TreeMapper object that allows you to build your
* mapper object one node type at a time. Then, you can execute your mapping by
* calling the `mapTree` method.
*
* For example:
* const renderers = buildMapper()
* .setContentMapper(this.renderContentNode)
* .setHintMapper(this.renderHintNode)
* .setTagsMapper(this.renderTagsNode)
* .setArrayMapper(this.hideSkippedQuestions)
* .mapTree(tree, shape);
*
* This will copy the given tree, apply the given transformations to the
* content, hint, and array nodes respectively, and return the resulting tree.
*
* For node types whose mappers aren't specified, we default to the identity
* function. (This builder interface enables us to implement that default
* behavior in a provably type-safe way, while not requiring the call site to
* be aware of all the node types. Hooray!)
*
* The call to `setArrayMapper` must come last, because the array mapper's
* argument types depend on the other mappers' types. See ArrayMapper for more
* details.
*
* WARNING: These functions trust that the provided tree conforms to the
* provided shape. If not, behavior is undefined and may not respect the type
* signatures specified here.
*/
var babelPluginFlowReactPropTypes_proptype_TagsShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_TagsShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_HintShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_HintShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ContentShape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_ContentShape || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Shape = __webpack_require__(171).babelPluginFlowReactPropTypes_proptype_Shape || __webpack_require__(43).PropTypes.any;
/**
* The sequence of edges that lead to a particular node in a Tree.
* Elements can be `string` to correspond to an ObjectNode key, or `number` to
* correspond to an ArrayNode index.
*/
var babelPluginFlowReactPropTypes_proptype_ObjectNode = __webpack_require__(188).babelPluginFlowReactPropTypes_proptype_ObjectNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_ArrayNode = __webpack_require__(188).babelPluginFlowReactPropTypes_proptype_ArrayNode || __webpack_require__(43).PropTypes.any;
var babelPluginFlowReactPropTypes_proptype_Tree = __webpack_require__(188).babelPluginFlowReactPropTypes_proptype_Tree || __webpack_require__(43).PropTypes.any;
/**
* These are function interfaces for mapping over various types of tree nodes.
*
* ArrayMapper is a bit more complicated than the leaf node mappers. It's
* executed in the context of a `mapTree` call, after we've finished mapping
* its child nodes, so the function has access to both the resulting array
* (with mapped elements) and the original array (with the original untouched
* elements).
*
* The ArrayMapper then has the opportunity to apply a final transformation to
* the resulting array, like filtering certain elements or (in the hacky
* MultiRenderer case) attaching a `renderHints` method to arrays of hint
* renderers :)
*
* This is why `TreeMapper#setArrayMapper` must be called last: ArrayMapper's
* types depend on the ContentMapper and HintMapper's types. And, since you can
* only specify one mapper at a time in this builder interface (which is
* necessary to provide default mappers in a type-safe way), you need your
* dependencies to already be in place by the time you call `setArrayMapper`.
* Otherwise, we'd have to set the ArrayMapper and *hope* that you *eventually*
* provide a compatible ContentMapper and HintMapper, which is difficult to
* prove at compile time.
*
* There's no ObjectMapper here, but not for any particular reason. We just
* don't have a use case for it yet, so we haven't built it yet.
*/
/**
* A TreeMapper is a collection of node mappers, which, together, compose the
* behavior for mapping over an entire tree.
*
* This serves as the interface for the two TreeMapper classes, including both
* the internal mapper properties that we care about, and the `mapTree`
* function that the call site will use.
*/
/**
* This is a TreeMapper that only has mappers specified for its leaf nodes; its
* array mapper is the identity function.
*
* This is the TreeMapper initially returned by `buildMapper`. It allows you to
* change the types of your ContentMapper and HintMapper, which is safe because
* none of the other mappers that depend on those types (aka ArrayMapper) have
* been specified yet. (Or, more specifically, the ArrayMapper is currently
* `identity`, which can trivially vary with the ContentMapper and HintMapper's
* types.)
*
* Once you call `setArrayMapper`, however, we move to the other class:
* TreeMapperForLeavesAndCollections.
*/
var TreeMapperJustForLeaves = function () {
function TreeMapperJustForLeaves(content, hint, tags) {
_classCallCheck(this, TreeMapperJustForLeaves);
this.content = content;
this.hint = hint;
this.tags = tags;
this.array = identity;
}
TreeMapperJustForLeaves.prototype.setContentMapper = function setContentMapper(newContentMapper) {
return new TreeMapperJustForLeaves(newContentMapper, this.hint, this.tags);
};
TreeMapperJustForLeaves.prototype.setHintMapper = function setHintMapper(newHintMapper) {
return new TreeMapperJustForLeaves(this.content, newHintMapper, this.tags);
};
TreeMapperJustForLeaves.prototype.setTagsMapper = function setTagsMapper(newTagsMapper) {
return new TreeMapperJustForLeaves(this.content, this.hint, newTagsMapper);
};
TreeMapperJustForLeaves.prototype.setArrayMapper = function setArrayMapper(newArrayMapper) {
return new TreeMapperForLeavesAndCollections(this.content, this.hint, this.tags, newArrayMapper);
};
TreeMapperJustForLeaves.prototype.mapTree = function mapTree(tree, shape) {
return _mapTree(tree, shape, [], this);
};
return TreeMapperJustForLeaves;
}();
/**
* This is a TreeMapper that already has an ArrayMapper specified, so its
* ContentMapper and HintMapper are now locked in.
*/
var TreeMapperForLeavesAndCollections = function () {
function TreeMapperForLeavesAndCollections(content, hint, tags, array) {
_classCallCheck(this, TreeMapperForLeavesAndCollections);
this.content = content;
this.hint = hint;
this.tags = tags;
this.array = array;
}
TreeMapperForLeavesAndCollections.prototype.setArrayMapper = function setArrayMapper(newArrayMapper) {
return new TreeMapperForLeavesAndCollections(this.content, this.hint, this.tags, newArrayMapper);
};
TreeMapperForLeavesAndCollections.prototype.mapTree = function mapTree(tree, shape) {
return _mapTree(tree, shape, [], this);
};
return TreeMapperForLeavesAndCollections;
}();
function identity(x) {
return x;
}
/**
* Return a new TreeMapper that will perform a no-op transformation on an input
* tree. To make it useful, chain any combination of `setContentMapper`,
* `setHintMapper`, `setTagMapper`, and `setArrayMapper` to specify
* transformations for the individual node types.
*/
function buildMapper() {
return new TreeMapperJustForLeaves(identity, identity, identity);
}
/**
* Copy the given tree, apply the corresponding transformation specified in the
* TreeMapper to each node, and return the resulting tree.
*/
function _mapTree(tree, shape, path, mappers) {
// We trust the shape of the multi-item to match the shape provided at
// runtime. Therefore, in each shape branch, we cast the node to `any` and
// reinterpret it as the expected node type.
if (shape.type === "content") {
var _content = tree;
return mappers.content(_content, shape, path);
} else if (shape.type === "hint") {
var _hint = tree;
return mappers.hint(_hint, shape, path);
} else if (shape.type === "tags") {
var _tags = tree;
return mappers.tags(_tags, shape, path);
} else if (shape.type === "array") {
var _array = tree;
if (!Array.isArray(_array)) {
throw new Error("Invalid object of type \"" + (typeof _array === "undefined" ? "undefined" : _typeof(_array)) + "\" found at path " + ([""].concat(path).join(".") + ". Expected array."));
}
var elementShape = shape.elementShape;
var mappedElements = _array.map(function (inner, i) {
return _mapTree(inner, elementShape, path.concat(i), mappers);
});
return mappers.array(mappedElements, _array, shape, path);
} else if (shape.type === "object") {
var object = tree;
if (object && (typeof object === "undefined" ? "undefined" : _typeof(object)) !== "object") {
throw new Error("Invalid object of type \"" + (typeof object === "undefined" ? "undefined" : _typeof(object)) + "\" found at " + ("path " + [""].concat(path).join(".") + ". Expected ") + "\"object\" type.");
}
var valueShapes = shape.shape;
if (!valueShapes) {
throw new Error("Unexpected shape " + JSON.stringify(shape) + " at path " + ([""].concat(path).join(".") + "."));
}
var newObject = {};
Object.keys(valueShapes).forEach(function (key) {
if (!(key in object)) {
throw new Error("Key \"" + key + "\" is missing from shape at path " + ([""].concat(path).join(".") + "."));
}
newObject[key] = _mapTree(object[key], valueShapes[key], path.concat(key), mappers);
});
return newObject;
} else {
throw new Error("unexpected shape type " + shape.type);
}
}
module.exports = {
buildMapper: buildMapper
};
/***/ },
/* 173 */
/***/ function(module, exports, __webpack_require__) {
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* global i18n */
var React = __webpack_require__(43);
var _ = __webpack_require__(56);
var Renderer = __webpack_require__(37);
var PassageRef = __webpack_require__(136);
var Util = __webpack_require__(17);
var BaseRadio = __webpack_require__(186);
var _require = __webpack_require__(52),
linterContextProps = _require.linterContextProps,
linterContextDefault = _require.linterContextDefault;
var Radio = React.createClass({
displayName: "Radio",
propTypes: {
apiOptions: BaseRadio.propTypes.apiOptions,
choices: React.PropTypes.arrayOf(React.PropTypes.shape({
content: React.PropTypes.string.isRequired,
// Clues are called "rationales" in most other places but are
// left as "clue"s here to preserve legacy widget data.
clue: React.PropTypes.string,
correct: React.PropTypes.bool,
isNoneOfTheAbove: React.PropTypes.bool,
originalIndex: React.PropTypes.number.isRequired
}).isRequired).isRequired,
deselectEnabled: React.PropTypes.bool,
displayCount: React.PropTypes.any,
findWidgets: React.PropTypes.func,
multipleSelect: React.PropTypes.bool,
countChoices: React.PropTypes.bool,
numCorrect: React.PropTypes.number,
onChange: React.PropTypes.func.isRequired,
questionCompleted: React.PropTypes.bool,
reviewModeRubric: BaseRadio.propTypes.reviewModeRubric,
trackInteraction: React.PropTypes.func.isRequired,
// values is the legacy choiceState data format
values: React.PropTypes.arrayOf(React.PropTypes.bool),
choiceStates: React.PropTypes.arrayOf(React.PropTypes.shape({
// Indicates whether this choice is selected. (Inside
// BaseRadio, this is called `checked`.)
selected: React.PropTypes.bool,
// Indicates whether the user has "crossed out" this choice,
// meaning that they don't think it's correct. This value does
// not affect scoring or other behavior; it's just a note for
// the user's reference.
crossedOut: React.PropTypes.bool,
highlighted: React.PropTypes.bool,
rationaleShown: React.PropTypes.bool,
correctnessShown: React.PropTypes.bool,
readOnly: React.PropTypes.bool
}).isRequired),
linterContext: linterContextProps,
static: React.PropTypes.bool
},
getDefaultProps: function getDefaultProps() {
return {
choices: [{}],
displayCount: null,
multipleSelect: false,
countChoices: false,
deselectEnabled: false,
linterContext: linterContextDefault
};
},
_renderRenderer: function _renderRenderer(content) {
content = content || "";
var nextPassageRefId = 1;
var widgets = {};
var modContent = content.replace(/\{\{passage-ref (\d+) (\d+)(?: "([^"]*)")?\}\}/g, function (match, passageNum, refNum, summaryText) {
var widgetId = "passage-ref " + nextPassageRefId;
nextPassageRefId++;
widgets[widgetId] = {
type: "passage-ref",
graded: false,
options: {
passageNumber: parseInt(passageNum),
referenceNumber: parseInt(refNum),
summaryText: summaryText
},
version: PassageRef.version
};
return "[[" + Util.snowman + " " + widgetId + "]]";
});
// alwaysUpdate={true} so that passage-refs findWidgets
// get called when the outer passage updates the renderer
// TODO(aria): This is really hacky
// We pass in a key here so that we avoid a semi-spurious
// react warning when the ChoiceNoneAbove renders a
// different renderer in the same place. Note this destroys
// state, but since all we're doing is outputting
// "None of the above", that is okay.
// TODO(mdr): Widgets inside this Renderer are not discoverable through
// the parent Renderer's `findWidgets` function.
return React.createElement(Renderer, {
key: "choiceContentRenderer",
content: modContent,
widgets: widgets,
findExternalWidgets: this.props.findWidgets,
alwaysUpdate: true,
linterContext: this.props.linterContext
});
},
focus: function focus(i) {
return this.refs.baseRadio.focus(i);
},
// When `BaseRadio`'s `onChange` handler is called, indicating a change in
// our choices' state, we need to call our `onChange` handler in order to
// persist those changes in the item's Perseus state.
//
// So, given the new values for each choice, construct the new
// `choiceStates` objects, and pass them to `this.props.onChange`.
//
// `newValueLists` is an object with two keys: `checked` and `crossedOut`.
// Each contains an array of boolean values, specifying the new checked and
// crossed-out value of each choice.
//
// NOTE(mdr): This method expects to be auto-bound. If this component is
// converted to an ES6 class, take care to auto-bind this method!
updateChoices: function updateChoices(newValueLists) {
var _props = this.props,
choiceStates = _props.choiceStates,
choices = _props.choices;
// Construct the baseline `choiceStates` objects. If this is the user's
// first interaction with the widget, we'll need to initialize them to
// new objects with all fields set to the default values. Otherwise, we
// should clone the old `choiceStates` objects, in preparation to
// mutate them.
var newChoiceStates = void 0;
if (choiceStates) {
newChoiceStates = choiceStates.map(function (state) {
return _extends({}, state);
});
} else {
newChoiceStates = choices.map(function () {
return {
selected: false,
crossedOut: false,
highlighted: false,
rationaleShown: false,
correctnessShown: false,
readOnly: false
};
});
}
// Mutate the new `choiceState` objects, according to the new `checked`
// and `crossedOut` values provided in `newValueLists`.
newChoiceStates.forEach(function (choiceState, i) {
choiceState.selected = newValueLists.checked[i];
choiceState.crossedOut = newValueLists.crossedOut[i];
});
this.props.onChange({ choiceStates: newChoiceStates });
this.props.trackInteraction();
},
getUserInput: function getUserInput() {
// Return checked inputs in the form {choicesSelected: [bool]}. (Dear
// future timeline implementers: this used to be {value: i} before
// multiple select was added)
if (this.props.choiceStates) {
var noneOfTheAboveIndex = null;
var noneOfTheAboveSelected = false;
var choiceStates = this.props.choiceStates;
var choicesSelected = choiceStates.map(function () {
return false;
});
var countChoices = this.props.countChoices;
var numCorrect = this.props.numCorrect;
for (var i = 0; i < choicesSelected.length; i++) {
var index = this.props.choices[i].originalIndex;
choicesSelected[index] = choiceStates[i].selected;
if (this.props.choices[i].isNoneOfTheAbove) {
noneOfTheAboveIndex = index;
if (choicesSelected[i]) {
noneOfTheAboveSelected = true;
}
}
}
return {
countChoices: countChoices,
choicesSelected: choicesSelected,
numCorrect: numCorrect,
noneOfTheAboveIndex: noneOfTheAboveIndex,
noneOfTheAboveSelected: noneOfTheAboveSelected
};
// Support legacy choiceState implementation
} else if (this.props.values) {
var _noneOfTheAboveIndex = null;
var _noneOfTheAboveSelected = false;
var values = this.props.values.slice();
var _countChoices = this.props.countChoices;
var _numCorrect = this.props.numCorrect;
for (var _i = 0; _i < this.props.values.length; _i++) {
var _index = this.props.choices[_i].originalIndex;
values[_index] = this.props.values[_i];
if (this.props.choices[_i].isNoneOfTheAbove) {
_noneOfTheAboveIndex = _index;
if (values[_i]) {
_noneOfTheAboveSelected = true;
}
}
}
return {
choicesSelected: values,
noneOfTheAboveIndex: _noneOfTheAboveIndex,
noneOfTheAboveSelected: _noneOfTheAboveSelected,
countChoices: _countChoices,
numCorrect: _numCorrect
};
} else {
// Nothing checked
return {
choicesSelected: _.map(this.props.choices, function () {
return false;
})
};
}
},
simpleValidate: function simpleValidate(rubric) {
return Radio.validate(this.getUserInput(), rubric);
},
enforceOrdering: function enforceOrdering(choices) {
var content = _.pluck(choices, "content");
if (_.isEqual(content, [i18n._("False"), i18n._("True")]) || _.isEqual(content, [i18n._("No"), i18n._("Yes")])) {
return [choices[1]].concat([choices[0]]);
}
return choices;
},
/**
* Turn on rationale display for the currently selected choices. Note that
* this leaves rationales on for choices that are already showing
* rationales.
*/
showRationalesForCurrentlySelectedChoices: function showRationalesForCurrentlySelectedChoices(rubric) {
if (this.props.choiceStates) {
var score = this.simpleValidate(rubric);
var widgetCorrect = score.type === "points" && score.total === score.earned;
var newStates = this.props.choiceStates.map(function (state) {
return _extends({}, state, {
highlighted: state.selected,
// If the choice is selected, show the rationale now
rationaleShown: state.selected ||
// If the choice already had a rationale, keep it shown
state.rationaleShown ||
// If the widget is correctly answered, show the rationale
// for all the choices
widgetCorrect,
// We use the same behavior for the readOnly flag as for
// rationaleShown, but we keep it separate in case other
// behaviors want to disable choices without showing rationales.
readOnly: state.selected || state.readOnly || widgetCorrect,
correctnessShown: state.selected || state.correctnessShown
});
});
this.props.onChange({
choiceStates: newStates
}, null, // cb
true // silent
);
}
},
/**
* Deselects any currently-selected choices that are not correct choices.
*/
deselectIncorrectSelectedChoices: function deselectIncorrectSelectedChoices() {
var _this = this;
if (this.props.choiceStates) {
var newStates = this.props.choiceStates.map(function (state, i) {
return _extends({}, state, {
selected: state.selected && !!_this.props.choices[i].correct,
highlighted: false
});
});
this.props.onChange({
choiceStates: newStates
}, null, // cb
false // silent
);
}
},
render: function render() {
var _this2 = this;
var choices = this.props.choices;
var choiceStates = void 0;
if (this.props.static) {
choiceStates = _.map(choices, function (val) {
return {
selected: val.correct,
crossedOut: val.crossedOut,
readOnly: true,
highlighted: false,
rationaleShown: true,
correctnessShown: true
};
});
} else if (this.props.choiceStates) {
choiceStates = this.props.choiceStates;
} else if (this.props.values) {
// Support legacy choiceStates implementation
choiceStates = _.map(this.props.values, function (val) {
return {
selected: val,
crossedOut: false,
readOnly: false,
highlighted: false,
rationaleShown: false,
correctnessShown: false
};
});
} else {
choiceStates = _.map(choices, function () {
return {
selected: false,
crossedOut: false,
readOnly: false,
highlighted: false,
rationaleShown: false,
correctnessShown: false
};
});
}
choices = _.map(choices, function (choice, i) {
var content = choice.isNoneOfTheAbove && !choice.content ? // we use i18n._ instead of $_ here because the content
// sent to a renderer needs to be a string, not a react
// node (/renderable/fragment).
i18n._("None of the above") : choice.content;
var _choiceStates$i = choiceStates[i],
selected = _choiceStates$i.selected,
crossedOut = _choiceStates$i.crossedOut,
rationaleShown = _choiceStates$i.rationaleShown,
correctnessShown = _choiceStates$i.correctnessShown,
readOnly = _choiceStates$i.readOnly,
highlighted = _choiceStates$i.highlighted;
var reviewChoice = _this2.props.reviewModeRubric && _this2.props.reviewModeRubric.choices[i];
return {
content: _this2._renderRenderer(content),
checked: selected,
// Current versions of the radio widget always pass in the
// "correct" value through the choices. Old serialized state
// for radio widgets doesn't have this though, so we have to
// pull the correctness out of the review mode rubric. This
// only works because all of the places we use
// `restoreSerializedState()` also turn on reviewMode, but is
// fine for now.
// TODO(emily): Come up with a more comprehensive way to solve
// this sort of "serialized state breaks when internal
// structure changes" problem.
correct: typeof choice.correct === "undefined" ? !!reviewChoice && reviewChoice.correct : choice.correct,
disabled: readOnly,
hasRationale: !!choice.clue,
rationale: _this2._renderRenderer(choice.clue),
showRationale: rationaleShown,
showCorrectness: correctnessShown,
isNoneOfTheAbove: choice.isNoneOfTheAbove,
revealNoneOfTheAbove: _this2.props.questionCompleted && selected,
crossedOut: crossedOut,
highlighted: highlighted
};
});
choices = this.enforceOrdering(choices);
return React.createElement(BaseRadio, {
ref: "baseRadio",
labelWrap: true,
multipleSelect: this.props.multipleSelect,
countChoices: this.props.countChoices,
numCorrect: this.props.numCorrect,
choices: choices,
onChange: this.updateChoices,
reviewModeRubric: this.props.reviewModeRubric,
deselectEnabled: this.props.deselectEnabled,
apiOptions: this.props.apiOptions
});
}
});
_.extend(Radio, {
validate: function validate(state, rubric) {
var numSelected = _.reduce(state.choicesSelected, function (sum, selected) {
return sum + (selected ? 1 : 0);
}, 0);
if (numSelected === 0) {
return {
type: "invalid",
message: null
};
} else if (state.countChoices && numSelected !== state.numCorrect) {
return {
type: "invalid",
message: i18n._("Please choose the correct number of answers.")
};
// If NOTA and some other answer are checked, ...
} else if (state.noneOfTheAboveSelected && numSelected > 1) {
return {
type: "invalid",
message: i18n._("'None of the above' may not be selected " + "when other answers are selected.")
};
} else {
/* jshint -W018 */
var correct = _.all(state.choicesSelected, function (selected, i) {
var isCorrect = void 0;
if (state.noneOfTheAboveIndex === i) {
isCorrect = _.all(rubric.choices, function (choice, j) {
return i === j || !choice.correct;
});
} else {
isCorrect = !!rubric.choices[i].correct;
}
return isCorrect === selected;
});
/* jshint +W018 */
return {
type: "points",
earned: correct ? 1 : 0,
total: 1,
message: null
};
}
}
});
module.exports = Radio;
/***/ },
/* 174 */
/***/ function(module, exports, __webpack_require__) {
// {K1: V1, K2: V2, ...} -> [[K1, V1], [K2, V2]]
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var objectToPairs = function objectToPairs(obj) {
return Object.keys(obj).map(function (key) {
return [key, obj[key]];
});
};
exports.objectToPairs = objectToPairs;
// [[K1, V1], [K2, V2]] -> {K1: V1, K2: V2, ...}
var pairsToObject = function pairsToObject(pairs) {
var result = {};
pairs.forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2);
var key = _ref2[0];
var val = _ref2[1];
result[key] = val;
});
return result;
};
var mapObj = function mapObj(obj, fn) {
return pairsToObject(objectToPairs(obj).map(fn));
};
exports.mapObj = mapObj;
// Flattens an array one level
// [[A], [B, C, [D]]] -> [A, B, C, [D]]
var flatten = function flatten(list) {
return list.reduce(function (memo, x) {
return memo.concat(x);
}, []);
};
exports.flatten = flatten;
var UPPERCASE_RE = /([A-Z])/g;
var MS_RE = /^ms-/;
var kebabify = function kebabify(string) {
return string.replace(UPPERCASE_RE, '-$1').toLowerCase();
};
var kebabifyStyleName = function kebabifyStyleName(string) {
return kebabify(string).replace(MS_RE, '-ms-');
};
exports.kebabifyStyleName = kebabifyStyleName;
var recursiveMerge = function recursiveMerge(a, b) {
// TODO(jlfwong): Handle malformed input where a and b are not the same
// type.
if (typeof a !== 'object') {
return b;
}
var ret = _extends({}, a);
Object.keys(b).forEach(function (key) {
if (ret.hasOwnProperty(key)) {
ret[key] = recursiveMerge(a[key], b[key]);
} else {
ret[key] = b[key];
}
});
return ret;
};
exports.recursiveMerge = recursiveMerge;
/**
* CSS properties which accept numbers but are not in units of "px".
* Taken from React's CSSProperty.js
*/
var isUnitlessNumber = {
animationIterationCount: true,
borderImageOutset: true,
borderImageSlice: true,
borderImageWidth: true,
boxFlex: true,
boxFlexGroup: true,
boxOrdinalGroup: true,
columnCount: true,
flex: true,
flexGrow: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
flexOrder: true,
gridRow: true,
gridColumn: true,
fontWeight: true,
lineClamp: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
tabSize: true,
widows: true,
zIndex: true,
zoom: true,
// SVG-related properties
fillOpacity: true,
floodOpacity: true,
stopOpacity: true,
strokeDasharray: true,
strokeDashoffset: true,
strokeMiterlimit: true,
strokeOpacity: true,
strokeWidth: true
};
/**
* Taken from React's CSSProperty.js
*
* @param {string} prefix vendor-specific prefix, eg: Webkit
* @param {string} key style name, eg: transitionDuration
* @return {string} style name prefixed with `prefix`, properly camelCased, eg:
* WebkitTransitionDuration
*/
function prefixKey(prefix, key) {
return prefix + key.charAt(0).toUpperCase() + key.substring(1);
}
/**
* Support style names that may come passed in prefixed by adding permutations
* of vendor prefixes.
* Taken from React's CSSProperty.js
*/
var prefixes = ['Webkit', 'ms', 'Moz', 'O'];
// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an
// infinite loop, because it iterates over the newly added props too.
// Taken from React's CSSProperty.js
Object.keys(isUnitlessNumber).forEach(function (prop) {
prefixes.forEach(function (prefix) {
isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
});
});
var stringifyValue = function stringifyValue(key, prop) {
if (typeof prop === "number") {
if (isUnitlessNumber[key]) {
return "" + prop;
} else {
return prop + "px";
}
} else {
return prop;
}
};
exports.stringifyValue = stringifyValue;
/**
* JS Implementation of MurmurHash2
*
* @author Gary Court
* @see http://github.com/garycourt/murmurhash-js
* @author Austin Appleby
* @see http://sites.google.com/site/murmurhash/
*
* @param {string} str ASCII only
* @return {string} Base 36 encoded hash result
*/
function murmurhash2_32_gc(str) {
var l = str.length;
var h = l;
var i = 0;
var k = undefined;
while (l >= 4) {
k = str.charCodeAt(i) & 0xff | (str.charCodeAt(++i) & 0xff) << 8 | (str.charCodeAt(++i) & 0xff) << 16 | (str.charCodeAt(++i) & 0xff) << 24;
k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0x5bd1e995 & 0xffff) << 16);
k ^= k >>> 24;
k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0x5bd1e995 & 0xffff) << 16);
h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16) ^ k;
l -= 4;
++i;
}
switch (l) {
case 3:
h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
case 2:
h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
case 1:
h ^= str.charCodeAt(i) & 0xff;
h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16);
}
h ^= h >>> 13;
h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16);
h ^= h >>> 15;
return (h >>> 0).toString(36);
}
// Hash a javascript object using JSON.stringify. This is very fast, about 3
// microseconds on my computer for a sample object:
// http://jsperf.com/test-hashfnv32a-hash/5
//
// Note that this uses JSON.stringify to stringify the objects so in order for
// this to produce consistent hashes browsers need to have a consistent
// ordering of objects. Ben Alpert says that Facebook depends on this, so we
// can probably depend on this too.
var hashObject = function hashObject(object) {
return murmurhash2_32_gc(JSON.stringify(object));
};
exports.hashObject = hashObject;
var IMPORTANT_RE = /^([^:]+:.*?)( !important)?;$/;
// Given a single style rule string like "a: b;", adds !important to generate
// "a: b !important;".
var importantify = function importantify(string) {
return string.replace(IMPORTANT_RE, function (_, base, important) {
return base + " !important;";
});
};
exports.importantify = importantify;
/***/ },
/* 175 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _asap = __webpack_require__(297);
var _asap2 = _interopRequireDefault(_asap);
var _generate = __webpack_require__(258);
var _util = __webpack_require__(174);
// The current