├── .babelrc ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gulpfile.js ├── package-lock.json ├── package.json ├── src └── latex.js └── test ├── index.js └── strings └── bracketString.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-class-properties"], 3 | "presets": [ 4 | "es2015", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "zzish", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "react/no-danger": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | build 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4' 5 | - '6' 6 | 7 | install: 8 | - yarn install 9 | - npm install react 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | # 1.3.1 4 | 5 | * React as dev dependency 6 | 7 | # 1.3.0 8 | 9 | * KaTeX updated to 0.10.2 10 | 11 | # 1.2.0 12 | 13 | * KaTeX updated to 0.9.0 14 | 15 | # 1.1.0 16 | 17 | * Updated dependencies to React 16 18 | * KaTeX updated to 0.8.2 19 | 20 | # 1.0.0 21 | 22 | * KaTeX updated to 0.7.1 23 | * Minimum version of React bumped to 15.3.0 24 | * Fixed `displayMode` not being sent properly to Katex 25 | 26 | # 0.1.1 27 | 28 | * Fix gulp nsp 29 | 30 | # 0.1.0 31 | 32 | * Added support for Katex displayMode (Thanks to @mathisonian) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Zzish (http://www.zzish.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-latex [![NPM version][npm-image]][npm-url] 2 | 3 | > React component to render latex strings, based on [Katex](https://github.com/Khan/KaTeX) 4 | 5 | ## Install 6 | 7 | ```sh 8 | $ npm install --save react-latex 9 | ``` 10 | 11 | ## Usage 12 | 13 | In javascript 14 | 15 | ### Before using Latex 16 | 17 | Include in your html Katex CSS 18 | 19 | ```html 20 | 21 | 22 | 26 | 27 | 28 | ``` 29 | 30 | ### Inline Latex 31 | 32 | ```js 33 | var Latex = require('react-latex'); 34 | 35 | ... 36 | render(){ 37 | return ( 38 |

39 | What is $(3\times 4) \div (5-3)$ 40 |

