├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── winterbe │ │ └── react │ │ ├── Application.java │ │ ├── Comment.java │ │ ├── CommentController.java │ │ ├── CommentService.java │ │ ├── MainController.java │ │ └── React.java ├── resources │ ├── application.properties │ ├── jsx │ │ └── commentBox.js │ └── static │ │ ├── commentBox.js │ │ ├── nashorn-polyfill.js │ │ └── vendor │ │ ├── react.js │ │ └── showdown.min.js └── webapp │ └── WEB-INF │ └── jsp │ └── index.jsp └── test └── java └── com └── winterbe └── react ├── ApplicationTests.java └── ReactTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .DS_Store 4 | target 5 | .module-cache 6 | out 7 | 8 | # Eclipse IDE 9 | .classpath 10 | .project 11 | .settings 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Benjamin Winterberg 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spring Boot React Example 2 | ============================ 3 | 4 | Example of the official [React.js Tutorial](http://facebook.github.io/react/docs/tutorial.html) using Spring Boot on the server-side. 5 | 6 | The `CommentBox` main view is isomorphic: HTML is initially rendered on the server with Nashorn by utilizing `React.renderToString`. All interactive DOM manipulations are handled by React directly in the browser. 7 | 8 | For further explanation read this [blog post](http://winterbe.com/posts/2015/02/16/isomorphic-react-webapps-on-the-jvm/) and [follow me on Twitter](https://twitter.com/winterbe_). 9 | 10 | See also [winterbe/react-samples](https://github.com/winterbe/react-samples) for a bunch of client-side React.js examples. 11 | 12 | --- 13 | 14 |

15 | ★★★ Like this project? Leave a star, follow on Twitter or donate to support my work! Thanks. ★★★ 16 |

