HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<script src="https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js"></script>
<!--
Hello Camper!
For now, the test suite only works in Chrome! Please read the README below in the JS Editor before beginning. Feel free to delete this message once you have read it. Good luck and Happy Coding!
- The freeCodeCamp Team
-->
<main></main>
@import 'https://fonts.googleapis.com/css?family=Share+Tech+Mono';
@font-face {
font-family: "Digital";
src: url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.eot");
src: url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.eot?#iefix") format("embedded-opentype"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.woff2") format("woff2"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.woff") format("woff"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.ttf") format("truetype"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.svg#Digital-7") format("svg");
}
* {
box-sizing: border-box;
font-size: 30px;
}
body {
width: 25%;
min-width: 250px;
max-width: 350px;
margin: 0 auto;
}
#calculator {
position: relative;
left: -11%;
font-family: Arial, sans-serif;
box-sizing: content-box;
margin-top: 2em;
display: flex;
flex-flow: column nowrap;
border: outset 4px #49494b;
border-radius: 0.5em;
width: 100%;
height: 25vh;
min-height: 400px;
padding: 1em;
justify-content: space-between;
background-color: #282828;
}
#screens {
font-family: digital;
flex: 1;
display: flex;
width: 100%;
padding: 0 0.3em;
flex-flow: column nowrap;
margin-bottom: 0.5em;
background-color: #a2af77;
border: inset 3px #666;
border-radius: 0.15em;
}
#screens label {
display: flex;
overflow: hidden;
text-overflow: ellipsis;
align-items: center;
}
#formula {
flex: 1 1.5em;
font-size: 0.75em;
}
#display {
flex: 1 2em;
font-size: 1em;
}
#buttons {
flex: 5;
display: grid;
width: 100%;
grid-template-columns: repeat(4, minmax(10px, 1fr));
gap: 0.5em;
}
#buttons button {
padding: 0.1em;
text-align: center;
color: white;
background-color: #58595b;
border: outset 2.5px #7c7d7f;
border-radius: 0.15em;
box-shadow: 2px 2px 3px 3px #1e1c1f;
}
#buttons button:focus {
outline: none;
}
#buttons button:active {
outline: none;
border-style: inset;
box-shadow: 0 0 0 0 #1e1c1f;
}
#buttons #clear,
#buttons #zero {
grid-column: 1 / 3;
}
#buttons #clear {
background-color: #ac3939;
border-color: #c05353;
}
#buttons #equals {
grid-column: 4 / 5;
grid-row: 4 / 6;
background-color: #f15a2b;
border-color: #ff6e3f;
}
/* MAIN COMPONENT */
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currentOperation: "",
currentChar: "0",
formula: "",
tokenisedFormula: [],
maxCharLimit: 15
};
this.handleButtonClick = this.handleButtonClick.bind(this);
this.buttons = [
{text: 'AC', id: 'clear'},
{text: '/', id: 'divide'},
{text: '*', id: 'multiply'},
{text: '7', id: 'seven'},
{text: '8', id: 'eight'},
{text: '9', id: 'nine'},
{text: '-', id: 'subtract'},
{text: '4', id: 'four'},
{text: '5', id: 'five'},
{text: '6', id: 'six'},
{text: '+', id: 'add'},
{text: '1', id: 'one'},
{text: '2', id: 'two'},
{text: '3', id: 'three'},
{text: '0', id: 'zero'},
{text: '.', id: 'decimal'},
{text: '=', id: 'equals'}
];
}
handleButtonClick(event) {
const char = event.target.textContent;
this.setState(processCharacter(char, this.state));
}
render() {
const buttons = this.buttons.map((elem) => {
return <Button
id={elem.id}
key={`button-${elem.text}`}
text={elem.text}
handleClick={this.handleButtonClick}
/>;
});
return (
<div id="calculator">
<Screen formula={this.state.formula} current={this.state.currentChar} />
<div id="buttons">{buttons}</div>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('main'));
/* END MAIN COMPONENT */
/* REACT COMPONENTS */
function Button(props) {
return (
<button id={props.id} key={props.key} onClick={props.handleClick}>{props.text}</button>
);
}
function Screen(props) {
return (
<div id="screens">
<label id="formula">{props.formula}</label>
<label id="display">{props.current}</label>
</div>
);
}
/* END REACT COMPONENTS */
/* CONSTANTS */
// Types: Various strings that represent the different types of buttons
// that exist on a calculator
const OP = "operator";
const DIGIT = "digit";
const DECIMAL = "decimal";
const EQUALS = "equals";
const CLEAR = "clear";
const MINUS = "minus";
// Operator Tokens: Various strings that will be used as tokens to represent
// the different mathematical operations in the string format of the formula
// which will be parsed in the evaluation process
const OP_NEG = "negative";
const OP_MUL = "multiply";
const OP_DIV = "divide";
const OP_PLUS = "add";
const OP_MINUS = "subtract";
// Tokens Table: Maps the different mathematical operations to an array that
// includes various combinations of the tokens defined above. It uses 2 different
// tokens for the '-' character depending on where it is applied. If it comes
// after a mathematical operator, then it is the negative sign. If it comes
// alone then it is the subtraction operator.
const TOKENS = {
"+": [OP_PLUS],
"-": [OP_MINUS],
"*": [OP_MUL],
"/": [OP_DIV],
"+-": [OP_PLUS, OP_NEG],
"--": [OP_MINUS, OP_NEG],
"*-": [OP_MUL, OP_NEG],
"/-": [OP_DIV, OP_NEG]
};
// Tokens To Operations: Maps the tokens to the operation that will be executed
// when each token is encountered in the formula in its string format
const OPERATIONS = {
[OP_NEG]: (x) => -x,
[OP_MUL]: (x, y) => x * y,
[OP_DIV]: (x, y) => x / y,
[OP_PLUS]: (x, y) => x + y,
[OP_MINUS]: (x, y) => x - y
};
/* END CONSTANTS */
/* INPUT PROCESSING */
function processCharacter(char, state) {
/*
This is the main entry point for the calculator logic. It takes a character
as input from the user and processes it based on its type using various
utility functions defined later.
In general, the calculator logic is separated from the React components
because it doesn't rely on React to actually work. It can work on its
own for a console calculator or be used by other front-end frameworks
libraries as well as with regular HTML, CSS and JS.
*/
switch (getCharBaseType(char)) {
case OP: {
if (state.formula.length < state.maxCharLimit || formulaEvaluated(state.formula)) {
return processOperator(char, state);
}
break;
}
case DIGIT: {
if (state.formula.length < state.maxCharLimit || formulaEvaluated(state.formula)) {
return processDigit(char, state);
}
break;
}
case DECIMAL: {
if (state.formula.length < state.maxCharLimit || formulaEvaluated(state.formula)) {
return processDecimal(char, state);
}
break;
}
case EQUALS: {
return processEquals(char, state);
}
case CLEAR: {
return {
currentOperation: "",
currentChar: "0",
formula: "",
tokenisedFormula: []
};
}
default: {
return {};
}
}
}
// Utilities
function processOperator(operator, state) {
/*
Processes mathematical operators displaying them in the UI for both
the current character and the formula.
It also adds the tokens for any numbers that were in the current
character display because once the user presses one of the operators,
it indicates that the previous operand is now complete and ready to
added to the tokenised formula.
*/
// Get the current state
let currentOperation = state.currentOperation;
let currentChar = state.currentChar;
let formula = state.formula;
let tokenisedFormula = state.tokenisedFormula;
// A boolean value that will be used to determine what to do with the operator
// depending on whether it is a single operator or a double where a minus is
// the second one to make the following number negative
let singleOpTest = true;
if (formulaClear(formula)) {
// If the user presses one of the operators with no numbers before, just
// add 0 at the beginning
tokenisedFormula = addToken("0", tokenisedFormula);
formula += "0" + operator;
} else if (formulaEvaluated(formula)) {
// If the user presses an operator with a formula that has already been
// evaluated, take the result of the formula and apply the operation to
// it
tokenisedFormula = addToken(currentChar, tokenisedFormula);
formula = currentChar + operator;
} else {
if (decimalAtEnd(formula)) {
// If an operator is pressed when the current number has a decimal
// point at the end with no digits after it, add a 0 before adding
// the operator to the formula so the number looks like this '3.0'
// instead of this '3.'
const token = currentChar + "0";
tokenisedFormula = addToken(token, tokenisedFormula);
formula += "0" + operator;
} else if (digitAtEnd(formula)) {
tokenisedFormula = addToken(currentChar, tokenisedFormula);
formula += operator;
} else {
// If an operator is already the last character the user pressed
// we determine what needs to be done. If there is only 1 operator
// at the end and the user presses minus, then we add the minus
// to that operator. If the user presses an operator other than
// a minus, then we need to trim the last 1 or 2 characters, depending
// on how many operators currently exist at the end, and replace
// them with the new operator
if (isMinus(operator) && singleOpAtEnd(formula)) {
formula += operator;
currentOperation += operator;
singleOpTest = false;
} else if (!isMinus(operator)) {
if (singleOpAtEnd(formula)) {
formula = trimNCharsAtEnd(formula, 1);
} else if (doubleOpAtEnd(formula)) {
formula = trimNCharsAtEnd(formula, 2);
}
formula += operator;
}
}
}
if (singleOpTest) {
currentOperation = operator;
}
currentChar = operator;
return {
currentOperation,
currentChar,
formula,
tokenisedFormula
};
}
function processDigit(digit, state) {
/*
Processes digits adding them to the displays in the UI.
It also tokenises the operators if a digit is pressed immediately
after an operator because this indicates that the user has already
settled on the operator s/he wants to apply.
*/
// Get the current state
let currentOperation = state.currentOperation;
let currentChar = state.currentChar;
let formula = state.formula;
let tokenisedFormula = state.tokenisedFormula;
if (formulaClear(formula) || formulaEvaluated(formula)) {
// If the displays are clear or the formula has already been
// evaluated, then just display the digit in the current and
// formula displays in the UI
currentChar = `${digit}`;
if (digit !== '0') {
formula = `${digit}`;
}
} else {
// We check that the digit the user is pressing is not zero
// or that there is no zero at the start of the current number
// before we add the digit to the formula. This is to ensure
// that the user cannot enter a number that has multiple zeroes
// at the beginning (e.g 00005)
if (digit !== '0' || !zeroAtStart(currentChar)) {
if (singleOpAtEnd(formula) || doubleOpAtEnd(formula)) {
// If the formula ends with an operator, just tokenise
// it and replace current input display with the digit
const token = tokeniseOperator(currentOperation);
tokenisedFormula = addToken(token, tokenisedFormula);
currentChar = `${digit}`;
} else if (digitAtEnd(formula) || decimalAtEnd(formula)) {
// If the formula ends with a digit, then add the digit
// to the current input display
currentChar += digit;
}
formula += digit;
}
}
return {
currentChar,
formula,
tokenisedFormula
};
}
function processDecimal(decimal, state) {
/*
Processes the decimal point character.
It also tokenises any operators that exist in the current
input display if the decimal point is pressed immediately
after an operator.
*/
// Get the current state
let currentOperation = state.currentOperation;
let currentChar = state.currentChar;
let formula = state.formula;
let tokenisedFormula = state.tokenisedFormula;
if (formulaClear(formula) || formulaEvaluated(formula)) {
// If the user presses the decimal point at the start of
// a formula or after a formula has been evaluated, then
// add a 0 before adding the decimal point, so it looks
// like this (0.) in the display rather than this (.)
currentChar = formula = "0.";
} else {
if (singleOpAtEnd(formula) || doubleOpAtEnd(formula)) {
// If the user presses the decimal point immediately
// after an operator then add a 0 before adding the
// decimal point, so it looks like this (0.) in the
// display rather than this (.)
// Additionally, tokenise the operator and add it to
// the tokenised formula
const token = tokeniseOperator(currentOperation);
tokenisedFormula = addToken(token, tokenisedFormula);
currentChar = "0.";
formula += "0.";
} else if (digitAtEnd(formula) || decimalAtEnd(formula)) {
// If the current input display has a number in, make
// sure that it doesn't already has a decimal point.
// This prevents users from adding multiple decimal
// points which would make the number invalid
if (!hasDecimalPoint(currentChar)) {
currentChar += ".";
formula += ".";
}
}
}
return {
currentChar,
formula,
tokenisedFormula
};
}
function processEquals(equals, state) {
/*
This evaluates the formula when the user presses equals
and calls the necessary functions to clear the tokens so
the calculator is ready for subsequent calculations
*/
// Get the current state
let currentChar = state.currentChar;
let formula = state.formula;
let tokenisedFormula = state.tokenisedFormula;
let result;
// Perform some cleanup before evaluating the formula. If
// the user pressed the equals button immediately after
// a decimal point, then just add a 0 so the formula ends
// with '.0' instead of '.'
// If the user pressed '=' immediately after an operator
// then remove the last 1 or 2 characters depending on how
// many operators are there at the end. This is to ensure
// that the formula will be evaluated successfully, as an
// operator at the end of the formula wouldn't have an
// operand which would produce errors when evaluating
if (decimalAtEnd(formula)) {
currentChar += "0";
formula += "0";
} else if (singleOpAtEnd(formula) || doubleOpAtEnd(formula)) {
const trimLength = singleOpAtEnd(formula) ? 1 : 2;
formula = trimNCharsAtEnd(formula, trimLength);
}
// If the formula ends with a number, make sure to add it
// to the tokenised formula, since it wouldn't have been
// added yet up to this point
if (digitAtEnd(currentChar)) {
tokenisedFormula = addToken(currentChar, tokenisedFormula);
}
// Checks to make sure that the formula is not already
// evaluated. This prevents any extra recalculation if
// the user presses '=' multiple times
if (!formulaEvaluated(formula)) {
result = evaluateFormula(tokenisedFormula);
}
// When the formula gets evaluated the 'result' variable
// will be updated with a value. In this case, display it
// properly in the UI.
// Currently, this slices the result if its length exceeds
// the maxmium number defined by the state. There might be
// a better way of addressing this issue rather than
// discarding the extra digits at the end.
if (result) {
formula += "=" + result;
currentChar = (result.length < state.maxCharLimit) ? `${result}` : `${String(result).slice(0, state.maxCharLimit)}`;
}
tokenisedFormula = clearTokens();
return {
currentChar,
formula,
tokenisedFormula
};
}
function addToken(token, tokenisedFormula) {
const output = tokenisedFormula.concat(token);
return output;
}
function clearTokens() {
return [];
}
function tokeniseOperator(operator) {
/* Convert the mathematical operators to their tokens */
return TOKENS[operator] ? TOKENS[operator] : undefined;
}
function trimNCharsAtEnd(str, N) {
return str.slice(0, str.length - N);
}
/* END INPUT PROCESSING */
/* EVALUATION */
function evaluateFormula(tokenisedFormula) {
/*
This is the main evaluation function. It takes the tokenised
formula and executes the mathematical operations in the proper
order, updating the tokenised formula along the way.
If at the end, it has a single value, then the evaluation
was successful, so it returns the result. Otherwise, it returns
NaN.
The order in which the evaluation goes:
1) Start with negation, going from start to end of the
formula negating any number preceeded by the negation
token.
2) Then, it moves to multiplication and division, going
from start to end of formula applying these operations
3) Finally, it applies the addition and subtraction.
*/
tokenisedFormula = executeOperations(tokenisedFormula, [OP_NEG]);
tokenisedFormula = executeOperations(tokenisedFormula, [OP_MUL, OP_DIV]);
tokenisedFormula = executeOperations(tokenisedFormula, [OP_PLUS, OP_MINUS]);
return tokenisedFormula.length === 1 ? tokenisedFormula[0] : NaN;
}
// Utilities
function executeOperations(formula, listOfOps) {
let output = formula;
// A function that returns true or false checking if the
// list of operations to be executed includes the operator
// passed to the function. This function will be used with
// the findIndex Array method to get all the operators
// that should be executed
const opTest = (elem) => listOfOps.includes(elem);
while (output.findIndex(opTest) !== -1) {
const opIndex = output.findIndex(opTest);
const operator = output[opIndex];
// The args list will include the operands in the order
// they should be evaluated. For example, if the formula
// is 2 - 3, then the args list will be [2, 3]
let args = [];
// The followin part determines whether the operation to
// be executed is unary (e.g. negation) or binary. Based
// on that, it defines multiple constants and variables
// that will be used to update the tokenised formula
// The slice boundary defines how many items will be
// removed from the tokenised formula before the operator
// that is currently being executed. If the operation is
// unary, then no items before the operator will be removed.
// When the operation is binary, then 1 item will be removed.
const sliceBoundary = OPERATIONS[operator].length === 2 ? 1 : 0;
// We only need to defined a firstOperand if the operation
// is binary
let firstOperand;
// The last operand is the item that comes after the operator
const lastOperand = Number(output[opIndex + 1]);
if (OPERATIONS[operator].length === 2) {
// If the operation is binary, then define the first
// operand as the one before the operator in the
// tokenised formula, and add it to the args list
firstOperand = Number(output[opIndex - 1]);
args = [firstOperand];
}
// Add the lastOperand to the args list. This has to be done
// here to make sure that, if the operation is binary, the
// first operand has already been added to the list.
args = [...args, lastOperand];
// Update the tokenised formula, removing the tokens for the
// operation that was executed and replacing them with the
// result of the operation.
output = [
...output.slice(0, opIndex - sliceBoundary),
OPERATIONS[operator](...args),
...output.slice(opIndex + 2)
];
}
return output;
}
/* END EVALUATION */
/* INPUT PROCESSING TESTS */
function zeroAtStart(str) {
return /^0$/.test(str);
}
function digitAtEnd(str) {
return /\d$/.test(str);
}
function decimalAtEnd(str) {
return /\.$/.test(str);
}
function singleOpAtEnd(str) {
return /\d[\+\-\*\/]$/.test(str);
}
function doubleOpAtEnd(str) {
return /\d[\+\-\*\/]\-$/.test(str);
}
function hasDecimalPoint(str) {
return /^\d*\.\d*$/.test(str);
}
function formulaEvaluated(str) {
return /\d+\=-?\d+\.?\d*$/.test(str);
}
function formulaClear(str) {
return !str.trim();
}
/* END INPUT PROCESSING TESTS */
/* CHARACTER IDENTITY TESTS */
function getCharBaseType(char) {
if (isOperator(char)) {
return OP;
} else if (isDigit(char)) {
return DIGIT;
} else if (isDecimal(char)) {
return DECIMAL;
} else if (isEquals(char)) {
return EQUALS;
} else if (isClear(char)) {
return CLEAR;
} else {
return undefined;
}
}
// Utilities
function isOperator(char) {
return /[\+\-\*\/]/.test(char);
}
function isDigit(char) {
return /\d/.test(char);
}
function isDecimal(char) {
return /\./.test(char);
}
function isEquals(char) {
return /\=/.test(char);
}
function isClear(char) {
return /^AC$/.test(char);
}
function isMinus(char) {
return /\-/.test(char);
}
/* END CHARACTER IDENTITY TESTS */
Also see: Tab Triggers