41 | ); 42 | } 43 | ... 44 | ``` 45 | 46 | ### Block Latex 47 | 48 | ```js 49 | var Latex = require('react-latex'); 50 | 51 | ... 52 | render(){ 53 | return ( 54 |

55 | $$(3\times 4) \div (5-3)$$ 56 |

57 | ); 58 | } 59 | ... 60 | ``` 61 | 62 | ### Options for Katex 63 | 64 | A number of options are now supported. For a comprehensive list please visit: [here](https://katex.org/docs/options.html) 65 | 66 | ## License 67 | 68 | MIT © [Zzish](http://www.zzish.com) 69 | 70 | [npm-image]: https://badge.fury.io/js/react-latex.svg 71 | [npm-url]: https://npmjs.org/package/react-latex 72 | [travis-image]: https://travis-ci.org/zzish/react-latex.svg?branch=master 73 | [travis-url]: https://travis-ci.org/zzish/react-latex 74 | [daviddm-image]: https://david-dm.org/zzish/react-latex.svg?theme=shields.io 75 | [daviddm-url]: https://david-dm.org/zzish/react-latex 76 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var gulp = require('gulp'); 3 | var excludeGitignore = require('gulp-exclude-gitignore'); 4 | var mocha = require('gulp-mocha'); 5 | var eslint = require('gulp-eslint'); 6 | var istanbul = require('gulp-istanbul'); 7 | var nsp = require('gulp-nsp'); 8 | var plumber = require('gulp-plumber'); 9 | var babel = require('gulp-babel'); 10 | var browserify = require('gulp-browserify'); 11 | var uglify = require('gulp-uglify'); 12 | var rename = require('gulp-rename'); 13 | 14 | var babelify = require('babelify'); 15 | var browserify = require('browserify'); 16 | var source = require('vinyl-source-stream'); 17 | 18 | 19 | // Initialize the babel transpiler so ES2015 files gets compiled 20 | // when they're loaded 21 | require('babel-core/register'); 22 | 23 | var handleErr = function (err) { 24 | console.log(err.message); 25 | process.exit(1); 26 | }; 27 | 28 | gulp.task('static', function () { 29 | return gulp.src('**/*.js') 30 | .pipe(excludeGitignore()) 31 | .pipe(eslint()) 32 | .pipe(eslint.format()) 33 | .pipe(eslint.failAfterError()) 34 | .on('error', handleErr); 35 | }); 36 | 37 | gulp.task('nsp', function (cb) { 38 | nsp({package: __dirname + '/package.json'}, cb); 39 | }); 40 | 41 | gulp.task('pre-test', function () { 42 | return gulp.src('lib/**/*.js') .pipe(babel()) 43 | 44 | .pipe(istanbul({includeUntested: true})) 45 | .pipe(istanbul.hookRequire()); 46 | }); 47 | 48 | gulp.task('test', ['pre-test'], function (cb) { 49 | var mochaErr; 50 | 51 | gulp.src('test/**/*.js') 52 | .pipe(plumber()) 53 | .pipe(mocha({reporter: 'spec'})) 54 | .on('error', function (err) { 55 | mochaErr = err; 56 | }) 57 | .pipe(istanbul.writeReports()) 58 | .on('end', function () { 59 | cb(mochaErr); 60 | }); 61 | }); 62 | 63 | gulp.task('babel', function () { 64 | return gulp.src('lib/**/*.js') 65 | .pipe(babel()) 66 | .pipe(rename('latex.npm.js')) 67 | .pipe(gulp.dest('dist')); 68 | }); 69 | 70 | gulp.task('modules', function() { 71 | return browserify({ 72 | entries: './lib/Latex.js', 73 | debug: false 74 | }) 75 | .transform(babelify) 76 | .bundle() 77 | .pipe(source('latex.js')) 78 | .pipe(gulp.dest('./dist')); 79 | }); 80 | 81 | gulp.task('minify', ['modules'], function(){ 82 | return gulp.src('dist/latex.js') 83 | .pipe(uglify({ 84 | compress: { 85 | drop_console: true, 86 | unsafe: true 87 | } 88 | })) 89 | .pipe(rename('latex.min.js')) 90 | .pipe(gulp.dest('dist')); 91 | }); 92 | 93 | gulp.task('prepublish', ['nsp', 'babel', 'modules', 'minify']); 94 | gulp.task('default', ['test']); 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-latex", 3 | "version": "2.0.0", 4 | "description": "React component to render latex strings", 5 | "repository": "zzish/react-latex", 6 | "author": { 7 | "name": "Zzish", 8 | "email": "developers@zzish.com", 9 | "url": "http://www.zzish.com" 10 | }, 11 | "files": [ 12 | "build" 13 | ], 14 | "main": "build/latex.js", 15 | "keywords": [ 16 | "react", 17 | "latex", 18 | "zzish", 19 | "component", 20 | "react-component", 21 | "react-ui" 22 | ], 23 | "devDependencies": { 24 | "babel-cli": "^6.24.1", 25 | "babel-core": "^6.24.1", 26 | "babel-plugin-transform-class-properties": "^6.24.1", 27 | "babel-preset-es2015": "^6.24.1", 28 | "babel-preset-react": "^6.24.1", 29 | "babel-register": "^6.24.1", 30 | "eslint": "^4.14.0", 31 | "eslint-config-zzish": "^0.8.0", 32 | "eslint-plugin-flowtype": "^2.40.1", 33 | "eslint-plugin-import": "^2.8.0", 34 | "eslint-plugin-jsx-a11y": "^6.0.3", 35 | "eslint-plugin-react": "^7.5.1", 36 | "mocha": "^6.2.0", 37 | "prettier": "^1.2.2", 38 | "prop-types": "^15.5.0", 39 | "react": "^16.2.0", 40 | "react-dom": "^16.2.0" 41 | }, 42 | "scripts": { 43 | "test": "mocha --require babel-register", 44 | "build": "mkdir build; babel ./src/latex.js --out-file ./build/latex.js", 45 | "lint": "eslint ./src/latex.js", 46 | "prepublish": "npm run build" 47 | }, 48 | "license": "MIT", 49 | "dependencies": { 50 | "katex": "^0.10.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/latex.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable eslint-plugin-import */ 3 | 4 | import katex from "katex"; 5 | // Eslint doesn't like react being in peerDependencies 6 | import React from "react"; //eslint-disable-line 7 | import PropTypes from "prop-types"; 8 | 9 | const latexify = (string, options) => { 10 | const regularExpression = /\$\$[\s\S]+?\$\$|\\\[[\s\S]+?\\\]|\\\([\s\S]+?\\\)|\$[^\$\\]*(?:\\.[^\$\\]*)*\$/g; 11 | const blockRegularExpression = /\$\$[\s\S]+?\$\$|\\\[[\s\S]+?\\\]/g; 12 | 13 | const stripDollars = (stringToStrip) => 14 | (stringToStrip[0] === "$" && stringToStrip[1] !== "$" 15 | ? stringToStrip.slice(1, -1) 16 | : stringToStrip.slice(2, -2)); 17 | 18 | const getDisplay = (stringToDisplay) => 19 | (stringToDisplay.match(blockRegularExpression) ? "block" : "inline"); 20 | 21 | const renderLatexString = (s, t) => { 22 | let renderedString; 23 | try { 24 | // returns HTML markup 25 | renderedString = katex.renderToString( 26 | s, 27 | t === "block" ? Object.assign({ displayMode: true }, options) : options 28 | ); 29 | } catch (err) { 30 | console.error("couldn`t convert string", s); 31 | return s; 32 | } 33 | return renderedString; 34 | }; 35 | 36 | const result = []; 37 | 38 | const latexMatch = string.match(regularExpression); 39 | const stringWithoutLatex = string.split(regularExpression); 40 | 41 | if (latexMatch) { 42 | stringWithoutLatex.forEach((s, index) => { 43 | result.push({ 44 | string: s, 45 | type: "text", 46 | }); 47 | if (latexMatch[index]) { 48 | result.push({ 49 | string: stripDollars(latexMatch[index]), 50 | type: getDisplay(latexMatch[index]), 51 | }); 52 | } 53 | }); 54 | } else { 55 | result.push({ 56 | string, 57 | type: "text", 58 | }); 59 | } 60 | 61 | const processResult = (resultToProcess) => { 62 | const newResult = resultToProcess.map((r) => { 63 | if (r.type === "text") { 64 | return r.string; 65 | } 66 | return (); 67 | }); 68 | 69 | return newResult; 70 | }; 71 | 72 | // Returns list of spans with latex and non-latex strings. 73 | return processResult(result); 74 | }; 75 | 76 | class Latex extends React.Component { 77 | static propTypes = { 78 | children: PropTypes.string, 79 | displayMode: PropTypes.bool, 80 | leqno: PropTypes.bool, 81 | fleqn: PropTypes.bool, 82 | throwOnError: PropTypes.bool, 83 | errorColor: PropTypes.string, 84 | macros: PropTypes.object, 85 | minRuleThickness: PropTypes.number, 86 | colorIsTextColor: PropTypes.bool, 87 | maxSize: PropTypes.number, 88 | maxExpand: PropTypes.number, 89 | strict: PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.func]), 90 | trust: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), 91 | }; 92 | 93 | static defaultProps = { 94 | children: "", 95 | displayMode: false, 96 | output: "htmlAndMathml", 97 | leqno: false, 98 | fleqn: false, 99 | throwOnError: true, 100 | errorColor: "#cc0000", 101 | macros: {}, 102 | minRuleThickness: 0, 103 | colorIsTextColor: false, 104 | strict: "warn", 105 | trust: false, 106 | }; 107 | 108 | render() { 109 | const { 110 | children, 111 | displayMode, 112 | leqno, 113 | fleqn, 114 | throwOnError, 115 | errorColor, 116 | macros, 117 | minRuleThickness, 118 | colorIsTextColor, 119 | maxSize, 120 | maxExpand, 121 | strict, 122 | trust, 123 | } = this.props; 124 | 125 | const renderUs = latexify( children, {displayMode, 126 | leqno, 127 | fleqn, 128 | throwOnError, 129 | errorColor, 130 | macros, 131 | minRuleThickness, 132 | colorIsTextColor, 133 | maxSize, 134 | maxExpand, 135 | strict, 136 | trust} ); 137 | renderUs.unshift(null); 138 | renderUs.unshift('span'); //put everything in a span 139 | // spread renderUs out to children args 140 | return React.createElement.apply(null, renderUs) 141 | } 142 | } 143 | 144 | if (module && module.exports) { 145 | module.exports = Latex; 146 | } else { 147 | window.Latex = Latex; 148 | } 149 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import assert from "assert"; 3 | import ReactDomServer from "react-dom/server"; 4 | import latex from "./../src/latex"; 5 | import bracketString from "./strings/bracketString"; 6 | 7 | class Test extends React.Component { 8 | render() { 9 | return ( 10 | React.createElement(latex, null, 11 | "What is $\\sqrt{8}$?" 12 | ) 13 | ); 14 | } 15 | } 16 | 17 | class TestBrackets extends React.Component { 18 | render() { 19 | return ( 20 | React.createElement(latex, null, 21 | "What is $\\sqrt{8}$?" 22 | ) 23 | ); 24 | } 25 | } 26 | 27 | 28 | describe("react-latex", () => { 29 | it("Should have katex class", () => { 30 | const testString = ReactDomServer.renderToStaticMarkup(); 31 | assert.notEqual(-1, testString.indexOf("")); 32 | }); 33 | 34 | // it("Should render string with brackets properly", () => { 35 | // const testStringWithBrackets = ReactDomServer.renderToStaticMarkup().trim(); 36 | // assert.equal(bracketString, testStringWithBrackets); 37 | // }); 38 | 39 | it("It should not strip gt and lt", () => { 40 | class TestLtGt extends React.Component { 41 | render () { 42 | return React.createElement(latex, null, "compact size (< 20'), spectral types >M4"); 43 | } 44 | } 45 | const testString = ReactDomServer.renderToStaticMarkup(); 46 | assert.equal( testString, "compact size (< 20'), spectral types >M4"); 47 | }); 48 | 49 | it("It should escape HTML tags outside of math", () => { 50 | class TestLtGt extends React.Component { 51 | render () { 52 | return React.createElement(latex, null, 53 | "compact size ( 20'), spectral $\\sqrt{8}$ types >M4"); 54 | } 55 | } 56 | const testString = ReactDomServer.renderToStaticMarkup(); 57 | assert.equal(-1, testString.indexOf("")); 58 | }); 59 | 60 | it("It should escape HTML tags inside of math", () => { 61 | class TestLtGt extends React.Component { 62 | render () { 63 | return React.createElement(latex, null, "compact size $$"); 64 | } 65 | } 66 | const testString = ReactDomServer.renderToStaticMarkup(); 67 | assert.equal(-1, testString.indexOf("")); 68 | }); 69 | 70 | 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /test/strings/bracketString.js: -------------------------------------------------------------------------------- 1 | export const bracketString = 'What is 8\\sqrt{8} ?'; 2 | export default bracketString; 3 | --------------------------------------------------------------------------------