17 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.winterbe.react 7 | spring-react-example 8 | 0.0.1-SNAPSHOT 9 | war 10 | 11 | spring-react-example 12 | Spring Boot React Example 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.2.3.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | com.winterbe.react.Application 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-tomcat 35 | 36 | 37 | org.apache.tomcat.embed 38 | tomcat-embed-jasper 39 | 40 | 41 | javax.servlet 42 | jstl 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | org.jsoup 51 | jsoup 52 | 1.7.3 53 | test 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-maven-plugin 62 | 63 | 64 | org.springframework 65 | springloaded 66 | 1.2.4.RELEASE 67 | 68 | 69 | 70 | 71 | -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/com/winterbe/react/Application.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | import org.springframework.boot.context.web.SpringBootServletInitializer; 7 | 8 | @SpringBootApplication 9 | public class Application extends SpringBootServletInitializer { 10 | 11 | @Override 12 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 13 | return application.sources(Application.class); 14 | } 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(Application.class, args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/winterbe/react/Comment.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | /** 4 | * @author Benjamin Winterberg 5 | */ 6 | public class Comment { 7 | private String author; 8 | private String text; 9 | 10 | public Comment() { 11 | 12 | } 13 | 14 | public Comment(String author, String text) { 15 | this.author = author; 16 | this.text = text; 17 | } 18 | 19 | public String getAuthor() { 20 | return author; 21 | } 22 | 23 | public void setAuthor(String author) { 24 | this.author = author; 25 | } 26 | 27 | public String getText() { 28 | return text; 29 | } 30 | 31 | public void setText(String text) { 32 | this.text = text; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/winterbe/react/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | /** 11 | * @author Benjamin Winterberg 12 | */ 13 | @RestController 14 | @RequestMapping("/comments.json") 15 | public class CommentController { 16 | 17 | private CommentService service; 18 | 19 | @Autowired 20 | public CommentController(CommentService service) { 21 | this.service = service; 22 | } 23 | 24 | @RequestMapping(method = RequestMethod.GET) 25 | public List getComments() { 26 | return service.getComments(); 27 | } 28 | 29 | @RequestMapping(method = RequestMethod.POST) 30 | public List addComment(Comment comment) { 31 | return service.addComment(comment); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/winterbe/react/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @Service 9 | public class CommentService { 10 | 11 | private List comments = new ArrayList<>(); 12 | 13 | public CommentService() { 14 | comments.add(new Comment("Peter Parker", "This is a comment.")); 15 | comments.add(new Comment("John Doe", "This is *another* comment.")); 16 | } 17 | 18 | public List getComments() { 19 | return comments; 20 | } 21 | 22 | public List addComment(Comment comment) { 23 | comments.add(comment); 24 | return comments; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/winterbe/react/MainController.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author Benjamin Winterberg 13 | */ 14 | @Controller 15 | public class MainController { 16 | 17 | private CommentService service; 18 | 19 | private React react; 20 | 21 | private ObjectMapper mapper; 22 | 23 | @Autowired 24 | public MainController(CommentService service) { 25 | this.service = service; 26 | this.react = new React(); 27 | this.mapper = new ObjectMapper(); 28 | } 29 | 30 | @RequestMapping("/") 31 | public String index(Map model) throws Exception { 32 | List comments = service.getComments(); 33 | String commentBox = react.renderCommentBox(comments); 34 | String data = mapper.writeValueAsString(comments); 35 | model.put("content", commentBox); 36 | model.put("data", data); 37 | return "index"; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/winterbe/react/React.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | import jdk.nashorn.api.scripting.NashornScriptEngine; 4 | 5 | import javax.script.ScriptEngineManager; 6 | import javax.script.ScriptException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.io.Reader; 10 | import java.util.List; 11 | 12 | public class React { 13 | 14 | private ThreadLocal engineHolder = new ThreadLocal() { 15 | @Override 16 | protected NashornScriptEngine initialValue() { 17 | NashornScriptEngine nashornScriptEngine = (NashornScriptEngine) new ScriptEngineManager().getEngineByName("nashorn"); 18 | try { 19 | nashornScriptEngine.eval(read("static/nashorn-polyfill.js")); 20 | nashornScriptEngine.eval(read("static/vendor/react.js")); 21 | nashornScriptEngine.eval(read("static/vendor/showdown.min.js")); 22 | nashornScriptEngine.eval(read("static/commentBox.js")); 23 | } catch (ScriptException e) { 24 | throw new RuntimeException(e); 25 | } 26 | return nashornScriptEngine; 27 | } 28 | }; 29 | 30 | public String renderCommentBox(List comments) { 31 | try { 32 | Object html = engineHolder.get().invokeFunction("renderServer", comments); 33 | return String.valueOf(html); 34 | } 35 | catch (Exception e) { 36 | throw new IllegalStateException("failed to render react component", e); 37 | } 38 | } 39 | 40 | private Reader read(String path) { 41 | InputStream in = getClass().getClassLoader().getResourceAsStream(path); 42 | return new InputStreamReader(in); 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.view.prefix: /WEB-INF/jsp/ 2 | spring.view.suffix: .jsp -------------------------------------------------------------------------------- /src/main/resources/jsx/commentBox.js: -------------------------------------------------------------------------------- 1 | var converter = new Showdown.converter(); 2 | 3 | var CommentForm = React.createClass({ 4 | handleSubmit: function (e) { 5 | e.preventDefault(); 6 | var author = this.refs.author.getDOMNode().value.trim(); 7 | var text = this.refs.text.getDOMNode().value.trim(); 8 | if (!author || !text) { 9 | return; 10 | } 11 | this.props.onCommentSubmit({author: author, text: text}); 12 | this.refs.author.getDOMNode().value = ''; 13 | this.refs.text.getDOMNode().value = ''; 14 | }, 15 | render: function () { 16 | return ( 17 |
18 | 19 | 20 | 21 |
22 | ); 23 | } 24 | }); 25 | 26 | var Comment = React.createClass({ 27 | render: function () { 28 | var rawMarkup = converter.makeHtml(this.props.children.toString()); 29 | return ( 30 |
31 |

{this.props.author}

32 | 33 |
34 | ); 35 | } 36 | }); 37 | 38 | var CommentList = React.createClass({ 39 | render: function () { 40 | var commentNodes = this.props.data.map(function (comment, index) { 41 | return ( 42 | 43 | {comment.text} 44 | 45 | ); 46 | }); 47 | return ( 48 |
49 | {commentNodes} 50 |
51 | ); 52 | } 53 | }); 54 | 55 | var CommentBox = React.createClass({ 56 | handleCommentSubmit: function (comment) { 57 | var comments = this.state.data; 58 | comments.push(comment); 59 | this.setState({data: comments}, function () { 60 | $.ajax({ 61 | url: this.props.url, 62 | dataType: 'json', 63 | type: 'POST', 64 | data: comment, 65 | success: function (data) { 66 | this.setState({data: data}); 67 | }.bind(this), 68 | error: function (xhr, status, err) { 69 | console.error(this.props.url, status, err.toString()); 70 | }.bind(this) 71 | }); 72 | }); 73 | }, 74 | loadCommentsFromServer: function () { 75 | $.ajax({ 76 | url: this.props.url, 77 | dataType: 'json', 78 | success: function (data) { 79 | this.setState({data: data}); 80 | }.bind(this), 81 | error: function (xhr, status, err) { 82 | console.error(this.props.url, status, err.toString()); 83 | }.bind(this) 84 | }); 85 | }, 86 | getInitialState: function () { 87 | return {data: this.props.data}; 88 | }, 89 | componentDidMount: function () { 90 | this.loadCommentsFromServer(); 91 | setInterval(this.loadCommentsFromServer, this.props.pollInterval); 92 | }, 93 | render: function () { 94 | return ( 95 |
96 |

Comments

97 | 98 | 99 |
100 | ); 101 | } 102 | }); 103 | 104 | var renderClient = function (comments) { 105 | var data = comments || []; 106 | React.render( 107 | , 108 | document.getElementById("content") 109 | ); 110 | }; 111 | 112 | var renderServer = function (comments) { 113 | var data = Java.from(comments); 114 | return React.renderToString( 115 | 116 | ); 117 | }; -------------------------------------------------------------------------------- /src/main/resources/static/commentBox.js: -------------------------------------------------------------------------------- 1 | var converter = new Showdown.converter(); 2 | 3 | var CommentForm = React.createClass({displayName: "CommentForm", 4 | handleSubmit: function (e) { 5 | e.preventDefault(); 6 | var author = this.refs.author.getDOMNode().value.trim(); 7 | var text = this.refs.text.getDOMNode().value.trim(); 8 | if (!author || !text) { 9 | return; 10 | } 11 | this.props.onCommentSubmit({author: author, text: text}); 12 | this.refs.author.getDOMNode().value = ''; 13 | this.refs.text.getDOMNode().value = ''; 14 | }, 15 | render: function () { 16 | return ( 17 | React.createElement("form", {className: "commentForm", onSubmit: this.handleSubmit}, 18 | React.createElement("input", {type: "text", placeholder: "Your name", ref: "author"}), 19 | React.createElement("input", {type: "text", placeholder: "Say something...", ref: "text"}), 20 | React.createElement("input", {type: "submit", value: "Post"}) 21 | ) 22 | ); 23 | } 24 | }); 25 | 26 | var Comment = React.createClass({displayName: "Comment", 27 | render: function () { 28 | var rawMarkup = converter.makeHtml(this.props.children.toString()); 29 | return ( 30 | React.createElement("div", {className: "comment"}, 31 | React.createElement("h2", null, this.props.author), 32 | React.createElement("span", {dangerouslySetInnerHTML: {__html: rawMarkup}}) 33 | ) 34 | ); 35 | } 36 | }); 37 | 38 | var CommentList = React.createClass({displayName: "CommentList", 39 | render: function () { 40 | var commentNodes = this.props.data.map(function (comment, index) { 41 | return ( 42 | React.createElement(Comment, {author: comment.author, key: index}, 43 | comment.text 44 | ) 45 | ); 46 | }); 47 | return ( 48 | React.createElement("div", {className: "commentList"}, 49 | commentNodes 50 | ) 51 | ); 52 | } 53 | }); 54 | 55 | var CommentBox = React.createClass({displayName: "CommentBox", 56 | handleCommentSubmit: function (comment) { 57 | var comments = this.state.data; 58 | comments.push(comment); 59 | this.setState({data: comments}, function () { 60 | $.ajax({ 61 | url: this.props.url, 62 | dataType: 'json', 63 | type: 'POST', 64 | data: comment, 65 | success: function (data) { 66 | this.setState({data: data}); 67 | }.bind(this), 68 | error: function (xhr, status, err) { 69 | console.error(this.props.url, status, err.toString()); 70 | }.bind(this) 71 | }); 72 | }); 73 | }, 74 | loadCommentsFromServer: function () { 75 | $.ajax({ 76 | url: this.props.url, 77 | dataType: 'json', 78 | success: function (data) { 79 | this.setState({data: data}); 80 | }.bind(this), 81 | error: function (xhr, status, err) { 82 | console.error(this.props.url, status, err.toString()); 83 | }.bind(this) 84 | }); 85 | }, 86 | getInitialState: function () { 87 | return {data: this.props.data}; 88 | }, 89 | componentDidMount: function () { 90 | this.loadCommentsFromServer(); 91 | setInterval(this.loadCommentsFromServer, this.props.pollInterval); 92 | }, 93 | render: function () { 94 | return ( 95 | React.createElement("div", {className: "commentBox"}, 96 | React.createElement("h1", null, "Comments"), 97 | React.createElement(CommentList, {data: this.state.data}), 98 | React.createElement(CommentForm, {onCommentSubmit: this.handleCommentSubmit}) 99 | ) 100 | ); 101 | } 102 | }); 103 | 104 | var renderClient = function (comments) { 105 | var data = comments || []; 106 | React.render( 107 | React.createElement(CommentBox, {data: data, url: "comments.json", pollInterval: 5000}), 108 | document.getElementById("content") 109 | ); 110 | }; 111 | 112 | var renderServer = function (comments) { 113 | var data = Java.from(comments); 114 | return React.renderToString( 115 | React.createElement(CommentBox, {data: data, url: "comments.json", pollInterval: 5000}) 116 | ); 117 | }; -------------------------------------------------------------------------------- /src/main/resources/static/nashorn-polyfill.js: -------------------------------------------------------------------------------- 1 | var global = this; 2 | 3 | var console = {}; 4 | console.debug = print; 5 | console.warn = print; 6 | console.log = print; -------------------------------------------------------------------------------- /src/main/resources/static/vendor/showdown.min.js: -------------------------------------------------------------------------------- 1 | // 2 | // showdown.js -- A javascript port of Markdown. 3 | // 4 | // Copyright (c) 2007 John Fraser. 5 | // 6 | // Original Markdown Copyright (c) 2004-2005 John Gruber 7 | // 8 | // 9 | // Redistributable under a BSD-style open source license. 10 | // See license.txt for more information. 11 | // 12 | // The full source distribution is at: 13 | // 14 | // A A L 15 | // T C A 16 | // T K B 17 | // 18 | // 19 | // 20 | // 21 | // Wherever possible, Showdown is a straight, line-by-line port 22 | // of the Perl version of Markdown. 23 | // 24 | // This is not a normal parser design; it's basically just a 25 | // series of string substitutions. It's hard to read and 26 | // maintain this way, but keeping Showdown close to the original 27 | // design makes it easier to port new features. 28 | // 29 | // More importantly, Showdown behaves like markdown.pl in most 30 | // edge cases. So web applications can do client-side preview 31 | // in Javascript, and then build identical HTML on the server. 32 | // 33 | // This port needs the new RegExp functionality of ECMA 262, 34 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 35 | // should do fine. Even with the new regular expression features, 36 | // We do a lot of work to emulate Perl's regex functionality. 37 | // The tricky changes in this file mostly have the "attacklab:" 38 | // label. Major or self-explanatory changes don't. 39 | // 40 | // Smart diff tools like Araxis Merge will be able to match up 41 | // this file with markdown.pl in a useful way. A little tweaking 42 | // helps: in a copy of markdown.pl, replace "#" with "//" and 43 | // replace "$text" with "text". Be sure to ignore whitespace 44 | // and line endings. 45 | // 46 | // 47 | // Showdown usage: 48 | // 49 | // var text = "Markdown *rocks*."; 50 | // 51 | // var converter = new Showdown.converter(); 52 | // var html = converter.makeHtml(text); 53 | // 54 | // alert(html); 55 | // 56 | // Note: move the sample code to the bottom of this 57 | // file before uncommenting it. 58 | // 59 | // 60 | // Showdown namespace 61 | // 62 | var Showdown={extensions:{}},forEach=Showdown.forEach=function(a,b){if(typeof a.forEach=="function")a.forEach(b);else{var c,d=a.length;for(c=0;c?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|(?=~0))/gm,function(a,d,e,f,g){return d=d.toLowerCase(),b[d]=G(e),f?f+g:(g&&(c[d]=g.replace(/"/g,""")),"")}),a=a.replace(/~0/,""),a},m=function(a){a=a.replace(/\n/g,"\n\n");var b="p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del|style|section|header|footer|nav|article|aside",c="p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|style|section|header|footer|nav|article|aside";return a=a.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,n),a=a.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|style|section|header|footer|nav|article|aside)\b[^\r]*?<\/\2>[ \t]*(?=\n+)\n)/gm,n),a=a.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,n),a=a.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,n),a=a.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,n),a=a.replace(/\n\n/g,"\n"),a},n=function(a,b){var c=b;return c=c.replace(/\n\n/g,"\n"),c=c.replace(/^\n/,""),c=c.replace(/\n+$/g,""),c="\n\n~K"+(d.push(c)-1)+"K\n\n",c},o=function(a){a=v(a);var b=A("
");return a=a.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,b),a=a.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,b),a=a.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,b),a=x(a),a=y(a),a=E(a),a=m(a),a=F(a),a},p=function(a){return a=B(a),a=q(a),a=H(a),a=t(a),a=r(a),a=I(a),a=G(a),a=D(a),a=a.replace(/ +\n/g,"
\n"),a},q=function(a){var b=/(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi;return a=a.replace(b,function(a){var b=a.replace(/(.)<\/?code>(?=.)/g,"$1`");return b=N(b,"\\`*_"),b}),a},r=function(a){return a=a.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,s),a=a.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,s),a=a.replace(/(\[([^\[\]]+)\])()()()()()/g,s),a},s=function(a,d,e,f,g,h,i,j){j==undefined&&(j="");var k=d,l=e,m=f.toLowerCase(),n=g,o=j;if(n==""){m==""&&(m=l.toLowerCase().replace(/ ?\n/g," ")),n="#"+m;if(b[m]!=undefined)n=b[m],c[m]!=undefined&&(o=c[m]);else{if(!(k.search(/\(\s*\)$/m)>-1))return k;n=""}}n=N(n,"*_");var p='",p},t=function(a){return a=a.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,u),a=a.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,u),a},u=function(a,d,e,f,g,h,i,j){var k=d,l=e,m=f.toLowerCase(),n=g,o=j;o||(o="");if(n==""){m==""&&(m=l.toLowerCase().replace(/ ?\n/g," ")),n="#"+m;if(b[m]==undefined)return k;n=b[m],c[m]!=undefined&&(o=c[m])}l=l.replace(/"/g,"""),n=N(n,"*_");var p=''+l+''+p(c)+"")}),a=a.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,function(a,c){return A('

'+p(c)+"

")}),a=a.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,function(a,c,d){var e=c.length;return A("'+p(d)+"")}),a},w,x=function(a){a+="~0";var b=/^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;return e?a=a.replace(b,function(a,b,c){var d=b,e=c.search(/[*+-]/g)>-1?"ul":"ol";d=d.replace(/\n{2,}/g,"\n\n\n");var f=w(d);return f=f.replace(/\s+$/,""),f="<"+e+">"+f+"\n",f}):(b=/(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g,a=a.replace(b,function(a,b,c,d){var e=b,f=c,g=d.search(/[*+-]/g)>-1?"ul":"ol",f=f.replace(/\n{2,}/g,"\n\n\n"),h=w(f);return h=e+"<"+g+">\n"+h+"\n",h})),a=a.replace(/~0/,""),a};w=function(a){return e++,a=a.replace(/\n{2,}$/,"\n"),a+="~0",a=a.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm,function(a,b,c,d,e){var f=e,g=b,h=c;return g||f.search(/\n{2,}/)>-1?f=o(L(f)):(f=x(L(f)),f=f.replace(/\n$/,""),f=p(f)),"
  • "+f+"
  • \n"}),a=a.replace(/~0/g,""),e--,a};var y=function(a){return a+="~0",a=a.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,function(a,b,c){var d=b,e=c;return d=C(L(d)),d=M(d),d=d.replace(/^\n+/g,""),d=d.replace(/\n+$/g,""),d="
    "+d+"\n
    ",A(d)+e}),a=a.replace(/~0/,""),a},z=function(a){return a+="~0",a=a.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g,function(a,b,c){var d=b,e=c;return e=C(e),e=M(e),e=e.replace(/^\n+/g,""),e=e.replace(/\n+$/g,""),e="
    "+e+"\n
    ",A(e)}),a=a.replace(/~0/,""),a},A=function(a){return a=a.replace(/(^\n+|\n+$)/g,""),"\n\n~K"+(d.push(a)-1)+"K\n\n"},B=function(a){return a=a.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(a,b,c,d,e){var f=d;return f=f.replace(/^([ \t]*)/g,""),f=f.replace(/[ \t]*$/g,""),f=C(f),b+""+f+""}),a},C=function(a){return a=a.replace(/&/g,"&"),a=a.replace(//g,">"),a=N(a,"*_{}[]\\",!1),a},D=function(a){return a=a.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,"$2"),a=a.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,"$2"),a},E=function(a){return a=a.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,function(a,b){var c=b;return c=c.replace(/^[ \t]*>[ \t]?/gm,"~0"),c=c.replace(/~0/g,""),c=c.replace(/^[ \t]+$/gm,""),c=o(c),c=c.replace(/(^|\n)/g,"$1 "),c=c.replace(/(\s*
    [^\r]+?<\/pre>)/gm,function(a,b){var c=b;return c=c.replace(/^  /mg,"~0"),c=c.replace(/~0/g,""),c}),A("
    \n"+c+"\n
    ")}),a},F=function(a){a=a.replace(/^\n+/g,""),a=a.replace(/\n+$/g,"");var b=a.split(/\n{2,}/g),c=[],e=b.length;for(var f=0;f=0?c.push(g):g.search(/\S/)>=0&&(g=p(g),g=g.replace(/^([ \t]*)/g,"

    "),g+="

    ",c.push(g))}e=c.length;for(var f=0;f=0){var h=d[RegExp.$1];h=h.replace(/\$/g,"$$$$"),c[f]=c[f].replace(/~K\d+K/,h)}return c.join("\n\n")},G=function(a){return a=a.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&"),a=a.replace(/<(?![a-z\/?\$!])/gi,"<"),a},H=function(a){return a=a.replace(/\\(\\)/g,O),a=a.replace(/\\([`*_{}\[\]()>#+-.!])/g,O),a},I=function(a){return a=a.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,'
    $1'),a=a.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,function(a,b){return J(K(b))}),a},J=function(a){var b=[function(a){return"&#"+a.charCodeAt(0)+";"},function(a){return"&#x"+a.charCodeAt(0).toString(16)+";"},function(a){return a}];return a="mailto:"+a,a=a.replace(/./g,function(a){if(a=="@")a=b[Math.floor(Math.random()*2)](a);else if(a!=":"){var c=Math.random();a=c>.9?b[2](a):c>.45?b[1](a):b[0](a)}return a}),a=''+a+"",a=a.replace(/">.+:/g,'">'),a},K=function(a){return a=a.replace(/~E(\d+)E/g,function(a,b){var c=parseInt(b);return String.fromCharCode(c)}),a},L=function(a){return a=a.replace(/^(\t|[ ]{1,4})/gm,"~0"),a=a.replace(/~0/g,""),a},M=function(a){return a=a.replace(/\t(?=\t)/g," "),a=a.replace(/\t/g,"~A~B"),a=a.replace(/~B(.+?)~A/g,function(a,b,c){var d=b,e=4-d.length%4;for(var f=0;f 2 | 3 | 4 | Hello React 5 | 6 | 7 | 8 | 9 | 10 |
    ${content}
    11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/java/com/winterbe/react/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.test.context.web.WebAppConfiguration; 6 | import org.springframework.boot.test.SpringApplicationConfiguration; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = Application.class) 11 | @WebAppConfiguration 12 | public class ApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/winterbe/react/ReactTest.java: -------------------------------------------------------------------------------- 1 | package com.winterbe.react; 2 | 3 | import static org.hamcrest.Matchers.is; 4 | import static org.hamcrest.Matchers.startsWith; 5 | import static org.junit.Assert.assertThat; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import org.jsoup.Jsoup; 11 | import org.jsoup.nodes.Document; 12 | import org.junit.Test; 13 | 14 | public class ReactTest { 15 | 16 | @Test 17 | public void testRenderCommentBox() throws Exception { 18 | List comments = new ArrayList<>(); 19 | comments.add(new Comment("Peter Parker", "This is a comment.")); 20 | comments.add(new Comment("John Doe", "This is *another* comment.")); 21 | 22 | React react = new React(); 23 | String html = react.renderCommentBox(comments); 24 | 25 | assertThat(html, startsWith("