deleteAll() {
69 | repository.deleteAll();
70 | return ResponseEntity.ok().build();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo-Backend client
6 |
7 |
8 |
9 |
10 |
11 |
18 |
19 |
23 |
24 |
36 |
41 |
49 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 3.1.3
9 |
10 |
11 | com.atomicjar
12 | testcontainers-java-spring-boot-quickstart
13 | 0.0.1-SNAPSHOT
14 | testcontainers-java-spring-boot-quickstart
15 | Testcontainers Java and Spring Boot QuickStart
16 |
17 | 17
18 | 1.19.0
19 |
20 |
21 |
22 | org.springframework.boot
23 | spring-boot-starter-data-jpa
24 |
25 |
26 | org.springframework.boot
27 | spring-boot-starter-validation
28 |
29 |
30 | org.springframework.boot
31 | spring-boot-starter-web
32 |
33 |
34 | org.flywaydb
35 | flyway-core
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-devtools
40 | runtime
41 | true
42 |
43 |
44 | org.postgresql
45 | postgresql
46 | runtime
47 |
48 |
49 | org.springframework.boot
50 | spring-boot-starter-test
51 | test
52 |
53 |
54 | org.springframework.boot
55 | spring-boot-testcontainers
56 | test
57 |
58 |
59 | org.testcontainers
60 | junit-jupiter
61 | test
62 |
63 |
64 | org.testcontainers
65 | postgresql
66 | test
67 |
68 |
69 | org.testcontainers
70 | selenium
71 | test
72 |
73 |
74 | org.seleniumhq.selenium
75 | selenium-remote-driver
76 | test
77 |
78 |
79 | org.seleniumhq.selenium
80 | selenium-chrome-driver
81 | test
82 |
83 |
84 | io.rest-assured
85 | rest-assured
86 | test
87 |
88 |
89 |
90 |
91 |
92 |
93 | org.springframework.boot
94 | spring-boot-maven-plugin
95 |
96 |
97 |
98 | org.projectlombok
99 | lombok
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/src/test/java/com/atomicjar/todos/SeleniumE2ETests.java:
--------------------------------------------------------------------------------
1 | package com.atomicjar.todos;
2 |
3 | import org.junit.jupiter.api.AfterAll;
4 | import org.junit.jupiter.api.BeforeAll;
5 | import org.junit.jupiter.api.Test;
6 | import org.junit.jupiter.api.io.TempDir;
7 | import org.openqa.selenium.By;
8 | import org.openqa.selenium.Keys;
9 | import org.openqa.selenium.WebElement;
10 | import org.openqa.selenium.chrome.ChromeOptions;
11 | import org.openqa.selenium.remote.RemoteWebDriver;
12 | import org.springframework.boot.test.context.SpringBootTest;
13 | import org.springframework.boot.test.web.server.LocalServerPort;
14 | import org.springframework.test.context.TestPropertySource;
15 | import org.testcontainers.containers.BrowserWebDriverContainer;
16 | import org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode;
17 | import org.testcontainers.containers.DefaultRecordingFileFactory;
18 | import org.testcontainers.junit.jupiter.Container;
19 | import org.testcontainers.junit.jupiter.Testcontainers;
20 | import org.testcontainers.lifecycle.TestDescription;
21 |
22 | import java.io.File;
23 | import java.time.Duration;
24 | import java.util.Optional;
25 |
26 | import static org.assertj.core.api.Assertions.assertThat;
27 |
28 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
29 | @TestPropertySource(properties = {
30 | "spring.datasource.url=jdbc:tc:postgresql:15-alpine:///todos"
31 | })
32 | @Testcontainers
33 | public class SeleniumE2ETests {
34 |
35 | @LocalServerPort
36 | private Integer localPort;
37 |
38 | @TempDir
39 | static File tempDir;
40 |
41 | @Container
42 | static BrowserWebDriverContainer> chrome = new BrowserWebDriverContainer<>("selenium/standalone-chrome:4.8.3")
43 | .withAccessToHost(true)
44 | .withRecordingMode(VncRecordingMode.RECORD_ALL, tempDir)
45 | .withRecordingFileFactory(new DefaultRecordingFileFactory())
46 | .withCapabilities(new ChromeOptions())
47 | ;
48 | static RemoteWebDriver driver;
49 |
50 | @BeforeAll
51 | static void beforeAll() {
52 | driver = new RemoteWebDriver(chrome.getSeleniumAddress(), new ChromeOptions());
53 | driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
54 | System.out.println("Selenium remote URL is: " + chrome.getSeleniumAddress());
55 | System.out.println("VNC URL is: " + chrome.getVncAddress());
56 | }
57 |
58 | @AfterAll
59 | static void afterAll() {
60 | saveVideoRecording();
61 | driver.quit();
62 | }
63 |
64 | @Test
65 | void testAddTodo() {
66 | org.testcontainers.Testcontainers.exposeHostPorts(localPort);
67 | String baseUrl = "http://host.testcontainers.internal:" + localPort;
68 | String apiUrl = baseUrl + "/todos";
69 | driver.get(baseUrl + "/?" + apiUrl);
70 |
71 | assertThat(driver.getTitle()).isEqualTo("Todo-Backend client");
72 |
73 | driver.findElement(By.id("new-todo")).sendKeys("first todo" + Keys.RETURN);
74 | WebElement element = driver.findElement(By.xpath("//*[@id=\"todo-list\"]/li[1]/div/label"));
75 | assertThat(element.getText()).isEqualTo("first todo");
76 | }
77 |
78 | private static void saveVideoRecording() {
79 | chrome.afterTest(
80 | new TestDescription() {
81 | @Override
82 | public String getTestId() {
83 | return getFilesystemFriendlyName();
84 | }
85 |
86 | @Override
87 | public String getFilesystemFriendlyName() {
88 | return "Todo-E2E-Tests";
89 | }
90 | },
91 | Optional.empty()
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/test/java/com/atomicjar/todos/web/TodoControllerTests.java:
--------------------------------------------------------------------------------
1 | package com.atomicjar.todos.web;
2 |
3 | import com.atomicjar.todos.entity.Todo;
4 | import com.atomicjar.todos.repository.TodoRepository;
5 | import io.restassured.RestAssured;
6 | import io.restassured.http.ContentType;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Test;
9 | import org.springframework.beans.factory.annotation.Autowired;
10 | import org.springframework.boot.test.context.SpringBootTest;
11 | import org.springframework.boot.test.web.server.LocalServerPort;
12 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
13 | import org.testcontainers.containers.PostgreSQLContainer;
14 | import org.testcontainers.junit.jupiter.Container;
15 | import org.testcontainers.junit.jupiter.Testcontainers;
16 |
17 | import java.util.List;
18 |
19 | import static io.restassured.RestAssured.given;
20 | import static org.assertj.core.api.Assertions.assertThat;
21 | import static org.hamcrest.CoreMatchers.is;
22 | import static org.hamcrest.Matchers.hasSize;
23 |
24 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
25 | @Testcontainers
26 | public class TodoControllerTests {
27 | @LocalServerPort
28 | private Integer port;
29 |
30 | @Container
31 | @ServiceConnection
32 | static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
33 |
34 | @Autowired
35 | TodoRepository todoRepository;
36 |
37 | @BeforeEach
38 | void setUp() {
39 | todoRepository.deleteAll();
40 | RestAssured.baseURI = "http://localhost:" + port;
41 | }
42 |
43 | @Test
44 | void shouldGetAllTodos() {
45 | List todos = List.of(
46 | new Todo(null, "Todo Item 1", false, 1),
47 | new Todo(null, "Todo Item 2", false, 2)
48 | );
49 | todoRepository.saveAll(todos);
50 |
51 | given()
52 | .contentType(ContentType.JSON)
53 | .when()
54 | .get("/todos")
55 | .then()
56 | .statusCode(200)
57 | .body(".", hasSize(2));
58 | }
59 |
60 | @Test
61 | void shouldGetTodoById() {
62 | Todo todo = todoRepository.save(new Todo(null, "Todo Item 1", false, 1));
63 |
64 | given()
65 | .contentType(ContentType.JSON)
66 | .when()
67 | .get("/todos/{id}", todo.getId())
68 | .then()
69 | .statusCode(200)
70 | .body("title", is("Todo Item 1"))
71 | .body("completed", is(false))
72 | .body("order", is(1));
73 | }
74 |
75 | @Test
76 | void shouldCreateTodoSuccessfully() {
77 | given()
78 | .contentType(ContentType.JSON)
79 | .body(
80 | """
81 | {
82 | "title": "Todo Item 1",
83 | "completed": false,
84 | "order": 1
85 | }
86 | """
87 | )
88 | .when()
89 | .post("/todos")
90 | .then()
91 | .statusCode(201)
92 | .body("title", is("Todo Item 1"))
93 | .body("completed", is(false))
94 | .body("order", is(1));
95 | }
96 |
97 | @Test
98 | void shouldDeleteTodoById() {
99 | Todo todo = todoRepository.save(new Todo(null, "Todo Item 1", false, 1));
100 |
101 | assertThat(todoRepository.findById(todo.getId())).isPresent();
102 | given()
103 | .contentType(ContentType.JSON)
104 | .when()
105 | .delete("/todos/{id}", todo.getId())
106 | .then()
107 | .statusCode(200);
108 |
109 | assertThat(todoRepository.findById(todo.getId())).isEmpty();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/resources/public/js/views/app-view.js:
--------------------------------------------------------------------------------
1 | /*global Backbone, jQuery, _, ENTER_KEY */
2 | var app = app || {};
3 |
4 | (function ($) {
5 | 'use strict';
6 |
7 | // The Application
8 | // ---------------
9 |
10 | // Our overall **AppView** is the top-level piece of UI.
11 | app.AppView = Backbone.View.extend({
12 |
13 | // Instead of generating a new element, bind to the existing skeleton of
14 | // the App already present in the HTML.
15 | el: '#todoapp',
16 |
17 | // Our template for the line of statistics at the bottom of the app.
18 | statsTemplate: _.template($('#stats-template').html()),
19 |
20 | // Delegated events for creating new items, and clearing completed ones.
21 | events: {
22 | 'keypress #new-todo': 'createOnEnter',
23 | 'click #clear-completed': 'clearCompleted',
24 | 'click #toggle-all': 'toggleAllComplete'
25 | },
26 |
27 | // At initialization we bind to the relevant events on the `Todos`
28 | // collection, when items are added or changed. Kick things off by
29 | // loading any preexisting todos that might be saved in *localStorage*.
30 | initialize: function () {
31 | this.allCheckbox = this.$('#toggle-all')[0];
32 | this.$input = this.$('#new-todo');
33 | this.$footer = this.$('#footer');
34 | this.$main = this.$('#main');
35 | this.$list = $('#todo-list');
36 |
37 | this.listenTo(app.todos, 'add', this.addOne);
38 | this.listenTo(app.todos, 'reset', this.addAll);
39 | this.listenTo(app.todos, 'change:completed', this.filterOne);
40 | this.listenTo(app.todos, 'filter', this.filterAll);
41 | this.listenTo(app.todos, 'all', this.render);
42 |
43 | // Suppresses 'add' events with {reset: true} and prevents the app view
44 | // from being re-rendered for every model. Only renders when the 'reset'
45 | // event is triggered at the end of the fetch.
46 | app.todos.fetch({reset: true});
47 | },
48 |
49 | // Re-rendering the App just means refreshing the statistics -- the rest
50 | // of the app doesn't change.
51 | render: function () {
52 | var completed = app.todos.completed().length;
53 | var remaining = app.todos.remaining().length;
54 |
55 | if (app.todos.length) {
56 | this.$main.show();
57 | this.$footer.show();
58 |
59 | this.$footer.html(this.statsTemplate({
60 | completed: completed,
61 | remaining: remaining
62 | }));
63 |
64 | this.$('#filters li a')
65 | .removeClass('selected')
66 | .filter('[href="#/' + (app.TodoFilter || '') + '"]')
67 | .addClass('selected');
68 | } else {
69 | this.$main.hide();
70 | this.$footer.hide();
71 | }
72 |
73 | this.allCheckbox.checked = !remaining;
74 | },
75 |
76 | // Add a single todo item to the list by creating a view for it, and
77 | // appending its element to the ``.
78 | addOne: function (todo) {
79 | var view = new app.TodoView({ model: todo });
80 | this.$list.append(view.render().el);
81 | },
82 |
83 | // Add all items in the **Todos** collection at once.
84 | addAll: function () {
85 | this.$list.html('');
86 | app.todos.each(this.addOne, this);
87 | },
88 |
89 | filterOne: function (todo) {
90 | todo.trigger('visible');
91 | },
92 |
93 | filterAll: function () {
94 | app.todos.each(this.filterOne, this);
95 | },
96 |
97 | // Generate the attributes for a new Todo item.
98 | newAttributes: function () {
99 | return {
100 | title: this.$input.val().trim(),
101 | order: app.todos.nextOrder(),
102 | completed: false
103 | };
104 | },
105 |
106 | // If you hit return in the main input field, create new **Todo** model,
107 | // persisting it to *localStorage*.
108 | createOnEnter: function (e) {
109 | if (e.which === ENTER_KEY && this.$input.val().trim()) {
110 | app.todos.create(this.newAttributes());
111 | this.$input.val('');
112 | }
113 | },
114 |
115 | // Clear all completed todo items, destroying their models.
116 | clearCompleted: function () {
117 | _.invoke(app.todos.completed(), 'destroy');
118 | return false;
119 | },
120 |
121 | toggleAllComplete: function () {
122 | var completed = this.allCheckbox.checked;
123 |
124 | app.todos.each(function (todo) {
125 | todo.save({
126 | completed: completed
127 | });
128 | });
129 | }
130 | });
131 | })(jQuery);
132 |
--------------------------------------------------------------------------------
/src/main/resources/public/js/views/todo-view.js:
--------------------------------------------------------------------------------
1 | /*global Backbone, jQuery, _, ENTER_KEY, ESC_KEY */
2 | var app = app || {};
3 |
4 | (function ($) {
5 | 'use strict';
6 |
7 | // Todo Item View
8 | // --------------
9 |
10 | // The DOM element for a todo item...
11 | app.TodoView = Backbone.View.extend({
12 | //... is a list tag.
13 | tagName: 'li',
14 |
15 | // Cache the template function for a single item.
16 | template: _.template($('#item-template').html()),
17 |
18 | // The DOM events specific to an item.
19 | events: {
20 | 'click .toggle': 'toggleCompleted',
21 | 'dblclick label': 'edit',
22 | 'click .destroy': 'clear',
23 | 'keypress .edit': 'updateOnEnter',
24 | 'keydown .edit': 'revertOnEscape',
25 | 'blur .edit': 'close'
26 | },
27 |
28 | // The TodoView listens for changes to its model, re-rendering. Since
29 | // there's a one-to-one correspondence between a **Todo** and a
30 | // **TodoView** in this app, we set a direct reference on the model for
31 | // convenience.
32 | initialize: function () {
33 | this.listenTo(this.model, 'change', this.render);
34 | this.listenTo(this.model, 'destroy', this.remove);
35 | this.listenTo(this.model, 'visible', this.toggleVisible);
36 | },
37 |
38 | // Re-render the titles of the todo item.
39 | render: function () {
40 | // Backbone LocalStorage is adding `id` attribute instantly after
41 | // creating a model. This causes our TodoView to render twice. Once
42 | // after creating a model and once on `id` change. We want to
43 | // filter out the second redundant render, which is caused by this
44 | // `id` change. It's known Backbone LocalStorage bug, therefore
45 | // we've to create a workaround.
46 | // https://github.com/tastejs/todomvc/issues/469
47 | if (this.model.changed.id !== undefined) {
48 | return;
49 | }
50 |
51 | this.$el.html(this.template(this.model.toJSON()));
52 | this.$el.toggleClass('completed', this.model.get('completed'));
53 | this.toggleVisible();
54 | this.$input = this.$('.edit');
55 | return this;
56 | },
57 |
58 | toggleVisible: function () {
59 | this.$el.toggleClass('hidden', this.isHidden());
60 | },
61 |
62 | isHidden: function () {
63 | var isCompleted = this.model.get('completed');
64 | return (// hidden cases only
65 | (!isCompleted && app.TodoFilter === 'completed') ||
66 | (isCompleted && app.TodoFilter === 'active')
67 | );
68 | },
69 |
70 | // Toggle the `"completed"` state of the model.
71 | toggleCompleted: function () {
72 | this.model.toggle();
73 | },
74 |
75 | // Switch this view into `"editing"` mode, displaying the input field.
76 | edit: function () {
77 | this.$el.addClass('editing');
78 | this.$input.focus();
79 | },
80 |
81 | // Close the `"editing"` mode, saving changes to the todo.
82 | close: function () {
83 | var value = this.$input.val();
84 | var trimmedValue = value.trim();
85 |
86 | // We don't want to handle blur events from an item that is no
87 | // longer being edited. Relying on the CSS class here has the
88 | // benefit of us not having to maintain state in the DOM and the
89 | // JavaScript logic.
90 | if (!this.$el.hasClass('editing')) {
91 | return;
92 | }
93 |
94 | if (trimmedValue) {
95 | this.model.save({ title: trimmedValue },{patch:true});
96 |
97 | if (value !== trimmedValue) {
98 | // Model values changes consisting of whitespaces only are
99 | // not causing change to be triggered Therefore we've to
100 | // compare untrimmed version with a trimmed one to check
101 | // whether anything changed
102 | // And if yes, we've to trigger change event ourselves
103 | this.model.trigger('change');
104 | }
105 | } else {
106 | this.clear();
107 | }
108 |
109 | this.$el.removeClass('editing');
110 | },
111 |
112 | // If you hit `enter`, we're through editing the item.
113 | updateOnEnter: function (e) {
114 | if (e.which === ENTER_KEY) {
115 | this.close();
116 | }
117 | },
118 |
119 | // If you're pressing `escape` we revert your change by simply leaving
120 | // the `editing` state.
121 | revertOnEscape: function (e) {
122 | if (e.which === ESC_KEY) {
123 | this.$el.removeClass('editing');
124 | // Also reset the hidden input back to the original value.
125 | this.$input.val(this.model.get('title'));
126 | }
127 | },
128 |
129 | // Remove the item, destroy the model from *localStorage* and delete its view.
130 | clear: function () {
131 | this.model.destroy();
132 | }
133 | });
134 | })(jQuery);
135 |
--------------------------------------------------------------------------------
/src/main/resources/public/js/vendor/todomvc-common.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | // Underscore's Template Module
5 | // Courtesy of underscorejs.org
6 | var _ = (function (_) {
7 | _.defaults = function (object) {
8 | if (!object) {
9 | return object;
10 | }
11 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
12 | var iterable = arguments[argsIndex];
13 | if (iterable) {
14 | for (var key in iterable) {
15 | if (object[key] == null) {
16 | object[key] = iterable[key];
17 | }
18 | }
19 | }
20 | }
21 | return object;
22 | }
23 |
24 | // By default, Underscore uses ERB-style template delimiters, change the
25 | // following template settings to use alternative delimiters.
26 | _.templateSettings = {
27 | evaluate : /<%([\s\S]+?)%>/g,
28 | interpolate : /<%=([\s\S]+?)%>/g,
29 | escape : /<%-([\s\S]+?)%>/g
30 | };
31 |
32 | // When customizing `templateSettings`, if you don't want to define an
33 | // interpolation, evaluation or escaping regex, we need one that is
34 | // guaranteed not to match.
35 | var noMatch = /(.)^/;
36 |
37 | // Certain characters need to be escaped so that they can be put into a
38 | // string literal.
39 | var escapes = {
40 | "'": "'",
41 | '\\': '\\',
42 | '\r': 'r',
43 | '\n': 'n',
44 | '\t': 't',
45 | '\u2028': 'u2028',
46 | '\u2029': 'u2029'
47 | };
48 |
49 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
50 |
51 | // JavaScript micro-templating, similar to John Resig's implementation.
52 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
53 | // and correctly escapes quotes within interpolated code.
54 | _.template = function(text, data, settings) {
55 | var render;
56 | settings = _.defaults({}, settings, _.templateSettings);
57 |
58 | // Combine delimiters into one regular expression via alternation.
59 | var matcher = new RegExp([
60 | (settings.escape || noMatch).source,
61 | (settings.interpolate || noMatch).source,
62 | (settings.evaluate || noMatch).source
63 | ].join('|') + '|$', 'g');
64 |
65 | // Compile the template source, escaping string literals appropriately.
66 | var index = 0;
67 | var source = "__p+='";
68 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
69 | source += text.slice(index, offset)
70 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
71 |
72 | if (escape) {
73 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
74 | }
75 | if (interpolate) {
76 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
77 | }
78 | if (evaluate) {
79 | source += "';\n" + evaluate + "\n__p+='";
80 | }
81 | index = offset + match.length;
82 | return match;
83 | });
84 | source += "';\n";
85 |
86 | // If a variable is not specified, place data values in local scope.
87 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
88 |
89 | source = "var __t,__p='',__j=Array.prototype.join," +
90 | "print=function(){__p+=__j.call(arguments,'');};\n" +
91 | source + "return __p;\n";
92 |
93 | try {
94 | render = new Function(settings.variable || 'obj', '_', source);
95 | } catch (e) {
96 | e.source = source;
97 | throw e;
98 | }
99 |
100 | if (data) return render(data, _);
101 | var template = function(data) {
102 | return render.call(this, data, _);
103 | };
104 |
105 | // Provide the compiled function source as a convenience for precompilation.
106 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
107 |
108 | return template;
109 | };
110 |
111 | return _;
112 | })({});
113 |
114 | if (location.hostname === 'todomvc.com') {
115 | window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
116 | }
117 |
118 | function redirect() {
119 | if (location.hostname === 'tastejs.github.io') {
120 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
121 | }
122 | }
123 |
124 | function findRoot() {
125 | var base = location.href.indexOf('examples/');
126 | return location.href.substr(0, base);
127 | }
128 |
129 | function getFile(file, callback) {
130 | if (!location.host) {
131 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
132 | }
133 |
134 | var xhr = new XMLHttpRequest();
135 |
136 | xhr.open('GET', findRoot() + file, true);
137 | xhr.send();
138 |
139 | xhr.onload = function () {
140 | if (xhr.status === 200 && callback) {
141 | callback(xhr.responseText);
142 | }
143 | };
144 | }
145 |
146 | function Learn(learnJSON, config) {
147 | if (!(this instanceof Learn)) {
148 | return new Learn(learnJSON, config);
149 | }
150 |
151 | var template, framework;
152 |
153 | if (typeof learnJSON !== 'object') {
154 | try {
155 | learnJSON = JSON.parse(learnJSON);
156 | } catch (e) {
157 | return;
158 | }
159 | }
160 |
161 | if (config) {
162 | template = config.template;
163 | framework = config.framework;
164 | }
165 |
166 | if (!template && learnJSON.templates) {
167 | template = learnJSON.templates.todomvc;
168 | }
169 |
170 | if (!framework && document.querySelector('[data-framework]')) {
171 | framework = document.querySelector('[data-framework]').dataset.framework;
172 | }
173 |
174 |
175 | if (template && learnJSON[framework]) {
176 | this.frameworkJSON = learnJSON[framework];
177 | this.template = template;
178 |
179 | this.append();
180 | }
181 | }
182 |
183 | Learn.prototype.append = function () {
184 | var aside = document.createElement('aside');
185 | aside.innerHTML = _.template(this.template, this.frameworkJSON);
186 | aside.className = 'learn';
187 |
188 | // Localize demo links
189 | var demoLinks = aside.querySelectorAll('.demo-link');
190 | Array.prototype.forEach.call(demoLinks, function (demoLink) {
191 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
192 | });
193 |
194 | document.body.className = (document.body.className + ' learn-bar').trim();
195 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
196 | };
197 |
198 | redirect();
199 | getFile('learn.json', Learn);
200 | })();
201 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
124 |
125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% ^
162 | %JVM_CONFIG_MAVEN_PROPS% ^
163 | %MAVEN_OPTS% ^
164 | %MAVEN_DEBUG_OPTS% ^
165 | -classpath %WRAPPER_JAR% ^
166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
168 | if ERRORLEVEL 1 goto error
169 | goto end
170 |
171 | :error
172 | set ERROR_CODE=1
173 |
174 | :end
175 | @endlocal & set ERROR_CODE=%ERROR_CODE%
176 |
177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
181 | :skipRcPost
182 |
183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
185 |
186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
187 |
188 | cmd /C exit /B %ERROR_CODE%
189 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 | # Collect all arguments for the java command;
201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
202 | # shell script including quotes and variable substitutions, so put them in
203 | # double quotes to make sure that they get re-expanded; and
204 | # * put everything else in single quotes, so that it's not re-expanded.
205 |
206 | set -- \
207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
208 | -classpath "$CLASSPATH" \
209 | org.gradle.wrapper.GradleWrapperMain \
210 | "$@"
211 |
212 | # Stop when "xargs" is not available.
213 | if ! command -v xargs >/dev/null 2>&1
214 | then
215 | die "xargs is not available"
216 | fi
217 |
218 | # Use "xargs" to parse quoted args.
219 | #
220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
221 | #
222 | # In Bash we could simply go:
223 | #
224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
225 | # set -- "${ARGS[@]}" "$@"
226 | #
227 | # but POSIX shell has neither arrays nor command substitution, so instead we
228 | # post-process each arg (as a line of input to sed) to backslash-escape any
229 | # character that might be a shell metacharacter, then use eval to reverse
230 | # that process (while maintaining the separation between arguments), and wrap
231 | # the whole thing up as a single "set" statement.
232 | #
233 | # This will of course break if any of these variables contains a newline or
234 | # an unmatched quote.
235 | #
236 |
237 | eval "set -- $(
238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
239 | xargs -n1 |
240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
241 | tr '\n' ' '
242 | )" '"$@"'
243 |
244 | exec "$JAVACMD" "$@"
245 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /usr/local/etc/mavenrc ] ; then
40 | . /usr/local/etc/mavenrc
41 | fi
42 |
43 | if [ -f /etc/mavenrc ] ; then
44 | . /etc/mavenrc
45 | fi
46 |
47 | if [ -f "$HOME/.mavenrc" ] ; then
48 | . "$HOME/.mavenrc"
49 | fi
50 |
51 | fi
52 |
53 | # OS specific support. $var _must_ be set to either true or false.
54 | cygwin=false;
55 | darwin=false;
56 | mingw=false
57 | case "`uname`" in
58 | CYGWIN*) cygwin=true ;;
59 | MINGW*) mingw=true;;
60 | Darwin*) darwin=true
61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
63 | if [ -z "$JAVA_HOME" ]; then
64 | if [ -x "/usr/libexec/java_home" ]; then
65 | export JAVA_HOME="`/usr/libexec/java_home`"
66 | else
67 | export JAVA_HOME="/Library/Java/Home"
68 | fi
69 | fi
70 | ;;
71 | esac
72 |
73 | if [ -z "$JAVA_HOME" ] ; then
74 | if [ -r /etc/gentoo-release ] ; then
75 | JAVA_HOME=`java-config --jre-home`
76 | fi
77 | fi
78 |
79 | if [ -z "$M2_HOME" ] ; then
80 | ## resolve links - $0 may be a link to maven's home
81 | PRG="$0"
82 |
83 | # need this for relative symlinks
84 | while [ -h "$PRG" ] ; do
85 | ls=`ls -ld "$PRG"`
86 | link=`expr "$ls" : '.*-> \(.*\)$'`
87 | if expr "$link" : '/.*' > /dev/null; then
88 | PRG="$link"
89 | else
90 | PRG="`dirname "$PRG"`/$link"
91 | fi
92 | done
93 |
94 | saveddir=`pwd`
95 |
96 | M2_HOME=`dirname "$PRG"`/..
97 |
98 | # make it fully qualified
99 | M2_HOME=`cd "$M2_HOME" && pwd`
100 |
101 | cd "$saveddir"
102 | # echo Using m2 at $M2_HOME
103 | fi
104 |
105 | # For Cygwin, ensure paths are in UNIX format before anything is touched
106 | if $cygwin ; then
107 | [ -n "$M2_HOME" ] &&
108 | M2_HOME=`cygpath --unix "$M2_HOME"`
109 | [ -n "$JAVA_HOME" ] &&
110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
111 | [ -n "$CLASSPATH" ] &&
112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
113 | fi
114 |
115 | # For Mingw, ensure paths are in UNIX format before anything is touched
116 | if $mingw ; then
117 | [ -n "$M2_HOME" ] &&
118 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
119 | [ -n "$JAVA_HOME" ] &&
120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
121 | fi
122 |
123 | if [ -z "$JAVA_HOME" ]; then
124 | javaExecutable="`which javac`"
125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
126 | # readlink(1) is not available as standard on Solaris 10.
127 | readLink=`which readlink`
128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
129 | if $darwin ; then
130 | javaHome="`dirname \"$javaExecutable\"`"
131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
132 | else
133 | javaExecutable="`readlink -f \"$javaExecutable\"`"
134 | fi
135 | javaHome="`dirname \"$javaExecutable\"`"
136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
137 | JAVA_HOME="$javaHome"
138 | export JAVA_HOME
139 | fi
140 | fi
141 | fi
142 |
143 | if [ -z "$JAVACMD" ] ; then
144 | if [ -n "$JAVA_HOME" ] ; then
145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
146 | # IBM's JDK on AIX uses strange locations for the executables
147 | JAVACMD="$JAVA_HOME/jre/sh/java"
148 | else
149 | JAVACMD="$JAVA_HOME/bin/java"
150 | fi
151 | else
152 | JAVACMD="`\\unset -f command; \\command -v java`"
153 | fi
154 | fi
155 |
156 | if [ ! -x "$JAVACMD" ] ; then
157 | echo "Error: JAVA_HOME is not defined correctly." >&2
158 | echo " We cannot execute $JAVACMD" >&2
159 | exit 1
160 | fi
161 |
162 | if [ -z "$JAVA_HOME" ] ; then
163 | echo "Warning: JAVA_HOME environment variable is not set."
164 | fi
165 |
166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
167 |
168 | # traverses directory structure from process work directory to filesystem root
169 | # first directory with .mvn subdirectory is considered project base directory
170 | find_maven_basedir() {
171 |
172 | if [ -z "$1" ]
173 | then
174 | echo "Path not specified to find_maven_basedir"
175 | return 1
176 | fi
177 |
178 | basedir="$1"
179 | wdir="$1"
180 | while [ "$wdir" != '/' ] ; do
181 | if [ -d "$wdir"/.mvn ] ; then
182 | basedir=$wdir
183 | break
184 | fi
185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
186 | if [ -d "${wdir}" ]; then
187 | wdir=`cd "$wdir/.."; pwd`
188 | fi
189 | # end of workaround
190 | done
191 | echo "${basedir}"
192 | }
193 |
194 | # concatenates all lines of a file
195 | concat_lines() {
196 | if [ -f "$1" ]; then
197 | echo "$(tr -s '\n' ' ' < "$1")"
198 | fi
199 | }
200 |
201 | BASE_DIR=`find_maven_basedir "$(pwd)"`
202 | if [ -z "$BASE_DIR" ]; then
203 | exit 1;
204 | fi
205 |
206 | ##########################################################################################
207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
208 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
209 | ##########################################################################################
210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Found .mvn/wrapper/maven-wrapper.jar"
213 | fi
214 | else
215 | if [ "$MVNW_VERBOSE" = true ]; then
216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
217 | fi
218 | if [ -n "$MVNW_REPOURL" ]; then
219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
220 | else
221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
222 | fi
223 | while IFS="=" read key value; do
224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
225 | esac
226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
227 | if [ "$MVNW_VERBOSE" = true ]; then
228 | echo "Downloading from: $jarUrl"
229 | fi
230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
231 | if $cygwin; then
232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
233 | fi
234 |
235 | if command -v wget > /dev/null; then
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Found wget ... using wget"
238 | fi
239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
241 | else
242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
243 | fi
244 | elif command -v curl > /dev/null; then
245 | if [ "$MVNW_VERBOSE" = true ]; then
246 | echo "Found curl ... using curl"
247 | fi
248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
249 | curl -o "$wrapperJarPath" "$jarUrl" -f
250 | else
251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
252 | fi
253 |
254 | else
255 | if [ "$MVNW_VERBOSE" = true ]; then
256 | echo "Falling back to using Java to download"
257 | fi
258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
259 | # For Cygwin, switch paths to Windows format before running javac
260 | if $cygwin; then
261 | javaClass=`cygpath --path --windows "$javaClass"`
262 | fi
263 | if [ -e "$javaClass" ]; then
264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
265 | if [ "$MVNW_VERBOSE" = true ]; then
266 | echo " - Compiling MavenWrapperDownloader.java ..."
267 | fi
268 | # Compiling the Java class
269 | ("$JAVA_HOME/bin/javac" "$javaClass")
270 | fi
271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
272 | # Running the downloader
273 | if [ "$MVNW_VERBOSE" = true ]; then
274 | echo " - Running MavenWrapperDownloader.java ..."
275 | fi
276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
277 | fi
278 | fi
279 | fi
280 | fi
281 | ##########################################################################################
282 | # End of extension
283 | ##########################################################################################
284 |
285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
286 | if [ "$MVNW_VERBOSE" = true ]; then
287 | echo $MAVEN_PROJECTBASEDIR
288 | fi
289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
290 |
291 | # For Cygwin, switch paths to Windows format before running java
292 | if $cygwin; then
293 | [ -n "$M2_HOME" ] &&
294 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
295 | [ -n "$JAVA_HOME" ] &&
296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
297 | [ -n "$CLASSPATH" ] &&
298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
299 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
301 | fi
302 |
303 | # Provide a "standardized" way to retrieve the CLI args that will
304 | # work with both Windows and non-Windows executions.
305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
306 | export MAVEN_CMD_LINE_ARGS
307 |
308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
309 |
310 | exec "$JAVACMD" \
311 | $MAVEN_OPTS \
312 | $MAVEN_DEBUG_OPTS \
313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
314 | "-Dmaven.home=${M2_HOME}" \
315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
317 |
--------------------------------------------------------------------------------
/src/main/resources/public/css/vendor/todomvc-common.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | color: inherit;
16 | -webkit-appearance: none;
17 | -ms-appearance: none;
18 | -o-appearance: none;
19 | appearance: none;
20 | }
21 |
22 | body {
23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
24 | line-height: 1.4em;
25 | background: #eaeaea url('bg.png');
26 | color: #4d4d4d;
27 | width: 550px;
28 | margin: 0 auto;
29 | -webkit-font-smoothing: antialiased;
30 | -moz-font-smoothing: antialiased;
31 | -ms-font-smoothing: antialiased;
32 | -o-font-smoothing: antialiased;
33 | font-smoothing: antialiased;
34 | }
35 |
36 | button,
37 | input[type="checkbox"] {
38 | outline: none;
39 | }
40 |
41 | #todoapp {
42 | background: #fff;
43 | background: rgba(255, 255, 255, 0.9);
44 | margin: 130px 0 40px 0;
45 | border: 1px solid #ccc;
46 | position: relative;
47 | border-top-left-radius: 2px;
48 | border-top-right-radius: 2px;
49 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
50 | 0 25px 50px 0 rgba(0, 0, 0, 0.15);
51 | }
52 |
53 | #todoapp:before {
54 | content: '';
55 | border-left: 1px solid #f5d6d6;
56 | border-right: 1px solid #f5d6d6;
57 | width: 2px;
58 | position: absolute;
59 | top: 0;
60 | left: 40px;
61 | height: 100%;
62 | }
63 |
64 | #todoapp input::-webkit-input-placeholder {
65 | font-style: italic;
66 | }
67 |
68 | #todoapp input::-moz-placeholder {
69 | font-style: italic;
70 | color: #a9a9a9;
71 | }
72 |
73 | #todoapp h1 {
74 | position: absolute;
75 | top: -120px;
76 | width: 100%;
77 | font-size: 70px;
78 | font-weight: bold;
79 | text-align: center;
80 | color: #b3b3b3;
81 | color: rgba(255, 255, 255, 0.3);
82 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
83 | -webkit-text-rendering: optimizeLegibility;
84 | -moz-text-rendering: optimizeLegibility;
85 | -ms-text-rendering: optimizeLegibility;
86 | -o-text-rendering: optimizeLegibility;
87 | text-rendering: optimizeLegibility;
88 | }
89 |
90 | #header {
91 | padding-top: 15px;
92 | border-radius: inherit;
93 | }
94 |
95 | #header:before {
96 | content: '';
97 | position: absolute;
98 | top: 0;
99 | right: 0;
100 | left: 0;
101 | height: 15px;
102 | z-index: 2;
103 | border-bottom: 1px solid #6c615c;
104 | background: #8d7d77;
105 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
106 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
107 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
108 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
109 | border-top-left-radius: 1px;
110 | border-top-right-radius: 1px;
111 | }
112 |
113 | #new-todo,
114 | .edit {
115 | position: relative;
116 | margin: 0;
117 | width: 100%;
118 | font-size: 24px;
119 | font-family: inherit;
120 | line-height: 1.4em;
121 | border: 0;
122 | outline: none;
123 | color: inherit;
124 | padding: 6px;
125 | border: 1px solid #999;
126 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
127 | -moz-box-sizing: border-box;
128 | -ms-box-sizing: border-box;
129 | -o-box-sizing: border-box;
130 | box-sizing: border-box;
131 | -webkit-font-smoothing: antialiased;
132 | -moz-font-smoothing: antialiased;
133 | -ms-font-smoothing: antialiased;
134 | -o-font-smoothing: antialiased;
135 | font-smoothing: antialiased;
136 | }
137 |
138 | #new-todo {
139 | padding: 16px 16px 16px 60px;
140 | border: none;
141 | background: rgba(0, 0, 0, 0.02);
142 | z-index: 2;
143 | box-shadow: none;
144 | }
145 |
146 | #main {
147 | position: relative;
148 | z-index: 2;
149 | border-top: 1px dotted #adadad;
150 | }
151 |
152 | label[for='toggle-all'] {
153 | display: none;
154 | }
155 |
156 | #toggle-all {
157 | position: absolute;
158 | top: -42px;
159 | left: -4px;
160 | width: 40px;
161 | text-align: center;
162 | /* Mobile Safari */
163 | border: none;
164 | }
165 |
166 | #toggle-all:before {
167 | content: '»';
168 | font-size: 28px;
169 | color: #d9d9d9;
170 | padding: 0 25px 7px;
171 | }
172 |
173 | #toggle-all:checked:before {
174 | color: #737373;
175 | }
176 |
177 | #todo-list {
178 | margin: 0;
179 | padding: 0;
180 | list-style: none;
181 | }
182 |
183 | #todo-list li {
184 | position: relative;
185 | font-size: 24px;
186 | border-bottom: 1px dotted #ccc;
187 | }
188 |
189 | #todo-list li:last-child {
190 | border-bottom: none;
191 | }
192 |
193 | #todo-list li.editing {
194 | border-bottom: none;
195 | padding: 0;
196 | }
197 |
198 | #todo-list li.editing .edit {
199 | display: block;
200 | width: 506px;
201 | padding: 13px 17px 12px 17px;
202 | margin: 0 0 0 43px;
203 | }
204 |
205 | #todo-list li.editing .view {
206 | display: none;
207 | }
208 |
209 | #todo-list li .toggle {
210 | text-align: center;
211 | width: 40px;
212 | /* auto, since non-WebKit browsers doesn't support input styling */
213 | height: auto;
214 | position: absolute;
215 | top: 0;
216 | bottom: 0;
217 | margin: auto 0;
218 | /* Mobile Safari */
219 | border: none;
220 | -webkit-appearance: none;
221 | -ms-appearance: none;
222 | -o-appearance: none;
223 | appearance: none;
224 | }
225 |
226 | #todo-list li .toggle:after {
227 | content: '✔';
228 | /* 40 + a couple of pixels visual adjustment */
229 | line-height: 43px;
230 | font-size: 20px;
231 | color: #d9d9d9;
232 | text-shadow: 0 -1px 0 #bfbfbf;
233 | }
234 |
235 | #todo-list li .toggle:checked:after {
236 | color: #85ada7;
237 | text-shadow: 0 1px 0 #669991;
238 | bottom: 1px;
239 | position: relative;
240 | }
241 |
242 | #todo-list li label {
243 | white-space: pre;
244 | word-break: break-word;
245 | padding: 15px 60px 15px 15px;
246 | margin-left: 45px;
247 | display: block;
248 | line-height: 1.2;
249 | -webkit-transition: color 0.4s;
250 | transition: color 0.4s;
251 | }
252 |
253 | #todo-list li.completed label {
254 | color: #a9a9a9;
255 | text-decoration: line-through;
256 | }
257 |
258 | #todo-list li .destroy {
259 | display: none;
260 | position: absolute;
261 | top: 0;
262 | right: 10px;
263 | bottom: 0;
264 | width: 40px;
265 | height: 40px;
266 | margin: auto 0;
267 | font-size: 22px;
268 | color: #a88a8a;
269 | -webkit-transition: all 0.2s;
270 | transition: all 0.2s;
271 | }
272 |
273 | #todo-list li .destroy:hover {
274 | text-shadow: 0 0 1px #000,
275 | 0 0 10px rgba(199, 107, 107, 0.8);
276 | -webkit-transform: scale(1.3);
277 | transform: scale(1.3);
278 | }
279 |
280 | #todo-list li .destroy:after {
281 | content: '✖';
282 | }
283 |
284 | #todo-list li:hover .destroy {
285 | display: block;
286 | }
287 |
288 | #todo-list li .edit {
289 | display: none;
290 | }
291 |
292 | #todo-list li.editing:last-child {
293 | margin-bottom: -1px;
294 | }
295 |
296 | #footer {
297 | color: #777;
298 | padding: 0 15px;
299 | position: absolute;
300 | right: 0;
301 | bottom: -31px;
302 | left: 0;
303 | height: 20px;
304 | z-index: 1;
305 | text-align: center;
306 | }
307 |
308 | #footer:before {
309 | content: '';
310 | position: absolute;
311 | right: 0;
312 | bottom: 31px;
313 | left: 0;
314 | height: 50px;
315 | z-index: -1;
316 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
317 | 0 6px 0 -3px rgba(255, 255, 255, 0.8),
318 | 0 7px 1px -3px rgba(0, 0, 0, 0.3),
319 | 0 43px 0 -6px rgba(255, 255, 255, 0.8),
320 | 0 44px 2px -6px rgba(0, 0, 0, 0.2);
321 | }
322 |
323 | #todo-count {
324 | float: left;
325 | text-align: left;
326 | }
327 |
328 | #filters {
329 | margin: 0;
330 | padding: 0;
331 | list-style: none;
332 | position: absolute;
333 | right: 0;
334 | left: 0;
335 | }
336 |
337 | #filters li {
338 | display: inline;
339 | }
340 |
341 | #filters li a {
342 | color: #83756f;
343 | margin: 2px;
344 | text-decoration: none;
345 | }
346 |
347 | #filters li a.selected {
348 | font-weight: bold;
349 | }
350 |
351 | #clear-completed {
352 | float: right;
353 | position: relative;
354 | line-height: 20px;
355 | text-decoration: none;
356 | background: rgba(0, 0, 0, 0.1);
357 | font-size: 11px;
358 | padding: 0 10px;
359 | border-radius: 3px;
360 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
361 | }
362 |
363 | #clear-completed:hover {
364 | background: rgba(0, 0, 0, 0.15);
365 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
366 | }
367 |
368 | #info {
369 | margin: 65px auto 0;
370 | color: #a6a6a6;
371 | font-size: 12px;
372 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
373 | text-align: center;
374 | }
375 |
376 | #info a {
377 | color: inherit;
378 | }
379 |
380 | /*
381 | Hack to remove background from Mobile Safari.
382 | Can't use it globally since it destroys checkboxes in Firefox and Opera
383 | */
384 |
385 | @media screen and (-webkit-min-device-pixel-ratio:0) {
386 | #toggle-all,
387 | #todo-list li .toggle {
388 | background: none;
389 | }
390 |
391 | #todo-list li .toggle {
392 | height: 40px;
393 | }
394 |
395 | #toggle-all {
396 | top: -56px;
397 | left: -15px;
398 | width: 65px;
399 | height: 41px;
400 | -webkit-transform: rotate(90deg);
401 | transform: rotate(90deg);
402 | -webkit-appearance: none;
403 | appearance: none;
404 | }
405 | }
406 |
407 | .hidden {
408 | display: none;
409 | }
410 |
411 | hr {
412 | margin: 20px 0;
413 | border: 0;
414 | border-top: 1px dashed #C5C5C5;
415 | border-bottom: 1px dashed #F7F7F7;
416 | }
417 |
418 | .learn a {
419 | font-weight: normal;
420 | text-decoration: none;
421 | color: #b83f45;
422 | }
423 |
424 | .learn a:hover {
425 | text-decoration: underline;
426 | color: #787e7e;
427 | }
428 |
429 | .learn h3,
430 | .learn h4,
431 | .learn h5 {
432 | margin: 10px 0;
433 | font-weight: 500;
434 | line-height: 1.2;
435 | color: #000;
436 | }
437 |
438 | .learn h3 {
439 | font-size: 24px;
440 | }
441 |
442 | .learn h4 {
443 | font-size: 18px;
444 | }
445 |
446 | .learn h5 {
447 | margin-bottom: 0;
448 | font-size: 14px;
449 | }
450 |
451 | .learn ul {
452 | padding: 0;
453 | margin: 0 0 30px 25px;
454 | }
455 |
456 | .learn li {
457 | line-height: 20px;
458 | }
459 |
460 | .learn p {
461 | font-size: 15px;
462 | font-weight: 300;
463 | line-height: 1.3;
464 | margin-top: 0;
465 | margin-bottom: 0;
466 | }
467 |
468 | .quote {
469 | border: none;
470 | margin: 20px 0 60px 0;
471 | }
472 |
473 | .quote p {
474 | font-style: italic;
475 | }
476 |
477 | .quote p:before {
478 | content: '“';
479 | font-size: 50px;
480 | opacity: .15;
481 | position: absolute;
482 | top: -20px;
483 | left: 3px;
484 | }
485 |
486 | .quote p:after {
487 | content: '”';
488 | font-size: 50px;
489 | opacity: .15;
490 | position: absolute;
491 | bottom: -42px;
492 | right: 3px;
493 | }
494 |
495 | .quote footer {
496 | position: absolute;
497 | bottom: -40px;
498 | right: 0;
499 | }
500 |
501 | .quote footer img {
502 | border-radius: 3px;
503 | }
504 |
505 | .quote footer a {
506 | margin-left: 5px;
507 | vertical-align: middle;
508 | }
509 |
510 | .speech-bubble {
511 | position: relative;
512 | padding: 10px;
513 | background: rgba(0, 0, 0, .04);
514 | border-radius: 5px;
515 | }
516 |
517 | .speech-bubble:after {
518 | content: '';
519 | position: absolute;
520 | top: 100%;
521 | right: 30px;
522 | border: 13px solid transparent;
523 | border-top-color: rgba(0, 0, 0, .04);
524 | }
525 |
526 | .learn-bar > .learn {
527 | position: absolute;
528 | width: 272px;
529 | top: 8px;
530 | left: -300px;
531 | padding: 10px;
532 | border-radius: 5px;
533 | background-color: rgba(255, 255, 255, .6);
534 | -webkit-transition-property: left;
535 | transition-property: left;
536 | -webkit-transition-duration: 500ms;
537 | transition-duration: 500ms;
538 | }
539 |
540 | @media (min-width: 899px) {
541 | .learn-bar {
542 | width: auto;
543 | margin: 0 0 0 300px;
544 | }
545 |
546 | .learn-bar > .learn {
547 | left: 8px;
548 | }
549 |
550 | .learn-bar #todoapp {
551 | width: 550px;
552 | margin: 130px auto 40px auto;
553 | }
554 | }
555 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Testcontainers SpringBoot Quickstart
2 | This quick starter will guide you to configure and use Testcontainers in a SpringBoot project.
3 |
4 | In this guide, we'll look at a sample Spring Boot application that uses Testcontainers for running unit tests with real dependencies.
5 | The initial implementation uses a relational database for storing data.
6 | We'll look at the necessary parts of the code that integrates Testcontainers into the app.
7 | Then we'll switch the relation database for MongoDB, and guide you through using Testcontainers for testing the app against a real instance of MongoDB running in a container.
8 |
9 | After the quick start, you'll have a working Spring Boot app with Testcontainers-based tests, and will be ready to explore integrations with other databases and other technologies via Testcontainers.
10 |
11 | ## 1. Setup Environment
12 | Make sure you have Java 8+ and a [compatible Docker environment](https://www.testcontainers.org/supported_docker_environment/) installed.
13 | If you are going to use Maven build tool then make sure Java 17+ is installed.
14 |
15 | For example:
16 | ```shell
17 | $ java -version
18 | openjdk version "17.0.4" 2022-07-19
19 | OpenJDK Runtime Environment Temurin-17.0.4+8 (build 17.0.4+8)
20 | OpenJDK 64-Bit Server VM Temurin-17.0.4+8 (build 17.0.4+8, mixed mode, sharing)
21 |
22 | $ docker version
23 | ...
24 | Server: Docker Desktop 4.12.0 (85629)
25 | Engine:
26 | Version: 20.10.17
27 | API version: 1.41 (minimum version 1.12)
28 | Go version: go1.17.11
29 | ...
30 | ```
31 | ## 2. Setup Project
32 | * Clone the repository `git clone https://github.com/testcontainers/testcontainers-java-spring-boot-quickstart.git && cd testcontainers-java-spring-boot-quickstart`
33 | * Open the **testcontainers-java-spring-boot-quickstart** project in your favorite IDE.
34 |
35 | ## 3. Run Tests
36 | The sample project uses JUnit tests and Testcontainers to run them against actual databases running in containers.
37 |
38 | Run the command to run the tests.
39 | ```shell
40 | $ ./gradlew test //for Gradle
41 | $ ./mvnw verify //for Maven
42 | ```
43 |
44 | The tests should pass.
45 |
46 | ## 4. Let's explore the code
47 | The **testcontainers-java-spring-boot-quickstart** project is a SpringBoot REST API using Java 17, Spring Data JPA, PostgreSQL, and Gradle/Maven.
48 | We are using [JUnit 5](https://junit.org/junit5/), [Testcontainers](https://testcontainers.org) and [RestAssured](https://rest-assured.io/) for testing.
49 |
50 | ### 4.1. Test Dependencies
51 | Following are the Testcontainers and RestAssured dependencies:
52 |
53 | **build.gradle**
54 | ```groovy
55 | ext {
56 | set('testcontainersVersion', "1.19.0")
57 | }
58 |
59 | dependencies {
60 | ...
61 | ...
62 | testImplementation 'org.springframework.boot:spring-boot-starter-test'
63 | testImplementation 'org.springframework.boot:spring-boot-testcontainers'
64 | testImplementation 'org.testcontainers:junit-jupiter'
65 | testImplementation 'org.testcontainers:postgresql'
66 | testImplementation 'io.rest-assured:rest-assured'
67 | }
68 |
69 | dependencyManagement {
70 | imports {
71 | mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
72 | }
73 | }
74 | ```
75 |
76 | For Maven build the Testcontainers and RestAssured dependencies are configured in **pom.xml** as follows:
77 |
78 | ```xml
79 |
83 |
84 | ...
85 | ...
86 | 1.19.0
87 |
88 |
89 | ...
90 | ...
91 |
92 | org.springframework.boot
93 | spring-boot-starter-test
94 | test
95 |
96 |
97 | org.springframework.boot
98 | spring-boot-testcontainers
99 | test
100 |
101 |
102 | org.testcontainers
103 | junit-jupiter
104 | test
105 |
106 |
107 | org.testcontainers
108 | postgresql
109 | test
110 |
111 |
112 | io.rest-assured
113 | rest-assured
114 | test
115 |
116 |
117 |
118 |
119 |
120 |
121 | org.testcontainers
122 | testcontainers-bom
123 | ${testcontainers.version}
124 | pom
125 | import
126 |
127 |
128 |
129 |
130 | ```
131 |
132 | ### 4.2. How to use Testcontainers?
133 | Testcontainers library can be used to spin up desired services as docker containers and run tests against those services.
134 | We can use our testing library lifecycle hooks to start/stop containers using Testcontainers API.
135 |
136 | ```java
137 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
138 | public class TodoControllerTests {
139 | @LocalServerPort
140 | private Integer port;
141 |
142 | static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
143 |
144 | @BeforeAll
145 | static void beforeAll() {
146 | postgres.start();
147 | }
148 |
149 | @AfterAll
150 | static void afterAll() {
151 | postgres.stop();
152 | }
153 |
154 | @DynamicPropertySource
155 | static void configureProperties(DynamicPropertyRegistry registry) {
156 | registry.add("spring.datasource.url", postgres::getJdbcUrl);
157 | registry.add("spring.datasource.username", postgres::getUsername);
158 | registry.add("spring.datasource.password", postgres::getPassword);
159 | }
160 |
161 | @Autowired
162 | TodoRepository todoRepository;
163 |
164 | @BeforeEach
165 | void setUp() {
166 | todoRepository.deleteAll();
167 | RestAssured.baseURI = "http://localhost:" + port;
168 | }
169 |
170 | @Test
171 | void shouldGetAllTodos() {
172 | List todos = List.of(
173 | new Todo(null, "Todo Item 1", false, 1),
174 | new Todo(null, "Todo Item 2", false, 2)
175 | );
176 | todoRepository.saveAll(todos);
177 |
178 | given()
179 | .contentType(ContentType.JSON)
180 | .when()
181 | .get("/todos")
182 | .then()
183 | .statusCode(200)
184 | .body(".", hasSize(2));
185 | }
186 | }
187 | ```
188 |
189 | Here we have defined a `PostgreSQLContainer` instance, started the container before executing tests and stopped it after executing all the tests using JUnit 5 test lifecycle hook methods.
190 |
191 | > **Note**
192 | >
193 | > If you are using any different Testing library like TestNG or Spock then you can use similar lifecycle callback methods provided by that testing library.
194 |
195 | The Postgresql container port (5432) will be mapped to a random available port on the host.
196 | This helps to avoid port conflicts and allows running tests in parallel.
197 | Then we are using SpringBoot's dynamic property registration support to add/override the `datasource` properties obtained from the Postgres container.
198 |
199 | ```java
200 | @DynamicPropertySource
201 | static void configureProperties(DynamicPropertyRegistry registry) {
202 | registry.add("spring.datasource.url", postgres::getJdbcUrl);
203 | registry.add("spring.datasource.username", postgres::getUsername);
204 | registry.add("spring.datasource.password", postgres::getPassword);
205 | }
206 | ```
207 |
208 | In `shouldGetAllTodos()` test we are saving two Todo entities into the database using `TodoRepository` and testing `GET /todos` API endpoint to fetch todos using RestAssured.
209 |
210 | You can run the tests directly from IDE or using the command `./gradlew test` from the terminal.
211 |
212 | ### 4.3. Using Testcontainers JUnit 5 Extension
213 | Instead of implementing JUnit 5 lifecycle callback methods to start and stop the Postgres container,
214 | we can use [Testcontainers JUnit 5 Extension annotations](https://www.testcontainers.org/quickstart/junit_5_quickstart/) to manage the container lifecycle as follows:
215 |
216 | ```java
217 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
218 | @Testcontainers
219 | public class TodoControllerTests {
220 | @Container
221 | static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
222 |
223 | @DynamicPropertySource
224 | static void configureProperties(DynamicPropertyRegistry registry) {
225 | registry.add("spring.datasource.url", postgres::getJdbcUrl);
226 | registry.add("spring.datasource.username", postgres::getUsername);
227 | registry.add("spring.datasource.password", postgres::getPassword);
228 | }
229 | }
230 | ```
231 |
232 | > **Note**
233 | >
234 | > The Testcontainers JUnit 5 Extension will take care of starting the container before tests and stopping it after tests.
235 | If the container is a `static` field then it will be started once before all the tests and stopped after all the tests.
236 | If it is a non-static field then the container will be started before each test and stopped after each test.
237 | >
238 | > Even if you don't stop the containers explicitly, Testcontainers will take care of removing the containers, using `ryuk` container behind the scenes, once all the tests are done.
239 | > But it is recommended to clean up the containers as soon as possible.
240 |
241 |
242 | ### 4.5. Using magical Testcontainers JDBC URL
243 | Testcontainers provides the [**special jdbc url** support](https://www.testcontainers.org/modules/databases/jdbc/) which automatically spins up the configured database as a container.
244 |
245 | ```java
246 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
247 | @TestPropertySource(properties = {
248 | "spring.datasource.url=jdbc:tc:postgresql:15-alpine:///todos"
249 | })
250 | class ApplicationTests {
251 |
252 | @Test
253 | void contextLoads() {
254 | }
255 | }
256 | ```
257 |
258 | By setting the datasource url to `jdbc:tc:postgresql:15-alpine:///todos` (notice the special `:tc` prefix),
259 | Testcontainers automatically spin up the Postgres database using `postgresql:15-alpine` docker image.
260 |
261 | For more information on Testcontainers JDBC Support refer https://www.testcontainers.org/modules/databases/jdbc/
262 |
263 | ### 4.6. Using Spring Boot 3.1.0 @ServiceConnection
264 | Spring Boot 3.1.0 introduced better support for Testcontainers that simplifies test configuration greatly.
265 | Instead of registering the postgres database connection properties using `@DynamicPropertySource`,
266 | we can use `@ServiceConnection` to register the Database connection as follows:
267 |
268 | ```java
269 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
270 | @Testcontainers
271 | public class TodoControllerTests {
272 | @Container
273 | @ServiceConnection
274 | static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
275 |
276 | @Test
277 | void test() {
278 | ...
279 | }
280 | }
281 | ```
282 |
283 |
284 | ## 5. Local Development using Testcontainers
285 | Spring Boot 3.1.0 introduced support for using Testcontainers at development time.
286 | You can configure your Spring Boot application to automatically start the required docker containers.
287 |
288 | First, create a configuration class to define the required containers as follows:
289 |
290 | ```java
291 | @TestConfiguration(proxyBeanMethods = false)
292 | public class ContainersConfig {
293 |
294 | @Bean
295 | @ServiceConnection
296 | @RestartScope
297 | PostgreSQLContainer> postgreSQLContainer(){
298 | return new PostgreSQLContainer<>("postgres:15-alpine");
299 | }
300 | }
301 | ```
302 |
303 | Next, create a `TestApplication` class under `src/test/java` as follows:
304 |
305 | ```java
306 | public class TestApplication {
307 | public static void main(String[] args) {
308 | SpringApplication
309 | .from(Application::main)
310 | .with(ContainersConfig.class)
311 | .run(args);
312 | }
313 | }
314 | ```
315 |
316 | Now you can either run `TestApplication` from your IDE or use your build tool to start the application as follows:
317 |
318 | ```shell
319 | $ ./gradlew bootTestRun //for Gradle
320 | $ ./mvnw spring-boot:test-run //for Maven
321 | ```
322 |
323 | You can access the application UI at http://localhost:8080 and enter http://localhost:8080/todos as API URL.
324 |
325 | ### 5.1 Using DevTools with Testcontainers at Development Time
326 | During development, you can use Spring Boot DevTools to reload the code changes without having to completely restart the application.
327 | You can also configure your containers to reuse the existing containers by adding `@RestartScope`.
328 |
329 | First, Add `spring-boot-devtools` dependency.
330 |
331 | **Gradle**
332 |
333 | ```groovy
334 | testImplementation 'org.springframework.boot:spring-boot-devtools'
335 | ```
336 |
337 | **Maven**
338 |
339 | ```xml
340 |
341 | org.springframework.boot
342 | spring-boot-devtools
343 | runtime
344 | true
345 |
346 | ```
347 |
348 | Next, add `@RestartScope` annotation on container bean definition as follows:
349 |
350 | ```java
351 | @TestConfiguration(proxyBeanMethods = false)
352 | public class ContainersConfig {
353 |
354 | @Bean
355 | @ServiceConnection
356 | @RestartScope
357 | PostgreSQLContainer> postgreSQLContainer(){
358 | return new PostgreSQLContainer<>("postgres:15-alpine");
359 | }
360 |
361 | }
362 | ```
363 |
364 | Now when devtools reloads your application, the same containers will be reused instead of re-creating them.
365 |
366 | ## 6. Switch to MongoDB
367 | Let's explore how Testcontainers allow using other technologies in your unit tests.
368 | In this chapter, we'll switch the application to use MongoDB as its data store, and will adapt the tests accordingly.
369 |
370 | The application has several tests in the `TodoControllerTests` class for testing various API endpoints.
371 | These high-level tests enable the developers to enhance or refactor the code without breaking the API contracts.
372 |
373 | Let us see how we can switch to MongoDB and use Testcontainers `MongoDBContainer` to ensure API endpoints are not broken and are working as expected.
374 |
375 | ### 6.1. Switch to MongoDB and Spring Data Mongo
376 | Following are the changes to use MongoDB instead of Postgres.
377 |
378 | #### 6.1.1. Update dependencies in `build.gradle`
379 | * Remove `spring-boot-starter-data-jpa`, `flyway-core`, `postgresql`, `org.testcontainers:postgresql` dependencies.
380 | * Add the following dependencies:
381 |
382 | * If you are using Gradle
383 | ```groovy
384 | dependencies {
385 | implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
386 | testImplementation 'org.testcontainers:mongodb'
387 | }
388 | ```
389 |
390 | * If you are using Maven
391 | ```xml
392 |
393 |
394 | org.springframework.boot
395 | spring-boot-starter-data-mongodb
396 |
397 |
398 | org.testcontainers
399 | mongodb
400 | test
401 |
402 |
403 | ```
404 | #### 6.1.2. Delete flyway migrations
405 | Delete flyway migrations under `src/main/resources/db/migration` folder.
406 |
407 | #### 6.1.3. Update `Todo.java`
408 | Update `Todo.java` which is currently a JPA entity to represent a Mongo Document using Spring Data Mongo as follows:
409 |
410 | ```java
411 | import org.springframework.data.annotation.Id;
412 | import org.springframework.data.mongodb.core.mapping.Document;
413 |
414 | @Document(collection = "todos")
415 | public class Todo {
416 | @Id
417 | private String id;
418 | private String title;
419 | private Boolean completed;
420 | private Integer order;
421 | //setter & getters
422 | ...
423 | }
424 | ```
425 | #### 6.1.4. Update `TodoControllerTests.java`
426 | Update `TodoControllerTests.java` to use `MongoDBContainer` instead of `PostgreSQLContainer`.
427 |
428 | ```java
429 |
430 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
431 | @Testcontainers
432 | class TodoControllerTest {
433 |
434 | @Container
435 | @ServiceConnection
436 | static MongoDBContainer mongodb = new MongoDBContainer("mongo:6.0.5");
437 |
438 | // tests
439 | }
440 | ```
441 |
442 | #### 6.1.5. Update `ApplicationTests.java`
443 | Update `ApplicationTests.java` to run MongoDB container using JUnit5 Extension.
444 |
445 | ```java
446 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
447 | @Testcontainers
448 | class ApplicationTests {
449 | @Container
450 | @ServiceConnection
451 | static MongoDBContainer mongodb = new MongoDBContainer("mongo:6.0.5");
452 |
453 | @Test
454 | void contextLoads() {
455 | }
456 |
457 | }
458 | ```
459 |
460 | We have made all the changes to migrate from Postgres to MongoDB. Let us verify it by running tests.
461 |
462 | ```shell
463 | $ ./gradlew test
464 | $ ./mvnw verify
465 | ```
466 |
467 | All tests should PASS.
468 |
469 | ## Conclusion
470 | Testcontainers enable using the real dependency services like SQL databases, NoSQL datastores, message brokers
471 | or any containerized services for that matter. This approach allows you to create reliable test suites improving confidence in your code.
472 |
--------------------------------------------------------------------------------
/src/main/resources/public/js/vendor/underscore.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.6.0
2 | // http://underscorejs.org
3 | // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4 | // Underscore may be freely distributed under the MIT license.
5 |
6 | (function() {
7 |
8 | // Baseline setup
9 | // --------------
10 |
11 | // Establish the root object, `window` in the browser, or `exports` on the server.
12 | var root = this;
13 |
14 | // Save the previous value of the `_` variable.
15 | var previousUnderscore = root._;
16 |
17 | // Establish the object that gets returned to break out of a loop iteration.
18 | var breaker = {};
19 |
20 | // Save bytes in the minified (but not gzipped) version:
21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
22 |
23 | // Create quick reference variables for speed access to core prototypes.
24 | var
25 | push = ArrayProto.push,
26 | slice = ArrayProto.slice,
27 | concat = ArrayProto.concat,
28 | toString = ObjProto.toString,
29 | hasOwnProperty = ObjProto.hasOwnProperty;
30 |
31 | // All **ECMAScript 5** native function implementations that we hope to use
32 | // are declared here.
33 | var
34 | nativeForEach = ArrayProto.forEach,
35 | nativeMap = ArrayProto.map,
36 | nativeReduce = ArrayProto.reduce,
37 | nativeReduceRight = ArrayProto.reduceRight,
38 | nativeFilter = ArrayProto.filter,
39 | nativeEvery = ArrayProto.every,
40 | nativeSome = ArrayProto.some,
41 | nativeIndexOf = ArrayProto.indexOf,
42 | nativeLastIndexOf = ArrayProto.lastIndexOf,
43 | nativeIsArray = Array.isArray,
44 | nativeKeys = Object.keys,
45 | nativeBind = FuncProto.bind;
46 |
47 | // Create a safe reference to the Underscore object for use below.
48 | var _ = function(obj) {
49 | if (obj instanceof _) return obj;
50 | if (!(this instanceof _)) return new _(obj);
51 | this._wrapped = obj;
52 | };
53 |
54 | // Export the Underscore object for **Node.js**, with
55 | // backwards-compatibility for the old `require()` API. If we're in
56 | // the browser, add `_` as a global object via a string identifier,
57 | // for Closure Compiler "advanced" mode.
58 | if (typeof exports !== 'undefined') {
59 | if (typeof module !== 'undefined' && module.exports) {
60 | exports = module.exports = _;
61 | }
62 | exports._ = _;
63 | } else {
64 | root._ = _;
65 | }
66 |
67 | // Current version.
68 | _.VERSION = '1.6.0';
69 |
70 | // Collection Functions
71 | // --------------------
72 |
73 | // The cornerstone, an `each` implementation, aka `forEach`.
74 | // Handles objects with the built-in `forEach`, arrays, and raw objects.
75 | // Delegates to **ECMAScript 5**'s native `forEach` if available.
76 | var each = _.each = _.forEach = function(obj, iterator, context) {
77 | if (obj == null) return obj;
78 | if (nativeForEach && obj.forEach === nativeForEach) {
79 | obj.forEach(iterator, context);
80 | } else if (obj.length === +obj.length) {
81 | for (var i = 0, length = obj.length; i < length; i++) {
82 | if (iterator.call(context, obj[i], i, obj) === breaker) return;
83 | }
84 | } else {
85 | var keys = _.keys(obj);
86 | for (var i = 0, length = keys.length; i < length; i++) {
87 | if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
88 | }
89 | }
90 | return obj;
91 | };
92 |
93 | // Return the results of applying the iterator to each element.
94 | // Delegates to **ECMAScript 5**'s native `map` if available.
95 | _.map = _.collect = function(obj, iterator, context) {
96 | var results = [];
97 | if (obj == null) return results;
98 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
99 | each(obj, function(value, index, list) {
100 | results.push(iterator.call(context, value, index, list));
101 | });
102 | return results;
103 | };
104 |
105 | var reduceError = 'Reduce of empty array with no initial value';
106 |
107 | // **Reduce** builds up a single result from a list of values, aka `inject`,
108 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
109 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
110 | var initial = arguments.length > 2;
111 | if (obj == null) obj = [];
112 | if (nativeReduce && obj.reduce === nativeReduce) {
113 | if (context) iterator = _.bind(iterator, context);
114 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
115 | }
116 | each(obj, function(value, index, list) {
117 | if (!initial) {
118 | memo = value;
119 | initial = true;
120 | } else {
121 | memo = iterator.call(context, memo, value, index, list);
122 | }
123 | });
124 | if (!initial) throw new TypeError(reduceError);
125 | return memo;
126 | };
127 |
128 | // The right-associative version of reduce, also known as `foldr`.
129 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
130 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
131 | var initial = arguments.length > 2;
132 | if (obj == null) obj = [];
133 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
134 | if (context) iterator = _.bind(iterator, context);
135 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
136 | }
137 | var length = obj.length;
138 | if (length !== +length) {
139 | var keys = _.keys(obj);
140 | length = keys.length;
141 | }
142 | each(obj, function(value, index, list) {
143 | index = keys ? keys[--length] : --length;
144 | if (!initial) {
145 | memo = obj[index];
146 | initial = true;
147 | } else {
148 | memo = iterator.call(context, memo, obj[index], index, list);
149 | }
150 | });
151 | if (!initial) throw new TypeError(reduceError);
152 | return memo;
153 | };
154 |
155 | // Return the first value which passes a truth test. Aliased as `detect`.
156 | _.find = _.detect = function(obj, predicate, context) {
157 | var result;
158 | any(obj, function(value, index, list) {
159 | if (predicate.call(context, value, index, list)) {
160 | result = value;
161 | return true;
162 | }
163 | });
164 | return result;
165 | };
166 |
167 | // Return all the elements that pass a truth test.
168 | // Delegates to **ECMAScript 5**'s native `filter` if available.
169 | // Aliased as `select`.
170 | _.filter = _.select = function(obj, predicate, context) {
171 | var results = [];
172 | if (obj == null) return results;
173 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context);
174 | each(obj, function(value, index, list) {
175 | if (predicate.call(context, value, index, list)) results.push(value);
176 | });
177 | return results;
178 | };
179 |
180 | // Return all the elements for which a truth test fails.
181 | _.reject = function(obj, predicate, context) {
182 | return _.filter(obj, function(value, index, list) {
183 | return !predicate.call(context, value, index, list);
184 | }, context);
185 | };
186 |
187 | // Determine whether all of the elements match a truth test.
188 | // Delegates to **ECMAScript 5**'s native `every` if available.
189 | // Aliased as `all`.
190 | _.every = _.all = function(obj, predicate, context) {
191 | predicate || (predicate = _.identity);
192 | var result = true;
193 | if (obj == null) return result;
194 | if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context);
195 | each(obj, function(value, index, list) {
196 | if (!(result = result && predicate.call(context, value, index, list))) return breaker;
197 | });
198 | return !!result;
199 | };
200 |
201 | // Determine if at least one element in the object matches a truth test.
202 | // Delegates to **ECMAScript 5**'s native `some` if available.
203 | // Aliased as `any`.
204 | var any = _.some = _.any = function(obj, predicate, context) {
205 | predicate || (predicate = _.identity);
206 | var result = false;
207 | if (obj == null) return result;
208 | if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context);
209 | each(obj, function(value, index, list) {
210 | if (result || (result = predicate.call(context, value, index, list))) return breaker;
211 | });
212 | return !!result;
213 | };
214 |
215 | // Determine if the array or object contains a given value (using `===`).
216 | // Aliased as `include`.
217 | _.contains = _.include = function(obj, target) {
218 | if (obj == null) return false;
219 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
220 | return any(obj, function(value) {
221 | return value === target;
222 | });
223 | };
224 |
225 | // Invoke a method (with arguments) on every item in a collection.
226 | _.invoke = function(obj, method) {
227 | var args = slice.call(arguments, 2);
228 | var isFunc = _.isFunction(method);
229 | return _.map(obj, function(value) {
230 | return (isFunc ? method : value[method]).apply(value, args);
231 | });
232 | };
233 |
234 | // Convenience version of a common use case of `map`: fetching a property.
235 | _.pluck = function(obj, key) {
236 | return _.map(obj, _.property(key));
237 | };
238 |
239 | // Convenience version of a common use case of `filter`: selecting only objects
240 | // containing specific `key:value` pairs.
241 | _.where = function(obj, attrs) {
242 | return _.filter(obj, _.matches(attrs));
243 | };
244 |
245 | // Convenience version of a common use case of `find`: getting the first object
246 | // containing specific `key:value` pairs.
247 | _.findWhere = function(obj, attrs) {
248 | return _.find(obj, _.matches(attrs));
249 | };
250 |
251 | // Return the maximum element or (element-based computation).
252 | // Can't optimize arrays of integers longer than 65,535 elements.
253 | // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797)
254 | _.max = function(obj, iterator, context) {
255 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
256 | return Math.max.apply(Math, obj);
257 | }
258 | var result = -Infinity, lastComputed = -Infinity;
259 | each(obj, function(value, index, list) {
260 | var computed = iterator ? iterator.call(context, value, index, list) : value;
261 | if (computed > lastComputed) {
262 | result = value;
263 | lastComputed = computed;
264 | }
265 | });
266 | return result;
267 | };
268 |
269 | // Return the minimum element (or element-based computation).
270 | _.min = function(obj, iterator, context) {
271 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
272 | return Math.min.apply(Math, obj);
273 | }
274 | var result = Infinity, lastComputed = Infinity;
275 | each(obj, function(value, index, list) {
276 | var computed = iterator ? iterator.call(context, value, index, list) : value;
277 | if (computed < lastComputed) {
278 | result = value;
279 | lastComputed = computed;
280 | }
281 | });
282 | return result;
283 | };
284 |
285 | // Shuffle an array, using the modern version of the
286 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
287 | _.shuffle = function(obj) {
288 | var rand;
289 | var index = 0;
290 | var shuffled = [];
291 | each(obj, function(value) {
292 | rand = _.random(index++);
293 | shuffled[index - 1] = shuffled[rand];
294 | shuffled[rand] = value;
295 | });
296 | return shuffled;
297 | };
298 |
299 | // Sample **n** random values from a collection.
300 | // If **n** is not specified, returns a single random element.
301 | // The internal `guard` argument allows it to work with `map`.
302 | _.sample = function(obj, n, guard) {
303 | if (n == null || guard) {
304 | if (obj.length !== +obj.length) obj = _.values(obj);
305 | return obj[_.random(obj.length - 1)];
306 | }
307 | return _.shuffle(obj).slice(0, Math.max(0, n));
308 | };
309 |
310 | // An internal function to generate lookup iterators.
311 | var lookupIterator = function(value) {
312 | if (value == null) return _.identity;
313 | if (_.isFunction(value)) return value;
314 | return _.property(value);
315 | };
316 |
317 | // Sort the object's values by a criterion produced by an iterator.
318 | _.sortBy = function(obj, iterator, context) {
319 | iterator = lookupIterator(iterator);
320 | return _.pluck(_.map(obj, function(value, index, list) {
321 | return {
322 | value: value,
323 | index: index,
324 | criteria: iterator.call(context, value, index, list)
325 | };
326 | }).sort(function(left, right) {
327 | var a = left.criteria;
328 | var b = right.criteria;
329 | if (a !== b) {
330 | if (a > b || a === void 0) return 1;
331 | if (a < b || b === void 0) return -1;
332 | }
333 | return left.index - right.index;
334 | }), 'value');
335 | };
336 |
337 | // An internal function used for aggregate "group by" operations.
338 | var group = function(behavior) {
339 | return function(obj, iterator, context) {
340 | var result = {};
341 | iterator = lookupIterator(iterator);
342 | each(obj, function(value, index) {
343 | var key = iterator.call(context, value, index, obj);
344 | behavior(result, key, value);
345 | });
346 | return result;
347 | };
348 | };
349 |
350 | // Groups the object's values by a criterion. Pass either a string attribute
351 | // to group by, or a function that returns the criterion.
352 | _.groupBy = group(function(result, key, value) {
353 | _.has(result, key) ? result[key].push(value) : result[key] = [value];
354 | });
355 |
356 | // Indexes the object's values by a criterion, similar to `groupBy`, but for
357 | // when you know that your index values will be unique.
358 | _.indexBy = group(function(result, key, value) {
359 | result[key] = value;
360 | });
361 |
362 | // Counts instances of an object that group by a certain criterion. Pass
363 | // either a string attribute to count by, or a function that returns the
364 | // criterion.
365 | _.countBy = group(function(result, key) {
366 | _.has(result, key) ? result[key]++ : result[key] = 1;
367 | });
368 |
369 | // Use a comparator function to figure out the smallest index at which
370 | // an object should be inserted so as to maintain order. Uses binary search.
371 | _.sortedIndex = function(array, obj, iterator, context) {
372 | iterator = lookupIterator(iterator);
373 | var value = iterator.call(context, obj);
374 | var low = 0, high = array.length;
375 | while (low < high) {
376 | var mid = (low + high) >>> 1;
377 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
378 | }
379 | return low;
380 | };
381 |
382 | // Safely create a real, live array from anything iterable.
383 | _.toArray = function(obj) {
384 | if (!obj) return [];
385 | if (_.isArray(obj)) return slice.call(obj);
386 | if (obj.length === +obj.length) return _.map(obj, _.identity);
387 | return _.values(obj);
388 | };
389 |
390 | // Return the number of elements in an object.
391 | _.size = function(obj) {
392 | if (obj == null) return 0;
393 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
394 | };
395 |
396 | // Array Functions
397 | // ---------------
398 |
399 | // Get the first element of an array. Passing **n** will return the first N
400 | // values in the array. Aliased as `head` and `take`. The **guard** check
401 | // allows it to work with `_.map`.
402 | _.first = _.head = _.take = function(array, n, guard) {
403 | if (array == null) return void 0;
404 | if ((n == null) || guard) return array[0];
405 | if (n < 0) return [];
406 | return slice.call(array, 0, n);
407 | };
408 |
409 | // Returns everything but the last entry of the array. Especially useful on
410 | // the arguments object. Passing **n** will return all the values in
411 | // the array, excluding the last N. The **guard** check allows it to work with
412 | // `_.map`.
413 | _.initial = function(array, n, guard) {
414 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
415 | };
416 |
417 | // Get the last element of an array. Passing **n** will return the last N
418 | // values in the array. The **guard** check allows it to work with `_.map`.
419 | _.last = function(array, n, guard) {
420 | if (array == null) return void 0;
421 | if ((n == null) || guard) return array[array.length - 1];
422 | return slice.call(array, Math.max(array.length - n, 0));
423 | };
424 |
425 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
426 | // Especially useful on the arguments object. Passing an **n** will return
427 | // the rest N values in the array. The **guard**
428 | // check allows it to work with `_.map`.
429 | _.rest = _.tail = _.drop = function(array, n, guard) {
430 | return slice.call(array, (n == null) || guard ? 1 : n);
431 | };
432 |
433 | // Trim out all falsy values from an array.
434 | _.compact = function(array) {
435 | return _.filter(array, _.identity);
436 | };
437 |
438 | // Internal implementation of a recursive `flatten` function.
439 | var flatten = function(input, shallow, output) {
440 | if (shallow && _.every(input, _.isArray)) {
441 | return concat.apply(output, input);
442 | }
443 | each(input, function(value) {
444 | if (_.isArray(value) || _.isArguments(value)) {
445 | shallow ? push.apply(output, value) : flatten(value, shallow, output);
446 | } else {
447 | output.push(value);
448 | }
449 | });
450 | return output;
451 | };
452 |
453 | // Flatten out an array, either recursively (by default), or just one level.
454 | _.flatten = function(array, shallow) {
455 | return flatten(array, shallow, []);
456 | };
457 |
458 | // Return a version of the array that does not contain the specified value(s).
459 | _.without = function(array) {
460 | return _.difference(array, slice.call(arguments, 1));
461 | };
462 |
463 | // Split an array into two arrays: one whose elements all satisfy the given
464 | // predicate, and one whose elements all do not satisfy the predicate.
465 | _.partition = function(array, predicate) {
466 | var pass = [], fail = [];
467 | each(array, function(elem) {
468 | (predicate(elem) ? pass : fail).push(elem);
469 | });
470 | return [pass, fail];
471 | };
472 |
473 | // Produce a duplicate-free version of the array. If the array has already
474 | // been sorted, you have the option of using a faster algorithm.
475 | // Aliased as `unique`.
476 | _.uniq = _.unique = function(array, isSorted, iterator, context) {
477 | if (_.isFunction(isSorted)) {
478 | context = iterator;
479 | iterator = isSorted;
480 | isSorted = false;
481 | }
482 | var initial = iterator ? _.map(array, iterator, context) : array;
483 | var results = [];
484 | var seen = [];
485 | each(initial, function(value, index) {
486 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
487 | seen.push(value);
488 | results.push(array[index]);
489 | }
490 | });
491 | return results;
492 | };
493 |
494 | // Produce an array that contains the union: each distinct element from all of
495 | // the passed-in arrays.
496 | _.union = function() {
497 | return _.uniq(_.flatten(arguments, true));
498 | };
499 |
500 | // Produce an array that contains every item shared between all the
501 | // passed-in arrays.
502 | _.intersection = function(array) {
503 | var rest = slice.call(arguments, 1);
504 | return _.filter(_.uniq(array), function(item) {
505 | return _.every(rest, function(other) {
506 | return _.contains(other, item);
507 | });
508 | });
509 | };
510 |
511 | // Take the difference between one array and a number of other arrays.
512 | // Only the elements present in just the first array will remain.
513 | _.difference = function(array) {
514 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
515 | return _.filter(array, function(value){ return !_.contains(rest, value); });
516 | };
517 |
518 | // Zip together multiple lists into a single array -- elements that share
519 | // an index go together.
520 | _.zip = function() {
521 | var length = _.max(_.pluck(arguments, 'length').concat(0));
522 | var results = new Array(length);
523 | for (var i = 0; i < length; i++) {
524 | results[i] = _.pluck(arguments, '' + i);
525 | }
526 | return results;
527 | };
528 |
529 | // Converts lists into objects. Pass either a single array of `[key, value]`
530 | // pairs, or two parallel arrays of the same length -- one of keys, and one of
531 | // the corresponding values.
532 | _.object = function(list, values) {
533 | if (list == null) return {};
534 | var result = {};
535 | for (var i = 0, length = list.length; i < length; i++) {
536 | if (values) {
537 | result[list[i]] = values[i];
538 | } else {
539 | result[list[i][0]] = list[i][1];
540 | }
541 | }
542 | return result;
543 | };
544 |
545 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
546 | // we need this function. Return the position of the first occurrence of an
547 | // item in an array, or -1 if the item is not included in the array.
548 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
549 | // If the array is large and already in sort order, pass `true`
550 | // for **isSorted** to use binary search.
551 | _.indexOf = function(array, item, isSorted) {
552 | if (array == null) return -1;
553 | var i = 0, length = array.length;
554 | if (isSorted) {
555 | if (typeof isSorted == 'number') {
556 | i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted);
557 | } else {
558 | i = _.sortedIndex(array, item);
559 | return array[i] === item ? i : -1;
560 | }
561 | }
562 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
563 | for (; i < length; i++) if (array[i] === item) return i;
564 | return -1;
565 | };
566 |
567 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
568 | _.lastIndexOf = function(array, item, from) {
569 | if (array == null) return -1;
570 | var hasIndex = from != null;
571 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
572 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
573 | }
574 | var i = (hasIndex ? from : array.length);
575 | while (i--) if (array[i] === item) return i;
576 | return -1;
577 | };
578 |
579 | // Generate an integer Array containing an arithmetic progression. A port of
580 | // the native Python `range()` function. See
581 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
582 | _.range = function(start, stop, step) {
583 | if (arguments.length <= 1) {
584 | stop = start || 0;
585 | start = 0;
586 | }
587 | step = arguments[2] || 1;
588 |
589 | var length = Math.max(Math.ceil((stop - start) / step), 0);
590 | var idx = 0;
591 | var range = new Array(length);
592 |
593 | while(idx < length) {
594 | range[idx++] = start;
595 | start += step;
596 | }
597 |
598 | return range;
599 | };
600 |
601 | // Function (ahem) Functions
602 | // ------------------
603 |
604 | // Reusable constructor function for prototype setting.
605 | var ctor = function(){};
606 |
607 | // Create a function bound to a given object (assigning `this`, and arguments,
608 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
609 | // available.
610 | _.bind = function(func, context) {
611 | var args, bound;
612 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
613 | if (!_.isFunction(func)) throw new TypeError;
614 | args = slice.call(arguments, 2);
615 | return bound = function() {
616 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
617 | ctor.prototype = func.prototype;
618 | var self = new ctor;
619 | ctor.prototype = null;
620 | var result = func.apply(self, args.concat(slice.call(arguments)));
621 | if (Object(result) === result) return result;
622 | return self;
623 | };
624 | };
625 |
626 | // Partially apply a function by creating a version that has had some of its
627 | // arguments pre-filled, without changing its dynamic `this` context. _ acts
628 | // as a placeholder, allowing any combination of arguments to be pre-filled.
629 | _.partial = function(func) {
630 | var boundArgs = slice.call(arguments, 1);
631 | return function() {
632 | var position = 0;
633 | var args = boundArgs.slice();
634 | for (var i = 0, length = args.length; i < length; i++) {
635 | if (args[i] === _) args[i] = arguments[position++];
636 | }
637 | while (position < arguments.length) args.push(arguments[position++]);
638 | return func.apply(this, args);
639 | };
640 | };
641 |
642 | // Bind a number of an object's methods to that object. Remaining arguments
643 | // are the method names to be bound. Useful for ensuring that all callbacks
644 | // defined on an object belong to it.
645 | _.bindAll = function(obj) {
646 | var funcs = slice.call(arguments, 1);
647 | if (funcs.length === 0) throw new Error('bindAll must be passed function names');
648 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
649 | return obj;
650 | };
651 |
652 | // Memoize an expensive function by storing its results.
653 | _.memoize = function(func, hasher) {
654 | var memo = {};
655 | hasher || (hasher = _.identity);
656 | return function() {
657 | var key = hasher.apply(this, arguments);
658 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
659 | };
660 | };
661 |
662 | // Delays a function for the given number of milliseconds, and then calls
663 | // it with the arguments supplied.
664 | _.delay = function(func, wait) {
665 | var args = slice.call(arguments, 2);
666 | return setTimeout(function(){ return func.apply(null, args); }, wait);
667 | };
668 |
669 | // Defers a function, scheduling it to run after the current call stack has
670 | // cleared.
671 | _.defer = function(func) {
672 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
673 | };
674 |
675 | // Returns a function, that, when invoked, will only be triggered at most once
676 | // during a given window of time. Normally, the throttled function will run
677 | // as much as it can, without ever going more than once per `wait` duration;
678 | // but if you'd like to disable the execution on the leading edge, pass
679 | // `{leading: false}`. To disable execution on the trailing edge, ditto.
680 | _.throttle = function(func, wait, options) {
681 | var context, args, result;
682 | var timeout = null;
683 | var previous = 0;
684 | options || (options = {});
685 | var later = function() {
686 | previous = options.leading === false ? 0 : _.now();
687 | timeout = null;
688 | result = func.apply(context, args);
689 | context = args = null;
690 | };
691 | return function() {
692 | var now = _.now();
693 | if (!previous && options.leading === false) previous = now;
694 | var remaining = wait - (now - previous);
695 | context = this;
696 | args = arguments;
697 | if (remaining <= 0) {
698 | clearTimeout(timeout);
699 | timeout = null;
700 | previous = now;
701 | result = func.apply(context, args);
702 | context = args = null;
703 | } else if (!timeout && options.trailing !== false) {
704 | timeout = setTimeout(later, remaining);
705 | }
706 | return result;
707 | };
708 | };
709 |
710 | // Returns a function, that, as long as it continues to be invoked, will not
711 | // be triggered. The function will be called after it stops being called for
712 | // N milliseconds. If `immediate` is passed, trigger the function on the
713 | // leading edge, instead of the trailing.
714 | _.debounce = function(func, wait, immediate) {
715 | var timeout, args, context, timestamp, result;
716 |
717 | var later = function() {
718 | var last = _.now() - timestamp;
719 | if (last < wait) {
720 | timeout = setTimeout(later, wait - last);
721 | } else {
722 | timeout = null;
723 | if (!immediate) {
724 | result = func.apply(context, args);
725 | context = args = null;
726 | }
727 | }
728 | };
729 |
730 | return function() {
731 | context = this;
732 | args = arguments;
733 | timestamp = _.now();
734 | var callNow = immediate && !timeout;
735 | if (!timeout) {
736 | timeout = setTimeout(later, wait);
737 | }
738 | if (callNow) {
739 | result = func.apply(context, args);
740 | context = args = null;
741 | }
742 |
743 | return result;
744 | };
745 | };
746 |
747 | // Returns a function that will be executed at most one time, no matter how
748 | // often you call it. Useful for lazy initialization.
749 | _.once = function(func) {
750 | var ran = false, memo;
751 | return function() {
752 | if (ran) return memo;
753 | ran = true;
754 | memo = func.apply(this, arguments);
755 | func = null;
756 | return memo;
757 | };
758 | };
759 |
760 | // Returns the first function passed as an argument to the second,
761 | // allowing you to adjust arguments, run code before and after, and
762 | // conditionally execute the original function.
763 | _.wrap = function(func, wrapper) {
764 | return _.partial(wrapper, func);
765 | };
766 |
767 | // Returns a function that is the composition of a list of functions, each
768 | // consuming the return value of the function that follows.
769 | _.compose = function() {
770 | var funcs = arguments;
771 | return function() {
772 | var args = arguments;
773 | for (var i = funcs.length - 1; i >= 0; i--) {
774 | args = [funcs[i].apply(this, args)];
775 | }
776 | return args[0];
777 | };
778 | };
779 |
780 | // Returns a function that will only be executed after being called N times.
781 | _.after = function(times, func) {
782 | return function() {
783 | if (--times < 1) {
784 | return func.apply(this, arguments);
785 | }
786 | };
787 | };
788 |
789 | // Object Functions
790 | // ----------------
791 |
792 | // Retrieve the names of an object's properties.
793 | // Delegates to **ECMAScript 5**'s native `Object.keys`
794 | _.keys = function(obj) {
795 | if (!_.isObject(obj)) return [];
796 | if (nativeKeys) return nativeKeys(obj);
797 | var keys = [];
798 | for (var key in obj) if (_.has(obj, key)) keys.push(key);
799 | return keys;
800 | };
801 |
802 | // Retrieve the values of an object's properties.
803 | _.values = function(obj) {
804 | var keys = _.keys(obj);
805 | var length = keys.length;
806 | var values = new Array(length);
807 | for (var i = 0; i < length; i++) {
808 | values[i] = obj[keys[i]];
809 | }
810 | return values;
811 | };
812 |
813 | // Convert an object into a list of `[key, value]` pairs.
814 | _.pairs = function(obj) {
815 | var keys = _.keys(obj);
816 | var length = keys.length;
817 | var pairs = new Array(length);
818 | for (var i = 0; i < length; i++) {
819 | pairs[i] = [keys[i], obj[keys[i]]];
820 | }
821 | return pairs;
822 | };
823 |
824 | // Invert the keys and values of an object. The values must be serializable.
825 | _.invert = function(obj) {
826 | var result = {};
827 | var keys = _.keys(obj);
828 | for (var i = 0, length = keys.length; i < length; i++) {
829 | result[obj[keys[i]]] = keys[i];
830 | }
831 | return result;
832 | };
833 |
834 | // Return a sorted list of the function names available on the object.
835 | // Aliased as `methods`
836 | _.functions = _.methods = function(obj) {
837 | var names = [];
838 | for (var key in obj) {
839 | if (_.isFunction(obj[key])) names.push(key);
840 | }
841 | return names.sort();
842 | };
843 |
844 | // Extend a given object with all the properties in passed-in object(s).
845 | _.extend = function(obj) {
846 | each(slice.call(arguments, 1), function(source) {
847 | if (source) {
848 | for (var prop in source) {
849 | obj[prop] = source[prop];
850 | }
851 | }
852 | });
853 | return obj;
854 | };
855 |
856 | // Return a copy of the object only containing the whitelisted properties.
857 | _.pick = function(obj) {
858 | var copy = {};
859 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
860 | each(keys, function(key) {
861 | if (key in obj) copy[key] = obj[key];
862 | });
863 | return copy;
864 | };
865 |
866 | // Return a copy of the object without the blacklisted properties.
867 | _.omit = function(obj) {
868 | var copy = {};
869 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
870 | for (var key in obj) {
871 | if (!_.contains(keys, key)) copy[key] = obj[key];
872 | }
873 | return copy;
874 | };
875 |
876 | // Fill in a given object with default properties.
877 | _.defaults = function(obj) {
878 | each(slice.call(arguments, 1), function(source) {
879 | if (source) {
880 | for (var prop in source) {
881 | if (obj[prop] === void 0) obj[prop] = source[prop];
882 | }
883 | }
884 | });
885 | return obj;
886 | };
887 |
888 | // Create a (shallow-cloned) duplicate of an object.
889 | _.clone = function(obj) {
890 | if (!_.isObject(obj)) return obj;
891 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
892 | };
893 |
894 | // Invokes interceptor with the obj, and then returns obj.
895 | // The primary purpose of this method is to "tap into" a method chain, in
896 | // order to perform operations on intermediate results within the chain.
897 | _.tap = function(obj, interceptor) {
898 | interceptor(obj);
899 | return obj;
900 | };
901 |
902 | // Internal recursive comparison function for `isEqual`.
903 | var eq = function(a, b, aStack, bStack) {
904 | // Identical objects are equal. `0 === -0`, but they aren't identical.
905 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
906 | if (a === b) return a !== 0 || 1 / a == 1 / b;
907 | // A strict comparison is necessary because `null == undefined`.
908 | if (a == null || b == null) return a === b;
909 | // Unwrap any wrapped objects.
910 | if (a instanceof _) a = a._wrapped;
911 | if (b instanceof _) b = b._wrapped;
912 | // Compare `[[Class]]` names.
913 | var className = toString.call(a);
914 | if (className != toString.call(b)) return false;
915 | switch (className) {
916 | // Strings, numbers, dates, and booleans are compared by value.
917 | case '[object String]':
918 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
919 | // equivalent to `new String("5")`.
920 | return a == String(b);
921 | case '[object Number]':
922 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
923 | // other numeric values.
924 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
925 | case '[object Date]':
926 | case '[object Boolean]':
927 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their
928 | // millisecond representations. Note that invalid dates with millisecond representations
929 | // of `NaN` are not equivalent.
930 | return +a == +b;
931 | // RegExps are compared by their source patterns and flags.
932 | case '[object RegExp]':
933 | return a.source == b.source &&
934 | a.global == b.global &&
935 | a.multiline == b.multiline &&
936 | a.ignoreCase == b.ignoreCase;
937 | }
938 | if (typeof a != 'object' || typeof b != 'object') return false;
939 | // Assume equality for cyclic structures. The algorithm for detecting cyclic
940 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
941 | var length = aStack.length;
942 | while (length--) {
943 | // Linear search. Performance is inversely proportional to the number of
944 | // unique nested structures.
945 | if (aStack[length] == a) return bStack[length] == b;
946 | }
947 | // Objects with different constructors are not equivalent, but `Object`s
948 | // from different frames are.
949 | var aCtor = a.constructor, bCtor = b.constructor;
950 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
951 | _.isFunction(bCtor) && (bCtor instanceof bCtor))
952 | && ('constructor' in a && 'constructor' in b)) {
953 | return false;
954 | }
955 | // Add the first object to the stack of traversed objects.
956 | aStack.push(a);
957 | bStack.push(b);
958 | var size = 0, result = true;
959 | // Recursively compare objects and arrays.
960 | if (className == '[object Array]') {
961 | // Compare array lengths to determine if a deep comparison is necessary.
962 | size = a.length;
963 | result = size == b.length;
964 | if (result) {
965 | // Deep compare the contents, ignoring non-numeric properties.
966 | while (size--) {
967 | if (!(result = eq(a[size], b[size], aStack, bStack))) break;
968 | }
969 | }
970 | } else {
971 | // Deep compare objects.
972 | for (var key in a) {
973 | if (_.has(a, key)) {
974 | // Count the expected number of properties.
975 | size++;
976 | // Deep compare each member.
977 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
978 | }
979 | }
980 | // Ensure that both objects contain the same number of properties.
981 | if (result) {
982 | for (key in b) {
983 | if (_.has(b, key) && !(size--)) break;
984 | }
985 | result = !size;
986 | }
987 | }
988 | // Remove the first object from the stack of traversed objects.
989 | aStack.pop();
990 | bStack.pop();
991 | return result;
992 | };
993 |
994 | // Perform a deep comparison to check if two objects are equal.
995 | _.isEqual = function(a, b) {
996 | return eq(a, b, [], []);
997 | };
998 |
999 | // Is a given array, string, or object empty?
1000 | // An "empty" object has no enumerable own-properties.
1001 | _.isEmpty = function(obj) {
1002 | if (obj == null) return true;
1003 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
1004 | for (var key in obj) if (_.has(obj, key)) return false;
1005 | return true;
1006 | };
1007 |
1008 | // Is a given value a DOM element?
1009 | _.isElement = function(obj) {
1010 | return !!(obj && obj.nodeType === 1);
1011 | };
1012 |
1013 | // Is a given value an array?
1014 | // Delegates to ECMA5's native Array.isArray
1015 | _.isArray = nativeIsArray || function(obj) {
1016 | return toString.call(obj) == '[object Array]';
1017 | };
1018 |
1019 | // Is a given variable an object?
1020 | _.isObject = function(obj) {
1021 | return obj === Object(obj);
1022 | };
1023 |
1024 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
1025 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
1026 | _['is' + name] = function(obj) {
1027 | return toString.call(obj) == '[object ' + name + ']';
1028 | };
1029 | });
1030 |
1031 | // Define a fallback version of the method in browsers (ahem, IE), where
1032 | // there isn't any inspectable "Arguments" type.
1033 | if (!_.isArguments(arguments)) {
1034 | _.isArguments = function(obj) {
1035 | return !!(obj && _.has(obj, 'callee'));
1036 | };
1037 | }
1038 |
1039 | // Optimize `isFunction` if appropriate.
1040 | if (typeof (/./) !== 'function') {
1041 | _.isFunction = function(obj) {
1042 | return typeof obj === 'function';
1043 | };
1044 | }
1045 |
1046 | // Is a given object a finite number?
1047 | _.isFinite = function(obj) {
1048 | return isFinite(obj) && !isNaN(parseFloat(obj));
1049 | };
1050 |
1051 | // Is the given value `NaN`? (NaN is the only number which does not equal itself).
1052 | _.isNaN = function(obj) {
1053 | return _.isNumber(obj) && obj != +obj;
1054 | };
1055 |
1056 | // Is a given value a boolean?
1057 | _.isBoolean = function(obj) {
1058 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
1059 | };
1060 |
1061 | // Is a given value equal to null?
1062 | _.isNull = function(obj) {
1063 | return obj === null;
1064 | };
1065 |
1066 | // Is a given variable undefined?
1067 | _.isUndefined = function(obj) {
1068 | return obj === void 0;
1069 | };
1070 |
1071 | // Shortcut function for checking if an object has a given property directly
1072 | // on itself (in other words, not on a prototype).
1073 | _.has = function(obj, key) {
1074 | return hasOwnProperty.call(obj, key);
1075 | };
1076 |
1077 | // Utility Functions
1078 | // -----------------
1079 |
1080 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
1081 | // previous owner. Returns a reference to the Underscore object.
1082 | _.noConflict = function() {
1083 | root._ = previousUnderscore;
1084 | return this;
1085 | };
1086 |
1087 | // Keep the identity function around for default iterators.
1088 | _.identity = function(value) {
1089 | return value;
1090 | };
1091 |
1092 | _.constant = function(value) {
1093 | return function () {
1094 | return value;
1095 | };
1096 | };
1097 |
1098 | _.property = function(key) {
1099 | return function(obj) {
1100 | return obj[key];
1101 | };
1102 | };
1103 |
1104 | // Returns a predicate for checking whether an object has a given set of `key:value` pairs.
1105 | _.matches = function(attrs) {
1106 | return function(obj) {
1107 | if (obj === attrs) return true; //avoid comparing an object to itself.
1108 | for (var key in attrs) {
1109 | if (attrs[key] !== obj[key])
1110 | return false;
1111 | }
1112 | return true;
1113 | }
1114 | };
1115 |
1116 | // Run a function **n** times.
1117 | _.times = function(n, iterator, context) {
1118 | var accum = Array(Math.max(0, n));
1119 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
1120 | return accum;
1121 | };
1122 |
1123 | // Return a random integer between min and max (inclusive).
1124 | _.random = function(min, max) {
1125 | if (max == null) {
1126 | max = min;
1127 | min = 0;
1128 | }
1129 | return min + Math.floor(Math.random() * (max - min + 1));
1130 | };
1131 |
1132 | // A (possibly faster) way to get the current timestamp as an integer.
1133 | _.now = Date.now || function() { return new Date().getTime(); };
1134 |
1135 | // List of HTML entities for escaping.
1136 | var entityMap = {
1137 | escape: {
1138 | '&': '&',
1139 | '<': '<',
1140 | '>': '>',
1141 | '"': '"',
1142 | "'": '''
1143 | }
1144 | };
1145 | entityMap.unescape = _.invert(entityMap.escape);
1146 |
1147 | // Regexes containing the keys and values listed immediately above.
1148 | var entityRegexes = {
1149 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
1150 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
1151 | };
1152 |
1153 | // Functions for escaping and unescaping strings to/from HTML interpolation.
1154 | _.each(['escape', 'unescape'], function(method) {
1155 | _[method] = function(string) {
1156 | if (string == null) return '';
1157 | return ('' + string).replace(entityRegexes[method], function(match) {
1158 | return entityMap[method][match];
1159 | });
1160 | };
1161 | });
1162 |
1163 | // If the value of the named `property` is a function then invoke it with the
1164 | // `object` as context; otherwise, return it.
1165 | _.result = function(object, property) {
1166 | if (object == null) return void 0;
1167 | var value = object[property];
1168 | return _.isFunction(value) ? value.call(object) : value;
1169 | };
1170 |
1171 | // Add your own custom functions to the Underscore object.
1172 | _.mixin = function(obj) {
1173 | each(_.functions(obj), function(name) {
1174 | var func = _[name] = obj[name];
1175 | _.prototype[name] = function() {
1176 | var args = [this._wrapped];
1177 | push.apply(args, arguments);
1178 | return result.call(this, func.apply(_, args));
1179 | };
1180 | });
1181 | };
1182 |
1183 | // Generate a unique integer id (unique within the entire client session).
1184 | // Useful for temporary DOM ids.
1185 | var idCounter = 0;
1186 | _.uniqueId = function(prefix) {
1187 | var id = ++idCounter + '';
1188 | return prefix ? prefix + id : id;
1189 | };
1190 |
1191 | // By default, Underscore uses ERB-style template delimiters, change the
1192 | // following template settings to use alternative delimiters.
1193 | _.templateSettings = {
1194 | evaluate : /<%([\s\S]+?)%>/g,
1195 | interpolate : /<%=([\s\S]+?)%>/g,
1196 | escape : /<%-([\s\S]+?)%>/g
1197 | };
1198 |
1199 | // When customizing `templateSettings`, if you don't want to define an
1200 | // interpolation, evaluation or escaping regex, we need one that is
1201 | // guaranteed not to match.
1202 | var noMatch = /(.)^/;
1203 |
1204 | // Certain characters need to be escaped so that they can be put into a
1205 | // string literal.
1206 | var escapes = {
1207 | "'": "'",
1208 | '\\': '\\',
1209 | '\r': 'r',
1210 | '\n': 'n',
1211 | '\t': 't',
1212 | '\u2028': 'u2028',
1213 | '\u2029': 'u2029'
1214 | };
1215 |
1216 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
1217 |
1218 | // JavaScript micro-templating, similar to John Resig's implementation.
1219 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
1220 | // and correctly escapes quotes within interpolated code.
1221 | _.template = function(text, data, settings) {
1222 | var render;
1223 | settings = _.defaults({}, settings, _.templateSettings);
1224 |
1225 | // Combine delimiters into one regular expression via alternation.
1226 | var matcher = new RegExp([
1227 | (settings.escape || noMatch).source,
1228 | (settings.interpolate || noMatch).source,
1229 | (settings.evaluate || noMatch).source
1230 | ].join('|') + '|$', 'g');
1231 |
1232 | // Compile the template source, escaping string literals appropriately.
1233 | var index = 0;
1234 | var source = "__p+='";
1235 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
1236 | source += text.slice(index, offset)
1237 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
1238 |
1239 | if (escape) {
1240 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
1241 | }
1242 | if (interpolate) {
1243 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
1244 | }
1245 | if (evaluate) {
1246 | source += "';\n" + evaluate + "\n__p+='";
1247 | }
1248 | index = offset + match.length;
1249 | return match;
1250 | });
1251 | source += "';\n";
1252 |
1253 | // If a variable is not specified, place data values in local scope.
1254 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
1255 |
1256 | source = "var __t,__p='',__j=Array.prototype.join," +
1257 | "print=function(){__p+=__j.call(arguments,'');};\n" +
1258 | source + "return __p;\n";
1259 |
1260 | try {
1261 | render = new Function(settings.variable || 'obj', '_', source);
1262 | } catch (e) {
1263 | e.source = source;
1264 | throw e;
1265 | }
1266 |
1267 | if (data) return render(data, _);
1268 | var template = function(data) {
1269 | return render.call(this, data, _);
1270 | };
1271 |
1272 | // Provide the compiled function source as a convenience for precompilation.
1273 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
1274 |
1275 | return template;
1276 | };
1277 |
1278 | // Add a "chain" function, which will delegate to the wrapper.
1279 | _.chain = function(obj) {
1280 | return _(obj).chain();
1281 | };
1282 |
1283 | // OOP
1284 | // ---------------
1285 | // If Underscore is called as a function, it returns a wrapped object that
1286 | // can be used OO-style. This wrapper holds altered versions of all the
1287 | // underscore functions. Wrapped objects may be chained.
1288 |
1289 | // Helper function to continue chaining intermediate results.
1290 | var result = function(obj) {
1291 | return this._chain ? _(obj).chain() : obj;
1292 | };
1293 |
1294 | // Add all of the Underscore functions to the wrapper object.
1295 | _.mixin(_);
1296 |
1297 | // Add all mutator Array functions to the wrapper.
1298 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1299 | var method = ArrayProto[name];
1300 | _.prototype[name] = function() {
1301 | var obj = this._wrapped;
1302 | method.apply(obj, arguments);
1303 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
1304 | return result.call(this, obj);
1305 | };
1306 | });
1307 |
1308 | // Add all accessor Array functions to the wrapper.
1309 | each(['concat', 'join', 'slice'], function(name) {
1310 | var method = ArrayProto[name];
1311 | _.prototype[name] = function() {
1312 | return result.call(this, method.apply(this._wrapped, arguments));
1313 | };
1314 | });
1315 |
1316 | _.extend(_.prototype, {
1317 |
1318 | // Start chaining a wrapped Underscore object.
1319 | chain: function() {
1320 | this._chain = true;
1321 | return this;
1322 | },
1323 |
1324 | // Extracts the result from a wrapped and chained object.
1325 | value: function() {
1326 | return this._wrapped;
1327 | }
1328 |
1329 | });
1330 |
1331 | // AMD registration happens at the end for compatibility with AMD loaders
1332 | // that may not enforce next-turn semantics on modules. Even though general
1333 | // practice for AMD registration is to be anonymous, underscore registers
1334 | // as a named module because, like jQuery, it is a base library that is
1335 | // popular enough to be bundled in a third party lib, but not be part of
1336 | // an AMD load request. Those cases could generate an error when an
1337 | // anonymous define() is called outside of a loader request.
1338 | if (typeof define === 'function' && define.amd) {
1339 | define('underscore', [], function() {
1340 | return _;
1341 | });
1342 | }
1343 | }).call(this);
1344 |
--------------------------------------------------------------------------------