diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000000..1e1862b0e82 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,25 @@ +name: MarkBind Action + +on: + push: + branches: + - master + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - name: Install Graphviz + run: sudo apt-get install graphviz + - name: Install Java + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build & Deploy MarkBind site + uses: MarkBind/markbind-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + rootDirectory: './docs' + baseUrl: '/tp' # replace with your repo name + version: '^5.1.0' diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..eab4c7db6a5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +docs/_markbind/logs/ diff --git a/README.md b/README.md index 13f5c77403f..b7381dae973 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,52 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) - -![Ui](docs/images/Ui.png) - -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2103T-W09-4/tp/actions) + +# JABPro +![ui image](https://raw.githubusercontent.com/ScarletBlanks/tp/master/docs/images/Ui.png) + +# Table of Contents + +## Features + +Search Job Applicants + +Add Contact Information + +Add Optional Comments + +List All Candidate Information + +Delete an Entry + +Set Applicant Status + +Add GitHub/LinkedIn Username for a Candidate + +Open User LinkedIn or Github Account + +### Applicant Management + +JABPro allows you to efficiently search and manage job applicants based on various criteria. You can search for applicants by name, email, phone number, and status. Additionally, you can combine multiple search categories to narrow down your results. + +### Command Types + +Search Job Applicants : Use Search categories to narrow down results + +Add Contact Information : You can add contact information for job applicants, including their name, email, and phone number. This feature helps you keep track of applicant details. + +Add Optional Comments : allows you to add optional comments or notes to a candidate's profile. These comments can be tagged with the candidate's details and provide additional information or insights about the applicant. + +List All Candidate Information : You can retrieve a comprehensive list of all job applicants and their relevant information, such as name, email, and phone number. This feature makes it easy to review and manage candidate profiles. + +Delete an Entry : If needed, you can remove a candidate's profile from the system. This feature helps you maintain an up-to-date database of applicants + +Set Applicant Status : You can set the application status for each candidate, including options like "Preliminary," "Interviewed," "Rejected," and "Offered." This feature helps you track the progress of each applicant. + +Add Github/LinkedIn Username for a Candidate : JASMS allows you to add the candidate's social profile usernames (Github or LinkedIn) to their existing contact details. This feature helps you link to their online profiles for further evaluation. + +Open User LinkedIn or Github Account : You can quickly access a candidate's LinkedIn or Github profile directly from JABPro. This feature provides easy access to additional information about the applicant. + +Visit our website for more details! +(https://github.com/AY2324S1-CS2103T-W09-4/tp) + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + diff --git a/build.gradle b/build.gradle index a2951cc709e..7de5c0c99b1 100644 --- a/build.gradle +++ b/build.gradle @@ -65,8 +65,12 @@ dependencies { testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion } +run { + enableAssertions = true +} + shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'jabpro.jar' } defaultTasks 'clean', 'test' diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000000..1748e487fbd --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,23 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +_markbind/logs/ + +# Dependency directories +node_modules/ + +# Production build files (change if you output the build to a different directory) +_site/ + +# Env +.env +.env.local + +# IDE configs +.vscode/ +.idea/* +*.iml diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..d15a4be6f97 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,59 +1,70 @@ --- -layout: page -title: About Us + layout: default.md + title: "About Us" --- +# About Us + We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +You can reach us at the email `e0958427[at]comp.nus.edu.sg` ## Project team -### John Doe +### Sean Koh Hak Guan + + - +[[github](https://github.com/sk2001git)] +[[portfolio](team/sk2001git.md)] -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +* Role: Developer +* Responsibilities: Deliverables & Deadlines, Integration +* In charge of: Storage -* Role: Project Advisor -### Jane Doe +### Ariella Thirza Callista - -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + + +[[github](http://github.com/AriellaCallista)] +[[portfolio](team/ariellacallista.md)] * Role: Team Lead -* Responsibilities: UI +* Responsibilities: Documentation +* In charge of: Model, UI -### Johnny Doe +### Misra Aditya - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/MadLamprey)] +[[portfolio](team/madlamprey.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Testing +* In charge of: Test cases and coverage + -### Jean Doe +### Catherine Liang - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/ketweeen)] +[[portfolio](team/ketweeen.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Scheduling and tracking +* In charge of: Commons -### James Doe +### Goh Peng Tat - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/ScarletBlanks)] +[[portfolio](team/scarletblanks.md)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Code Quality +* In charge of: Logic diff --git a/docs/Configuration.md b/docs/Configuration.md index 13cf0faea16..32f6255f3b9 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,6 +1,8 @@ --- -layout: page -title: Configuration guide + layout: default.md + title: "Configuration guide" --- +# Configuration guide + Certain properties of the application can be controlled (e.g user preferences file location, logging level) through the configuration file (default: `config.json`). diff --git a/docs/DevOps.md b/docs/DevOps.md index d2fd91a6001..8228c845e86 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -1,12 +1,15 @@ --- -layout: page -title: DevOps guide + layout: default.md + title: "DevOps guide" + pageNav: 3 --- -* Table of Contents -{:toc} +# DevOps guide --------------------------------------------------------------------------------------------------------------------- + + + + ## Build automation diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 8a861859bfd..e01c4a19a60 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,15 +1,20 @@ --- -layout: page -title: Developer Guide + layout: default.md + title: "Developer Guide" + pageNav: 3 --- -* Table of Contents -{:toc} + +# JABPro Developer Guide + + + -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +This is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + -------------------------------------------------------------------------------------------------------------------- @@ -21,14 +26,9 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). ## **Design** -
- -:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. -
- ### Architecture - + The ***Architecture Diagram*** given above explains the high-level design of the App. @@ -53,7 +53,7 @@ The bulk of the app's work is done by the following four components: The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. - + Each of the four main components (also shown in the diagram above), @@ -62,7 +62,7 @@ Each of the four main components (also shown in the diagram above), For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. - + The sections below give more details of each component. @@ -70,10 +70,9 @@ The sections below give more details of each component. The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) -![Structure of the UI Component](images/UiClassDiagram.png) - -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. + +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `PersonInformationPanel`, `SummaryStatisticScreen` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -89,14 +88,16 @@ The `UI` component, Here's a (partial) class diagram of the `Logic` component: - + -The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. +The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete st/interviewed t/developer")` API call as an example. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) + -
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. -
+ + +**Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + How the `Logic` component works: @@ -107,7 +108,7 @@ How the `Logic` component works: Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: - + How the parsing works: * When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. @@ -116,32 +117,36 @@ How the parsing works: ### Model component **API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - + The `Model` component, * stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). +* stores the event book data i.e., all `Event` objects (which are contained in a `UniqueEventList`) * stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the currently 'selected' `Event` objects as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+ + +**Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
- + -
+ ### Storage component **API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) - + The `Storage` component, -* can save both address book data and user preference data in JSON format, and read them back into corresponding objects. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). +* can save address book data, event book data and user preference data in JSON format, and read them back into corresponding objects. +* inherits from `AddressBookStorage`, `EventBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). * depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) ### Common classes @@ -154,89 +159,411 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### LinkedIn/Github feature + +#### Implementation + +The LinkedIn/Github feature consists of two parts - adding the linkedin/github username to the candidate's data, and viewing the profile. + +The addition is performed by the `AddLCommand` and `AddGCommand` classes. They extend `Command` and override the `execute()` method to add the username to the candidate's existing details. + +It involves the command `AddLCommand` and `AddGCommand` classes, and their corresponding parsers `AddLCommandParser` and `AddGCommandParser`, that take in user input and return either `AddLCommand` or `AddGCommand` objects. + +When executed, `AddLCommand` and `AddGCommand` find the person at the specified index in the list, and adds either their linkedin or github username to the `LinkedIn` or `Github` class associated with that `Person`. The list is then updated, and a `CommandResult` object is returned. + +The viewing is performed by the `LinkedInCommand` and `GithubCommand` classes. They extend `Command` and override the `execute()` method to redirect the user to the candidate's LinkedIn or Github profile on the browser. + +It involves the command `LinkedInCommand` and `GithubCommand` classes, and their corresponding parsers `LinkedInCommandParser` and `GithubCommandParser`, that take in user input and return either `LinkedInCommand` or `GithubCommand` objects. + +When executed, `LinkedInCommand` and `GithubCommand` append the username of the person to the base URLs for LinkedIn and Github, and open the URL in the browser. + + + +Given below is an example usage scenario and how the linkedin and github feature behaves at each step. + +**Step 1.** The user launches the application. `JABPro` will be initialized with the current saved state + +User should see the UI as shown below. + +![Ui](images/Ui.png) + +**Step 2.** The user wants to add the LinkedIn username to the first person in the list. The user enters the command `addL 1 u/alexyeoh` to add the username to the candidate's existing details. + +The following sequence diagram shows how the AddL and AddG operations work: + + + + + +User should see the UI as shown below after entering `addL 1 u/alexyeoh` + +![AddL](images/addLState.png) + +**Step 3.** The user can then view the linkedin profile for the candidate at index 1. The user enters the command `linkedin 1`. + +User should see the UI asa shown below after entering `linkedin 1` + +![LinkedIn](images/linkedinState.png) + +The following activity diagram shows how the `addL/addG` commands and `linkedin/github` commands are used together to bring together the `Linked/Github` feature: + + + +#### Alternatives considered #### + +Alternative 1 (Chosen): + +`LinkedIn` and `Github` are attributes of a `Person` that can be added using the `AddLCommand` and `AddGCommand` and are initially provided with default string values when a Person is initialised. + +Pros: There may be candidates who do not provide their LinkedIn or Github profiles as part of their application. + +Cons: Person is always initialised with empty values for LinkedIn and Github, and those values would have to be manually set using the commands. + +Alternative 2: + +`LinkedIn` and `Github`, like all other attributes of `Person`, are required to be non null. + +Pros: These details will be provided at the time of addition of a new Person, without having to add them later separately. + +Cons: It limits the scope for candidates that do not possess a LinkedIn or Github account. + +Alternative 3: + +`LinkedIn` and `Github` are attributes of `Person` but are allowed to be null. + +Pros: This accounts for candidates that do not have those social profiles, and allows for their addition at the time of Person creation + +Cons: With defensive programming in mind, not the best approach having to deal with null values. + + +### View feature + +#### Implementation + +The view feature is implemented using the `ViewCommand` class. It extends `Command` and overrides the `execute()` method to display the person's details in full in a new window. + +Like every other command class, it involves a command `ViewCommand` class and a parser `ViewCommandParser`. `ViewCommand Parser` takes in the user input and returns a `ViewCommand` object. + +When executed, `ViewCommand` saves the index of the person to be viewed as `LastViewedPersonIndex` in the `Model` and returns a `CommandResult` object with `isView` property being true. + +By having a `isView` property in `CommandResult`, the `MainWindow` component is able to toggle the `UI` to the view the person of the `LastViewedPersonIndex` after the command has been executed. + +Additionally by allowing `isView` as a property in `CommandResult`, we are able to view the person's profile after executing other commands such as `add`, `edit`, `set`, `remark`, `addL`, `addG` without having to enter the `view` command again. + +We only need to set the `isView` property to true in the `CommandResult` object if the command being executed targets a person. This is because the `ViewCommand` itself originally require an index to be specified to view the person's profile. Thus, if the command does not target a person, the view command will not be triggered and the Person Information Panel and Summary Statistic Screen will not be updated. + +Given below is an example usage scenario and how the view feature behaves at each step. + +Step 1. The user launches the application. The `AddressBook` will be initialized with the current saved address book state + +User should see the UI as shown below. + +![Ui](images/Ui.png) + +Step 2. The user wants to see the full information displayed for the first person in the displayed list. The user enters the command `view 1` to view the first person in the list. + +The following sequence diagram shows how the view operation works: + + + + + +**Note:** The lifeline for `ViewCommand` and `ViewCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + + +User should see the UI as shown below after entering `view 1` (The command shown in Command Box is re-inputted for the sake of clarity. After entering the command, command should be cleared) +![View](images/viewState.png) + +Step 3. The user can then read or process the information stored for the viewed person. + + + +**Note:** The view command can be most effectively used with `search` and `list`. Since the view index is dependent on the Index on the filtered list shown, the user can view the profile after filtering for specific properties in a person using `search` and sorting them using `list`. + + + +Alternatives considered + +Alternative 1 (Chosen): +The view feature is implemented using the `ViewCommand` class. It extends `Command` and overrides the `execute()` method to display the person's details in full in a new window. + +Pros: Follows the Software Design Patterns of Command. This is the same pattern used for all other commands thus creating consistency. + +Cons: Tougher to implement since other commands do not have the ability to trigger the `ViewCommand` in their execution. That is we specifically need to set the isView property to true if we want the `ViewCommand` to occur simultaneously with another command. + +Alternative 2 (Not Chosen): +The view feature is implemented using the `ViewCommand` class. It extends `Command` and overrides the `execute()` method to display the person's details in full in a new window. +Commands that involved viewing will extend `ViewCommand` instead of the `Command` class. All of them are returned as `ViewCommand` to ensure toggling of the UI after command is executed. + +Pros: Arguably a more OOP approach since all commands that trigger view IS-A `ViewCommand`. + +Cons: You cannot implement any command that does not involve viewing but inherits from any command that is a children of `ViewCommand`. +An example could be trying to create identical commands that does not toggle the UI after execution. This would require duplication of the exact same command code but inheriting from `Command` instead of `ViewCommand`. + +
+ +The following activity diagram shows how command such as `add`, `edit`, `set`, `remark`, `addL`, `addG` (Commands that trigger view) lead to the update of the Person Information Panel in the UI. +This is done by setting the `isView` property to true in the `CommandResult` object. + + + +### Create feature + +#### Implementation + +The `create` feature is implemented using the `CreateTagCommand` class. It extends `Command` and overrides the `execute()` method +to create tags of specific categories. + +To initiate the creation of tags, users utilize the create command with the following format: `create t/CATEGORY TAGNAME`. Each `/t` prefix denotes the start of a new tag definition. If users intend to create multiple tags, they can employ multiple `/t` prefixes in the command, such as `create t/CATEGORY1 TAGNAME t/CATEGORY2 TAGNAME`. + +Parsing of the create command's tag parameters is handled by the `parse` method in the `CreateTagCommandParser` class. This method receives a string containing user input and separates it into individual tags based on the /t prefix. The resulting tags are then passed as an array of tag category and name pairs to the constructor of the CreateTagsCommand class. + +Finally, the `execute()` method of the `CreateTagCommand` creates a `Tag` object for each element in the array of tag category and name pairs. These newly created tags are then added to the model. + +The following activity diagram summarize what happens when a user attempts to execute the `create` command. + + + +### Search feature + +#### Implementation + +The `search` feature is implemented using the `FindCommand` class. It extends `Command` and overrides the `execute()` method to +filter users by the specified parameters. + +The search parameters from the user input are parsed using the parse method in the `FindCommandParser` class. `FindCommandParser::Parse` +takes in the search parameters from the user input and combines them into a list of predicates. This list of predicates is then +passed as an argument to the `FindCommand` constructor and the method returns a `FindCommand` instance with the associated list of predicates. + +Currently, the search parameters could belong to any of the three following categories: `Name`, `Status`, and `Tag`. Prefixes +`n/`, `st/` and `t/` are used to denote the category of the search parameters respectively. E.g. `search n/alex st/interviewed t/swe` + +The list of predicates is a list comprising predicate objects whose classes implement the `Predicate` class in Java. +Each category has its own predicate class i.e. `NameContainsKeywordPredicate`, `StatusContainsKeywordPredicate`, `TagContainsKeywordPredicate` +and each class overrides the `test` method which returns true if the persons list contains any of the given names/status/tags. + +Finally, the execute method in `FindCommand` class invokes the `updateFilteredPersonList(predicatesList)` which will +update the list of persons displayed. + +Given below is an example usage scenario and how the search mechanism behaves at each step. + +Step 1. The user launches the application. + +Step 2. The user executes `search t/intern` command to filter candidates whose status are offered. + +The following sequence diagram shows how the search operation works: + + +**Note:** +* The lifeline for `FindCommand` and `FindCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +* The `parse` method of `FindCommandParsers` also creates objects of the `NameContainsKeywordPredicate`, `StatusContainsKeywordPredicate` and `TagContainsKeywordPredicate` but these + were instantiations not included in the sequence diagram below for the sake of brevity. However, do note that the `n`, `s`, `t` arguments in `getPredicatesList(n, s, t)` in the sequence + diagram below refer to instances of `NameContainsKeywordPredicate`, `StatusContainstKeywordPredicate` and `TagContainsKeywordPredicate` respectively. + + + + +Step 3. The user should see the UI below upon entering `search t/intern`. + +![Search](images/search-dg.png) + +The following activity diagram shows summarizes what happens when a user attempts to execute the `search` command. + + + + + +**Note:** The current implementation of search allows users to search by any of the categories individually or by different combinations of the categories e.g. `search n/alex bernice st/offered t/intern` +It also allows users to specify more than one search parameter for each category e.g. `search n/alex bernice` + + + +### Delete feature + +#### Implementation + +The delete feature is implemented using the `DeleteCommand` class. It extends `Command` and overrides the `execute()` method to +filter users by the specified parameters. + +The delete parameters from the user input are parsed using the parse method in the `DeleteCommandParser` class. `DeleteCommandParser::Parse` +takes in the search parameters from the user input and, depending on the input, either leave it as a number (for delete by index) or combines them into a list of predicates +(for delete tags & status). + +The `DeleteCommand` constructor can take either a positive integer number (for delete by index) or a list of predicates (for delete by tags & status) and both constructor +will always return a `DeleteCommand` instance with a number and a list of predicates. +For delete by index, the constructor will return a `DeleteCommand` instance with the associated input number and an empty list of predicates. +For delete by tags & status, the constructor will return a `DeleteCommand` instance with a default index and the associated list of predicates. + +Currently, the delete parameters for delete by tags & status could only belong to either `Tag` or `Status`. +Prefixes `t/` and `st/` are used to denote the category of the delete parameters respectively. E.g. `delete st/interviewed t/developer` + +The list of predicates is a list comprising predicate objects whose classes implement the `Predicate` class in Java. +Each category has its own predicate class i.e. `TagContainsKeywordPredicate`, `StatusContainsKeywordPredicate`, +and each class overrides the `test` method which returns true if the persons list contains any of the given tags/status. + +Finally, the execute method in `DeleteCommand` class retrieves a person or a list of persons to delete and +invokes the `deletePerson(personToDelete)` method from the Model class that deletes the associated person(s). +Additionally, it also retrieves a list of events associated with the person(s) to delete and +invokes the `deleteEvent(eventToDelete)` method from the Model class that deletes the event(s) associated with the deleted person(s). + +Given below is an example usage scenario and how the search mechanism behaves at each step. + +Step 1. The user launches the application. + +Step 2. The user executes `delete st/interviewed t/developer` command to delete applicants that has been interviewed and tagged as swe. + +The following sequence diagram shows how the search operation works: -#### Proposed Implementation + -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +**Note:** The lifeline for `DeleteCommand` and `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. + -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. + -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +Step 3. Assuming Bernice is the applicant matching the requirements, the user should see the UI below upon entering `delete st/interviewed t/developer`. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +![View](images/delete.png) -![UndoRedoState0](images/UndoRedoState0.png) +The following activity diagram shows summarizes what happens when a user attempts to execute the `delete` command. -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. + -![UndoRedoState1](images/UndoRedoState1.png) + -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +**Note:** The current implementation of delete by tags & status allows users to search by any of the categories individually or by different combinations of the categories. +It also allows users to specify more than one delete parameter for each category e.g. `delete t/intern manager` + -![UndoRedoState2](images/UndoRedoState2.png) +### Set feature -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +#### Implementation -
+The set feature is implemented using the `SetCommand` class. It extends `Command` and overrides the `execute()` method to +edit the status of the user. -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +The set parameters from the user input are parsed using the parse method in the `SetCommandParser` class. `SetCommandParser::Parse` +takes in the set parameters from the user input and combines them into a list of predicates. This list of predicates is then +passed as an argument to the `SetCommand` constructor and the method returns a `SetCommand` instance with the associated list of predicates. -![UndoRedoState3](images/UndoRedoState3.png) +Currently, the parameters are , is limited to "Preliminary", "Interviewed", "Rejected", "Accepted". -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +Finally, the execute method in `SetCommand` class returns a new updated `Person` which will +be used to update the status of the Person displayed. -
+Given below is an example usage scenario and how the set mechanism behaves at each step. -The following sequence diagram shows how the undo operation works: +Step 1. The user launches the application. -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +Step 2. The user executes `set 1 Interviewed` command to set the first user to "Interviewed". -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +Step 3. The user should see the update upon calling the `view` command on the Person again. In this case, `view 1` -
+The following activity diagram shows how the set operation works: -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. + -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +### Export feature -
+#### Implementation -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +The export feature is implemented using the `ExportCommand` class. It extends `Command` and overrides the `execute()` method to +export to a csv file. -![UndoRedoState4](images/UndoRedoState4.png) +The export parameters from the user input are parsed using the parse method in the `ExportCommandParser` class. `ExportCommandParser::Parse` +takes in the export parameters from the user input and combines them into a list of predicates. +However, export does not need to be used with any parameters. -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +Finally, the execute method in `ExportCommand` class exports to the /data/export.csv file and returns a `CommandResult` that +indicates success or failure. -![UndoRedoState5](images/UndoRedoState5.png) +Given below is an example usage scenario and how the export mechanism behaves at each step. -The following activity diagram summarizes what happens when a user executes a new command: +Step 1. The user launches the application. - +Step 2. The user executes `export` command. -#### Design considerations: +Step 3. The user should see the exported .csv file in the directory /data/export.csv. -**Aspect: How undo & redo executes:** +The following activity diagram shows how the export operation works: -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. + -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. +## Events feature -_{more aspects and alternatives to be added}_ +#### Implementation -### \[Proposed\] Data archiving +The Events feature extends the original ideas of the `AddressBook` to store two types of entities - Candidates and Events associated with candidates. -_{Explain here how the data archiving feature will be implemented}_ +It consists of two parts - adding the events to JABPro, and viewing the events. + +THe addition is performed by the `EventCommand` class. It extends `Command` and overrides the `execute()` method to add the event to JABPro. + +It involves the `EventCommand` class and its corresponding parser, `EventCommandParser`, that takes in the user input and returns an `EventCommand` object. + +When executed, `EventCommand` finds the person the event is associated with, and adds the name of the person, and other details such as description and start and end time, as input by the user. The existing `EventBook` is then updated to reflect this addition, and a `CommandResult` object is returned. + +The viewing is performed by the `ScheduleCommand`. It extends `Command` and overrides the `execute()` method to open the `Events Window` and display the events. + + + +**Note:** There is another way of viewing events, that is through the `Events` tab. Refer to the [UserGuide](UserGuide.md#viewing-events-schedule) for more information. + + + + +Given below is an example usage scenario highlighting how the `Events` feature behaves at each step. + +**Step 1.** The user launches the application. The `EventBook` will be initialized with the current saved event book state. + +User should see the UI as shown below. + +![Ui](images/Ui.png) + +**Step 2.** The user wishes to add an "Interview" event for the first candidate in the list. The user enters the following command: + +`event 1 d/Interview bt/2023-11-12 10:00 et/2023-11-12 12:00` + +This command adds a new event to the `EventBook` that is associated with the first person in the list, with the description as "Interview" and the start and end time as specified. + + + +**Note:** The start and end times are required to follow a specific format. Refer to the [UserGuide](UserGuide.md#adding-an-event-event) for more information. + + + +The following sequence diagram shows how the `Event` operation works: + + + +User should see the UI as shown below after executing the aforementioned command [It is assumed that the first candidate in the list is Alex Yeoh]. + +EventWindow + +The following activity diagram shows how the `event` and `schedule` command can be used together to schedule events: + + + +#### Alternatives considered #### + +***Duplicate Events*** + +Alternative 1 (chosen): + +`Events` associated with the same `Person`, having the same description are allowed. It is upto the user to input more detailed descriptions to differentiate between them, and to promote readability. + +Pros: It makes the `EventBook` a complete product of the user's choice as no restrictions are imposed, and the user can enter data as per their preferences and their convenience. + +Cons: It might lead to confusion if multiple events of the same person, having the same description are added. Hence, it is recommended that the user enters detailed descriptions to distinguish events from one another. + +Alternative 2: + +`Events` associated with the same `Person`, having the same description are considered duplicate `Events` and hence, are rejected by the system. + +Pros: It prevents redundant data from being stored and accidental addition of multiple events of the same type for the same person + +Cons: It restricts users from entering data that might be understandable or convenient for them. [For example: If the user creates two Events with the description "Interview" for the same person, they might have a distinct idea of what each of those Events mean, but the system prevents them for making this addition]. -------------------------------------------------------------------------------------------------------------------- @@ -257,82 +584,447 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts +* has a need to manage a significant number of candidates contacts +* wants an organized way to keep track of candidates information +* wants to view and manage candidates information in a single place +* wants to filter and sort candidates based on their details +* wants to view a schedule/summary of events relating to the candidates +* wants to attach a score to candidate performance over interview and assessments +* wants to be able to use scores in order to quantitatively compare candidates * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: + +JABPro aims to solve the problem of HR managers having to sort through tons of job applications. + +It makes their life easier by allowing them to easily fetch important info about job applicants such as their contact details, application status etc. In addition, +JABPro also provides them with the ability to quickly fetch summary stats to get an insight on the overall performance of applicants in their interviews. Lastly, JABPro also +has a built-in event management app that allows them to efficiently note down interview schedules with applicants. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| Priority | As a …​ | I want to …​ | So that…​ | +|----------|----------------|------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `* * *` | Hiring Manager | add a candidate's contact information, including name, email, phone number | I can easily access and reach out to candidates when needed | +| `* * *` | Hiring Manager | add notes and comments to candidate profiles to document interview feedback and impressions | I can maintain a record of interactions and feedback | +| `* * *` | Hiring Manager | delete job applicants information | I can remove redundant/unecessary data bloat and also to abide to privacy laws | +| `* * *` | Hiring Manager | view a specific job applicant's resume or portfolio | I can check whether they meet the requirements requested by other department heads | +| `* * *` | Hiring Manager | search for all job applicants matching a given profile | I can find suitable candidates for a project | +| `* * *` | Hiring Manager | list all applicants information | I can easily get an overview of all of the applicants | +| `* * *` | Hiring Manager | update the application status for a candidate (e.g. "Interviewed", "Rejected", "Offered") | I can keep track of each candidate's progress in the hiring process | +| `* * *` | Hiring Manager | record the score of the different activities such as interviewsor assessments that an applicant might go through | I can use them for effective comparison and filter the candidates easily | +| `* * *` | Hiring Manager | create tags and categorise them | I can colour code them based on categories and use them to tag applicants to easily distinguish them | +| `* * *` | Hiring Manager | list all tags I have created | I can easily get an overview of all of the tags that I have made | +| `* * *` | Hiring Manager | compare candidates using their performance in their assessments or interviews | I can choose the best candidates to move to the next stage of the hiring process and get the best performing candidates objectively | +| `* * *` | Hiring Manager | add social profile (LinkedIn/Github) candidate's information and view with ease | I can get a more holistic view of the candidate's abilities | +| `* *` | Hiring Manager | view a schedule/summary of events relating to the candidates | I can make preparations and arrangements for the events beforehand, and also get an idea of where each candidate is in the hiring process. | +| `* *` | Hiring Manager | export candidate information and application data to a spreadsheet
| I can perform further analysis using alternate tools on candidate data | +| `*` | Hiring Manager | get data on which positions are lacking job applicants | I can update the external recruitment team to focus on head hunting applicants for these roles | +| `*` | Hiring Manager | get data on which positions already have too many applicants | I can forward this to the department heads to see if they still want to keep the job posting or close it | + -*{More to be added}* ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `JABPro` and the **Actor** is the `hiring manager`, unless specified otherwise) + +**Use case: Add a person** + +**Guarantees**: The person is added to the list of persons. The person is also saved in the database. + + +**MSS** +1. User requests to add a person. +2. JABPro shows that command has been executed successfully. +3. JABPro adds the person to the list of persons. +4. JABPro shows the person added at the end of the list of persons. +Use case ends. + +**Extensions** +* 1a. User does not provide the correct information for a person to be added. + * 1a1. JABPro shows an error message and provides course of action for remedy. + Use case resumes at step 1. +* 1b. User has already been added to the list of persons. + * 1b1. JABPro shows an error message and provides course of action for remedy. + Use case resumes at step 1. + +**Use case: Add a remark to a person** + +**Guarantees**: The remark is added to the person. The remark is also saved in the database. + +**MSS** +1. User requests to add a remark to a person. +2. JABPro shows that command has been executed successfully. +3. JABPro adds the remark to the person. +Use case ends. + +**Extensions** +* 1a. User provides invalid index. + * 1a1. JABPro shows an error message and provides course of action for remedy. + Use case resumes at step 1. +* 1b. User does not provide a remark. + * 1b1. JABPro adds an empty remark to the person, remark no longer seen. + Use case ends. + +**Use case: List all persons** + +**MSS** +1. User requests to view a list of all persons. +2. JABPro retrieves the list of all persons from the database. +3. JABPro displays the list of all persons to the user. +4. If the user specifies an attribute to sort by, e.g., "s/name" for sorting by name, the application sorts the list accordingly. If no attribute is provided, the list remains unsorted. +5. The sorted or unsorted list is displayed to the user in ascending order based on the specified attribute. +Use case ends. + +**Extensions** +* 1a. User provides an incorrect attribute for sorting (e.g., "list s/phone"). + * 1a1. JABPro shows an error message and provides course of action for remedy. + Use case resumes at step 1. +* 1b. User attempts to list persons when there are no entries in the address book. + * 1b1. JABPro shows a message indicating that there are no persons to display. + Use case ends. + +**Use case: Search persons matching the given profile** + +**MSS** +1. User requests to search users matching the given profile as specified by the search parameters. +2. JABPro requests details of the profile to be searched. +3. User enters the profile details. +4. JABPro retrieves the list of all persons from the database whose profiles match the given search parameters. +5. JABPro displays the filtered list of persons to the user. +Use case ends. + +**Extensions** + +* 3a. The given name/status/tag parameter is invalid. + * 3a1. JABPro shows an error message and provides course of action for remedy. + Use case resumes at step 3. +* 5a. No person match the given profile. + * 5a1. JABPro shows a message indicating that there are no persons to display. + Use case ends. **Use case: Delete a person** **MSS** +1. User requests to list persons. +2. AddressBook shows a list of persons. +3. User requests to delete a specific person in the list by providing either the person's index or tags. +4. AddressBook deletes the specified person(s). +Use case ends. -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +**Extensions** +* 2a. The list is empty. + * 2a1. AddressBook displays a message indicating that the list is empty. + Use case ends. +* 3a. The given index is invalid. + * 3a1. AddressBook shows an error message indicating that the specified index is invalid. + Use case resumes at step 3. +* 3b. The given tags do not match any persons. + * 3b1. AddressBook shows an error message indicating that no persons with the specified tags were found. + Use case resumes at step 3 +* 4a. Deletion encounters an error + * 4a1. AddressBook displays an error message indicating that the deletion process failed. + Use case ends. +**Use case: Set a person's status** + +**MSS** +1. User requests to list persons. +2. AddressBook shows a list of persons. +3. User requests to set the status of a specific person in the list. +4. AddressBook sets the status of that person in the list. Use case ends. **Extensions** +* 2a. The list is empty. + * 2a1. AddressBook displays a message indicating that the list is empty. + Use case ends. +* 3a. The given index is invalid. + * 3a1. AddressBook shows an error message indicating that the specified index is invalid. + Use case resumes at step 3. +* 3b. The given status is invalid. + * 3b1. AddressBook shows an error message indicating that the specified status is invalid. + Use case resumes at step 3. +**Use case: View a person's details** + +**Guarantees**: Person's details are displayed in full in a new window. + +**MSS** +1. User requests to list persons. +2. JABPro shows a list of persons. +3. User requests to view a specific person in the current displayed AddressBook. +4. JABPro shows the details of that person. + Use case ends. + +**Extensions** * 2a. The list is empty. + * 2a1. AddressBook displays a message indicating that the list is empty. + Use case ends. +* 3a. The given index is invalid. + * 3a1. AddressBook shows an error message indicating that the specified index is invalid. + Use case resumes at step 3. + +* 3b. User decides to search for a person based on a criteria such as name. + * 3b1. Displayed AddressBook changes to match that of search result. + Use case resumes at step 3. + +**Use case: Using scores to compare and filter people** + +**Guarantees**: Persons are displayed has a score that exceed the specified threshold based on the metric and value provided for a tag. + + +**MSS** +1. User requests to list persons. +2. JABPro shows a list of persons. +3. User edits the scores of persons tying it to a tag +4. User requests to filter persons by their scores for a particular tag based on a particular metric. +5. JABPro shows a list of persons with scores. + +**Extensions** +* 3a. The tag or score is invalid. + * 3a1. JABPro shows an error message indicating that the specified tag or score is invalid. + Use case resumes at step 3. +* 4a. The parameters provided are invalid. + * 4a1. JABPro shows an error message indicating that the specified method is invalid. + Use case resumes at step 4. + + +**Use case: Export the current data to excel** + +**MSS** +1. User requests to export persons. +2. JABPro exports the list of persons to a .csv file. + Use case ends. + +**Extensions** +* 2a. The .csv file to edit is open + * 2a1. AddressBook displays a message indicating that we cannot write to it as it is open. + Use case ends. +**Use case: Add social profile to person's details** + +**MSS** +1. User requests to add social profile (linkedin or github). +2. JABPro shows that command has been executed successfully. +3. JABPro adds the social profile to the person's existing details in the list. + Use case ends. + +**Extensions** +* 2a. User does not provide valid information for the person. + * 2a1. JABPro displays error message. + Use case resumes at Step 1. +* 2b. User requests to add social profile other than LinkedIn or Github + * 2b1. JABPro displays error message. + Use case resumes at Step 1. + +**Use case: Open social profile for a person** + +**MSS** +1. User requests to open social profile for a person. +2. JABPro shows that command has been executed successfully. +3. JABPro redirects to the webpage of the corresponding profile. + Use case ends. + +**Extension** +* 1a. Person does not exist in the list. + * 1a1. JABPro displays error message. + Use case ends. +* 1b. Social profile requested other than LinkedIn or Github. + * 1b1. JABPro displays error message. + Use case ends. +* 3a. Person does not exist on the social platform. Use case ends. -* 3a. The given index is invalid. +**Use case: Add events relating to candidates** - * 3a1. AddressBook shows an error message. +**MSS** +1. User requests to add an event relating to a candidate. +2. JABPro shows that command has been executed successfully. +3. JABPro adds the event to the list of events. + Use case ends. - Use case resumes at step 2. +**Extension** +* 2a. User does not provide the correct information for an event to be added. + * 2a1. JABPro shows an error message anf provides course of action for remedy. Use case resumes at step 1. +* 2b. Event has already been added to the list of events. + * 2b1. JABPro shows an error message and provides course of action for remedy. Use case resumes at step 1. -*{More to be added}* +**Use case: Tag a candidate with a newly created tag** + +**MSS** +1. User requests to create a tag of a specific category. +2. JABPro adds the tag to the list of tags. +3. JABPro shows a message indicating that the tag has been added successfully. +4. User requests to view all persons. +5. User requests to add the newly created tag to add tag to a person. +6. JABPro adds tag to the person. + Use case ends. + +**Extension** +* 1a. The given tag name is invalid + * 1a1. JABPro displays an error message +* 1b. User did not provide the complete command (missing category or name). Use case resumes at step 1. + * 1b1. JABPro displays an error message and provides a course of action for remedy. Use case resumes at step 1 +* 2a. Tag already exists in JABPro + * 2b1. JABPro displays an error message saying tag already exists. Use case resumes at step 1. +* 5a. Multiple tags exist with the same name in different categories + * 5a1. JABPro displays an error message and provides a course of action for remedy. Use case resumes at step 5. ### Non-Functional Requirements 1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +2. Should be able to `hold up to 1000 persons` without a noticeable sluggishness in performance for typical usage. +3. A user with above `average typing speed for regular English text` (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. System should respond within a `reasonable amount of time` (e.g. < 2 second) for typical usage (e.g. listing 1000 persons). +5. Should not deal with `external database` or `network connections`. +6. Product is expected to be used by a `single user` only. +7. Product is `not expected to contain API` for other software to communicate with. +8. Product is expected to be used in a system with at least 4GB RAM and `1GB free disk space`. +9. Product Visuals should be `unambiguous` and clear to the user +10. Commands should be `easy to remember` and `intuitive` to use +11. Product should be `consistent` in its visuals and commands formatting -*{More to be added}* ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Hiring Manager**: Inter-changable with users in this context +* **Candidate**: Inter-changable with persons/applicants +* **AddressBook**: Inter-changable with JABPro in this context since this is an evolve project built on-top of AB3 functionality +* **Keyword**: Search parameter -------------------------------------------------------------------------------------------------------------------- +## Appendix: Planned Enhancements + +### Improve on Delete feature: `delete` +**Improve the delete feature**: +Currently, JABPro only supports applicant related operations such as delete applicants by index, status and tags. +However, it does not support deletion of specific events and tags. We would like to improve on this by expanding the `delete` feature +functionality to also support deletion of specific events and tags. This makes it easier for users to delete events and tags, without +having to manually edit the json file. + +### Improve on Clear feature: `clear` +**Improve the clear feature**: +Currently, `clear` will wipe out all applicants' data and events. However, it does not wipe out the tags created by the user +which falls short of the user's expectations of what a `clear` command is supposed to do. Thus, we would like to enhance `clear` +to also include the deletion of created tags. + +### Improve on Export feature: `export` +**Improve the export feature**: +Currently, `export` is limited to only exporting applicants' data and it does not support a function to export events. +This feature will be implemented in future iterations. + +### Add and general uniqueness constraint: `add` +**Improve what identifies as a unique person**: +Currently, a person is uniquely identified by their name. This is not ideal as there may be multiple persons with the same name. We would like to improve this by using a unique identifier such as internal ID or NRIC number. +This would allow us to have a more robust system that can handle multiple persons with the same name. + +### Improve on Edit feature: `edit` +**Improve the edit feature**: +Currently, it falls short of the user's expectations as it does not allow the user to add on to existing tags. We would like to improve this by creating a feature that allows the user to add on to existing tags. +Thereby users can add on to existing tags without having to retype all the previous tags. + +### Improve on Summary Statistics +**Improve the summary statistic implementation**: +Currently, if the applicants do not have a good spread of scores, the summary statistics will not be very useful. We would like to improve on this by implementing a more robust summary statistics that can handle small sample sizes and outliers. +Additionally, you can only use the summary statistic table for comparison after you have inputted most of the scores. At that point, it might be more relevant to use the filter feature instead. Thereby we hope to add more visualisations like graphs to the summary statistics table to make it more useful for comparison. + +### Improve on Filter feature: `filter` +**Improve the filter feature** +Currently, the filter feature might be too flexible for the user. That is it works on the displayed list and not across the board. We would like to improve on this by implementing a filter feature that works across the database. +This makes it more intuitive and logical for the user to use since the user would expect the filter feature to work across the database and not just the displayed list. + +### Error Message for `filter` feature +**Improve the error message for filter feature** +Currently if you were use the filter feature with an invalid tag, that is a tag which does not have a alphanumeric name or is an `assessment` tag, it shows the error message: `Invalid tag provided. Needs to be non-empty name`. +This is attained from a sample input like `filter t/Intern met/median` where `Intern` is a tag that is on applicant but does not have a `assessment` category. Error message should contain `Check that the tag contains an asssessment category, use create command if it does not` +Additionally inputs like `filter t/Int@/a met/median` will also show the same error message. The current error message is too vague in nature, and should contain `Check that the tag name is not empty and is alphanumeric (a valid tag name) and does not contain space`. +The error message should be split into two in order to not confuse the user about what is wrong with the tag. +Therefore, the final output should have 1 error message for invalid tag names that are not alphanumeric or contains space and another error message for when the tag does not have an assessment category in `listT`. +` + +### Improve on `remark` feature +**Improve the remark feature** +Currently, if you use multiple prefix , it is allowed and only the last prefix will be used. We would like to improve on this by only allowing 1 prefix to be used. Thus, we verify for duplicate `r/` prefix since it does not make sense to have multiple remarks for the same person. + +### Improve on `edit` feature +**Improve on the edit feature** +Currently, for tagging, once you add a tag to a person, it does not update when you add the tag categories to the same tag name using `create`. +This is a feature flaw since a user would expect the tag to be updated and would not need to be re-tag again. We would like to improve on this by updating the tag when the tag category is updated. +That is if you add an extra category to the tag name, the tag will be updated to include the tag category. Thus, a user would not need to re-tag the person again to update the tag category. + + +## **Appendix: Effort** + +### ScoreList, Score, Summary Statistic, Filter +In terms of integration of features, this was by far the biggest and most complex. This is due to how `Summary Statistic` relied on `Score`, `ScoreList` and `Tag` to function and Filter requiring all of them to function properly. +The thought process came when we first tried to implement 1 score for each applicant. This was the naive approach. However, we realise that with only 1 score, would mean that it can only represent 1 thing. +This means that the Hiring Manager would have to keep track of what each score means individually and any point in arranging these scores would lead to massive confusion. Hence, we decided to attach a score to a tag. +However, this meant that with optional and possibly multiple tagging, we will need a flexible way of attaching scores to tags. This led to the HashMap implementation of `ScoreList`. +Additionally, we had to decide on the constraint of scores. While a common approach was to let it be between 0 and 100, we wanted to maintain great flexibility and thus constraint it as a positive integer >= 0. + +For storage wise, we had to learn how to store `ScoreList` in a key-value format and also decode it. So that was one big challenge in terms of storing it in a json format that can be decoded. + +One of the biggest problem with using the `Parser` and `Command` model is that the `Parser` has no access to the model. So at the parsing stage, you are unable to determine if the tag exists or is valid since we only want to enable it for `assessment` tags. +This meant that it had to be carried out at the `Command` stage of execution. Additionally, it might have been a misstep trying to implement the editing of scores in the `edit` command. +This is due to the complexity of how the `edit` command was implemented. Since you could add a tag in edit while simultaneously trying to add a score to the tag, it led to a lot of need to check. +Additionally since tags can be easily edited, it meant that we needed to check if the ScoreList was updated and if the tag was edited. +This also led to many errors, since you could remove all tags, and the ScoreList would still remain. Especially since EditPersonDescriptor did not have access to the tags that the person currently have. +Additionally, since tag was not cumulative, you would want to keep track of the scores of the current tag if they are being kept. This meant that you would need to keep track of the current tag and the current score while also allowing for changes. + +After that, there was the choice of implementing Summary Statistic. A lot of thought went into how we can make the scores useful. What would be a great visualisation. We chose the couple of simple statistic that we thought would be useful. +However, the calculation of the statistic was not simple since we decided to use streams for implementation. This meant that misconception of how streams work would lead to a lot of errors. The most common 1 being that the stream is closed due to a close operation. +Additionally, percentile was originally implemented incorrectly. This was because we did not account for the edge case of people scoring the same. This meant that the percentile would vary for same score. This was fixed by then subtracting the number of people with the same score from the total number of people. + + +Following that we had filter which relied on all of the above. This was simpler to implement once all the logic was down. +Overall, the implementation of the feature was complex due to nature of Streams and UI updates for JavaFX. + +### View +UI generation was rather tedious in terms of using CSS and trying to get things to align properly. Additionally, the design implementation using `Commands` meant that commands did not have access to other commands during runtime. +This meant that we had to add an extra parameter to the CommandResult in order to get commands to auto execute view after executing the command. The implementation was simple but the design was not. + +### Flexibility for further analysis +We acknowledge that there will be some who would prefer to analyse data outside JABPro - and that is completely fine. +With the ability to export to a .csv file using JABPro, users are empowered with the ability to conduct analysis in +other applications that they are more familiar with. + +The introduction of the export command showcases our commitment towards user flexibility, and focus towards +making JABPro a tool that adds towards hiring manager's ecosystem, as opposed to a trade-off that requires them +to only rely on JABPro. We strongly believe that users should have the flexibility and ability to conduct +further analysis outside JABPro if they wish to. + + +### Storage Complications and Effort +In relation to saving of data to storage, we largely followed the same format as AB3. The only strict deviation from the AB3 method of saving was using a different data structure. +In the case of `ScoreList` and `UniqueTagList`, we used a hashmap to save the details of the score and tags for `ScoreList` or tag category and tag name for `Tags`. This was more complicated to implement as the conversion from hashmap to json and vice versa was more complicated than the conversion of a list to json and vice versa. + +### Redirection Implementation + +Adhering closely to our viewpoint of hiring managers, we realised that JABPro can only add value to their workflows if it goes above and beyond what existing low-level applications such as spreadsheets can provide. This helped us identify a key aspect of reviewing applications - the existence of a social profile linked with candidates. +At the same time, we were bound by the limitations of what could and could not be implemented. For instance, the use of APIs or external libraries was prohibited. +Hence, we stumbled upon the idea of implementing a feature that simply redirects the user to the social profile, without the need for any external libraries. + +We believe this highlights the scope of adaptation that AB3 provided, and we could recognise. There still existed the notion of allowing users to provide the link, which could subsequently be copied to view the social profile. However, in lieu of making JABPro user-friendly, we focused on building redirection. + ## **Appendix: Instructions for manual testing** Given below are instructions to test the app manually. -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; + + +**Note:** These instructions only provide a starting point for testers to work on; testers are expected to do more *exploratory* testing. -
+ ### Launch and shutdown @@ -340,38 +1032,296 @@ testers are expected to do more *exploratory* testing. 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 2. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. -1. Saving window preferences +2. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 2. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ - -### Deleting a person - -1. Deleting a person while all persons are being shown +### Adding a person while all persons are being shown +1. Adding a person while all persons are being shown 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test case 1 (Positive Test Case): + `add n/John Poh p/98765432 e/johnpoe@gmail.com a/ 311, Clementi Ave 2, #02-25 r/ 2 years of experience in software development` + + **Note**: The current AddressBook cannot contain anyone named `John Poh`
+ **Expected**: New person is added to the list. Details of the new person shown in the status message. + + 3. Test case (Negative Test Case): + `add n/John Poh p/98765432 e/johnpoe@gmail.com a/ 311, Clementi Ave 2, #02-25 r/ 2 years of experience in software development` + **Note**: The current AddressBook should contain a person named `John Poh`
+ **Expected**: No person is added. Error details shown in the status message. List of persons remains the same. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. - - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. - - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. - -1. _{ more test cases …​ }_ +### Deleting a person +1. Deleting a person while all persons are being shown + 1. Prerequisites: + 1. List all persons using the `list` command. Multiple persons in the list. + 2. Set one of the applicants' status in the sample data to `interviewed` by doing `set 1 interviewed` + + 2. Test case: `delete st/interviewed` + **Expected**: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + + 3. Test case: `delete 1`
+ **Expected**: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + + 4. Test case: `delete 0`
+ **Expected**: No person is deleted. Error details shown in the status message. Status bar remains the same. + + 5. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ **Expected**: Similar to previous. + +### Viewing a person's details while all persons are being shown +1. Viewing a person's details while all persons are being shown + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test case 1 (Positive Test Case): + `view 1` + **Expected**: Person information panel is updated to reflect the details of the person. + + 3. Test case 2 (Negative Test Case): + `view 0` + **Expected**: No person is viewed. Error details shown in the status message. Person information panel remains the same. + +### Adding a remark to a person while all persons are being shown + +1. Adding a remark to a person while all persons are being shown + 1. Prerequisites: List all persons using the `list` command. + + 2. Test case 1 (Positive Test Case): + `remark 1 r/John is a good candidate` + **Expected**: New remark is added to the person. Details of remark displayed on the person information panel. + + 3. Test case 2 (Positive Test Case): + `remark 1 r/**REMARK** Furthermore, hes capable of working in a team` + **Expected**: Remark is added on from the previous existing remark. Person information panel is updated to reflect the addition of the remark. + Specifically `John is a good candidate` is followed by `Furthermore, hes capable of working in a team` on the person information panel. + + 4. Test case 3 (Negative Test Case): + `remark 1 r/` + **Expected**: Previous remark is deleted. Person information panel is updated to reflect the deletion of the remark and is blank. + + 5. Test case 4: + `remark 0 r/John is a good candidate` + **Expected**: No remark is added. Error details shown in the status message. Person information panel remains the same. + +### Searching for applicants matching a given profile + +1. Searching for applicants matching a given profile + 1. Prerequisites: + 1. Set one of the applicants' status in the sample data to `interviewed` by doing `set 1 interviewed` + + 2. Test case 1: + `search n/alex bernice` + **Note**: JABPro does an `OR` search for each category meaning it will look for applicants whose names match any of the given names.
+ **Expected**: If you are using our sample data, you should see `Alex Yeoh` and `Bernice Yu` being listed. + + 3. Test case 2: + `search st/interviewed rejected` + **Note**: JABPro does an `OR` search for each category meaning it will look for applicants whose status match any of the given status.
+ **Expected**: If you are using our sample data, you should see `Alex Yeoh` being displayed as he is the only applicant whose status is `interviewed` + (other applicants' status are `preliminary` by default). + + 4. Test case 3: + `search t/intern developer` + **Note**: JABPro does an `OR` search for each category meaning it will look for applicants who have **any** of the given tags.
+ **Expected**: If you are using our sample data, you should see all the applicants listed since all of them + contain either of the given tags. + + 5. Test case 4: + `search n/alex bernice st/interviewed rejected t/intern developer` + **Note**: JABPro does an `AND` search for multiple categories meaning it will look for applicants meaning it will look for applicants + whose name contains either alex or bernice `AND` status is either interviewed or rejected `AND` has any of the given tags.
+ **Expected**: If you are using our sample data, you should see `Alex Yeoh` being the only applicant listed as he is the only applicant matching + the given profile. + + Some incorrect commands to try: `search`, `search n/`, `search t/`, `search st/`, `search n/@`, `search t/@`, `search st/accepted` (note this is incorrect because status has to be either: preliminary, interviewed, rejected or offered) + **Expected**: JABPro displays an error message along with a corrective action you can take to amend the command. + +### Creating tags + +1. Creating tags + 1. Prerequisites: nil + + 2. Test case 1: + `create t/dept finance` + **Expected**: JABPro displays a message indicating the successful execution of command. You can also check that the tag + has been successfully created by using the `listT` command to view the newly created tag. + + 3. Test case 2: + `create t/finance` + **Note**: The creation of tags **must** specify both the **tag category** and **tag name**.
+ **Expected**: JABPro displays an error message. + + 4. Test case 3: + `create t/role analyst, role engineer` + **Note**: Creation of multiple tags is done through using multiple tag prefixes i.e. `create t/role analyst t/role engineer` + **Expected**: JABPro displays an error message. + + 5. Test case 4: + `create t/role software developer` + **Note**: + * Tag names must **not** contain any spaces. Instead, we encourage users to use either `camelCase` or `PascalCase`. This was + done so that the tags would appear neater in the UI. + * Similarly, any other word keyed in after the tag name and without a proper prefix would prompt a similar behaviour. + * JABPro's default behaviour will create the first valid tag it sees. + **Expected**: JABPro displays a message indicating the successful creation of tags. Upon executing `listT` it will be evident that + the tag `software` would be created under the category `role`. Any word that comes after this and not enclosed in a `/t` prefix + is ignored. + + Some other incorrect commands to try: `create t/`, `create t/role @analyst` + **Expected**: JABPro displays an error message along with a corrective action you can take to amend the command. + +### Editing a person's score for an assessment type tag while all persons are being shown + +1. Editing a person's score for an assessment type tag while all persons are being shown + 1. Prerequisites: + 1. List all persons using the `list` command. The list should have at least one person. + 2. Create an `assessment` type tag named `Interview` using the `create` command. This is done by entering `create t/assessment Interview` in the command box. + + 2. Test case 1 (Positive Test Case): + `edit 1 t/Interview sc/Interview 70` + **Note**: The score value should be a positive integer and must contain a space between the tag and the score value.
+ **Expected**: Score for the assessment type tag `Interview` is updated to 70. + Both Person List and Person Information Panel is updated to reflect the new `Interview` Tag. The new score is reflected on the Summary Statistics Screen(Third panel from the left). + + 3. Test case 2 (Negative Test Case): + `edit 1 t/swe sc/swe 70` + **Note**: Tag `swe` is not categorised as an assessment type tag. Thus, you cannot edit the score for this tag.
+ **Expected**: Neither score nor tag is updated for person. Error details shown in the status message. + + 4. Test case 3 (Negative Test Case): + `edit 1 t/Interview sc/Interview -10` + **Note**: The score value should be a positive integer and must contain a space between the tag and the score value.
+ **Expected**: Neither score nor tag is updated for person. Error details shown in the status message. + +### Setting a person's status (Preliminary, Interviewed, Accepted/Rejected) +1. Setting a person's status in the list + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test case 1: + `set 1 Interviewed` + **Expected**: On the next 'view' command, the personnel's details shows as Interviewed. + + 3. Test case 2: + `Set 0 Interviewed` + **Expected**: No person is set to 'Interviewed'. Error details shown in the status message. Person information panel remains the same. + +### Exporting the information into a csv file +1. Exporting all current user data to csv + 1. Prerequisites: Necessary write permissions to the /data/export.csv location + + 2. Test case 1: + `export` + **Expected**: File is successfully exported to the location /data/export.csv + + 3. Test case 2: + `export` (without write permissions) + **Expected**: /data/export.csv file is not updated. Error details shown in the status message. + +### Filtering persons by their scores for a particular tag based on a particular metric while all persons are being shown +1. Filtering persons by their scores for a particular tag based on a particular metric while all persons are being shown + 1. Prerequisites: + 1. List all persons using the `list` command. The list should have at least one person. + 2. Create an `assessment` type tag named `Interview` using the `create` command. This is done by entering `create t/assessment Interview` in the command box. + 3. Edit the score for the `Interview` tag for at least two people using the `edit` command. This is done by entering `edit 1 t/Interview sc/Interview 70` and `edit 2 t/Interview sc/Interview 50` in the command box. + + 2. Test case 1 (Positive Test Case): + `filter t/Interview met/score val/60` + **Expected**: Person list is updated to reflect the persons with scores greater than 60 for the `Interview` tag. In this case its only the person with index 1. + + 3. Test case 2 (Positive Test Case): + `filter t/Interview met/median` + **Expected**: Person list is updated to reflect the persons with scores greater than the median score for the `Interview` tag. In this case its only the person with index 1. + + 4. Test case 3 (Positive Test Case): + `filter t/Interview met/percentile val/0` + **Expected**: Person list is updated to reflect the persons with scores greater than the 0th percentile score for the `Interview` tag. In this case it will be all the persons with the `Interview` tag. + + 5. Test case 4 (Negative Test Case): + `filter t/swe met/score val/60` + **Expected**: No person is filtered. Error details shown in the status message. Person list remains the same. + + 6. Test case 5 (Negative Test Case): + `filter t/Interview met/score val/-10` + **Expected**: No person is filtered. Error details shown in the status message. Person list remains the same. + + 7. Test case 6 (Negative Test Case): + `filter t/Interview met/variance val/100` + **Expected**: No person is filtered. Error details shown in the status message. Person list remains the same. + + 8. Test case 7 (Negative Test Case): + `filter t/Interview met/percentile` + **Expected**: No person is filtered. Error details shown in the status message. Person list remains the same. + +### Adding LinkedIn/GitHub username to a person while all persons are being shown ### + +1. Adding LinkedIn/GitHub username to a person while all persons are being shown + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test case (Positive test case):
`addL 1 u/alexyeoh`
+ **Expected:** LinkedIn username added to the person's profile. Displayed in person card. + + 3. Test case (Negative test case):
`addG -1 u/madlad`
+ **Expected:** No Github username is added to any person. Error details shown in the status message. Person information panel remains the same. + + 4. Test case (Negative test case):
`addL u/maxcodes`
+ **Expected:** No LinkedIn username is added to any person. Error details shown in the status message. Person information panel remains the same. + + 5. Test case (Negative test case):
`addG 1`
+ **Expected:** No Github username is added to any person. Error details shown in the status message. Person information panel remains the same. + +### Viewing person's social profile ### + +1. Viewing a person's social profile + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list, with their usernames previously added. + + 2. Test case (Positive test case):
`linkedin 1`
+ **Expected:** Redirected to LinkedIn profile of the person, in the browser. Success message displayed on JABPro. + + 3. Test case (Negative test case):
`github 0`
+ **Expected:** No Github profile shown. Error details shown in the status message. + + 4. Test case (Negative test case):
`linkedin 2` + [Assumption: LinkedIn username has not been previously added for candidate 2]
+ **Expected:** No LInkedin profile shown. Error details shown in the status message. + + 5. Test case (Negative test case):
`github`
+ **Expected:** No Github profile shown. Error details shown in the status message. + +### Adding Event relating to a candidate ### + +1. Adding event relating to a candidate + 1. Prerequisites: List all persons using the `list` command. Multiple person in the list. + + 2. Test case (Positive test case):
`event 1 d/Interview bt/2023-11-12 10:00 et/2023-11-12 12:00`
+ **Expected:** Event added to EventBook. Success message displayed. Event visible in Events window. + + 3. Test case (Negative test case):
`event 0 d/Interview bt/2023-11-12 10:00 et/2023-11-12 12:00`
+ **Expected:** No event added to EventBook. Error details shown in status message. Event Window remains the same. + + 4. Test case (Negative test case):
`event 1 bt/2023-11-12 10:00 et/2023-11-12 12:00`
+ **Expected:** No event added to EventBook. Error details shown in status message. Event Window remains the same. + + 5. Test case (Negative test case):
`event 1 d/Interview bt/12-11-2023 10:00 et/12-11-2023 12:00`
+ **Expected:** No event added to EventBook. Error details shown in status message. Event Window remains the same. + + 6. Test case (Negative test case):
`event 1 d/Interview bt/2023-11-12 12:00 et/2023-11-12 10:00`
+ **Expected:** No event added to EventBook. Error details shown in status message. Event Window remains the same. + + 7. Test case (Negative test case):
`event 1 d/Interview bt/2023-11-31 10:00 et/2023-12-01 10:00`
+ **Expected:** No event added to EventBook. Error details shown in status message. Event Window remains the same. ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ - -1. _{ more test cases …​ }_ + 1. On your command terminal, `cd` into the folder where your jar file is in. + 2. Run `cd Data` to enter the directory where all of JABPro's data is stored. + 3. Run `ls` to view all files stored in this directory. + 4. If you see any of the following json files: `addressbook.json` or `eventbook.json`, delete them using + the `rm` command e.g. `rm addressbook.json` to start fresh and remove any corrupted data files. diff --git a/docs/Documentation.md b/docs/Documentation.md index 3e68ea364e7..082e652d947 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -1,29 +1,21 @@ --- -layout: page -title: Documentation guide + layout: default.md + title: "Documentation guide" + pageNav: 3 --- -**Setting up and maintaining the project website:** - -* We use [**Jekyll**](https://jekyllrb.com/) to manage documentation. -* The `docs/` folder is used for documentation. -* To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html). -* Note these points when adapting the documentation to a different project/product: - * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar. - * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format). -* If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping) +# Documentation Guide +* We use [**MarkBind**](https://markbind.org/) to manage documentation. +* The `docs/` folder contains the source files for the documentation website. +* To learn how set it up and maintain the project website, follow the guide [[se-edu/guides] Working with Forked MarkBind sites](https://se-education.org/guides/tutorials/markbind-forked-sites.html) for project documentation. **Style guidance:** * Follow the [**_Google developer documentation style guide_**](https://developers.google.com/style). +* Also relevant is the [_se-edu/guides **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html). -* Also relevant is the [_[se-edu/guides] **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html) - -**Diagrams:** - -* See the [_[se-edu/guides] **Using PlantUML**_](https://se-education.org/guides/tutorials/plantUml.html) -**Converting a document to the PDF format:** +**Converting to PDF** -* See the guide [_[se-edu/guides] **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html) +* See the guide [_se-edu/guides **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html). diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index c8385d85874..00000000000 --- a/docs/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - -gem 'jekyll' -gem 'github-pages', group: :jekyll_plugins -gem 'wdm', '~> 0.1.0' if Gem.win_platform? -gem 'webrick' diff --git a/docs/Logging.md b/docs/Logging.md index 5e4fb9bc217..589644ad5c6 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -1,8 +1,10 @@ --- -layout: page -title: Logging guide + layout: default.md + title: "Logging guide" --- +# Logging guide + * We are using `java.util.logging` package for logging. * The `LogsCenter` class is used to manage the logging levels and logging destinations. * The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..03df0295bd2 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -1,27 +1,32 @@ --- -layout: page -title: Setting up and getting started + layout: default.md + title: "Setting up and getting started" + pageNav: 3 --- -* Table of Contents -{:toc} +# Setting up and getting started + + -------------------------------------------------------------------------------------------------------------------- ## Setting up the project in your computer -
:exclamation: **Caution:** + +**Caution:** Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps. -
+ First, **fork** this repo, and **clone** the fork into your computer. If you plan to use Intellij IDEA (highly recommended): 1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to to ensure Intellij is configured to use **JDK 11**. -1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
- :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. +1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA. + + Note: Importing a Gradle project is slightly different from importing a normal Java project. + 1. **Verify the setup**: 1. Run the `seedu.address.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. @@ -34,10 +39,11 @@ If you plan to use Intellij IDEA (highly recommended): If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours. -
:bulb: **Tip:** + + **Tip:** Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code. -
+ 1. **Set up CI** diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..78ddc57e670 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -1,12 +1,15 @@ --- -layout: page -title: Testing guide + layout: default.md + title: "Testing guide" + pageNav: 3 --- -* Table of Contents -{:toc} +# Testing guide --------------------------------------------------------------------------------------------------------------------- + + + + ## Running tests @@ -19,8 +22,10 @@ There are two ways to run tests. * **Method 2: Using Gradle** * Open a console and run the command `gradlew clean test` (Mac/Linux: `./gradlew clean test`) -
:link: **Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle. -
+ + +**Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle. + -------------------------------------------------------------------------------------------------------------------- diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 57437026c7b..c10772948ac 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,197 +1,1329 @@ --- -layout: page -title: User Guide + layout: default.md + title: "User Guide" + pageNav: 3 --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +# JABPro User Guide -* Table of Contents -{:toc} +-------------------------------------------------------------------------------------------------------------------- + +## Table of Contents +* [Product Overview](#product-overview) +* [Features Overview](#overview-of-main-features) +* [Key Definitions](#key-definitions) +* [Quick Start](#quick-start) +* [Command Failures](#command-failure) +* [Viewing help](#viewing-help-help) +* [Features](#features) + * [Managing Tags](#1-managing-tags) + * [Creating tags](#creating-tags-create) + * [Listing all tags](#listing-all-tags-listt) + * [Managing Applicants](#2-managing-applicants) + * [Adding an applicant](#adding-an-applicant-add) + * [Adding a remark to an applicant](#adding-a-remark-to-an-applicant-remark) + * [Adding applicant's LinkedIn/GitHub](#adding-linkedingithub-username-for-a-user-addl-or-addg) + * [Opening applicant's LinkedIn/GitHub](#opening-user-linkedin-or-github-account-linkedin-or-github) + * [Setting an applicant's status](#setting-an-applicants-status-set) + * [Viewing an applicant's details](#viewing-a-applicants-details-view) + * [Editing an applicant's detail](#editing-a-applicant-edit) + * [Deleting an applicant](#deleting-job-applicants-delete) + * [Filtering and Listing Applicants](#3-filtering-and-listing-applicants) + * [Searching for applicants](#searching-job-applicants-by-category-search) + * [Filtering applicants](#filter-job-applicants-by-statistics-filter) + * [Listing all applicants](#listing-all-applicant-list) + * [Managing Events](#4-managing-events) + * [Adding an event](#adding-an-event-event) + * [Viewing all events](#viewing-events-schedule) + * [Others](#5-others) + * [Exporting all entries](#exporting-the-existing-data-to-csv-export) + * [Clear all entries](#clearing-all-entries-clear) + * [Exiting the program](#exiting-the-program-exit) +* [Additional Information](#additional-information) + * [Summary Statistics](#summary-statistics) + * [Saving Data](#saving-the-data) + * [Editing Data](#editing-the-data-file) +* [FAQ](#faq) +* [Command Summary](#command-summary) +* [Prefix Summary](#prefix-summary) +* [Glossary](#glossary) -------------------------------------------------------------------------------------------------------------------- -## Quick start +## Product Overview + +Are you a Hiring Manager who's tired of managing applicant applications through cumbersome spreadsheets? + +Upgrade your hiring process with **JABPro (JobApplicationsBook Pro)**, a CLI based desktop app that allows you to: + +* easily manage job applicants' contact details, +* schedule interviews, +* and gain valuable insights on their interview performance!
+ +From interns to full-time roles, software to marketing, JABPro’s versatile interface allows you to keep track of all kinds of job applicants in various industries. + +**If you are a Hiring Manager, looking to improve your applications management workflow, JABPro is the tool for you!** + +Access our self-curated user guide below to learn more on how you can integrate various JABPro’s functions into your workflow. + +[Jump back to Table of Contents](#table-of-contents) + +## Features Overview + + +While **JABPro** offers a whole range of features, we believe that the following features are likely to be the most useful to you: + +**Viewing details of applicants:** +* Viewing applicant's information: `view` + +**Tag colouring and categorisation:** +* Creating a tag with a specified category: `create` +* Listing all tags: `listT` +* Adding and editing an applicant's tags and tag scores: `edit` + +**Event management and Scheduling:** +* Adding an event: `event` +* Viewing events: `schedule` + +**Comparing and filtering applicants:** +* Filtering applicants by statistics: `filter` + +These features address the complications that Hiring Managers face when managing applicants: +1. **Visual Noise and Clutter** from using other applicant management software like Excel + +`Viewing details of applicants` and `Tag colouring and categorisation` address this by creating an organized and intuitive way to view applicants and their details. This is done through colour coding and minimalistic design. + +2. **Toggling between different software** to manage applicants and schedule events + +`Event Management and Scheduling` address this by allowing you to schedule events and view them in the same software. This means that you do not have to toggle between different software to manage applicants and schedule events. + +3. Having to manually calculate summary statistics and compare applicants +`Comparing and filtering applicants` address this by allowing you to filter applicants by statistics and compare them. This means that you do not have to manually calculate summary statistics and compare applicants. + +-------------------------------------------------------------------------------------------------------------------- +Go to the [Table of Contents](#table-of-contents) to navigate to the feature that you are interested in + +
+ +-------------------------------------------------------------------------------------------------------------------- + +## Key Definitions + +Our user guide takes into consideration your level of expertise in JABPro. + +You are a **beginner** user if ... + +1. You are new to JABPro (used JABPro less than 5 times) *and*, +2. you wish to fully rely on the JABPro interface. + +ALl the **notes** and **tips** mentioned in this user guide are directed towards beginners unless otherwise stated. + +You are an **advanced** user if ... + +1. You have used JABPro multiple times now *and*, +2. you use JABPro's search and summary statistics extensively for comparison *or* +3. you would like to challenge yourself to go beyond the JABPro user interface and manually edit files. + +**Notes** and **tips** curated for you are labeled by the following box! + + +
+

Advanced

+
+ +
+ +**Also, take note of the following icons and their meanings.** + + + +This is a warning. Watch out for these! + + + + +This refers to highlighted information that you should take note of! + + + + +This is a tip. It's good to know but not a must-have! + + +[Jump back to Table of Contents](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +## Quick Start 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `jabpro.jar` from [here](https://github.com/AY2324S1-CS2103T-W09-4/tp/releases). + +3. Copy the file to the folder you want to use as the _home folder_ for your JabPro. + +4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar jabpro.jar` command to run the application.
+ A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data. + + ![Ui](images/main.png) + + + +**Caution:** ensure your JABPro contains some sample data. If it starts off empty then there might be some issues with the launch which might result in +some commands to not run properly. See below to learn how to troubleshoot this problem! + + + + -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +**My app does not contain any sample data!** +Not to worry, here are some steps you can take to fix this: +1. On your command terminal, `cd` into the folder where you put the jar file in. +2. Run `cd Data` to navigate to the `Data` folder. This is the folder where JABPro stores the application data on your computer. +3. Run `ls` to view all the files stored in this folder. +4. You should be able to see a file titled `addressbook.json`. Run `rm addressbook.json` to delete this file. +5. Run `cd ..` to navigate back to the folder you were in before. +6. Run `java -jar jabpro.jar` to relaunch the application. You should be able to see a GUI similar to the one above. -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +
-1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +5. If your UI looks **compressed and words are being cut off such as that seen below**, you should **resize** the window to a larger size by dragging the corners of the application window. The UI should now look like the example given above. +
+ + ![Ui](images/UICompressed.png) - * `list` : Lists all contacts. +
- * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + **Here's what each part of the GUI signifies:**
- * `delete 3` : Deletes the 3rd contact shown in the current list. + ![UiBreakdown](images/uibreak.png) - * `clear` : Deletes all contacts. - * `exit` : Exits the app. +| Colour | Component | Description | +|----------------------------------------------------------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| | Menu Bar | Provides buttons for exiting, opening Help window, and opening Events window. | +| | Command Box | Allows you to enter a command. | +| | Result Display | Displays the result of the command execution. | +| | Applicant List Panel | Displays a list of all applicants in JABPro. | +| | Applicant Card | Displays certain details of an applicant for quick view, such as name, address, phone, email, tags, LinkedIn/GitHub username. | +| | Applicant Information Panel | Displays a detailed view of an applicant, providing information of status and remarks, in addition to the basic information about the applicant. | +| | Summary Statistics Panel | Displays summary statistics for a particular applicant pertaining to a specific tag. | -1. Refer to the [Features](#features) below for details of each command. +
+ +In addition, there are windows such as: + * Help Window [accessed by the `help` command, or through Menu Bar]. + * Events Window [accessed by the `schedule` command, or through Menu Bar]. + * TagList window [accessed through the `listT` command]. + + Details for each have been provided with the respective commands.
+ +6. Type the command in the command box and press Enter to execute it. Go to our [command summary](#command-summary) to see + some example commands you can try. + +[Jump back to Table of Contents](#table-of-contents) + +
-------------------------------------------------------------------------------------------------------------------- +## Command Failure + + +**How to know if your command has failed?** +1. You will see the command that you have entered being highlighted in red. +2. The command will not be cleared from the command box. +3. The error message will be shown in the result display panel. +4. The UI below will not be updated if your command has failed. + + +**The example below shows a command failure for `view`:** +![CommandFailure](images/view-command-failure.png) +
+ +[Jump back to Table of Contents](#table-of-contents) + +## Viewing help: `help` + + +Opens the `Help window` that leads you to the User Guide for assistance on working with JABPro. + +Format: `help` + + + +**Tip:** The `Help window` can also be accessed by clicking `Help > Help F1` in the menu bar, located at the top of the window. + +Additionally, pressing the `F1` key also opens the `Help Window`. + + + + + +**Example of successful execution of the `help` command:** + +1. Enter the command `help` +2. This is the result of the successful `help` command: + +![Help](images/helpwindow-mh.png) + +The `Help Window` opens up as follows: + +![HelpWindow](images/hw.png) + +[Jump back to Table of Contents](#table-of-contents) + +
+ ## Features -
+ -**:information_source: Notes about the command format:**
+**Notes about the command format:**
* Words in `UPPER_CASE` are the parameters to be supplied by the user.
e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g. `n/NAME [t/TAGNAME]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + +* When using parentheses ( ) with items separated by the slash symbol /, at least one item must be included.
+ e.g. in the command `search (n/NAME... st/STATUS... t/TAGNAME...)`, it is necessary to specify at least one search category. * Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. + e.g. `t/TAGNAME…​` can be used as ` ` (i.e. 0 times), `t/swe t/intern` for `add` commands or `t/swe intern` for `search` and `delete` commands. * Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. + e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* Extraneous parameters for commands that do not take in parameters (such as `help`, `listT`, `exit` and `clear`) will be ignored.
+ e.g. if the command specifies `help 123`, it will be interpreted as `help`. * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. +
+ +[Jump back to Table of Contents](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +### 1. Managing Tags + +Tags are meant to help you easily remember applicants details by appending different colour coding to different types of information. + +#### Creating tags: `create` + + +Creates a tag and categorises it to the specified category. + +Format: `create t/CATEGORY TAGNAME…​` + +| Type | Field | Constraints | +|-----------|----------------------|----------------------------------------------------------------------------------------------------------| +| Mandatory | `t/CATEGORY TAGNAME` | `TAGNAME` must be alphanumeric (letters and numbers, no spaces and symbols allowed such as `/`, `,` ...) | + +**Note:** +* JABPro offers 3 predefined tag categories namely `employment`, `role`, and `dept`. However, you can define up to 3 more tag categories of your own! +* The tags created using this command can be used to tag applicants using the `add` or `edit` command. Tagging + applicant without previously creating the tags using `create` would still work but the tags would be *uncategorised*. +* `create` only allows tags to be categorised at creation meaning tags that have already been created, cannot be categorised further i.e. cannot edit tag categories of tags. +* The field `t/CATEGORY TAGNAME` must strictly contain only two words hence it is advisable for you to keep the `TAGNAME` alphanumerical (contains no spaces). Any other word + that comes after `t/CATEGORY TAGNAME` that is not preceded by a `t/` prefix would be ignored and the tag for the first valid tag is created. +
+ Example: `create t/role software developer` would create the tag **software** and ignore the word developer. + + + +**Tip:** +* You can create multiple tags at once i.e. `create t/dept marketing t/role developer ...` +* Use this command for frequently used tags for better efficiency in tagging applicants. +* You can view all of your tags by keying in the `listT` command. + + +**An example of the `create` command being successfully executed:** +1. Enter the command `create t/dept marketing t/role developer` +2. This is what you should see upon successful execution of command. + + ![create-success](images/create-success.png) + +3. View your newly created tags using the `listT` command. + + ![listT-create](images/listT-create-success.png) + +**Failed to create tags? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| Missing create keyword: `create` | Unknown command | Follow the command format of `create t/CATEGORY TAGNAME…​` closely | +| Missing mandatory field e.g. `create` | Invalid command format! | Ensure that you specify at least one tag category and tag name of the tag you would like to create. | +| Incomplete field e.g. `create t/test` | Invalid command format! | Ensure that both parts of the field are included i.e. specify both tag category and tag name. | +| Invalid tag name e.g. `create t/developer@` | Tags names should be alphanumeric. | Ensure that the tag name does not contain any non-alphanumeric characters i.e. no symbols and whitespaces. | +| Tag already exists | This tag already exists in the address book! | Since the tag already exists, there is no need for you to create a new one. You can reuse this same tag to tag applicants! | +| Using commas as delimiters of different prefixes instead of whitespaces e.g. `create t/dept software, t/role marketing` | Invalid command format! | Remove the comma(s) e.g. `create t/dept software, t/role marketing` | | + +[Jump back to Table of Contents](#table-of-contents) + +#### Listing all tags: `listT` + + +Shows a list of all tags in JABPro + +Format: `listT` + +* The `listT` command does not require any additional parameters or arguments. +* Tags listed by the `listT` command are unique and do not repeat. + +**Example:** +* `listT` Shows a list of all tags. + +![ListT](images/listT.png) + +[Jump back to Table of Contents](#table-of-contents) + +
+ + +### 2. Managing Applicants + + +#### Adding an applicant: `add` + + +Adds an applicant to JABPro. + +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/[CATEGORY] TAGNAME]…​` + +| Type | Field | Constraints | +|-----------|------------------------|-----------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `n/NAME` | `NAME` must be alphanumeric (Letters and numbers, no symbols allowed such as `/`, `,` ...). | +| Mandatory | `p/PHONE_NUMBER` | `PHONE_NUMBER` must contain numbers only and should be at-least 3 digits long. | +| Mandatory | `e/EMAIL` | `EMAIL` must be the standard email address format (There must be an email-prefix followed by `@` symbol and email domain). | +| Mandatory | `a/ADDRESS` | `ADDRESS` can be any value, including special characters such as `#`, `,` ... | +| Optional | `t/[CATEGORY] TAGNAME` | `[CATEGORY]` is optional. TAGNAME` must be alphanumeric with no spaces. Any details after the space will be ignored. | + +**Notes regarding additional constraint on `add` command:** +* The uniqueness of the applicant is determined by the name only. This means that you cannot have 2 applicants with the same name in the application book. +* All other fields other than name can be identical between different people in JABPro. +* Applicants added using the `add` command will be added to the end of the overall list of applicants (i.e. the list that you would get when you do `list`). + +**Notes on adding tags:** +* If you would like to tag a user with a tag that has not been categorised yet using the `create` command, + you can specify the category that you would like it to be categorised to in the `add` command. e.g. `...t/role swe` +* If you are using a tag that has not been categorised yet, and you did not specify its category in the `add` command, + the tag would still be saved, but it would be _uncategorized_ by default. +* If you have multiple tags in different categories with the same name, you must specify the category when you want to + add one of these tags to the applicant you are adding. + + + +**Tip:** +* An applicant can have any number of tags (including 0)! + + +**An example of the `add` command being successfully executed:** +1. Enter the command `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/dept finance` +2. This is the result of the successful `add` command (Take note that command entered will not be shown in the result): + + ![Add-Success](images/add-command-success.png) +
+ +**Failed to add applicants? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-----------------------------------------------|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| Missing add keyword: `add` | Unknown command | Follow the command format of `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAGNAME]…​` closely | +| Missing mandatory fields | Invalid command format! | Ensure that all mandatory fields are filled up. | +| Duplicate name | This person already exists in the address book | Ensure that the name of the applicant is unique. That is you cannot add the same name twice. Use some form of extra identification like a number | +| Invalid phone number | Phone numbers should only contain numbers, and it should be at least 3 digits long | Ensure that the phone number only contains number and should be at least 3 digits long | +| Invalid email | Emails should be of the format local-part@domain and adhere to the following constraints: | Ensure that the prefix and domain of the email is correct following the constraints stated by the error | +| Invalid tag name | Tag names should only contain alphanumeric characters and should not be blank | Ensure that the tag name only contains alphanumeric characters and should not be blank | +| Multiple prefixes of the same type being used | Multiple values specified for the following single-valued field(s): `prefix/` | Remove the duplicate prefix. The command should only have 1 of every prefix except for `t/` | + +[Jump back to Table of Contents](#table-of-contents) + +#### Adding a remark to an applicant: `remark` + + +Edits a remark of an existing applicant in JABPro. +Format: `remark INDEX r/REMARK` + +| Type | Field | Constraints | +|-----------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be an existing index in the displayed applicant list and it must not be greater than the total number of applicants in JABPro. | +| Optional | `r/ [REMARK]` | `REMARK` can be any value, including special characters such as `#`, `,` ... | + +**Notes regarding `remark` command:** +* The previous remark is not saved, and instead is replaced by the inputted remark. The command does not add to the existing remark. +* You can empty out a remark by inputting `r/` without any text after it or by omitting the `r/` prefix. +* You can get the remark previously inputted by using the `**REMARK**` keyword. It will be replaced with the previous remark (see example below). The keyword `**REMARK**` is case-sensitive. This means that `remark 1 r/**remark**` will just replace the remark with the word `**remark**`. +* You can use multiple prefix for `remark` but only the last prefix will be used. This means that `remark 1 r/remark r/remark2` will just replace the remark with `remark2`. + + +**An example of the `remark` command being successfully executed:** +1. Enter the command `remark 1 r/Great attitude, hardworking` +2. This is the result of the successful `remark` command (Take note that command entered will not be shown in the result): + ![Remark-Success](images/remark-command-success.png) +
+ +
+ +**An example of the `remark` command being successfully executed with the **REMARK** keyword:** +1. Enter the command `remark 1 r/**REMARK** furthermore he is great at teamwork` +2. This is the result of the successful `remark` command (Take note that command entered will not be shown in the result): + ![Remark-Success](images/remark-command-enhanced-success.png) +
+ +**Failed to add remark to an applicant? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|----------------------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------| +| Missing remark keyword: `remark` | Unknown command | Follow the command format of `remark INDEX r/[REMARK]` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | + | Negative or 0 Index | Invalid command format! | Ensure that the index is a positive integer and is also a number that is on the displayed applicant list. | + +**Additional Examples:** +* `remark 1` Empties the remark of the 1st applicant. It is equivalent to `remark 1 r/`. + +[Jump back to Table of Contents](#table-of-contents) + +#### Adding LinkedIn/GitHub username for a user: `addL` or `addG` + + +Adds the username for their social profile [LinkedIn/GitHub] to the existing contact details of applicants. + +Format: `addL INDEX u/USERNAME` or `addG INDEX u/USERNAME` + +| Type | Field | Constraints | +|-----------|--------------|-----------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be a non-zero unsigned integer and it must not be greater than the total number of applicants in JABPro. | +| Mandatory | `u/USERNAME` | `USERNAME` must be a string value. Only the prefix (i.e. `u/`) is mandatory. | + +**Notes regarding `addL` and `addG` command:** + +* Any set of characters entered after the prefix `u/` is taken to be the username, except if the prefix `u/` occurs multiple times. +* You may provide the username multiple times with the prefix `u/`, however, JABPro only considers the set of characters entered after the last occurring instance of `u/` as the username. +* You are expected to ensure that `USERNAME` is a valid username for the respective social profile. If it is not a valid username, user will be redirected to the error page of the corresponding social profile when `linkedin` or `github` command is invoked. JABPro does not perform checks for the validity of the username for the corresponding social profile. +* Invoking the `addL` or `addG` command for an applicant for whom a username has already been added, will simply overwrite the existing username with the new one. +* You may run the command `addL INDEX u/` or `addG INDEX u/`, i.e. providing no username, or simply providing blanks for the username. Such inputs are accepted by JABPro. However, it will prove to be erroneous when `linkedin` or `github` command is invoked. + +**Example of successful execution of the `addL` command:** + +1. Enter the command `addL 1 u/alexyeoh` +2. This is the result of the successful `addL` command + +![AddL](images/addL.png) + +**`addG` command is invoked in the same way.** + +**Failed to add LinkedIn/Github username? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|----------------------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------| +| Missing `addL` or `addG` keyword | Unknown command | Follow the command format of `addL INDEX u/USERNAME` or `addG INDEX u/USERNAME` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | +| Negative or 0 Index | Invalid command format! | Ensure that the index is a positive integer and is also a number that is on the displayed applicant list. | +| Missing username | Invalid command format! | Ensure that the username is filled up | + +[Jump back to Table of Contents](#table-of-contents) + +#### Opening user LinkedIn or GitHub account: `linkedin` or `github` + + +Redirects user to applicant's LinkedIn or GitHub account. + +Format: `linkedin INDEX` or `github INDEX` + +| Type | Field | Constraints | +|-----------|---------|-----------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be a non-zero unsigned integer and it must not be greater than the total number of applicants in JABPro. | + +**Notes regarding `LinkedIn` and `GitHub` commands:** + +* User is expected to enter `INDEX` for an applicant for whom username [that is not blank, or does not comprise of only spaces] has been added previously. +* User is redirected to the page of the social profile regardless of the validity of the username for that particular social profile (i.e. whether the social profile exists) + +**Example of successful execution of `github` command:** + +1. Enter the command `github 1` +2. This is the result of the successful `github` command [It is assumed an applicant exists in JABPro, with Github username previously added]: + +![Github](images/linkedin.png) + +The GitHub window opens as follows, displaying the profile with the specified username, or error page in case profile with that username does not exist: + +![GithubProfile](images/github.png) + +`linkedin` command is invoked in the same manner. + +**Failed to redirect to social profile? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|----------------------------------------|----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| +| Missing `linkedin` or `github` keyword | Unknown command | Follow the command format of `linkedin INDEX` or `github INDEX` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | +| Negative or 0 Index | Invalid command format! | Ensure that the index is a positive integer and is also a number that is on the displayed applicant list. | +| Missing account for provided Index | No LinkedIn account has been added for this candidate. or No Github account has been added for this candidate. | Ensure that username has been previously added to the specified candidate | + +[Jump back to Table of Contents](#table-of-contents) + +#### Setting an applicant's status: `set` + + +Sets the applicant to a specific status ("Preliminary"/ "Interviewed"/ "Rejected"/ "Offered"). + +Format: `set INDEX STATUS` + +| Type | Field | Constraints | +|-----------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be an existing index in the displayed applicant list and it must not be greater than the total number of applicant in JABPro. It must also be a positive integer | +| Mandatory | `STATUS` | `STATUS` must be one of the following - "Preliminary", "Interviewed", "Rejected", "Accepted". It is case-insensitive. | + +**Examples:** +* `list` followed by `set 2 Interviewed` sets the 2nd applicant in the address book to "Interviewed". + +[Jump back to Table of Contents](#table-of-contents) + +#### Viewing an applicant's details: `view` + + +Creates a complete view for details of an applicant in the applicant information panel and summary statistics (if applicable) of an applicant in the summary statistics panel. + +Format: `view INDEX` + +| Type | Field | Constraints | +|-----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be an existing index in the displayed applicant list and it must not be greater than the total number of applicant in JABPro. It must also be a positive integer. | + +**Notes regarding `view` command:** +* Refer to the [Summary Statistics](#summary-statistics) section for more details on the summary statistics. + + + +**Tip:** Other operations that affect user's data will trigger a refresh of the view. +These include `add`, `edit`, `set`, `remark`, `addL`, `addG`. +This means that the view will be updated to reflect the latest changes to the data for that particular applicant. + + + +**An example of the `view` command being successfully executed:** +1. Enter the command `view 3` +2. This is the result of the successful `view` command (Take note that command entered will not be shown in the result): + + ![View-Success](images/view-command-success.png) +
+ +**An example of the `view` command being successfully executed for applicant with tags and score:** +1. Enter the command `view 2` (**Applicant with tags and score**) +2. This is the result of the successful `view` command (Take note that command entered will not be shown in the result): + + ![View-Success](images/view-command-with-stats-success.png) +
+ + +**Failed to execute `view` command? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|------------------------------|--------------------------------------|-------------------------------------------------------------------------------------------------| +| Missing view keyword: `view` | Unknown command | Follow the command format of `view INDEX` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | + +[Jump back to Table of Contents](#table-of-contents) + +#### Editing an applicant: `edit` + + +Edits an existing applicant's detail in JABPro. It also includes the functionality to add scores to a specific applicant. + +Format: `edit INDEX ([n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAGNAME]…​ [sc/TAGNAME SCORE])` + +| Type | Field | Constraints | +|------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be a non-zero unsigned integer and must also not be greater than the total number of applicants in JABPro. | +| Mandatory* | `n/NAME` | `NAME` must be alphanumeric (Letters and numbers, no symbols allowed such as `/`, `,` ...). | +| Mandatory* | `p/PHONE_NUMBER` | `PHONE_NUMBER` must contain numbers only and should be at-least 3 digits long. | +| Mandatory* | `e/EMAIL` | `EMAIL` must be the standard email address format (There must be an email-prefix followed by `@` symbol and email domain). | +| Mandatory* | `a/ADDRESS` | `ADDRESS` can be any value, including special characters such as `#`, `,` ... | +| Mandatory* | `t/[CATEGORY] TAGNAME` | `TAGNAME` must be alphanumeric with no spaces. Any details after the space will be ignored. | +| Mandatory* | `sc/TAGNAME SCORE` | `TAGNAME` a tag that is being created or already exist for that applicant. `SCORE` must be a non-negative integer. | + +*it is mandatory if and only if it is the only field used in the command. + +**Notes regarding `edit` command:** +* At least one of the optional fields must be provided. +* Existing attributes will be updated to the parameters. +* There is a way to edit tags to an existing applicant and their categories at the same time. Look at the notes for editing tags with categories `t/[CATEGORY] TAGNAME` for more details. + + + +**Tip:** Editing an applicant's details will trigger a refresh of the view. This means that the view will be updated to reflect the latest changes to the data for that particular applicant. + + +**Editing tags of an applicant? Take note of the following**: +* When editing the tags of a specific applicant, the existing tags of the applicant will be removed i.e. adding of tags is **not** cumulative. + You will have to re-tag the applicant with the existing tags and the new tags. +* Doing `edit INDEX /t` removes the tags of the applicant at that index. +* If you used a tag that has not been created using `create` in an `edit` command, the tag would still be added to the applicant, but it would be _uncategorized_. +* If you have **multiple tags in different categories with the same name**, you must **specify the category** when you want to tag the specified applicant with one of these tags e.g. `edit INDEX t/CATEGORY DUPLICATETAGNAME` + + +
+

Advanced

-### Viewing help : `help` -Shows a message explaning how to access the help page. +* If you still would like to tag an applicant that has **not** been created, but you do not want this tag to be _uncategorized_, you can do so by doing +`edit INDEX t/CATEGORY TAGNAME`. -![help message](images/helpMessage.png) + -Format: `help` +**Caution:** +If you tag a person with a tag that has not been categorised yet without specifying the category i.e. `edit 1 t/UNCATEGORIZEDTAG`, and then you created that same tag with a specific category using `create`, +you need to re-tag the person with the same tag name again in order to overwrite the previous uncategorized tag with the categorized tag. +This is because, `create` only creates **new** categorized tags. It does **not** categorize existing tags when you do `create t/CATEGORY EXISTINGTAGNAME` + -### Adding a person: `add` +**Editing the score of an applicant? Take note of the following**: +* The `TAG` in `sc/TAGNAME SCORE` must be a tag of the category `assessment`. You cannot use the `sc/TAG SCORE` field for tags that are not of the `assessment` category. +* The `SCORE` in `sc/TAG SCORE` is non-negative, that is `SCORE` must be more than or equal to 0. +* To clear a tag's score, just re-tag it with the same tag name, but without using the `sc/TAG SCORE` field e.g. `edit 1 t/SCORETAG` +* You can only edit the score of an applicant i.e. `edit INDEX sc/TAGNAME SCORE` if they have been tagged with the assessment-related `TAGNAME`. -Adds a person to the address book. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` -
:bulb: **Tip:** -A person can have any number of tags (including 0) +
+

Advanced

-Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` -### Listing all persons : `list` +* You can also tag the applicant and edit their score **at the same time** by doing `edit INDEX t/TAGNAME sc/TAGNAME SCORE`. +* Note that the tag has to have already been created using `create` with the category `assessment`. You **cannot** do this with a tag that hasn't been created even if you specified its category _assessment_ i.e. `edit INDEX t/assessment interview1 sc/interview1 100` does **NOT** +work. This is because, you cannot create tag and edit score at the same time as editing score relies on the fact that the tag has already existed. -Shows a list of all persons in the address book. + -Format: `list` +**How is creating tags using `edit` different from `create`?** +
+Both will create categorized tags and add them to the tag list but `create` serves the singular purpose of creating categorized tags, while the main purpose of `edit` +is to _edit_ the details of an applicant which includes tags. +
+The reason why we've allowed you to create tags in the event you use `edit` with a tag that +hasn't been created is for the sake of convenience (i.e. if you had forgotten to create the tag, it would still be added to the applicant you were editing). +
+Nonetheless, we **strongly recommend you to use `create` to create categorized tags** if your only intention is to _create tags_. +
-### Editing a person : `edit` -Edits an existing person in the address book. +**An example of the `edit` command being successfully executed:** +1. Enter the command `edit 1 n/Alex Ho p/91234567` (**edits name and phone number**) +2. This is the result of the successful `edit` command (Take note that command entered will not be shown in the result): + + ![Edit-Success](images/edit-command-success.png) +
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +**An example of the `edit` command being successfully executed with tags and score:** +1. Ensure that you have created a tag `Interview` under the `assessment` category using the `create` command. Enter the command `create t/assessment Interview` +2. Enter the command `edit 1 t/Interview sc/Interview 80` (**edits tag and score**) +3. This is the result of the successful `edit` command (Take note that command entered will not be shown in the result): + + ![Edit-Success](images/edit-command-with-stats-success.png) +
+ +**An example of the `edit` command being successfully executed to clear a tags and score:** +1. Enter the command `edit 1 t/` (**Clear all tags**) +2. This is the result of the successful `edit` command (Take note that command entered will not be shown in the result): + + ![Edit-Success](images/edit-command-clear-tags-success.png) +
+ +**Failed to edit an applicant's details? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|----------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Missing edit keyword: `edit` | Unknown command | Follow the command format of `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAGNAME]…​ [sc/TAGNAME SCORE]` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is a valid number on the displayed applicant list. | +| Missing at least one of the field | At least one field to edit must be provided. | Ensure that at least one of the field is filled up and to be changed. | +| Duplicate name | This person already exists in the address book | Ensure that the name of the applicant is unique. That is, you cannot add the same name twice. Use some form of extra identification like a number | +| Invalid phone number | Phone numbers should only contain numbers, and it should be at least 3 digits long | Ensure that the phone number only contains numbers and should be at least 3 digits long | +| Invalid email | Emails should be of the format local-part@domain and adhere to the following constraints: ... | Ensure that the prefix and domain of the email is correct, following the constraints stated by the error | +| Invalid tag name | Tag names should only contain alphanumeric characters and should not be blank | Ensure that the tag name only contains alphanumeric characters and should not be blank | +| Multiple prefixes of the same type being used (does not apply to tag prefix `t`) | Multiple values specified for the following single-valued field(s): `prefix/` | Remove the duplicate prefix. The command should only have 1 of every prefix except for `t/` | +| Missing score for tag | Invalid score, score must be non-negative integer. | Ensure that the score is filled up and is separated from the `TAGNAME` by a whitespace (NOT commas). | +| Invalid tag to attach score | Invalid score tag, tag must a tag of the category assessment and must exist on the applicant | Ensure that the tag is of the category assessment and it exists on the applicant. If it's the wrong category, use `create` i.e. `create t/assessment TAGNAME` to firstly create the categorized tag. If it is not tagged to the person use `edit` i.e. `edit INDEX t/TAGNAME` | +| Missing valid score-tag on applicant | The tag does not exist, cannot attach a score to it | Ensure that the applicant has the tag and it is of category `assessment`, this is what is considered a valid score-tag. This is done by creating an `assessment` category for the tag name using `create` and update tag using `edit INDEX t/TAGNAME ...` | +| Tag ambiguity | Multiple tags exists with the same name! Specify the category of the tag when adding it to a person e.g. edit 1 t/experience 3 | Specify the category of the tag you want to add in the field e.g.`edit INDEX t/CATEGORY DUPLICATETAGNAME` | + + +[Jump back to Table of Contents](#table-of-contents) + +#### Deleting job applicants: `delete` + + +Deletes the specified job applicants from the address book. + +Format: `delete INDEX` + +| Type | Field | Constraints | +|-----------|---------|-------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be a non-zero unsigned integer and must also not be greater than the total number of applicants in JABPro. | + +Format: `delete (t/TAGNAME... st/STATUS...)` + +| Type | Field | Constraints | +|------------|-------------|------------------------------------------------------------------------------| +| Mandatory* | `t/TAGNAME` | `TAGNAME` must be alphanumeric and contains no spaces. | +| Mandatory* | `st/STATUS` | `STATUS` must either be `preliminary`, `interviewed`, `offered`, `rejected`. | + +*it is mandatory if and only if it is the only field used in the command. + +**Note:** +* User **cannot** delete by index and delete by tags & status in a single command.
+ E.g. `delete 1 t/hardworking` is **not** allowed. +* `delete` by tags & status requires at least ONE delete parameter i.e. `st/STATUS`, or `t/TAGNAME`. +* Each prefix can only be used at most once. +* Multiple delete parameters for a specific category are divided by spaces (not commas!) i.e. `delete st/preliminary interviewed` +* Delete parameters are case-insensitive i.e. doing `delete t/interN` is the same as `delete t/intern` + + +
+

Advanced

+
+ + + +**Note:** +* You can combine multiple delete categories in a single `delete` command e.g. `delete st/interviewed t/intern` + +**An example of **delete by index** command being successfully executed:** +1. Enter the command `delete Bernice` + + ![delete-by-index-pre](images/delete-by-index-pre.png) + +2. Enter the command `delete 1` + + ![delete-by-index-success](images/delete-by-index-success.png) + +Similarly, here are some examples of **delete by tags & status** command being successfully executed: + +Assuming this as the data after executing `list`: +![delete-by-tags-status-pre](images/delete-by-tags-status-pre.png) + +1. `delete t/marketing software` + + ![delete-by-tags-status-pre](images/delete-by-tags-status-1.png) + + The above `delete` command deleted all applicants whose tags match ANY of the given keywords. This is because + `delete` does an `OR` search **within a specific category**.
+ +2. `delete st/interviewed t/software` + + ![delete-by-tags-status-pre](images/delete-by-tags-status-2.png) + + The above `delete` command only deleted Bernice because `delete` does an `AND` search **across multiple categories**.
+ + + +**What does it mean to do an `OR` delete within a single category and an `AND` delete across multiple categories?** +
+It's best to explain this by breaking down an example `delete` command!
+`delete st/interviewed rejected t/intern manager` will delete applicants whose: +* status is either **interviewed** `OR` **rejected** +* `AND` has a tag `intern` or `manager`. + +
+ +**Failed to execute the `delete` command? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| Missing delete keyword | Unknown command! | Follow the command format `delete INDEX` OR `delete (st/STATUS... t/TAGNAME...)` | +| Missing delete categories e.g. `delete` | Invalid command format! | Make sure you include **at least one** of the delete categories i.e. `/st`, `/t`. | +| Invalid INDEX e.g. `delete 0`, `delete -1` | Invalid command format! | Make sure `INDEX` is a positive integer. | +| Invalid status e.g. `delete st/in`, `delete st/` | Status should be either one of the following: 'Preliminary','Interviewed', 'Offered', 'Rejected' and it should not be blank. | Check that the `status` is one of the following: `preliminary`, `interviewed`, `rejected`, `offered`. | +| Invalid tag e.g. `delete t/intern@`, `delete t/` | Tags names should be alphanumeric. | Ensure that `tag` does not contain any non alphanumeric characters such as &, $, @, -, %, *, _, empty space, etc. | +| Multiple prefixes of the same category being used e.g. `delete t/intern t/manager` | Multiple values specified for the following single-valued field(s): `prefix/`. | Remove the duplicate prefix. The command should only have at most **one** of every prefix. | +| Using commas as delimiters of different parameters instead of spaces e.g. `delete t/intern, manager` | Status should be either one of the following: 'Preliminary','Interviewed', 'Offered', 'Rejected' and it should not be blank
Tags names should be alphanumeric. | Remove the comma(s) e.g. `delete t/intern manager` | +| Using commas as delimiters of different parameters instead of spaces e.g. `delete st/interviewed, t/intern` | Should display the error message for either invalid `status` or `tag` depending on the first prefix because it will consider the comma and anything that comes after it as part of the first prefix. | Remove the comma(s) e.g. `delete n/alex t/intern` | + +[Jump back to Table of Contents](#table-of-contents) +
+ +-------------------------------------------------------------------------------------------------------------------- +### 3. Filtering and Listing Applicants + + +#### Searching job applicants by category: `search` + + +Finds job applicants whose profiles match the specified categories' keywords. The search categories are: name, status, tag. + +Format: `search (n/NAME... st/STATUS... t/TAGNAME...)` + +| Type | Field | Constraints | +|------------|-------------|---------------------------------------------------------------------------------------------| +| Mandatory* | `n/NAME` | `NAME` must be alphanumeric (Letters and numbers, no symbols allowed such as `/`, `,` ...). | +| Mandatory* | `st/STATUS` | `STATUS` must either be `preliminary`, `interviewed`, `offered`, `rejected`. | +| Mandatory* | `t/TAGNAME` | `TAGNAME` must be alphanumeric and contains no spaces. | + +*it is mandatory if and only if it is the only field used in the command. + +**Note**: +* `search` requires at least ONE search parameter i.e. `n/NAME`, `st/STATUS`, or `t/TAGNAME`. +* Each prefix can only be used at most once. +* Multiple search parameters for a specific category are divided by spaces (not commas!) i.e. `search st/preliminary interviewed` +* Search parameters are case-insensitive i.e. doing `search n/aLeX` is the same as `search n/alex` + + +
+

Advanced

+
+ + + +**Note:** +* You can combine multiple search categories in a single `search` command e.g. `search n/alex st/interviewed t/intern` + +**Here are some examples of **search** command being successfully executed:** +1. Enter `search n/alex bernice` + + ![search-success-1](images/search-success-1.png) + + The above `search` command displayed all applicants whose name match ANY of the given keywords. This is because + `search` does an `OR` search **within a specific category**.
+ +2. Enter `search n/alex bernice st/interviewed t/intern` + + ![search-success](images/search-success.png) + + Notice how the above `search` command did not display "Alex" despite his profile matching + the `name` and `tag` categories. This is because `search` does an `AND` search **across multiple categories**.
+ + + +**What does it mean to do an `OR` search within a single category and an `AND` search across multiple categories?**
+It's best to explain this by breaking down an example `search` command! +
+`search n/alex bernice st/interviewed t/intern` will output applicants whose: +* names contain either Alex `OR` Bernice +* `AND` status is either interviewed +* `AND` has a tag `intern` + +
+ +**Failed to execute the `search` command? Here are some possible reasons why** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| Missing search keyword | Unknown command! | Follow the command format `search (n/NAME... st/STATUS... t/TAGNAME...)` | +| Missing search categories e.g. `search` | Invalid command format! | Make sure you include **at least one** of the search categories i.e. `/n`, `/st`, `/t`. | +| Invalid name e.g. `search n/alex@`, `search n/` | Names should only contain alphanumeric characters and spaces, and it should not be blank. | Ensure that `name` does not contain any non-alphanumeric characters such as &, $, @, -, %, *, _, etc. | +| Invalid status e.g. `search st/in`, `search st/` | Status should be either one of the following: 'Preliminary','Interviewed', 'Offered', 'Rejected' and it should not be blank. | Check that the `status` is one of the following: `preliminary`, `interviewed`, `rejected`, `offered`. Enter the command again with any of the 4 metric | +| Invalid tag e.g. `search t/intern@`, `search t/` | Tags names should be alphanumeric. | Ensure that `tag` does not contain any non alphanumeric characters such as &, $, @, -, %, *, _, empty space, etc. | +| Multiple prefixes of the same category being used e.g. `search n/alex n/bernice` | Multiple values specified for the following single-valued field(s): `prefix/`. | Remove the duplicate prefix. The command should only have at most **one** of every prefix. | +| Using commas as delimiters of different parameters instead of spaces e.g. `search n/alex, bernice` | Names should only contain alphanumeric characters and spaces, and it should not be blank
Status should be either one of the following: 'Preliminary','Interviewed', 'Offered', 'Rejected' and it should not be blank
Tags names should be alphanumeric. | Remove the comma(s) e.g. `search n/alex yeoh` | +| Using commas as delimiters of different parameters instead of spaces e.g. `search n/alex, t/intern` | Should display the error message for either invalid `name`, `status`, or `tag` depending on the first prefix because it will consider the comma and anything that comes after it as part of the first prefix. | Remove the comma(s) e.g. `search n/alex t/intern` | + +[Jump back to Table of Contents](#table-of-contents) -Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +#### Filter job applicants by statistics: `filter` + -### Locating persons by name: `find` +Filters and display applicants in the current displayed applicant list using statistical metrics and values. +
+In essence, this allows you to find job applicants whose performance rating is above a certain percentile, score or mean/median score for that tag. +
+Ideally, this feature can then be used to find the best applicants easily and quickly without having to manually look through the list of applicants. -Finds persons whose names contain any of the given keywords. +Format:`filter t/TAGNAME met/METRIC val/VALUE` or `filter t/TAGNAME met/METRIC` -Format: `find KEYWORD [MORE_KEYWORDS]` +| Type | Field | Constraints | +|------------|--------------------|---------------------------------------------------------------------------------------------------| +| Mandatory | `t/TAGNAME` | `TAGNAME` must be a tag that is of the category `assessment`. | +| Mandatory | `met/METRIC` | `METRIC` must be either `score`, `percentile`, `mean`, `median`. | +| Mandatory* | `val/VALUE` | `VALUE` must be a non-negative integer and is a mandatory field only for `score` and `percentile` | -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +*`val/VALUE` is an **optional** field for `mean` and `median` -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) + -### Deleting a person : `delete` +* For `METRIC` that is `score` or `percentile`, `filter` displays job applicants whose **value** is **greater than or equal** to the specified value for the specified statistic metric. +* For `METRIC` that is `mean` or `median`, the `VALUE` is optional. Specifying a `VALUE` here will be ignored accordingly. `filter t/TAGNAME met/METRIC` is equivalent to `filter t/TAGNAME met/METRIC val/X` where `X` is any positive integer. +* Do look at the [Summary Statistics](#summary-statistics) section for more details on the summary statistics metrics. + -Deletes the specified person from the address book. +**Notes:** +* Filter works only on the **current list of job applicants displayed**. It is **essential** that you enter `list` before using `filter` to ensure that you are filtering the correct list of job applicants. +* Filter does not edit, update or in any way change the data of the job applicants. It only filters and displays the job applicants. +* Filter does not trigger view, that is your view panels represent the previous applicant you viewed before filtering. +* To get back the **original list with all the applicants**, simply type `list` again. -Format: `delete INDEX` + -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +You should use `filter` after you have tagged most of the job applicants with a tag that has a score. +This is because some metrics such as `percentile`, `mean` and `median` require a certain number of scores to be considered meaningful. +Read more about this in the [Summary Statistics](#summary-statistics) section. -Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. + -### Clearing all entries : `clear` +Set up for examples when you first start JABPro with default data: +1. `list` +2. `create t/assessment interview` to create a tag `interview` under the `assessment` category. +3. `edit 1 t/interview sc/interview 80` +4. `edit 2 t/interview sc/interview 90` +5. `edit 3 t/interview sc/interview 70` +6. The result of the above commands should look like this: -Clears all entries from the address book. + ![Filter-Setup](images/filter-setup.png) +
+ + +**An example of the `filter` command being successfully executed:** +1. Enter the command `list` +2. Enter the command `filter t/interview met/percentile val/80` (**Filter by percentile**) +3. This is the result of the successful `filter` command (Take note that command entered will not be shown in the result): + + ![Filter-Success](images/filter-command-success.png) +
+ + +**An example of the `filter` command being successfully executed with `median`:** +1. Enter the command `list` +2. Enter the command `filter t/interview met/median` (**Filter by median**) +3. This is the result of the successful `filter` command (Take note that command entered will not be shown in the result): + + ![Filter-Success](images/filter-command-median-success.png) +
+ +**Failed to execute the `filter` command? Here are some possible reasons why:** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-----------------------------------------------|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Missing filter keyword: `filter` | Unknown command! | Follow the command format strictly of `filter t/TAGNAME met/METRIC val/VALUE` for score and percentile or `filter t/TAGNAME met/METRIC` for mean or median. | +| Missing parameters | Incomplete parameter inputs. t/TAG and met/SCORE are compulsory fields. | Enter the command again with the correct parameters. | +| Tag missing on applicants | Tag does not exist! | Check that the tag is used on at least one applicant and that the tag is a `assessment` tag also. Add the tag to the applicants using `edit` | +| Invalid tag name | Invalid tag provided. Needs to be non-empty name | Check that the tag name is not empty and is alphanumeric (a valid tag name) and does not contain space. Additionally, the tag must already have a category of `assessment`, ensure this by using `listT` | | | | | | +| Invalid metric | Invalid metric provided. Needs to be one of: score, mean, median, percentile | Check that the metric is one of the following: `score`, `mean`, `median`, `percentile` and that it is spelt correctly. Enter the command again with any of the 4 metric | + | Invalid value | Invalid value provided. Needs to be a non negative integer that is more than or equal to 0 | Check that the value is a non-negative integer that is more than or equal to 0. Enter the command again with the correct value. | + | Missing value | val/VALUE is missing, it is compulsory. | Enter a value for `val/VALUE` since the metric requires it. | +| Multiple prefixes of the same type being used | Multiple values specified for the following single-valued field(s): `prefix/` | Remove the duplicate prefix. The command should only have 1 of every prefix | + +[Jump back to Table of Contents](#table-of-contents) + + +#### Listing all applicant: `list` + + +Shows a list of all applicants in JABPro + +Format: +`list [so/ATTRIBUTE]` + +| Type | Field | Constraints | +|----------|----------------|-----------------------------------------------| +| Optional | `so/ATTRIBUTE` | `ATTRIBUTE` must either be `name` or `email`. | + +**Note:** +* Attribute is case-insensitive: `list so/NAME` and `list so/name` return the same result. +* The result will be sorted in **ascending** order. +* The sorting algorithm is case-sensitive, which means it will treat uppercase and + lowercase letters as distinct. This may result in names being sorted as A B C a b c, rather than A a B b C c. + +[Jump back to Table of Contents](#table-of-contents) +
+ +-------------------------------------------------------------------------------------------------------------------- + +### 4. Managing Events + + +#### Adding an Event: `event` + + +Adds an event, associated with an applicant, to JABPro. + +Format: `event INDEX d/DESCRIPTION bt/BEGIN_TIME et/END_TIME` + +| Type | Field | Constraints | +|------------|-----------------|---------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be a non-zero unsigned integer and it must not be greater than the total number of applicant in JABPro | +| Mandatory | `d/DESCRIPTION` | `DESCRIPTION` must be a string value. Only the prefix (i.e. `d/`) is mandatory. | +| Mandatory | `bt/BEGIN_TIME` | `BEGIN_TIME` must be a valid date-time, in the format `yyyy-MM-dd HH:mm` | +| Mandatory | `et/END_TIME` | `END_TIME` must be a valid date-time, in the format `yyyy-MM-dd HH:mm`, and after the `BEGIN_TIME` | + +**Note:** + +* JABPro allows the addition of multiple events associated with the same applicant, having the same description. It is up to the user to provided detailed descriptions to distinguish events from one another. +* Events added to JABPro can also be found in the `data/eventbook.json` file. Existing events are also read from the file when JABPro starts up. +* Events with empty `DESCRIPTION`s can also be added. However, the prefix `d/` must still be present. + +**Example of successful execution of `event` command:** + +1. Enter the command `event 1 d/Interview bt/2023-11-12 10:00 et/2023-11-12 12:00` +2. This is the result of the successful `event` command [It is assumed an applicant called Alex Yeoh exists in JABPro]: + +![Event](images/event.png) + +The changes in UI take place in the `Events Window`. Please find more details in [Viewing Events](UserGuide.md#viewing-events-schedule). + +**Failed to execute teh `event` command? Here are some possible reasons why:** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-------------------------------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------| +| Missing event keyword: `event` | Unknown command | Follow the command format of `event INDEX d/DESCRIPTION bt/START_TIME et/END_TIME` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | +| Negative or 0 Index | Invalid command format! | Ensure that the index is a positive integer and is also a number that is on the displayed applicant list. | +| Missing description | Invalid command format! | Ensure that the description is filled up | +| Missing start time | Invalid command format! | Ensure that the start time is filled up | +| Missing end time | Invalid command format! | Ensure that the end time is filled up | +| Start time/End time not in correct format | Date is not in correct format! | Ensure that the start time/end time is in yyyy-MM-dd format | +| Start time/End time not a valid date-time | Date entered is invalid! | Ensure that the start time/end time is a semantically valid date | +| End time before or same as start time | End time must be after start time! | Ensure that the end time is after the start time | | | + +[Jump back to Table of Contents](#table-of-contents) + +#### Viewing events: `schedule` + + +Displays all events that have been added to JABPro. + +Format: `schedule` + +**Note:** + +* `schedule` command will open the `Events` window regardless of whether there are events in JABPro or not. +* Any set of characters added after the `schedule` keyword will be ignored. E.g.: `schedule a1b2c3` + + + +**Tip:** The `Events` window can also be accessed by clicking `Events > Events` in the menu bar, located at the top of the window. + + + +**Example of successful execution of `schedule` command:** + +1. Enter the command `schedule` +2. This is the result of the successful `schedule` command [It is assumed that an event was previously added using the command `event 1 d/Interview bt/2023-11-12 10:00 et/2023-11-12 12:00]: + +![Schedule](images/schedule.png) + +The `Events` window opens up: + +![EventWindow](images/eventwin.png) + +There is no possibility of a "failed" execution of the `schedule` command. + +[Jump back to Table of Contents](#table-of-contents) +
+ +-------------------------------------------------------------------------------------------------------------------- +### 5. Others + + +#### Exporting the existing data to csv: `export` + + +Format: `export` + +Exports the entries into a .csv file located in the current directory as (/data/export.csv) + +**Note:** +* Export only exports the following: Name, Phone, Email, Address, Tags, Linkedin, Github, Remark, Status. +* JABPro must have write permissions, this means that if the .csv file is open, + exporting again will not be possible. + +**Examples:** +* `export` exports the data to /data/export.csv + +[Jump back to Table of Contents](#table-of-contents) + +#### Clearing all entries: `clear` + + +Clears all entries from JABPro - including applicants and events. Format: `clear` -### Exiting the program : `exit` +**Note:** + +* Usage of the `clear` command empties all records, not only in the current running instance of JABPro, but from the `json` files as well where the data is written to/read from. Hence, a subsequent launch of JABPro will display an empty application. +* `clear` command can be invoked on an already empty instance of JABPro as well, without any errors being raised. There is no effect of running this command. + +**Example of the successful execution of the `clear` command:** + +![Clear](images/clear.png) + +[Jump back to Table of Contents](#table-of-contents) + +#### Exiting the program: `exit` + Exits the program. Format: `exit` +[Jump back to Table of Contents](#table-of-contents) + +
+ +-------------------------------------------------------------------------------------------------------------------- +## Additional information + + +
+

Advanced

+
+ + + +### Summary Statistics + +Summary Statistics is a table generated by JABPro that displays the following information about an applicant: +It is generated for tags that are categorised under the `assessment` category. + +| Statistic / Metric | Description | +|--------------------|-----------------------------------------------| +| `score` | The score of the applicant for the tag. | +| `mean` | The mean score of applicant with that tag. | +| `median` | The median score of applicant with that tag. | +| `minimum` | The minimum score of applicant with that tag. | +| `maximum` | The maximum score of applicant with that tag. | +| `percentile` | The percentile of the applicant for that tag. | + + + +You should ensure that you have **sufficient candidates of more than 20** with a score for the tag you are interested in, before using the summary statistics to make comparisons. + + +**Notes on why you should have sufficient applicants with a score for the tag you are interested in:** +* This is due to the fact that these summary statistics rely on concepts such as mean, median and percentile, which are statistical concepts that require a sufficient sample size to be meaningful. +* For example, if you have only assigned 5 out of 100 applicants, the summary statistics will not be representative of the actual mean, median and percentile for that tag. +* In this case, you should assign more applicants with a score for that tag, before using the summary statistics to make comparisons. +* If you have assigned a sufficient number of applicants with a score for that tag, you can use the summary statistics to make comparisons. For example, you want to check if an applicant's score for a tag is more than or equal to half of all the applicant who have a score for that tag, you can use the median to make this comparison. +* A **sufficient number** could be deemed as **any number that is more than 20**, but this is not a hard and fast rule. You should use your own discretion to determine if the number of applicant with a score for that tag is sufficient. + + + +**Tip:** +1. Use mostly `median` and `percentile` to make your judgement on the performance of an applicant. +2. `median` to find applicants who are the better performing half +3. `percentile` as where this applicant stands among all other applicants (treat it like a ranking system, the higher the percentile, the better the applicant is performing) + + +
+

Advanced

+
+ + + +**Note**: +* Understand that `percentile` has limited functionality in some context. This is because if two applicants have the same score, they are `rank` the same. This means that the percentile of both applicants will be the same. + * If all applicants have the same score, their percentile will all be 0.0. This is because they are all `rank` the same. + * Additionally, when the spread of scores is small, the percentile will not be able to differentiate between applicants with similar scores. + +**Formula used to calculate the summary statistics:** +* **mean** is calculated by using the formula `sum of all scores with that tag / number of applicants with that tag` +* **median** is calculated by using the formula `middle score of all scores with that tag` +* **minimum** is calculated by using the formula `lowest score of all scores with that tag` +* **maximum** is calculated by using the formula `highest score of all scores with that tag` +* **percentile** is calculated by using the formula `number of applicants with a score strictly lower than the applicant / total number of applicants with that tag` + +[Jump back to Table of Contents](#table-of-contents) + ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +JABPro data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +JABPro data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. -
+ -### Archiving data files `[coming in v2.0]` +**Caution:** +If your changes to the data file makes its format invalid, JABPro will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. + -_Details coming soon ..._ +[Jump back to Table of Contents](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- ## FAQ +**Q**: What would happen if I used JABPro for the first time without the sample data? Would the app break? +**A**: Not necessarily, we've tested out the JABPro launch without any sample data and basic functionalities are working fine. However, +we do not recommend this because there could be unexpected behaviours that we might have not accounted for. It is always advisable to follow +the [quickstart instructions](#quick-start) and launch JABPro with the sample data! + **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous JABPro home folder. --------------------------------------------------------------------------------------------------------------------- +[Jump back to Table of Contents](#table-of-contents) -## Known issues -1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +
-------------------------------------------------------------------------------------------------------------------- -## Command summary - -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +## Command Summary + +| Action | Format, Examples | +|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/[CATEGORY] TAGNAME]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/developer t/intern` | +| **Remark** | `remark r/REMARK`
e.g., `remark 1 r/Great attitude, hardworking` | +| **Add LinkedIn/Github** | `addL INDEX u/USERNAME` or `addG INDEX u/USERNAME` e.g., `addL 1 u/alex-yeoh`, `addG 2 u/bernicesanders123` | +| **Open LinkedIn/Github** | `linkedin INDEX` or `github INDEX` e.g., `linkedin 1`, `github 2` | +| **Set** | `set INDEX STATUS`
e.g., `set 2 Interviewed` | +| **View** | `view INDEX`
e.g., `view 1` | +| **Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAGNAME]​ [sc/TAGNAME SCORE]`
e.g.,`edit 2 n/James Lee e/jameslee@example.com t\MarketingInterview sc\MarketingInterview 50` | +| **Delete** | `delete INDEX` or `delete (t/TAGNAME... st/STATUS...)`
e.g., `delete 3`, `delete t/intern st/rejected` | +| **Create** | `create t/CATEGORY NAME…​`
e.g. `create t/dept software` | +| **ListT** | `listT` | +| **Search** | `search (n/NAME... st/STATUS... t/TAGNAME...)`
e.g., `search n/alex` | +| **Filter** | `filter t/TAGNAME met/METRIC val/VALUE`
e.g., `filter t/interview met/score val/80` | +| **List** | `list so/ATTRIBUTE`
e.g. `list so/name` | +| **Event** | `event INDEX d/DESCRIPTION bt/START_TIME et/END_TIME` | +| **Schedule** | `schedule` | +| **Help** | `help` | +| **Export** | `export` | +| **Clear** | `clear` | + +[Jump back to Table of Contents](#table-of-contents) + +## Prefix Summary + +| Prefix | Description | Commands | Constraint | +|--------|--------------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------| +| `n/` | Name | Add, Edit, Search | Must be alphanumeric (Letters and numbers, no symbols allowed such as `/`, `,` ...). | +| `p/` | Phone Number | Add, Edit | Must contain numbers only and should be at-least 3 digits long. | +| `e/` | Email | Add, Edit | Must be the standard email address format (There must be an email-prefix followed by @ symbol and email domain). | +| `a/` | Address | Add, Edit | can be any value, including special characters such as `#`, `,` ... | +| `t/` | Tag | Add, Edit, Search, Create, Delete | Must be alphanumeric with no spaces. Any details after the space will be ignored. | +| `sc/` | Score | Edit | Must be a non-negative integer. | +| `st/` | Status | Search, Delete, Set | Must either be `preliminary`, `interviewed`, `offered`, `rejected`. | +| `r/` | Remark | Remark | can be any value, including special characters such as `#`, `,` ... | +| `u/` | Username | Add Github/LinkedIn | Must be a string value. | +| `met/` | Metric | Filter | Must be either `score`, `percentile`, `mean`, `median`. | +| `val/` | Value | Filter | Optional only for mean and median. Otherwise, must be a non-negative integer. | +| `so/` | Sort | List | Must be either `name` or `email`. | +| `d/` | Description | Event | Must be a string value. | +| `bt/` | Begin Time | Event | Must be a valid date-time, in the format `yyyy-MM-dd HH:mm`. | +| `et/` | End Time | Event | Must be a valid date-time, in the format `yyyy-MM-dd HH:mm`. | + +[Jump back to Table of Contents](#table-of-contents) + +## Glossary +| Keyword | Definition | +|--------------|-----------------------------------------------------------------------------------------| +| Alphanumeric | Letters and numbers. Should NOT contain symbols, or whitespaces. | +| Index | The index of the applicant of the currently displayed list in the applicant list panel. | +| Parameter | Details about the job applicant that will be included in the command. | +| Command | Instructions that `JABPro` will execute. | +| JAR | Compressed file of `JABPro` that is in the form of a Java Archive. | + +[Jump back to Table of Contents](#table-of-contents) diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 6bd245d8f4e..00000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,15 +0,0 @@ -title: "AB-3" -theme: minima - -header_pages: - - UserGuide.md - - DeveloperGuide.md - - AboutUs.md - -markdown: kramdown - -repository: "se-edu/addressbook-level3" -github_icon: "images/github-icon.png" - -plugins: - - jemoji diff --git a/docs/_data/projects.yml b/docs/_data/projects.yml deleted file mode 100644 index 8f3e50cb601..00000000000 --- a/docs/_data/projects.yml +++ /dev/null @@ -1,23 +0,0 @@ -- name: "AB-1" - url: https://se-edu.github.io/addressbook-level1 - -- name: "AB-2" - url: https://se-edu.github.io/addressbook-level2 - -- name: "AB-3" - url: https://se-edu.github.io/addressbook-level3 - -- name: "AB-4" - url: https://se-edu.github.io/addressbook-level4 - -- name: "Duke" - url: https://se-edu.github.io/duke - -- name: "Collate" - url: https://se-edu.github.io/collate - -- name: "Book" - url: https://se-edu.github.io/se-book - -- name: "Resources" - url: https://se-edu.github.io/resources diff --git a/docs/_includes/custom-head.html b/docs/_includes/custom-head.html deleted file mode 100644 index 8559a67ffad..00000000000 --- a/docs/_includes/custom-head.html +++ /dev/null @@ -1,6 +0,0 @@ -{% comment %} - Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons: - - 1. Head over to https://realfavicongenerator.net/ to add your own favicons. - 2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet. -{% endcomment %} diff --git a/docs/_includes/head.html b/docs/_includes/head.html deleted file mode 100644 index 83ac5326933..00000000000 --- a/docs/_includes/head.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - {%- include custom-head.html -%} - - {{page.title}} - - diff --git a/docs/_includes/header.html b/docs/_includes/header.html deleted file mode 100644 index 33badcd4f99..00000000000 --- a/docs/_includes/header.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/docs/_layouts/alt-page.html b/docs/_layouts/alt-page.html deleted file mode 100644 index 5dbc6ef245f..00000000000 --- a/docs/_layouts/alt-page.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default ---- -
- -
-

{{ page.alt_title | escape }}

-
- -
- {{ content }} -
- -
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html deleted file mode 100644 index e092cd572e0..00000000000 --- a/docs/_layouts/default.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - {%- include head.html -%} - - - - {%- include header.html -%} - -
-
- {{ content }} -
-
- - - - diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html deleted file mode 100644 index 01e4b2a93b8..00000000000 --- a/docs/_layouts/page.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default ---- -
- -
-

{{ page.title | escape }}

-
- -
- {{ content }} -
- -
diff --git a/docs/_markbind/layouts/default.md b/docs/_markbind/layouts/default.md new file mode 100644 index 00000000000..2567e82660d --- /dev/null +++ b/docs/_markbind/layouts/default.md @@ -0,0 +1,66 @@ + + + + +
+ + JABPro +
  • User Guide
  • +
  • Developer Guide
  • +
  • About Us
  • +
  • :fab-github: +
  • +
  • + +
  • +
    +
    + +
    + +
    + {{ content }} +
    + + +
    + +
    + +
    + [**Powered by** {{MarkBind}}, generated on {{timestamp}}] +
    +
    diff --git a/docs/_markbind/variables.json b/docs/_markbind/variables.json new file mode 100644 index 00000000000..9d89eb0358b --- /dev/null +++ b/docs/_markbind/variables.json @@ -0,0 +1,3 @@ +{ + "jsonVariableExample": "Your variables can be defined here as well" +} diff --git a/docs/_markbind/variables.md b/docs/_markbind/variables.md new file mode 100644 index 00000000000..89ae5318fa4 --- /dev/null +++ b/docs/_markbind/variables.md @@ -0,0 +1,4 @@ + +To inject this HTML segment in your markbind files, use {{ example }} where you want to place it. +More generally, surround the segment's id with double curly braces. + diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss deleted file mode 100644 index 0d3f6e80ced..00000000000 --- a/docs/_sass/minima/_base.scss +++ /dev/null @@ -1,295 +0,0 @@ -html { - font-size: $base-font-size; -} - -/** - * Reset some basic elements - */ -body, h1, h2, h3, h4, h5, h6, -p, blockquote, pre, hr, -dl, dd, ol, ul, figure { - margin: 0; - padding: 0; - -} - - - -/** - * Basic styling - */ -body { - font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family; - color: $text-color; - background-color: $background-color; - -webkit-text-size-adjust: 100%; - -webkit-font-feature-settings: "kern" 1; - -moz-font-feature-settings: "kern" 1; - -o-font-feature-settings: "kern" 1; - font-feature-settings: "kern" 1; - font-kerning: normal; - display: flex; - min-height: 100vh; - flex-direction: column; - overflow-wrap: break-word; -} - - - -/** - * Set `margin-bottom` to maintain vertical rhythm - */ -h1, h2, h3, h4, h5, h6, -p, blockquote, pre, -ul, ol, dl, figure, -%vertical-rhythm { - margin-bottom: $spacing-unit / 2; -} - -hr { - margin-top: $spacing-unit; - margin-bottom: $spacing-unit; -} - -/** - * `main` element - */ -main { - display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */ -} - - - -/** - * Images - */ -img { - max-width: 100%; - vertical-align: middle; -} - - - -/** - * Figures - */ -figure > img { - display: block; -} - -figcaption { - font-size: $small-font-size; -} - - - -/** - * Lists - */ -ul, ol { - margin-left: $spacing-unit; -} - -li { - > ul, - > ol { - margin-bottom: 0; - } -} - - - -/** - * Headings - */ -h1, h2, h3, h4, h5, h6 { - font-weight: $base-font-weight; -} - - - -/** - * Links - */ -a { - color: $link-base-color; - text-decoration: none; - - &:visited { - color: $link-visited-color; - } - - &:hover { - color: $text-color; - text-decoration: underline; - } - - .social-media-list &:hover { - text-decoration: none; - - .username { - text-decoration: underline; - } - } -} - - -/** - * Blockquotes - */ -blockquote { - color: $brand-color; - border-left: 4px solid $brand-color-light; - padding-left: $spacing-unit / 2; - @include relative-font-size(1.125); - font-style: italic; - - > :last-child { - margin-bottom: 0; - } - - i, em { - font-style: normal; - } -} - - - -/** - * Code formatting - */ -pre, -code { - font-family: $code-font-family; - font-size: 0.9375em; - border: 1px solid $brand-color-light; - border-radius: 3px; - background-color: $code-background-color; -} - -code { - padding: 1px 5px; -} - -pre { - padding: 8px 12px; - overflow-x: auto; - - > code { - border: 0; - padding-right: 0; - padding-left: 0; - } -} - -.highlight { - border-radius: 3px; - background: $code-background-color; - @extend %vertical-rhythm; - - .highlighter-rouge & { - background: $code-background-color; - } -} - - - -/** - * Wrapper - */ -.wrapper { - max-width: calc(#{$content-width} - (#{$spacing-unit})); - margin-right: auto; - margin-left: auto; - padding-right: $spacing-unit / 2; - padding-left: $spacing-unit / 2; - @extend %clearfix; - - @media screen and (min-width: $on-large) { - max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); - padding-right: $spacing-unit; - padding-left: $spacing-unit; - } -} - - - -/** - * Clearfix - */ -%clearfix:after { - content: ""; - display: table; - clear: both; -} - - - -/** - * Icons - */ - -.orange { - color: #f66a0a; -} - -.grey { - color: #828282; -} - -/** - * Tables - */ -table { - margin-bottom: $spacing-unit; - width: 100%; - text-align: $table-text-align; - color: $table-text-color; - border-collapse: collapse; - border: 1px solid $table-border-color; - tr { - &:nth-child(even) { - background-color: $table-zebra-color; - } - } - th, td { - padding: ($spacing-unit / 3) ($spacing-unit / 2); - } - th { - background-color: $table-header-bg-color; - border: 1px solid $table-header-border; - } - td { - border: 1px solid $table-border-color; - } - - @include media-query($on-laptop) { - display: block; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - } -} - -@media print { - /** - * Prevents page break from cutting through content when printing - */ - body { - display: block; - } - /** - * Replaces the top navigation menu with the project name when printing - */ - .site-header .wrapper { - display: none; - } - .site-header { - text-align: center; - } - .site-header:before { - content: "AB-3"; - font-size: 32px; - } -} - diff --git a/docs/_sass/minima/_layout.scss b/docs/_sass/minima/_layout.scss deleted file mode 100644 index ca99f981701..00000000000 --- a/docs/_sass/minima/_layout.scss +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Site header - */ -.site-header { - border-top: 5px solid $brand-color-dark; - border-bottom: 1px solid $brand-color-light; - min-height: $spacing-unit * 1.865; - line-height: $base-line-height * $base-font-size * 2.25; - - // Positioning context for the mobile navigation icon - position: relative; -} - -.site-title { - @include relative-font-size(1.625); - font-weight: 300; - letter-spacing: -1px; - margin-bottom: 0; - float: left; - - @include media-query($on-palm) { - padding-right: 45px; - } - - &, - &:visited { - color: $brand-color-dark; - } -} - -.site-nav { - position: absolute; - top: 9px; - right: $spacing-unit / 2; - background-color: $background-color; - border: 1px solid $brand-color-light; - border-radius: 5px; - text-align: right; - - .nav-trigger { - display: none; - } - - .menu-icon { - float: right; - width: 36px; - height: 26px; - line-height: 0; - padding-top: 10px; - text-align: center; - - > svg path { - fill: $brand-color-dark; - } - } - - label[for="nav-trigger"] { - display: block; - float: right; - width: 36px; - height: 36px; - z-index: 2; - cursor: pointer; - } - - input ~ .trigger { - clear: both; - display: none; - } - - input:checked ~ .trigger { - display: block; - padding-bottom: 5px; - } - - .page-link { - color: $text-color; - line-height: $base-line-height; - display: block; - padding: 5px 10px; - - // Gaps between nav items, but not on the last one - &:not(:last-child) { - margin-right: 0; - } - margin-left: 20px; - } - - @media screen and (min-width: $on-medium) { - position: static; - float: right; - border: none; - background-color: inherit; - - label[for="nav-trigger"] { - display: none; - } - - .menu-icon { - display: none; - } - - input ~ .trigger { - display: block; - } - - .page-link { - display: inline; - padding: 0; - - &:not(:last-child) { - margin-right: 20px; - } - margin-left: auto; - } - } -} - - - -/** - * Page content - */ -.page-content { - padding: $spacing-unit 0; - flex: 1 0 auto; -} - -.page-heading { - @include relative-font-size(2); -} - -.post-list-heading { - @include relative-font-size(1.75); -} - -.post-list { - margin-left: 0; - list-style: none; - - > li { - margin-bottom: $spacing-unit; - } -} - -.post-meta { - font-size: $small-font-size; - color: $brand-color; -} - -.post-link { - display: block; - @include relative-font-size(1.5); -} - - - -/** - * Posts - */ -.post-header { - margin-bottom: $spacing-unit; -} - -.post-title, -.post-content h1 { - @include relative-font-size(2.625); - letter-spacing: -1px; - line-height: 1.15; - - @media screen and (min-width: $on-large) { - @include relative-font-size(2.625); - } -} - -.post-content { - margin-bottom: $spacing-unit; - - h1, h2, h3 { margin-top: $spacing-unit * 2 } - h4, h5, h6 { margin-top: $spacing-unit } - - h2 { - @include relative-font-size(1.75); - - @media screen and (min-width: $on-large) { - @include relative-font-size(2); - } - } - - h3 { - @include relative-font-size(1.375); - - @media screen and (min-width: $on-large) { - @include relative-font-size(1.625); - } - } - - h4 { - @include relative-font-size(1.25); - } - - h5 { - @include relative-font-size(1.125); - } - h6 { - @include relative-font-size(1.0625); - } -} - - -.social-media-list { - display: table; - margin: 0 auto; - li { - float: left; - margin: 5px 10px 5px 0; - &:last-of-type { margin-right: 0 } - a { - display: block; - padding: $spacing-unit / 4; - border: 1px solid $brand-color-light; - &:hover { border-color: darken($brand-color-light, 10%) } - } - } -} - - - -/** - * Pagination navbar - */ -.pagination { - margin-bottom: $spacing-unit; - @extend .social-media-list; - li { - a, div { - min-width: 41px; - text-align: center; - box-sizing: border-box; - } - div { - display: block; - padding: $spacing-unit / 4; - border: 1px solid transparent; - - &.pager-edge { - color: darken($brand-color-light, 5%); - border: 1px dashed; - } - } - } -} - - - -/** - * Grid helpers - */ -@media screen and (min-width: $on-large) { - .one-half { - width: calc(50% - (#{$spacing-unit} / 2)); - } -} diff --git a/docs/_sass/minima/custom-mixins.scss b/docs/_sass/minima/custom-mixins.scss deleted file mode 100644 index 9d4bedc1c67..00000000000 --- a/docs/_sass/minima/custom-mixins.scss +++ /dev/null @@ -1,21 +0,0 @@ -@mixin alert-variant($background, $border, $color) { - color: $color; - @include gradient-bg($background); - border-color: $border; - - .alert-link { - color: darken($color, 10%); - } -} - -@mixin gradient-bg($color, $foreground: null) { - @if $enable-gradients { - @if $foreground { - background-image: $foreground, linear-gradient(180deg, mix($body-bg, $color, 15%), $color); - } @else { - background-image: linear-gradient(180deg, mix($body-bg, $color, 15%), $color); - } - } @else { - background-color: $color; - } -} diff --git a/docs/_sass/minima/custom-styles.scss b/docs/_sass/minima/custom-styles.scss deleted file mode 100644 index 56b5d56b430..00000000000 --- a/docs/_sass/minima/custom-styles.scss +++ /dev/null @@ -1,34 +0,0 @@ -// Placeholder to allow defining custom styles that override everything else. -// (Use `_sass/minima/custom-variables.scss` to override variable defaults) -h2, h3, h4, h5, h6 { - color: #e46c0a; -} - -// Bootstrap style alerts -.alert { - position: relative; - padding: $alert-padding-y $alert-padding-x; - margin-bottom: $alert-margin-bottom; - border: $alert-border-width solid transparent; - border-radius : $alert-border-radius; -} - -// Headings for larger alerts -.alert-heading { - // Specified to prevent conflicts of changing $headings-color - color: inherit; -} - -// Provide class for links that match alerts -.alert-link { - font-weight: $alert-link-font-weight; -} - -// Generate contextual modifier classes for colorizing the alert. - -@each $color, $value in $theme-colors { - .alert-#{$color} { - @include alert-variant(color-level($value, $alert-bg-level), color-level($value, $alert-border-level), color-level($value, $alert-color-level)); - } -} - diff --git a/docs/_sass/minima/custom-variables.scss b/docs/_sass/minima/custom-variables.scss deleted file mode 100644 index a128970cbe7..00000000000 --- a/docs/_sass/minima/custom-variables.scss +++ /dev/null @@ -1,76 +0,0 @@ -// Placeholder to allow overriding predefined variables smoothly. - -//Bootstrap's default -$white: #fff !default; -$gray-100: #f8f9fa !default; -$gray-200: #e9ecef !default; -$gray-300: #dee2e6 !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #6c757d !default; -$gray-700: #495057 !default; -$gray-800: #343a40 !default; -$gray-900: #212529 !default; -$black: #000 !default; -$blue: #0d6efd !default; -$indigo: #6610f2 !default; -$purple: #6f42c1 !default; -$pink: #d63384 !default; -$red: #dc3545 !default; -$orange: #fd7e14 !default; -$yellow: #ffc107 !default; -$green: #28a745 !default; -$teal: #20c997 !default; -$cyan: #17a2b8 !default; - -$primary: $blue !default; -$secondary: $gray-600 !default; -$success: $green !default; -$info: $cyan !default; -$warning: $yellow !default; -$danger: $red !default; -$light: $gray-100 !default; -$dark: $gray-800 !default; - -$theme-colors: ( - "primary": $primary, - "secondary": $secondary, - "success": $success, - "info": $info, - "warning": $warning, - "danger": $danger, - "light": $light, - "dark": $dark -) !default; - -$theme-color-interval: 8% !default; - -$body-bg: $white !default; -$body-color: $gray-900 !default; -$body-text-align: null !default; - -$enable-gradients: true; - -// Define alert colors, border radius, and padding. -$border-radius: .25rem !default; -$border-width: 1px !default; -$font-weight-bold: 700 !default; - -$alert-padding-y: .75rem !default; -$alert-padding-x: 1.25rem !default; -$alert-margin-bottom: 1rem !default; -$alert-border-radius: $border-radius !default; -$alert-link-font-weight: $font-weight-bold !default; -$alert-border-width: $border-width !default; - -$alert-bg-level: -10 !default; -$alert-border-level: -9 !default; -$alert-color-level: 6 !default; - -// Request a color level -// scss-docs-start color-level -@function color-level($color: $primary, $level: 0) { - $color-base: if($level > 0, $black, $white); - $level: abs($level); - @return mix($color-base, $color, $level * $theme-color-interval); -} diff --git a/docs/_sass/minima/initialize.scss b/docs/_sass/minima/initialize.scss deleted file mode 100644 index 30288811151..00000000000 --- a/docs/_sass/minima/initialize.scss +++ /dev/null @@ -1,51 +0,0 @@ -@charset "utf-8"; - -// Define defaults for each variable. - -$base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Segoe UI Symbol", "Segoe UI Emoji", "Apple Color Emoji", Roboto, Helvetica, Arial, sans-serif !default; -$code-font-family: "Menlo", "Inconsolata", "Consolas", "Roboto Mono", "Ubuntu Mono", "Liberation Mono", "Courier New", monospace; -$base-font-size: 16px !default; -$base-font-weight: 400 !default; -$small-font-size: $base-font-size * 0.875 !default; -$base-line-height: 1.5 !default; - -$spacing-unit: 30px !default; - -$table-text-align: left !default; - -// Width of the content area -$content-width: 800px !default; - -$on-palm: 600px !default; -$on-laptop: 800px !default; - -$on-medium: $on-palm !default; -$on-large: $on-laptop !default; - -// Use media queries like this: -// @include media-query($on-palm) { -// .wrapper { -// padding-right: $spacing-unit / 2; -// padding-left: $spacing-unit / 2; -// } -// } -// Notice the following mixin uses max-width, in a deprecated, desktop-first -// approach, whereas media queries used elsewhere now use min-width. -@mixin media-query($device) { - @media screen and (max-width: $device) { - @content; - } -} - -@mixin relative-font-size($ratio) { - font-size: #{$ratio}rem; -} - -// Import pre-styling-overrides hook and style-partials. -@import - "minima/custom-variables", // Hook to override predefined variables. - "minima/custom-mixins", // Hook to add custom mixins. - "minima/base", // Defines element resets. - "minima/layout", // Defines structure and style based on CSS selectors. - "minima/custom-styles" // Hook to override existing styles. -; diff --git a/docs/_sass/minima/skins/classic.scss b/docs/_sass/minima/skins/classic.scss deleted file mode 100644 index 37ea9c5244c..00000000000 --- a/docs/_sass/minima/skins/classic.scss +++ /dev/null @@ -1,84 +0,0 @@ -@charset "utf-8"; - -$brand-color: #828282 !default; -$brand-color-light: lighten($brand-color, 40%) !default; -$brand-color-dark: darken($brand-color, 25%) !default; - -$text-color: #111 !default; -$background-color: #fdfdfd !default; -$code-background-color: #eef !default; - -$link-base-color: #2a7ae2 !default; -$link-visited-color: darken($link-base-color, 15%) !default; - -$table-text-color: lighten($text-color, 18%) !default; -$table-zebra-color: lighten($brand-color, 46%) !default; -$table-header-bg-color: lighten($brand-color, 43%) !default; -$table-header-border: lighten($brand-color, 36%) !default; -$table-border-color: $brand-color-light !default; - - -// Syntax highlighting styles should be adjusted appropriately for every "skin" -// ---------------------------------------------------------------------------- - -.highlight { - .c { color: #998; font-style: italic } // Comment - .err { color: #a61717; background-color: #e3d2d2 } // Error - .k { font-weight: bold } // Keyword - .o { font-weight: bold } // Operator - .cm { color: #998; font-style: italic } // Comment.Multiline - .cp { color: #999; font-weight: bold } // Comment.Preproc - .c1 { color: #998; font-style: italic } // Comment.Single - .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special - .gd { color: #000; background-color: #fdd } // Generic.Deleted - .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific - .ge { font-style: italic } // Generic.Emph - .gr { color: #a00 } // Generic.Error - .gh { color: #999 } // Generic.Heading - .gi { color: #000; background-color: #dfd } // Generic.Inserted - .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific - .go { color: #888 } // Generic.Output - .gp { color: #555 } // Generic.Prompt - .gs { font-weight: bold } // Generic.Strong - .gu { color: #aaa } // Generic.Subheading - .gt { color: #a00 } // Generic.Traceback - .kc { font-weight: bold } // Keyword.Constant - .kd { font-weight: bold } // Keyword.Declaration - .kp { font-weight: bold } // Keyword.Pseudo - .kr { font-weight: bold } // Keyword.Reserved - .kt { color: #458; font-weight: bold } // Keyword.Type - .m { color: #099 } // Literal.Number - .s { color: #d14 } // Literal.String - .na { color: #008080 } // Name.Attribute - .nb { color: #0086B3 } // Name.Builtin - .nc { color: #458; font-weight: bold } // Name.Class - .no { color: #008080 } // Name.Constant - .ni { color: #800080 } // Name.Entity - .ne { color: #900; font-weight: bold } // Name.Exception - .nf { color: #900; font-weight: bold } // Name.Function - .nn { color: #555 } // Name.Namespace - .nt { color: #000080 } // Name.Tag - .nv { color: #008080 } // Name.Variable - .ow { font-weight: bold } // Operator.Word - .w { color: #bbb } // Text.Whitespace - .mf { color: #099 } // Literal.Number.Float - .mh { color: #099 } // Literal.Number.Hex - .mi { color: #099 } // Literal.Number.Integer - .mo { color: #099 } // Literal.Number.Oct - .sb { color: #d14 } // Literal.String.Backtick - .sc { color: #d14 } // Literal.String.Char - .sd { color: #d14 } // Literal.String.Doc - .s2 { color: #d14 } // Literal.String.Double - .se { color: #d14 } // Literal.String.Escape - .sh { color: #d14 } // Literal.String.Heredoc - .si { color: #d14 } // Literal.String.Interpol - .sx { color: #d14 } // Literal.String.Other - .sr { color: #009926 } // Literal.String.Regex - .s1 { color: #d14 } // Literal.String.Single - .ss { color: #990073 } // Literal.String.Symbol - .bp { color: #999 } // Name.Builtin.Pseudo - .vc { color: #008080 } // Name.Variable.Class - .vg { color: #008080 } // Name.Variable.Global - .vi { color: #008080 } // Name.Variable.Instance - .il { color: #099 } // Literal.Number.Integer.Long -} diff --git a/docs/_sass/minima/skins/solarized-dark.scss b/docs/_sass/minima/skins/solarized-dark.scss deleted file mode 100644 index f3b1f387de0..00000000000 --- a/docs/_sass/minima/skins/solarized-dark.scss +++ /dev/null @@ -1,4 +0,0 @@ -@charset "utf-8"; - -$sol-is-dark: true; -@import "minima/skins/solarized"; diff --git a/docs/_sass/minima/skins/solarized.scss b/docs/_sass/minima/skins/solarized.scss deleted file mode 100644 index 982bd7f2990..00000000000 --- a/docs/_sass/minima/skins/solarized.scss +++ /dev/null @@ -1,133 +0,0 @@ -@charset "utf-8"; - -// Solarized skin -// ============== -// Created by Sander Voerman using the Solarized -// color scheme by Ethan Schoonover . - -// This style sheet implements two options for the minima.skin setting: -// "solarized" for light mode and "solarized-dark" for dark mode. -$sol-is-dark: false !default; - - -// Color scheme -// ------------ -// The inline comments show the canonical L*a*b values for each color. - -$sol-base03: #002b36; // 15 -12 -12 -$sol-base02: #073642; // 20 -12 -12 -$sol-base01: #586e75; // 45 -07 -07 -$sol-base00: #657b83; // 50 -07 -07 -$sol-base0: #839496; // 60 -06 -03 -$sol-base1: #93a1a1; // 65 -05 -02 -$sol-base2: #eee8d5; // 92 -00 10 -$sol-base3: #fdf6e3; // 97 00 10 -$sol-yellow: #b58900; // 60 10 65 -$sol-orange: #cb4b16; // 50 50 55 -$sol-red: #dc322f; // 50 65 45 -$sol-magenta: #d33682; // 50 65 -05 -$sol-violet: #6c71c4; // 50 15 -45 -$sol-blue: #268bd2; // 55 -10 -45 -$sol-cyan: #2aa198; // 60 -35 -05 -$sol-green: #859900; // 60 -20 65 - -$sol-mono3: $sol-base3; -$sol-mono2: $sol-base2; -$sol-mono1: $sol-base1; -$sol-mono00: $sol-base00; -$sol-mono01: $sol-base01; - -@if $sol-is-dark { - $sol-mono3: $sol-base03; - $sol-mono2: $sol-base02; - $sol-mono1: $sol-base01; - $sol-mono00: $sol-base0; - $sol-mono01: $sol-base1; -} - - -// Minima color variables -// ---------------------- - -$brand-color: $sol-mono1 !default; -$brand-color-light: mix($sol-mono1, $sol-mono3) !default; -$brand-color-dark: $sol-mono00 !default; - -$text-color: $sol-mono01 !default; -$background-color: $sol-mono3 !default; -$code-background-color: $sol-mono2 !default; - -$link-base-color: $sol-blue !default; -$link-visited-color: mix($sol-blue, $sol-mono00) !default; - -$table-text-color: $sol-mono00 !default; -$table-zebra-color: mix($sol-mono2, $sol-mono3) !default; -$table-header-bg-color: $sol-mono2 !default; -$table-header-border: $sol-mono1 !default; -$table-border-color: $sol-mono1 !default; - - -// Syntax highlighting styles -// -------------------------- - -.highlight { - .c { color: $sol-mono1; font-style: italic } // Comment - .err { color: $sol-red } // Error - .k { color: $sol-mono01; font-weight: bold } // Keyword - .o { color: $sol-mono01; font-weight: bold } // Operator - .cm { color: $sol-mono1; font-style: italic } // Comment.Multiline - .cp { color: $sol-mono1; font-weight: bold } // Comment.Preproc - .c1 { color: $sol-mono1; font-style: italic } // Comment.Single - .cs { color: $sol-mono1; font-weight: bold; font-style: italic } // Comment.Special - .gd { color: $sol-red } // Generic.Deleted - .gd .x { color: $sol-red } // Generic.Deleted.Specific - .ge { color: $sol-mono00; font-style: italic } // Generic.Emph - .gr { color: $sol-red } // Generic.Error - .gh { color: $sol-mono1 } // Generic.Heading - .gi { color: $sol-green } // Generic.Inserted - .gi .x { color: $sol-green } // Generic.Inserted.Specific - .go { color: $sol-mono00 } // Generic.Output - .gp { color: $sol-mono00 } // Generic.Prompt - .gs { color: $sol-mono01; font-weight: bold } // Generic.Strong - .gu { color: $sol-mono1 } // Generic.Subheading - .gt { color: $sol-red } // Generic.Traceback - .kc { color: $sol-mono01; font-weight: bold } // Keyword.Constant - .kd { color: $sol-mono01; font-weight: bold } // Keyword.Declaration - .kp { color: $sol-mono01; font-weight: bold } // Keyword.Pseudo - .kr { color: $sol-mono01; font-weight: bold } // Keyword.Reserved - .kt { color: $sol-violet; font-weight: bold } // Keyword.Type - .m { color: $sol-cyan } // Literal.Number - .s { color: $sol-magenta } // Literal.String - .na { color: $sol-cyan } // Name.Attribute - .nb { color: $sol-blue } // Name.Builtin - .nc { color: $sol-violet; font-weight: bold } // Name.Class - .no { color: $sol-cyan } // Name.Constant - .ni { color: $sol-violet } // Name.Entity - .ne { color: $sol-violet; font-weight: bold } // Name.Exception - .nf { color: $sol-blue; font-weight: bold } // Name.Function - .nn { color: $sol-mono00 } // Name.Namespace - .nt { color: $sol-blue } // Name.Tag - .nv { color: $sol-cyan } // Name.Variable - .ow { color: $sol-mono01; font-weight: bold } // Operator.Word - .w { color: $sol-mono1 } // Text.Whitespace - .mf { color: $sol-cyan } // Literal.Number.Float - .mh { color: $sol-cyan } // Literal.Number.Hex - .mi { color: $sol-cyan } // Literal.Number.Integer - .mo { color: $sol-cyan } // Literal.Number.Oct - .sb { color: $sol-magenta } // Literal.String.Backtick - .sc { color: $sol-magenta } // Literal.String.Char - .sd { color: $sol-magenta } // Literal.String.Doc - .s2 { color: $sol-magenta } // Literal.String.Double - .se { color: $sol-magenta } // Literal.String.Escape - .sh { color: $sol-magenta } // Literal.String.Heredoc - .si { color: $sol-magenta } // Literal.String.Interpol - .sx { color: $sol-magenta } // Literal.String.Other - .sr { color: $sol-green } // Literal.String.Regex - .s1 { color: $sol-magenta } // Literal.String.Single - .ss { color: $sol-magenta } // Literal.String.Symbol - .bp { color: $sol-mono1 } // Name.Builtin.Pseudo - .vc { color: $sol-cyan } // Name.Variable.Class - .vg { color: $sol-cyan } // Name.Variable.Global - .vi { color: $sol-cyan } // Name.Variable.Instance - .il { color: $sol-cyan } // Literal.Number.Integer.Long -} diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss deleted file mode 100644 index b5ec6976efa..00000000000 --- a/docs/assets/css/style.scss +++ /dev/null @@ -1,12 +0,0 @@ ---- -# Only the main Sass file needs front matter (the dashes are enough) ---- - -@import - "minima/skins/{{ site.minima.skin | default: 'classic' }}", - "minima/initialize"; - -.icon { - height: 21px; - width: 21px -} diff --git a/docs/diagrams/AddGSequenceDiagram.puml b/docs/diagrams/AddGSequenceDiagram.puml new file mode 100644 index 00000000000..a36d7a00363 --- /dev/null +++ b/docs/diagrams/AddGSequenceDiagram.puml @@ -0,0 +1,82 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddGCommandParser" as AddGCommandParser LOGIC_COLOR +participant "a:AddGCommand" as AddGCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("addG 1 u/alexyeoh") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("addG 1 u/alexyeoh") +activate AddressBookParser + +create AddGCommandParser +AddressBookParser -> AddGCommandParser +activate AddGCommandParser + +AddGCommandParser --> AddressBookParser +deactivate AddGCommandParser + +AddressBookParser -> AddGCommandParser : parse("1 u/alexyeoh") +activate AddGCommandParser + +create AddGCommand +AddGCommandParser -> AddGCommand +activate AddGCommand + +AddGCommand --> AddGCommandParser : a +deactivate AddGCommand + +AddGCommandParser --> AddressBookParser : a +deactivate AddGCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddGCommandParser -[hidden]-> AddressBookParser +destroy AddGCommandParser + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> AddGCommand : execute() +activate AddGCommand + +AddGCommand -> Model : getFilteredPersonList() +activate Model + +Model --> AddGCommand +deactivate Model + +AddGCommand -> Model : setLastViewedPersonIndex() +activate Model + +Model -> AddGCommand +deactivate Model + +AddGCommand -> Model : setPerson() +activate Model + +Model -> AddGCommand +deactivate Model + +create CommandResult +AddGCommand -> CommandResult +activate CommandResult + +CommandResult --> AddGCommand +deactivate CommandResult + +AddGCommand --> LogicManager : result +deactivate AddGCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/AddLSequenceDiagram.puml b/docs/diagrams/AddLSequenceDiagram.puml new file mode 100644 index 00000000000..27c07a8f111 --- /dev/null +++ b/docs/diagrams/AddLSequenceDiagram.puml @@ -0,0 +1,82 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddLCommandParser" as AddLCommandParser LOGIC_COLOR +participant "a:AddLCommand" as AddLCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("addL 1 u/alexyeoh") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("addL 1 u/alexyeoh") +activate AddressBookParser + +create AddLCommandParser +AddressBookParser -> AddLCommandParser +activate AddLCommandParser + +AddLCommandParser --> AddressBookParser +deactivate AddLCommandParser + +AddressBookParser -> AddLCommandParser : parse("1 u/alexyeoh") +activate AddLCommandParser + +create AddLCommand +AddLCommandParser -> AddLCommand +activate AddLCommand + +AddLCommand --> AddLCommandParser : a +deactivate AddLCommand + +AddLCommandParser --> AddressBookParser : a +deactivate AddLCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddLCommandParser -[hidden]-> AddressBookParser +destroy AddLCommandParser + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> AddLCommand : execute() +activate AddLCommand + +AddLCommand -> Model : getFilteredPersonList() +activate Model + +Model --> AddLCommand +deactivate Model + +AddLCommand -> Model : setLastViewedPersonIndex() +activate Model + +Model -> AddLCommand +deactivate Model + +AddLCommand -> Model : setPerson() +activate Model + +Model -> AddLCommand +deactivate Model + +create CommandResult +AddLCommand -> CommandResult +activate CommandResult + +CommandResult --> AddLCommand +deactivate CommandResult + +AddLCommand --> LogicManager : result +deactivate AddLCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/CreateTagActivityDiagram.puml b/docs/diagrams/CreateTagActivityDiagram.puml new file mode 100644 index 00000000000..a1897e16d14 --- /dev/null +++ b/docs/diagrams/CreateTagActivityDiagram.puml @@ -0,0 +1,19 @@ +@startuml +'https://plantuml.com/activity-diagram-beta +|User| +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:Start JABPro; +:User enters create command; + +if () then ([Command successfully executed]) + |JABPro| + :Create tag and + add to global tag list; +else ([Command fails to execute]) + :Show error message; +endif +stop +@enduml diff --git a/docs/diagrams/DeleteActivityDiagram.puml b/docs/diagrams/DeleteActivityDiagram.puml new file mode 100644 index 00000000000..b50eacac27f --- /dev/null +++ b/docs/diagrams/DeleteActivityDiagram.puml @@ -0,0 +1,19 @@ +@startuml +'https://plantuml.com/activity-diagram-beta +|User| +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:Start JABPro; +:User enters delete command; + +if () then ([Command successfully executed]) + |JABPro| + :Display list without + the deleted applicant(s); +else ([Command fails to execute]) + :Show error message; +endif +stop +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 40ea6c9dc4c..f35d9d8671a 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -14,10 +14,10 @@ box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("delete st/interviewed t/developer") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") +LogicManager -> AddressBookParser : parseCommand("delete st/interviewed t/developer") activate AddressBookParser create DeleteCommandParser @@ -27,11 +27,11 @@ activate DeleteCommandParser DeleteCommandParser --> AddressBookParser deactivate DeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") +AddressBookParser -> DeleteCommandParser : parse("delete st/interviewed t/developer") activate DeleteCommandParser create DeleteCommand -DeleteCommandParser -> DeleteCommand +DeleteCommandParser -> DeleteCommand : DeleteCommand(predicatesList) activate DeleteCommand DeleteCommand --> DeleteCommandParser : d @@ -49,7 +49,13 @@ deactivate AddressBookParser LogicManager -> DeleteCommand : execute() activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) +DeleteCommand -> Model : deletePerson(personsToDelete) +activate Model + +Model --> DeleteCommand +deactivate Model + +DeleteCommand -> Model : deleteEvent(eventsToDelete) activate Model Model --> DeleteCommand diff --git a/docs/diagrams/EventSequenceDiagram.puml b/docs/diagrams/EventSequenceDiagram.puml new file mode 100644 index 00000000000..6d391c0dce9 --- /dev/null +++ b/docs/diagrams/EventSequenceDiagram.puml @@ -0,0 +1,76 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EventCommandParser" as EventCommandParser LOGIC_COLOR +participant "e:EventCommand" as EventCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("event 1 d/...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("event 1 d/") +activate AddressBookParser + +create EventCommandParser +AddressBookParser -> EventCommandParser +activate EventCommandParser + +EventCommandParser --> AddressBookParser +deactivate EventCommandParser + +AddressBookParser -> EventCommandParser : parse("1 d/Interview bt/...") +activate EventCommandParser + +create EventCommand +EventCommandParser -> EventCommand +activate EventCommand + +EventCommand --> EventCommandParser : e +deactivate EventCommand + +EventCommandParser --> AddressBookParser : e +deactivate EventCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EventCommandParser -[hidden]-> AddressBookParser +destroy EventCommandParser + +AddressBookParser --> LogicManager : e +deactivate AddressBookParser + +LogicManager -> EventCommand : execute() +activate EventCommand + +EventCommand -> Model: getFilteredPersonList() +activate Model + +Model --> EventCommand +deactivate Model + +EventCommand -> Model : addEvent() +activate Model + +Model --> EventCommand +deactivate Model + +create CommandResult +EventCommand -> CommandResult +activate CommandResult + +CommandResult --> EventCommand +deactivate CommandResult + +EventCommand --> LogicManager : result +deactivate EventCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ExportActivityDiagram.puml b/docs/diagrams/ExportActivityDiagram.puml new file mode 100644 index 00000000000..344e4fb73c5 --- /dev/null +++ b/docs/diagrams/ExportActivityDiagram.puml @@ -0,0 +1,25 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +'https://plantuml.com/activity-diagram-beta +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +|User| +start +:Start JABPro; +:Enter export command; +if () then ([Command executes successfully]) + |JABPro| + :Exports existing data to /data/export.csv; +else ([Command fails to execute]) + |JABPro| + :Display error message and provide remedy; + |User| + +endif + +stop + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..1e3466f70c5 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -6,12 +6,18 @@ skinparam classBackgroundColor MODEL_COLOR Package Model as ModelPackage <>{ Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook +Class "<>\nReadOnlyEventBook" as ReadOnlyEventBook Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs +Class "<>\nReadOnlySummaryStatistic" as ReadOnlySummaryStatistic Class "<>\nModel" as Model Class AddressBook +Class EventBook Class ModelManager Class UserPrefs +Class SummaryStatistic +Class ObservablePersonList +Class UniqueTagList Class UniquePersonList Class Person Class Address @@ -19,6 +25,16 @@ Class Email Class Name Class Phone Class Tag +Class Remark +Class ScoreList +Class Score +Class Status +Class Github +Class Linkedin + + +Class Event +Class UniqueEventList Class I #FFFFFF } @@ -27,21 +43,45 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Model AddressBook .up.|> ReadOnlyAddressBook +EventBook .up.|> ReadOnlyEventBook + ModelManager .up.|> Model Model .right.> ReadOnlyUserPrefs +Model .right.> ReadOnlySummaryStatistic Model .left.> ReadOnlyAddressBook +Model .left.> ReadOnlyEventBook + ModelManager -left-> "1" AddressBook +ModelManager -left-> "1" EventBook +ModelManager -right-> "1" SummaryStatistic ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs +SummaryStatistic .up.|> ReadOnlySummaryStatistic + +SummaryStatistic *--> "1" ObservablePersonList +EventBook *--> "1" UniqueEventList +UniqueEventList --> "~* all" Event +AddressBook *--> "1" UniqueTagList AddressBook *--> "1" UniquePersonList +AddressBook *--> "1" ObservablePersonList UniquePersonList --> "~* all" Person +UniqueTagList --> "~*" Tag +ObservablePersonList --> "~* filtered" Person Person *--> Name Person *--> Phone Person *--> Email Person *--> Address -Person *--> "*" Tag +Person *--> Remark +Person *--> ScoreList +Person -left-> "*" Tag +Person *--> Status +Person *--> Github +Person *--> Linkedin + +ScoreList -up-> "*" Tag +ScoreList -down-> "*" Score Person -[hidden]up--> I UniquePersonList -[hidden]right-> I @@ -49,6 +89,8 @@ UniquePersonList -[hidden]right-> I Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email +Tag -[hidden]right-> ScoreList ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered" Event @enduml diff --git a/docs/diagrams/SearchActivityDiagram.puml b/docs/diagrams/SearchActivityDiagram.puml new file mode 100644 index 00000000000..430eb1b4b84 --- /dev/null +++ b/docs/diagrams/SearchActivityDiagram.puml @@ -0,0 +1,21 @@ +@startuml +'https://plantuml.com/activity-diagram-beta +|User| +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:Start JABPro; +:User enters command; + +if () then ([command successfully executed]) + |JABPro| + :Display filtered + list based on the + predicate (i.e. name, + and/status, and/ tag); +else ([command fails to execute]) + :Show error message; +endif +stop +@enduml diff --git a/docs/diagrams/SearchSequenceDiagram.puml b/docs/diagrams/SearchSequenceDiagram.puml new file mode 100644 index 00000000000..86eab9f8ce1 --- /dev/null +++ b/docs/diagrams/SearchSequenceDiagram.puml @@ -0,0 +1,81 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindCommandParser" as FindCommandParser LOGIC_COLOR +participant "f:FindCommand" as FindCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("search st/offered") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("search st/offered") +activate AddressBookParser + +create FindCommandParser +AddressBookParser -> FindCommandParser +activate FindCommandParser + +FindCommandParser --> AddressBookParser +deactivate FindCommandParser + +AddressBookParser -> FindCommandParser : parse("search st/offered") +activate FindCommandParser + + +FindCommandParser -> FindCommandParser: getPredicatesList(n, s, t) +activate FindCommandParser +return predicatesList + +create FindCommand +FindCommandParser -> FindCommand : findCommand(predicatesList) +activate FindCommand + + +FindCommand --> FindCommandParser : f +deactivate FindCommand + +FindCommandParser --> AddressBookParser : f +deactivate FindCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindCommandParser -[hidden]-> AddressBookParser +destroy FindCommandParser + +AddressBookParser --> LogicManager : f +deactivate AddressBookParser + +LogicManager -> FindCommand : execute() +activate FindCommand + +FindCommand -> Model : updateFilteredPersonList(predicatesList) +activate Model + +Model --> FindCommand +deactivate Model + +create CommandResult +FindCommand -> CommandResult +activate CommandResult + +CommandResult --> FindCommand +deactivate CommandResult + +FindCommand --> LogicManager : result +deactivate FindCommand + + + +[<--LogicManager +deactivate LogicManager + + + +@enduml diff --git a/docs/diagrams/SetActivityDiagram.puml b/docs/diagrams/SetActivityDiagram.puml new file mode 100644 index 00000000000..8c0115c66fc --- /dev/null +++ b/docs/diagrams/SetActivityDiagram.puml @@ -0,0 +1,27 @@ +@startuml +'https://plantuml.com/activity-diagram-beta +|User| +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:Start JABPro; +:Enter Set command; +if () then ([Command executes successfully]) + |JABPro| + :Set specified user to the specified status; + |User| + :Enter view command; + |JABPro| + :Show the updated user profile; + |User| +else ([Command fails to execute]) + |JABPro| + :Display error message and provide remedy; + |User| + +endif + +stop + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..4eb1e5c4a6c 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -18,8 +18,18 @@ package "AddressBook Storage" #F4F6F6{ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook -Class JsonAdaptedPerson -Class JsonAdaptedTag +Class JsonAdaptedPerson { + - tags: List> +} +Class JsonAdaptedScoreList +} + + +package "EventBook Storage" #F4F6F6{ +Class "<>\nEventBookStorage" as EventBookStorage +Class JsonEventBookStorage +Class JsonSerializableEventBook +Class JsonAdaptedEvent } } @@ -30,14 +40,22 @@ HiddenOutside ..> Storage StorageManager .up.|> Storage StorageManager -up-> "1" UserPrefsStorage StorageManager -up-> "1" AddressBookStorage +StorageManager -up-> "1" EventBookStorage Storage -left-|> UserPrefsStorage Storage -right-|> AddressBookStorage +Storage -right-|> EventBookStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedPerson --> "1" JsonAdaptedScoreList + +JsonEventBookStorage .up.|> EventBookStorage +JsonEventBookStorage ..> JsonSerializableEventBook +JsonSerializableEventBook --> "*" JsonAdaptedEvent + + @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..f37a0710972 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -9,12 +9,15 @@ Class "<>\nUi" as Ui Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow -Class HelpWindow Class ResultDisplay Class PersonListPanel Class PersonCard -Class StatusBarFooter +Class PersonInformationPanel +Class SummaryStatisticScreen Class CommandBox +Class EventWindow +Class EventListPanel +Class EventCard } package Model <> { @@ -33,8 +36,12 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel -MainWindow *-down-> "1" StatusBarFooter -MainWindow --> "0..1" HelpWindow +MainWindow --> "0..1" EventWindow +EventWindow *-down-> "1" EventCard +EventWindow *-down-> "1" EventListPanel +MainWindow *-down-> "1" PersonInformationPanel +MainWindow *-down-> "1" SummaryStatisticScreen + PersonListPanel -down-> "*" PersonCard @@ -42,19 +49,21 @@ MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart +PersonInformationPanel --|> UiPart +SummaryStatisticScreen --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow --|> UiPart + + +PersonInformationPanel ..> Model +SummaryStatisticScreen ..> Model PersonCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic - -PersonListPanel -[hidden]left- HelpWindow -HelpWindow -[hidden]left- CommandBox -CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter +CommandBox -[hidden]left-ResultDisplay +PersonInformationPanel -[hidden]left-CommandBox +SummaryStatisticScreen -[hidden]left-PersonInformationPanel MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/diagrams/ViewActivityDiagram.puml b/docs/diagrams/ViewActivityDiagram.puml new file mode 100644 index 00000000000..a615184ed70 --- /dev/null +++ b/docs/diagrams/ViewActivityDiagram.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/activity-diagram-beta +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +|User| +start +:Start JABPro; +:Enter Command that Triggers View; +if () then ([CommandExecuted]) + |JABPro| + :Carry out relevant changes to the specified applicant; + |JABPro| + :Details of specified applicant are saved to the database; + + |JABPro| + :Open the updated Person Information Panel; + |User| +else ([Invalid Command]) + |JABPro| + :Display error message and provide remedy; + |User| + +endif + +stop + +@enduml diff --git a/docs/diagrams/ViewSequenceDiagram.puml b/docs/diagrams/ViewSequenceDiagram.puml new file mode 100644 index 00000000000..8a0afe2fbdd --- /dev/null +++ b/docs/diagrams/ViewSequenceDiagram.puml @@ -0,0 +1,85 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ViewCommandParser" as ViewCommandParser LOGIC_COLOR +participant "v:ViewCommand" as ViewCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +activate MainWindow + + +MainWindow -> LogicManager : execute("view 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("view 1") +activate AddressBookParser + +create ViewCommandParser +AddressBookParser -> ViewCommandParser +activate ViewCommandParser + +ViewCommandParser --> AddressBookParser +deactivate ViewCommandParser + +AddressBookParser -> ViewCommandParser : parse("1") +activate ViewCommandParser + +create ViewCommand +ViewCommandParser -> ViewCommand +activate ViewCommand + +ViewCommand --> ViewCommandParser : v +deactivate ViewCommand + +ViewCommandParser --> AddressBookParser : v +deactivate ViewCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ViewCommandParser -[hidden]-> AddressBookParser +destroy ViewCommandParser + +AddressBookParser --> LogicManager : v +deactivate AddressBookParser + +LogicManager -> ViewCommand : execute() +activate ViewCommand + +ViewCommand -> Model : setLastViewedPersonIndex(1) +activate Model + +Model --> ViewCommand +deactivate Model + +create CommandResult +ViewCommand -> CommandResult +activate CommandResult + +CommandResult --> ViewCommand +deactivate CommandResult + +ViewCommand --> LogicManager : result +deactivate ViewCommand + +LogicManager --> MainWindow : result +deactivate LogicManager + +MainWindow --> MainWindow : handleView() + +destroy ViewCommand + + + + +@enduml diff --git a/docs/diagrams/eventactivitydiagram.puml b/docs/diagrams/eventactivitydiagram.puml new file mode 100644 index 00000000000..88deed41656 --- /dev/null +++ b/docs/diagrams/eventactivitydiagram.puml @@ -0,0 +1,28 @@ +@startuml +'https://plantuml.com/activity-diagram-beta +|User| +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start + +:Start JABPro; +:Enter event command; +if () then ([Command executed successfully]) + |JABPro| + :Add event to EventBook; + |User| + :Enter schedule command; + |JABPro| + :Open the Events window; + |User| +else ([Command execution failed]) + |JABPro| + :Display error message and provide remedy; + |User| + +endif + +stop + +@enduml diff --git a/docs/diagrams/linkedinactivitydiagram.puml b/docs/diagrams/linkedinactivitydiagram.puml new file mode 100644 index 00000000000..087a4d50d94 --- /dev/null +++ b/docs/diagrams/linkedinactivitydiagram.puml @@ -0,0 +1,30 @@ +@startuml +'https://plantuml.com/activity-diagram-beta +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +|User| +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:Start JABPro; +:Enter addL/addG command; +if () then ([Command executed successfully]) + |JABPro| + :Add username to candidates existing details; + |User| + :Enter linkedin/github command; + |JABPro| + :Redirect user to page of the social profile; + |User| +else ([Command execution failed]) + |JABPro| + :Display error message and provide remedy; + |User| + +endif + +stop + +@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index f7d7347ae84..eee6cf8c986 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -44,7 +44,12 @@ skinparam Class { FontSize 15 BorderThickness 1 BorderColor #FFFFFF - StereotypeFontColor #FFFFFF + StereotypeFontColor #000000 + FontName Arial +} + +skinparam ClassAttribute { + FontColor #FFFFFF FontName Arial } @@ -75,5 +80,5 @@ skinparam DefaultTextAlignment center skinparam packageStyle Rectangle hide footbox -hide members hide circle +hide empty members diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png deleted file mode 100644 index cd540665053..00000000000 Binary files a/docs/images/ArchitectureDiagram.png and /dev/null differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png deleted file mode 100644 index 37ad06a2803..00000000000 Binary files a/docs/images/ArchitectureSequenceDiagram.png and /dev/null differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png deleted file mode 100644 index 02a42e35e76..00000000000 Binary files a/docs/images/BetterModelClassDiagram.png and /dev/null differ diff --git a/docs/images/CommitActivityDiagram.png b/docs/images/CommitActivityDiagram.png deleted file mode 100644 index 5b464126b35..00000000000 Binary files a/docs/images/CommitActivityDiagram.png and /dev/null differ diff --git a/docs/images/ComponentManagers.png b/docs/images/ComponentManagers.png deleted file mode 100644 index ae52a35718a..00000000000 Binary files a/docs/images/ComponentManagers.png and /dev/null differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png deleted file mode 100644 index e186f7ba096..00000000000 Binary files a/docs/images/DeleteSequenceDiagram.png and /dev/null differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png deleted file mode 100644 index e3b784310fe..00000000000 Binary files a/docs/images/LogicClassDiagram.png and /dev/null differ diff --git a/docs/images/LogicStorageDIP.png b/docs/images/LogicStorageDIP.png deleted file mode 100644 index 871157f5a9c..00000000000 Binary files a/docs/images/LogicStorageDIP.png and /dev/null differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png deleted file mode 100644 index a19fb1b4ac8..00000000000 Binary files a/docs/images/ModelClassDiagram.png and /dev/null differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png deleted file mode 100644 index edfd1ff7897..00000000000 Binary files a/docs/images/ParserClasses.png and /dev/null differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png deleted file mode 100644 index 18fa4d0d51f..00000000000 Binary files a/docs/images/StorageClassDiagram.png and /dev/null differ diff --git a/docs/images/UICompressed.png b/docs/images/UICompressed.png new file mode 100644 index 00000000000..1e2aaeb53fa Binary files /dev/null and b/docs/images/UICompressed.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..cc9f50bbc30 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png deleted file mode 100644 index 11f06d68671..00000000000 Binary files a/docs/images/UiClassDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index c5f91b58533..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index 2d3ad09c047..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 20853694e03..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 1a9551b31be..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 46dfae78c94..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index f45889b5fdf..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png deleted file mode 100644 index c7a7e637266..00000000000 Binary files a/docs/images/UndoSequenceDiagram.png and /dev/null differ diff --git a/docs/images/add-command-duplicate-person.png b/docs/images/add-command-duplicate-person.png new file mode 100644 index 00000000000..93e60cbe152 Binary files /dev/null and b/docs/images/add-command-duplicate-person.png differ diff --git a/docs/images/add-command-failure.png b/docs/images/add-command-failure.png new file mode 100644 index 00000000000..85d58175150 Binary files /dev/null and b/docs/images/add-command-failure.png differ diff --git a/docs/images/add-command-success.png b/docs/images/add-command-success.png new file mode 100644 index 00000000000..a0e25efbb6e Binary files /dev/null and b/docs/images/add-command-success.png differ diff --git a/docs/images/addL.png b/docs/images/addL.png new file mode 100644 index 00000000000..a2262abda2a Binary files /dev/null and b/docs/images/addL.png differ diff --git a/docs/images/addLState.png b/docs/images/addLState.png new file mode 100644 index 00000000000..4579605548d Binary files /dev/null and b/docs/images/addLState.png differ diff --git a/docs/images/addLfail.png b/docs/images/addLfail.png new file mode 100644 index 00000000000..1d2d6abe445 Binary files /dev/null and b/docs/images/addLfail.png differ diff --git a/docs/images/ariellacallista.png b/docs/images/ariellacallista.png new file mode 100644 index 00000000000..a285b9ec641 Binary files /dev/null and b/docs/images/ariellacallista.png differ diff --git a/docs/images/blue.png b/docs/images/blue.png new file mode 100644 index 00000000000..42874eedce0 Binary files /dev/null and b/docs/images/blue.png differ diff --git a/docs/images/brown.png b/docs/images/brown.png new file mode 100644 index 00000000000..0fc51e90e34 Binary files /dev/null and b/docs/images/brown.png differ diff --git a/docs/images/clear.png b/docs/images/clear.png new file mode 100644 index 00000000000..734583fc30b Binary files /dev/null and b/docs/images/clear.png differ diff --git a/docs/images/create-success.png b/docs/images/create-success.png new file mode 100644 index 00000000000..57485921786 Binary files /dev/null and b/docs/images/create-success.png differ diff --git a/docs/images/create.png b/docs/images/create.png new file mode 100644 index 00000000000..5dec64c7d6f Binary files /dev/null and b/docs/images/create.png differ diff --git a/docs/images/delete-by-index-pre.png b/docs/images/delete-by-index-pre.png new file mode 100644 index 00000000000..382240b9f54 Binary files /dev/null and b/docs/images/delete-by-index-pre.png differ diff --git a/docs/images/delete-by-index-success.png b/docs/images/delete-by-index-success.png new file mode 100644 index 00000000000..e4e0284d7ce Binary files /dev/null and b/docs/images/delete-by-index-success.png differ diff --git a/docs/images/delete-by-tags-status-1.png b/docs/images/delete-by-tags-status-1.png new file mode 100644 index 00000000000..ba7db1dbbb6 Binary files /dev/null and b/docs/images/delete-by-tags-status-1.png differ diff --git a/docs/images/delete-by-tags-status-2.png b/docs/images/delete-by-tags-status-2.png new file mode 100644 index 00000000000..e2b62f9a0ff Binary files /dev/null and b/docs/images/delete-by-tags-status-2.png differ diff --git a/docs/images/delete-by-tags-status-pre.png b/docs/images/delete-by-tags-status-pre.png new file mode 100644 index 00000000000..81841667de8 Binary files /dev/null and b/docs/images/delete-by-tags-status-pre.png differ diff --git a/docs/images/delete.png b/docs/images/delete.png new file mode 100644 index 00000000000..8bfda68ef61 Binary files /dev/null and b/docs/images/delete.png differ diff --git a/docs/images/edit-command-clear-tags-success.png b/docs/images/edit-command-clear-tags-success.png new file mode 100644 index 00000000000..0762d0c7e24 Binary files /dev/null and b/docs/images/edit-command-clear-tags-success.png differ diff --git a/docs/images/edit-command-failure.png b/docs/images/edit-command-failure.png new file mode 100644 index 00000000000..167a9728f47 Binary files /dev/null and b/docs/images/edit-command-failure.png differ diff --git a/docs/images/edit-command-success.png b/docs/images/edit-command-success.png new file mode 100644 index 00000000000..b4e7ba5f86b Binary files /dev/null and b/docs/images/edit-command-success.png differ diff --git a/docs/images/edit-command-with-stats-success.png b/docs/images/edit-command-with-stats-success.png new file mode 100644 index 00000000000..4f925141920 Binary files /dev/null and b/docs/images/edit-command-with-stats-success.png differ diff --git a/docs/images/editscore.png b/docs/images/editscore.png new file mode 100644 index 00000000000..f5baeb8a8c9 Binary files /dev/null and b/docs/images/editscore.png differ diff --git a/docs/images/enhancedremark.png b/docs/images/enhancedremark.png new file mode 100644 index 00000000000..cbd1fbe6796 Binary files /dev/null and b/docs/images/enhancedremark.png differ diff --git a/docs/images/event.png b/docs/images/event.png new file mode 100644 index 00000000000..44fb4a7fdbf Binary files /dev/null and b/docs/images/event.png differ diff --git a/docs/images/eventfail.png b/docs/images/eventfail.png new file mode 100644 index 00000000000..225d80931ea Binary files /dev/null and b/docs/images/eventfail.png differ diff --git a/docs/images/eventstab.png b/docs/images/eventstab.png new file mode 100644 index 00000000000..58ba21139d5 Binary files /dev/null and b/docs/images/eventstab.png differ diff --git a/docs/images/eventwin.png b/docs/images/eventwin.png new file mode 100644 index 00000000000..3757ca20cfe Binary files /dev/null and b/docs/images/eventwin.png differ diff --git a/docs/images/filter-command-failure-2.png b/docs/images/filter-command-failure-2.png new file mode 100644 index 00000000000..9bc56087ebf Binary files /dev/null and b/docs/images/filter-command-failure-2.png differ diff --git a/docs/images/filter-command-failure.png b/docs/images/filter-command-failure.png new file mode 100644 index 00000000000..809cdbd8e05 Binary files /dev/null and b/docs/images/filter-command-failure.png differ diff --git a/docs/images/filter-command-median-success.png b/docs/images/filter-command-median-success.png new file mode 100644 index 00000000000..1b62c858de8 Binary files /dev/null and b/docs/images/filter-command-median-success.png differ diff --git a/docs/images/filter-command-success.png b/docs/images/filter-command-success.png new file mode 100644 index 00000000000..f0b767bf415 Binary files /dev/null and b/docs/images/filter-command-success.png differ diff --git a/docs/images/filter-setup.png b/docs/images/filter-setup.png new file mode 100644 index 00000000000..f0d99ae1a6f Binary files /dev/null and b/docs/images/filter-setup.png differ diff --git a/docs/images/filter.png b/docs/images/filter.png new file mode 100644 index 00000000000..7e23518f4be Binary files /dev/null and b/docs/images/filter.png differ diff --git a/docs/images/github.png b/docs/images/github.png new file mode 100644 index 00000000000..12b63da28b2 Binary files /dev/null and b/docs/images/github.png differ diff --git a/docs/images/green.png b/docs/images/green.png new file mode 100644 index 00000000000..2f87bab39d2 Binary files /dev/null and b/docs/images/green.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..b9e02d2e659 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/helptip.png b/docs/images/helptip.png new file mode 100644 index 00000000000..ba0b8ae4ba3 Binary files /dev/null and b/docs/images/helptip.png differ diff --git a/docs/images/helpwindow-mh.png b/docs/images/helpwindow-mh.png new file mode 100644 index 00000000000..16b82c19062 Binary files /dev/null and b/docs/images/helpwindow-mh.png differ diff --git a/docs/images/helpwindow.png b/docs/images/helpwindow.png new file mode 100644 index 00000000000..a92e9186de7 Binary files /dev/null and b/docs/images/helpwindow.png differ diff --git a/docs/images/hw.png b/docs/images/hw.png new file mode 100644 index 00000000000..e53f3de8a9a Binary files /dev/null and b/docs/images/hw.png differ diff --git a/docs/images/jabpro.png b/docs/images/jabpro.png new file mode 100644 index 00000000000..f6d535e953f Binary files /dev/null and b/docs/images/jabpro.png differ diff --git a/docs/images/ketweeen.png b/docs/images/ketweeen.png new file mode 100644 index 00000000000..251f35c8284 Binary files /dev/null and b/docs/images/ketweeen.png differ diff --git a/docs/images/linkedin.png b/docs/images/linkedin.png new file mode 100644 index 00000000000..86ce77beb68 Binary files /dev/null and b/docs/images/linkedin.png differ diff --git a/docs/images/linkedinState.png b/docs/images/linkedinState.png new file mode 100644 index 00000000000..c174297bed0 Binary files /dev/null and b/docs/images/linkedinState.png differ diff --git a/docs/images/linkedinfail.png b/docs/images/linkedinfail.png new file mode 100644 index 00000000000..a190f1c01d0 Binary files /dev/null and b/docs/images/linkedinfail.png differ diff --git a/docs/images/listT-create-success.png b/docs/images/listT-create-success.png new file mode 100644 index 00000000000..ad59044f5af Binary files /dev/null and b/docs/images/listT-create-success.png differ diff --git a/docs/images/listT-create.png b/docs/images/listT-create.png new file mode 100644 index 00000000000..33977cf3cbd Binary files /dev/null and b/docs/images/listT-create.png differ diff --git a/docs/images/listT.png b/docs/images/listT.png new file mode 100644 index 00000000000..819cafbcc34 Binary files /dev/null and b/docs/images/listT.png differ diff --git a/docs/images/madlamprey.png b/docs/images/madlamprey.png new file mode 100644 index 00000000000..0a349468a63 Binary files /dev/null and b/docs/images/madlamprey.png differ diff --git a/docs/images/main.png b/docs/images/main.png new file mode 100644 index 00000000000..15db79da55a Binary files /dev/null and b/docs/images/main.png differ diff --git a/docs/images/orange.png b/docs/images/orange.png new file mode 100644 index 00000000000..bc624c381cb Binary files /dev/null and b/docs/images/orange.png differ diff --git a/docs/images/purple.png b/docs/images/purple.png new file mode 100644 index 00000000000..328004c6694 Binary files /dev/null and b/docs/images/purple.png differ diff --git a/docs/images/red_1.png b/docs/images/red_1.png new file mode 100644 index 00000000000..bf4de8387d6 Binary files /dev/null and b/docs/images/red_1.png differ diff --git a/docs/images/remark-command-clear-remark.png b/docs/images/remark-command-clear-remark.png new file mode 100644 index 00000000000..c161693dbc3 Binary files /dev/null and b/docs/images/remark-command-clear-remark.png differ diff --git a/docs/images/remark-command-enhanced-success.png b/docs/images/remark-command-enhanced-success.png new file mode 100644 index 00000000000..bba70bf28fb Binary files /dev/null and b/docs/images/remark-command-enhanced-success.png differ diff --git a/docs/images/remark-command-success.png b/docs/images/remark-command-success.png new file mode 100644 index 00000000000..58998b38a04 Binary files /dev/null and b/docs/images/remark-command-success.png differ diff --git a/docs/images/remark.png b/docs/images/remark.png new file mode 100644 index 00000000000..2fe69d0380d Binary files /dev/null and b/docs/images/remark.png differ diff --git a/docs/images/scarletblanks.png b/docs/images/scarletblanks.png new file mode 100644 index 00000000000..1ce7ce16dc8 Binary files /dev/null and b/docs/images/scarletblanks.png differ diff --git a/docs/images/schedule.png b/docs/images/schedule.png new file mode 100644 index 00000000000..eecd4c41d55 Binary files /dev/null and b/docs/images/schedule.png differ diff --git a/docs/images/search-2.png b/docs/images/search-2.png new file mode 100644 index 00000000000..0a373a6a4ff Binary files /dev/null and b/docs/images/search-2.png differ diff --git a/docs/images/search-dg.png b/docs/images/search-dg.png new file mode 100644 index 00000000000..8000baefd4f Binary files /dev/null and b/docs/images/search-dg.png differ diff --git a/docs/images/search-success-1.png b/docs/images/search-success-1.png new file mode 100644 index 00000000000..013b5b2f410 Binary files /dev/null and b/docs/images/search-success-1.png differ diff --git a/docs/images/search-success.png b/docs/images/search-success.png new file mode 100644 index 00000000000..c53e09c461e Binary files /dev/null and b/docs/images/search-success.png differ diff --git a/docs/images/search.png b/docs/images/search.png new file mode 100644 index 00000000000..6a8c8208602 Binary files /dev/null and b/docs/images/search.png differ diff --git a/docs/images/sk2001git.png b/docs/images/sk2001git.png new file mode 100644 index 00000000000..084151f385e Binary files /dev/null and b/docs/images/sk2001git.png differ diff --git a/docs/images/tracing/LogicSequenceDiagram.png b/docs/images/tracing/LogicSequenceDiagram.png deleted file mode 100644 index 25c8b66b9f1..00000000000 Binary files a/docs/images/tracing/LogicSequenceDiagram.png and /dev/null differ diff --git a/docs/images/uibreak.png b/docs/images/uibreak.png new file mode 100644 index 00000000000..fc5f7fa819a Binary files /dev/null and b/docs/images/uibreak.png differ diff --git a/docs/images/view-command-failure.png b/docs/images/view-command-failure.png new file mode 100644 index 00000000000..4c278101cd4 Binary files /dev/null and b/docs/images/view-command-failure.png differ diff --git a/docs/images/view-command-success.png b/docs/images/view-command-success.png new file mode 100644 index 00000000000..fb8d4445bef Binary files /dev/null and b/docs/images/view-command-success.png differ diff --git a/docs/images/view-command-with-stats-success.png b/docs/images/view-command-with-stats-success.png new file mode 100644 index 00000000000..1f9d05aae93 Binary files /dev/null and b/docs/images/view-command-with-stats-success.png differ diff --git a/docs/images/view.png b/docs/images/view.png new file mode 100644 index 00000000000..479d1bf7244 Binary files /dev/null and b/docs/images/view.png differ diff --git a/docs/images/viewState.png b/docs/images/viewState.png new file mode 100644 index 00000000000..42a6a451821 Binary files /dev/null and b/docs/images/viewState.png differ diff --git a/docs/images/yellow.png b/docs/images/yellow.png new file mode 100644 index 00000000000..07cda3a193e Binary files /dev/null and b/docs/images/yellow.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..070362829fa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,19 @@ --- -layout: page -title: AddressBook Level-3 + layout: default.md + title: "" --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +# JABPro + +[![CI Status](https://github.com/ay2324s1-cs2103t-w09-4/tp/workflows/Java%20CI/badge.svg)](https://github.com/ay2324s1-cs2103t-w09-4/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S1-CS2103T-W09-4/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2324S1-CS2103T-W09-4/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**JABPro is a desktop application used for managing job applications received by you.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using JABPro, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing JABpro, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000000..63a232e05dc --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,8587 @@ +{ + "name": "docs", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "1.0.0", + "devDependencies": { + "markbind-cli": "^5.1.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "node_modules/@markbind/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core/-/core-5.1.0.tgz", + "integrity": "sha512-YAXjH+qCXnrBzpKIAJkayVLmyIUaG/8Dms3Gpd2VIufeZyW8w0diXdgKSsymjzodTMgghZMdxG3Qpng833ARPg==", + "dev": true, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.4.0", + "@markbind/core-web": "5.1.0", + "@primer/octicons": "^15.0.1", + "@sindresorhus/slugify": "^0.9.1", + "@tlylt/markdown-it-imsize": "^3.0.0", + "bluebird": "^3.7.2", + "bootswatch": "5.1.3", + "cheerio": "^0.22.0", + "crypto-js": "^4.0.0", + "csv-parse": "^4.14.2", + "ensure-posix-path": "^1.1.1", + "fastmatter": "^2.1.1", + "fs-extra": "^9.0.1", + "gh-pages": "^2.1.1", + "highlight.js": "^10.4.1", + "htmlparser2": "^3.10.1", + "ignore": "^5.1.4", + "js-beautify": "1.14.3", + "katex": "^0.15.6", + "lodash": "^4.17.15", + "markdown-it": "^12.3.2", + "markdown-it-attrs": "^4.1.3", + "markdown-it-emoji": "^1.4.0", + "markdown-it-linkify-images": "^3.0.0", + "markdown-it-mark": "^3.0.0", + "markdown-it-regexp": "^0.4.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-table-of-contents": "^0.4.4", + "markdown-it-task-lists": "^2.1.1", + "markdown-it-texmath": "^1.0.0", + "markdown-it-video": "^0.6.3", + "material-icons": "^1.9.1", + "moment": "^2.29.4", + "nunjucks": "3.2.2", + "path-is-inside": "^1.0.2", + "simple-git": "^2.17.0", + "url-parse": "^1.5.10", + "uuid": "^8.3.1", + "vue": "2.6.14", + "vue-server-renderer": "2.6.14", + "vue-template-compiler": "2.6.14", + "walk-sync": "^2.0.2", + "winston": "^2.4.4" + } + }, + "node_modules/@markbind/core-web": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core-web/-/core-web-5.1.0.tgz", + "integrity": "sha512-TRzz8ZCr25pylKvFxF/WwXDi4Gbtsb2OLXV61WyTFqVy03tFoEJ2mqncpbliI9DrfDdKWcm1YZPgDCedVkYjKA==", + "dev": true + }, + "node_modules/@primer/octicons": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-15.2.0.tgz", + "integrity": "sha512-4cHZzcZ3F/HQNL4EKSaFyVsW7XtITiJkTeB1JDDmRuP/XobyWyF9gWxuV9c+byUa8dOB5KNQn37iRvNrIehPUQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1" + } + }, + "node_modules/@sindresorhus/slugify": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-0.9.1.tgz", + "integrity": "sha512-b6heYM9dzZD13t2GOiEQTDE0qX+I1GyOotMwKh9VQqzuNiVdPVT8dM43fe9HNb/3ul+Qwd5oKSEDrDIfhq3bnQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "lodash.deburr": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@tlylt/markdown-it-imsize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tlylt/markdown-it-imsize/-/markdown-it-imsize-3.0.0.tgz", + "integrity": "sha512-6kTM+vRJTuN2UxNPyJ8yC+NHrzS+MxVHV+z+bDxSr/Fd7eTah2+otLKC2B17YI/1lQnSumA2qokPGuzsA98c6g==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apache-crypt": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.5.tgz", + "integrity": "sha512-ICnYQH+DFVmw+S4Q0QY2XRXD8Ne8ewh8HgbuFH4K7022zCxgHM0Hz1xkRnUlEfAXNbwp1Cnhbedu60USIfDxvg==", + "dev": true, + "dependencies": { + "unix-crypt-td-js": "^1.1.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/apache-md5": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.7.tgz", + "integrity": "sha512-JtHjzZmJxtzfTSjsCyHgPR155HBe5WGyUyHTaEkfy46qhwCFKx1Epm6nAxgUG3WfUZP1dWhGqj9Z2NOBeZ+uBw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bootswatch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.1.3.tgz", + "integrity": "sha512-NmZFN6rOCoXWQ/PkzmD8FFWDe24kocX9OXWHNVaLxVVnpqpAzEbMFsf8bAfKwVtpNXibasZCzv09B5fLieAh2g==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "dev": true, + "dependencies": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, + "node_modules/css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "node_modules/css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "dependencies": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "bin": { + "editorconfig": "bin/editorconfig" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/email-addresses": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", + "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/event-stream/node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/event-stream/node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dev": true, + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fastmatter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fastmatter/-/fastmatter-2.1.1.tgz", + "integrity": "sha512-NFrjZEPJZTexoJEuyM5J7n4uFaLf0dOI7Ok4b2IZXOYBqCp1Bh5RskANmQ2TuDsz3M35B1yL2AP/Rn+kp85KeA==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.0", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through2": "^3.0.1" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "dev": true + }, + "node_modules/figlet": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", + "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", + "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", + "dev": true, + "dependencies": { + "moment": "^2.11.2" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "node_modules/filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha512-UZArj7+U+2reBBVCvVmRlyq9D7EYQdUtuNN+1iz7pF1jGcJ2L0TjiRCxsTZfj2xFbM4c25uGCUDpKTHA7L2TKg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha512-DKVP0WQcB7WaIMSwDETqImRej2fepPqvXQjaVib7LRZn9Rxn5UbvK2tYTqGf1A1DkIprQQkG4XSQXSOZp7Q3GQ==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filenamify-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", + "integrity": "sha512-O9K9JcZeF5VdZWM1qR92NSv1WY2EofwudQayPx5dbnnFl9k0IcZha4eV/FGkjnBK+1irOQInij0yiooCHu/0Fg==", + "dev": true, + "dependencies": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-2.2.0.tgz", + "integrity": "sha512-c+yPkNOPMFGNisYg9r4qvsMIjVYikJv7ImFOhPIVPt0+AcRUamZ7zkGRLHz7FKB0xrlZ+ddSOJsZv9XAFVXLmA==", + "dev": true, + "dependencies": { + "async": "^2.6.1", + "commander": "^2.18.0", + "email-addresses": "^3.0.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^8.1.0", + "globby": "^6.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/gh-pages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/gh-pages/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/gh-pages/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/http-auth": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", + "integrity": "sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==", + "dev": true, + "dependencies": { + "apache-crypt": "^1.1.2", + "apache-md5": "^1.0.6", + "bcryptjs": "^2.3.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">=4.6.1" + } + }, + "node_modules/http-auth/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/humanize-url": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", + "integrity": "sha512-RtgTzXCPVb/te+e82NDhAc5paj+DuKSratIGAr+v+HZK24eAQ8LMoBGYoL7N/O+9iEc33AKHg45dOMKw3DNldQ==", + "dev": true, + "dependencies": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/js-beautify": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz", + "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "nopt": "^5.0.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.6.tgz", + "integrity": "sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/live-server": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.1.tgz", + "integrity": "sha512-Yn2XCVjErTkqnM3FfTmM7/kWy3zP7+cEtC7x6u+wUzlQ+1UW3zEYbbyJrc0jNDwiMDZI0m4a0i3dxlGHVyXczw==", + "dev": true, + "dependencies": { + "chokidar": "^2.0.4", + "colors": "latest", + "connect": "^3.6.6", + "cors": "latest", + "event-stream": "3.3.4", + "faye-websocket": "0.11.x", + "http-auth": "3.1.x", + "morgan": "^1.9.1", + "object-assign": "latest", + "opn": "latest", + "proxy-middleware": "latest", + "send": "latest", + "serve-index": "^1.9.1" + }, + "bin": { + "live-server": "live-server.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/live-server/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/live-server/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/live-server/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/live-server/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/live-server/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/live-server/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/live-server/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/live-server/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "node_modules/lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "dev": true + }, + "node_modules/lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "dev": true + }, + "node_modules/lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "dev": true + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "dev": true + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "dev": true + }, + "node_modules/lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "dev": true + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/logform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", + "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", + "dev": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.2.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markbind-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/markbind-cli/-/markbind-cli-5.1.0.tgz", + "integrity": "sha512-6POI1Q++2aZa+Udk/oQ6LX1oNPbKUBDY0mN3Up7VOFeK+XYW51faxuCk2Q91JTBxYRKLNtshxf0y12kB4Cj9Qw==", + "dev": true, + "dependencies": { + "@markbind/core": "5.1.0", + "@markbind/core-web": "5.1.0", + "bluebird": "^3.7.2", + "chalk": "^3.0.0", + "cheerio": "^0.22.0", + "chokidar": "^3.3.0", + "colors": "1.4.0", + "commander": "^8.1.0", + "figlet": "^1.2.4", + "find-up": "^4.1.0", + "fs-extra": "^9.0.1", + "live-server": "1.2.1", + "lodash": "^4.17.15", + "url-parse": "^1.5.10", + "winston": "^2.4.4", + "winston-daily-rotate-file": "^3.10.0" + }, + "bin": { + "markbind": "index.js" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-attrs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz", + "integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "markdown-it": ">= 9.0.0" + } + }, + "node_modules/markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", + "dev": true + }, + "node_modules/markdown-it-linkify-images": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz", + "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==", + "dev": true, + "dependencies": { + "markdown-it": "^13.0.1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdown-it-linkify-images/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/markdown-it-linkify-images/node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-mark": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", + "integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==", + "dev": true + }, + "node_modules/markdown-it-regexp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-regexp/-/markdown-it-regexp-0.4.0.tgz", + "integrity": "sha512-0XQmr46K/rMKnI93Y3CLXsHj4jIioRETTAiVnJnjrZCEkGaDOmUxTbZj/aZ17G5NlRcVpWBYjqpwSlQ9lj+Kxw==", + "dev": true + }, + "node_modules/markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q==", + "dev": true + }, + "node_modules/markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==", + "dev": true + }, + "node_modules/markdown-it-table-of-contents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", + "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", + "dev": true, + "engines": { + "node": ">6.4.0" + } + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true + }, + "node_modules/markdown-it-texmath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", + "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", + "dev": true + }, + "node_modules/markdown-it-video": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", + "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==", + "dev": true + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/material-icons": { + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.11.tgz", + "integrity": "sha512-kp2oAdaqo/Zp6hpTZW01rOgDPWmxBUszSdDzkRm1idCjjNvdUMnqu8qu58cll6CObo+o0cydOiPLdoSugLm+mQ==", + "dev": true + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "dev": true, + "optional": true + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/nunjucks": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.2.tgz", + "integrity": "sha512-KUi85OoF2NMygwODAy28Lh9qHmq5hO3rBlbkYoC8v377h4l8Pt5qFjILl0LWpMbOrZ18CzfVVUvIHUIrtED3sA==", + "dev": true, + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "optionalDependencies": { + "chokidar": "^3.3.0" + } + }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "deprecated": "The package has been renamed to `open`", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proxy-middleware": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", + "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "node_modules/simple-git": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.48.0.tgz", + "integrity": "sha512-z4qtrRuaAFJS4PUd0g+xy7aN4y+RvEt/QTJpR184lhJguBA1S/LsVlvE/CM95RsYMOFJG3NGGDjqFCzKU19S/A==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/steveukx/" + } + }, + "node_modules/simple-git/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/simple-git/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-url-auth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", + "integrity": "sha512-++41PnXftlL3pvI6lpvhSEO+89g1kIJC4MYB5E6yH+WHa5InIqz51yGd1YOGd7VNSNdoEOfzTMqbAM/2PbgaHQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unix-crypt-td-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vue": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==", + "dev": true + }, + "node_modules/vue-server-renderer": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.6.14.tgz", + "integrity": "sha512-HifYRa/LW7cKywg9gd4ZtvtRuBlstQBao5ZCWlg40fyB4OPoGfEXAzxb0emSLv4pBDOHYx0UjpqvxpiQFEuoLA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "hash-sum": "^1.0.2", + "he": "^1.1.0", + "lodash.template": "^4.5.0", + "lodash.uniq": "^4.5.0", + "resolve": "^1.2.0", + "serialize-javascript": "^3.1.0", + "source-map": "0.5.6" + } + }, + "node_modules/vue-server-renderer/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-server-renderer/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-server-renderer/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/winston": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/winston-compat": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.5.tgz", + "integrity": "sha512-EPvPcHT604AV3Ji6d3+vX8ENKIml9VSxMRnPQ+cuK/FX6f3hvPP2hxyoeeCOCFvDrJEujalfcKWlWPvAnFyS9g==", + "dev": true, + "dependencies": { + "cycle": "~1.0.3", + "logform": "^1.6.0", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.10.0.tgz", + "integrity": "sha512-KO8CfbI2CvdR3PaFApEH02GPXiwJ+vbkF1mCkTlvRIoXFI8EFlf1ACcuaahXTEiDEKCii6cNe95gsL4ZkbnphA==", + "dev": true, + "dependencies": { + "file-stream-rotator": "^0.4.1", + "object-hash": "^1.3.0", + "semver": "^6.2.0", + "triple-beam": "^1.3.0", + "winston-compat": "^0.1.4", + "winston-transport": "^4.2.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "winston": "^2 || ^3" + } + }, + "node_modules/winston-daily-rotate-file/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dev": true, + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport/node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "node_modules/winston-transport/node_modules/logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/winston-transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/winston/node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/winston/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + }, + "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true + }, + "@fortawesome/fontawesome-free": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "dev": true + }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "requires": { + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "@markbind/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core/-/core-5.1.0.tgz", + "integrity": "sha512-YAXjH+qCXnrBzpKIAJkayVLmyIUaG/8Dms3Gpd2VIufeZyW8w0diXdgKSsymjzodTMgghZMdxG3Qpng833ARPg==", + "dev": true, + "requires": { + "@fortawesome/fontawesome-free": "^6.4.0", + "@markbind/core-web": "5.1.0", + "@primer/octicons": "^15.0.1", + "@sindresorhus/slugify": "^0.9.1", + "@tlylt/markdown-it-imsize": "^3.0.0", + "bluebird": "^3.7.2", + "bootswatch": "5.1.3", + "cheerio": "^0.22.0", + "crypto-js": "^4.0.0", + "csv-parse": "^4.14.2", + "ensure-posix-path": "^1.1.1", + "fastmatter": "^2.1.1", + "fs-extra": "^9.0.1", + "gh-pages": "^2.1.1", + "highlight.js": "^10.4.1", + "htmlparser2": "^3.10.1", + "ignore": "^5.1.4", + "js-beautify": "1.14.3", + "katex": "^0.15.6", + "lodash": "^4.17.15", + "markdown-it": "^12.3.2", + "markdown-it-attrs": "^4.1.3", + "markdown-it-emoji": "^1.4.0", + "markdown-it-linkify-images": "^3.0.0", + "markdown-it-mark": "^3.0.0", + "markdown-it-regexp": "^0.4.0", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", + "markdown-it-table-of-contents": "^0.4.4", + "markdown-it-task-lists": "^2.1.1", + "markdown-it-texmath": "^1.0.0", + "markdown-it-video": "^0.6.3", + "material-icons": "^1.9.1", + "moment": "^2.29.4", + "nunjucks": "3.2.2", + "path-is-inside": "^1.0.2", + "simple-git": "^2.17.0", + "url-parse": "^1.5.10", + "uuid": "^8.3.1", + "vue": "2.6.14", + "vue-server-renderer": "2.6.14", + "vue-template-compiler": "2.6.14", + "walk-sync": "^2.0.2", + "winston": "^2.4.4" + } + }, + "@markbind/core-web": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@markbind/core-web/-/core-web-5.1.0.tgz", + "integrity": "sha512-TRzz8ZCr25pylKvFxF/WwXDi4Gbtsb2OLXV61WyTFqVy03tFoEJ2mqncpbliI9DrfDdKWcm1YZPgDCedVkYjKA==", + "dev": true + }, + "@primer/octicons": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-15.2.0.tgz", + "integrity": "sha512-4cHZzcZ3F/HQNL4EKSaFyVsW7XtITiJkTeB1JDDmRuP/XobyWyF9gWxuV9c+byUa8dOB5KNQn37iRvNrIehPUQ==", + "dev": true, + "requires": { + "object-assign": "^4.1.1" + } + }, + "@sindresorhus/slugify": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-0.9.1.tgz", + "integrity": "sha512-b6heYM9dzZD13t2GOiEQTDE0qX+I1GyOotMwKh9VQqzuNiVdPVT8dM43fe9HNb/3ul+Qwd5oKSEDrDIfhq3bnQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "lodash.deburr": "^4.1.0" + } + }, + "@tlylt/markdown-it-imsize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tlylt/markdown-it-imsize/-/markdown-it-imsize-3.0.0.tgz", + "integrity": "sha512-6kTM+vRJTuN2UxNPyJ8yC+NHrzS+MxVHV+z+bDxSr/Fd7eTah2+otLKC2B17YI/1lQnSumA2qokPGuzsA98c6g==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "apache-crypt": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.5.tgz", + "integrity": "sha512-ICnYQH+DFVmw+S4Q0QY2XRXD8Ne8ewh8HgbuFH4K7022zCxgHM0Hz1xkRnUlEfAXNbwp1Cnhbedu60USIfDxvg==", + "dev": true, + "requires": { + "unix-crypt-td-js": "^1.1.4" + } + }, + "apache-md5": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.7.tgz", + "integrity": "sha512-JtHjzZmJxtzfTSjsCyHgPR155HBe5WGyUyHTaEkfy46qhwCFKx1Epm6nAxgUG3WfUZP1dWhGqj9Z2NOBeZ+uBw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "bootswatch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.1.3.tgz", + "integrity": "sha512-NmZFN6rOCoXWQ/PkzmD8FFWDe24kocX9OXWHNVaLxVVnpqpAzEbMFsf8bAfKwVtpNXibasZCzv09B5fLieAh2g==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "dev": true, + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", + "dev": true + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "email-addresses": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", + "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + }, + "dependencies": { + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "requires": { + "through": "2" + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "fastmatter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fastmatter/-/fastmatter-2.1.1.tgz", + "integrity": "sha512-NFrjZEPJZTexoJEuyM5J7n4uFaLf0dOI7Ok4b2IZXOYBqCp1Bh5RskANmQ2TuDsz3M35B1yL2AP/Rn+kp85KeA==", + "dev": true, + "requires": { + "js-yaml": "^3.13.0", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through2": "^3.0.1" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "dev": true + }, + "figlet": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", + "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", + "dev": true + }, + "file-stream-rotator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", + "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", + "dev": true, + "requires": { + "moment": "^2.11.2" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha512-UZArj7+U+2reBBVCvVmRlyq9D7EYQdUtuNN+1iz7pF1jGcJ2L0TjiRCxsTZfj2xFbM4c25uGCUDpKTHA7L2TKg==", + "dev": true + }, + "filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha512-DKVP0WQcB7WaIMSwDETqImRej2fepPqvXQjaVib7LRZn9Rxn5UbvK2tYTqGf1A1DkIprQQkG4XSQXSOZp7Q3GQ==", + "dev": true, + "requires": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "filenamify-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", + "integrity": "sha512-O9K9JcZeF5VdZWM1qR92NSv1WY2EofwudQayPx5dbnnFl9k0IcZha4eV/FGkjnBK+1irOQInij0yiooCHu/0Fg==", + "dev": true, + "requires": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true + }, + "gh-pages": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-2.2.0.tgz", + "integrity": "sha512-c+yPkNOPMFGNisYg9r4qvsMIjVYikJv7ImFOhPIVPt0+AcRUamZ7zkGRLHz7FKB0xrlZ+ddSOJsZv9XAFVXLmA==", + "dev": true, + "requires": { + "async": "^2.6.1", + "commander": "^2.18.0", + "email-addresses": "^3.0.1", + "filenamify-url": "^1.0.0", + "fs-extra": "^8.1.0", + "globby": "^6.1.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-auth": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", + "integrity": "sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==", + "dev": true, + "requires": { + "apache-crypt": "^1.1.2", + "apache-md5": "^1.0.6", + "bcryptjs": "^2.3.0", + "uuid": "^3.0.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "humanize-url": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", + "integrity": "sha512-RtgTzXCPVb/te+e82NDhAc5paj+DuKSratIGAr+v+HZK24eAQ8LMoBGYoL7N/O+9iEc33AKHg45dOMKw3DNldQ==", + "dev": true, + "requires": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "js-beautify": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz", + "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==", + "dev": true, + "requires": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "nopt": "^5.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "katex": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.15.6.tgz", + "integrity": "sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==", + "dev": true, + "requires": { + "commander": "^8.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "live-server": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.1.tgz", + "integrity": "sha512-Yn2XCVjErTkqnM3FfTmM7/kWy3zP7+cEtC7x6u+wUzlQ+1UW3zEYbbyJrc0jNDwiMDZI0m4a0i3dxlGHVyXczw==", + "dev": true, + "requires": { + "chokidar": "^2.0.4", + "colors": "latest", + "connect": "^3.6.6", + "cors": "latest", + "event-stream": "3.3.4", + "faye-websocket": "0.11.x", + "http-auth": "3.1.x", + "morgan": "^1.9.1", + "object-assign": "latest", + "opn": "latest", + "proxy-middleware": "latest", + "send": "latest", + "serve-index": "^1.9.1" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "dev": true + }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "dev": true + }, + "lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "dev": true + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "dev": true + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "dev": true + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "dev": true + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "logform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", + "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", + "dev": true, + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markbind-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/markbind-cli/-/markbind-cli-5.1.0.tgz", + "integrity": "sha512-6POI1Q++2aZa+Udk/oQ6LX1oNPbKUBDY0mN3Up7VOFeK+XYW51faxuCk2Q91JTBxYRKLNtshxf0y12kB4Cj9Qw==", + "dev": true, + "requires": { + "@markbind/core": "5.1.0", + "@markbind/core-web": "5.1.0", + "bluebird": "^3.7.2", + "chalk": "^3.0.0", + "cheerio": "^0.22.0", + "chokidar": "^3.3.0", + "colors": "1.4.0", + "commander": "^8.1.0", + "figlet": "^1.2.4", + "find-up": "^4.1.0", + "fs-extra": "^9.0.1", + "live-server": "1.2.1", + "lodash": "^4.17.15", + "url-parse": "^1.5.10", + "winston": "^2.4.4", + "winston-daily-rotate-file": "^3.10.0" + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true + } + } + }, + "markdown-it-attrs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz", + "integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==", + "dev": true, + "requires": {} + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", + "dev": true + }, + "markdown-it-linkify-images": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz", + "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==", + "dev": true, + "requires": { + "markdown-it": "^13.0.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true + }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + } + } + }, + "markdown-it-mark": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", + "integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==", + "dev": true + }, + "markdown-it-regexp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-regexp/-/markdown-it-regexp-0.4.0.tgz", + "integrity": "sha512-0XQmr46K/rMKnI93Y3CLXsHj4jIioRETTAiVnJnjrZCEkGaDOmUxTbZj/aZ17G5NlRcVpWBYjqpwSlQ9lj+Kxw==", + "dev": true + }, + "markdown-it-sub": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz", + "integrity": "sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q==", + "dev": true + }, + "markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==", + "dev": true + }, + "markdown-it-table-of-contents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", + "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", + "dev": true + }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true + }, + "markdown-it-texmath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", + "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", + "dev": true + }, + "markdown-it-video": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", + "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==", + "dev": true + }, + "matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + } + }, + "material-icons": { + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.11.tgz", + "integrity": "sha512-kp2oAdaqo/Zp6hpTZW01rOgDPWmxBUszSdDzkRm1idCjjNvdUMnqu8qu58cll6CObo+o0cydOiPLdoSugLm+mQ==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + } + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "nunjucks": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.2.tgz", + "integrity": "sha512-KUi85OoF2NMygwODAy28Lh9qHmq5hO3rBlbkYoC8v377h4l8Pt5qFjILl0LWpMbOrZ18CzfVVUvIHUIrtED3sA==", + "dev": true, + "requires": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "chokidar": "^3.3.0", + "commander": "^5.1.0" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + } + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "proxy-middleware": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", + "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "simple-git": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.48.0.tgz", + "integrity": "sha512-z4qtrRuaAFJS4PUd0g+xy7aN4y+RvEt/QTJpR184lhJguBA1S/LsVlvE/CM95RsYMOFJG3NGGDjqFCzKU19S/A==", + "dev": true, + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + }, + "stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "strip-url-auth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", + "integrity": "sha512-++41PnXftlL3pvI6lpvhSEO+89g1kIJC4MYB5E6yH+WHa5InIqz51yGd1YOGd7VNSNdoEOfzTMqbAM/2PbgaHQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "unix-crypt-td-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "dev": true + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "vue": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==", + "dev": true + }, + "vue-server-renderer": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.6.14.tgz", + "integrity": "sha512-HifYRa/LW7cKywg9gd4ZtvtRuBlstQBao5ZCWlg40fyB4OPoGfEXAzxb0emSLv4pBDOHYx0UjpqvxpiQFEuoLA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "hash-sum": "^1.0.2", + "he": "^1.1.0", + "lodash.template": "^4.5.0", + "lodash.uniq": "^4.5.0", + "resolve": "^1.2.0", + "serialize-javascript": "^3.1.0", + "source-map": "0.5.6" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "vue-template-compiler": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz", + "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "winston": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", + "dev": true, + "requires": { + "async": "^3.2.3", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true + } + } + }, + "winston-compat": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.5.tgz", + "integrity": "sha512-EPvPcHT604AV3Ji6d3+vX8ENKIml9VSxMRnPQ+cuK/FX6f3hvPP2hxyoeeCOCFvDrJEujalfcKWlWPvAnFyS9g==", + "dev": true, + "requires": { + "cycle": "~1.0.3", + "logform": "^1.6.0", + "triple-beam": "^1.2.0" + } + }, + "winston-daily-rotate-file": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.10.0.tgz", + "integrity": "sha512-KO8CfbI2CvdR3PaFApEH02GPXiwJ+vbkF1mCkTlvRIoXFI8EFlf1ACcuaahXTEiDEKCii6cNe95gsL4ZkbnphA==", + "dev": true, + "requires": { + "file-stream-rotator": "^0.4.1", + "object-hash": "^1.3.0", + "semver": "^6.2.0", + "triple-beam": "^1.3.0", + "winston-compat": "^0.1.4", + "winston-transport": "^4.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dev": true, + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000000..aa7083fd8a7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,14 @@ +{ + "name": "docs", + "version": "1.0.0", + "description": "AB-3 docs", + "scripts": { + "init": "markbind init", + "build": "markbind build", + "serve": "markbind serve", + "deploy": "markbind deploy" + }, + "devDependencies": { + "markbind-cli": "^5.1.0" + } +} diff --git a/docs/site.json b/docs/site.json new file mode 100644 index 00000000000..bc123ce7aa8 --- /dev/null +++ b/docs/site.json @@ -0,0 +1,28 @@ +{ + "baseUrl": "", + "titlePrefix": "JABPro", + "faviconPath": "images/jabpro.png", + "style": { + "codeTheme": "light" + }, + "ignore": [ + "_markbind/layouts/*", + "_markbind/logs/*", + "_site/*", + "site.json", + "*.md", + "*.njk", + ".git/*", + "node_modules/*" + ], + "pagesExclude": ["node_modules/*"], + "pages": [ + { + "glob": ["**/index.md", "**/*.md"] + } + ], + "deploy": { + "message": "Site Update." + }, + "timeZone": "Asia/Singapore" +} diff --git a/docs/stylesheets/main.css b/docs/stylesheets/main.css new file mode 100644 index 00000000000..1074ade42dd --- /dev/null +++ b/docs/stylesheets/main.css @@ -0,0 +1,144 @@ +mark { + background-color: #ff0; + border-radius: 5px; + padding-top: 0; + padding-bottom: 0; +} + +.indented { + padding-left: 20px; +} + +.theme-card img { + width: 100%; +} + +/* Scrollbar */ + +.slim-scroll::-webkit-scrollbar { + width: 5px; +} + +.slim-scroll::-webkit-scrollbar-thumb { + background: #808080; + border-radius: 20px; +} + +.slim-scroll::-webkit-scrollbar-track { + background: transparent; + border-radius: 20px; +} + +.slim-scroll-blue::-webkit-scrollbar { + width: 5px; +} + +.slim-scroll-blue::-webkit-scrollbar-thumb { + background: #00b0ef; + border-radius: 20px; +} + +.slim-scroll-blue::-webkit-scrollbar-track { + background: transparent; + border-radius: 20px; +} + +/* Layout containers */ + +#flex-body { + display: flex; + flex: 1; + align-items: start; +} + +#content-wrapper { + flex: 1; + margin: 0 auto; + min-width: 0; + max-width: 1000px; + overflow-x: auto; + padding: 0.8rem 20px 0 20px; + transition: 0.4s; + -webkit-transition: 0.4s; +} + +#site-nav, +#page-nav { + display: flex; + flex-direction: column; + position: sticky; + top: var(--sticky-header-height); + flex: 0 0 auto; + max-width: 300px; + max-height: calc(100vh - var(--sticky-header-height)); + width: 300px; +} + +#site-nav { + border-right: 1px solid lightgrey; + padding-bottom: 20px; + z-index: 999; +} + +.site-nav-top { + margin: 0.8rem 0; + padding: 0 12px 12px 12px; +} + +.nav-component { + overflow-y: auto; +} + +#page-nav { + border-left: 1px solid lightgrey; +} + +@media screen and (max-width: 1299.98px) { + #page-nav { + display: none; + } +} + +/* Bootstrap medium(md) responsive breakpoint */ +@media screen and (max-width: 991.98px) { + #site-nav { + display: none; + } +} + +/* Bootstrap small(sm) responsive breakpoint */ +@media (max-width: 767.98px) { + .indented { + padding-left: 10px; + } + + #content-wrapper { + padding: 0 10px; + } +} + +/* Bootstrap extra small(xs) responsive breakpoint */ +@media screen and (max-width: 575.98px) { + #site-nav { + display: none; + } +} + +/* Hide site navigation when printing */ +@media print { + #site-nav { + display: none; + } + + #page-nav { + display: none; + } +} + +h2, +h3, +h4, +h5, +h6 { + color: #e46c0a; +} diff --git a/docs/team/ariellacallista.md b/docs/team/ariellacallista.md new file mode 100644 index 00000000000..85b4c117565 --- /dev/null +++ b/docs/team/ariellacallista.md @@ -0,0 +1,42 @@ +--- +layout: default.md +title: "Ariella Callista's Project Portfolio Page" +--- + +# Project: JABPRO +## Overview +JABPro aims to solve the problem of HR managers having to sort through tons of job applications. +It makes their life easier by allowing them to easily fetch important info about job applicants such as their contact details, application status etc. It serves as a one-stop addressbook for job applications. +## Summary of Contributions +Code contributed: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=ariellacallista&breakdown=true) + +Enhancement implemented: +* Implemented a versatile search functionality allowing users to search by different categories such as name, status, and tags +* Introduced a command for creating and categorizing tags +* Designed a user interface to visually represent tags with color coding based on categories +* Modified the implementation of tag to enhance functionality and better adhere to oop + +Contribution to the UG: +* Updated the User Guide sections for the search and create commands +* Added statements in the User Guide emphasizing the value proposition of JABPro +* Contributed to FAQ and some troubleshooting under quick-start section + +Contribution to DG: +* Contributed to DG for non-functional requirements +* Contributed to DG for use cases regarding search and create tags features +* Contributed to DG for UML diagrams for search feature and create features +* Contributed to DG for feature implementation details for search and create +* Standardized some DG format i.e. activity diagrams and adding notes boxes +* Contributed to DG for planned enhancements +* Contributed to DG for manual test cases + +Contribution to team-based tasks: +* Ensured the timely and accurate submissions of team deliverables +* Monitored the TP dashboard to ensure the team met weekly goals +* Maintained project/meeting notes + +Review/mentoring contributions: +* Conducted thorough reviews of pull requests and provided constructive comments + +Contributions beyond the project team: +* Helped summarize weekly tasks for CS2103T and CS2101 to make sure tasks are coordinated well diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md index 773a07794e2..86aa7ebfc34 100644 --- a/docs/team/johndoe.md +++ b/docs/team/johndoe.md @@ -1,6 +1,6 @@ --- -layout: page -title: John Doe's Project Portfolio Page + layout: default.md + title: "John Doe's Project Portfolio Page" --- ### Project: AddressBook Level 3 diff --git a/docs/team/ketweeen.md b/docs/team/ketweeen.md new file mode 100644 index 00000000000..384efed01a2 --- /dev/null +++ b/docs/team/ketweeen.md @@ -0,0 +1,45 @@ +--- +layout: default.md +title: "Catherine Liang's Project Portfolio Page" +--- + +# Project: JABPRO +## Overview +JABPro aims to solve the problem of HR managers having to sort through tons of job applications. +It makes their life easier by allowing them to easily fetch important info about job applicants such as their contact details, application status etc. It serves as a one-stop addressbook for job applications. +## Summary of Contributions +**Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=ketweeen&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + +**Enhancement implemented**: +* Added the ability to sort the list based on specific attribute such as name +* Introduced delete by tags +* Introduced delete by status +* Implemented list tags + +**Contribution to the UG**: +* Updated the `list`and `delete` command sections of the user guide +* Added `listT` command section +* Added prefix summary and glossary tables +* Added table of contents +* Fixed grammar and formatting issues +* Clean up & standardized the user guide so that it looks cohesive + +**Contribution to DG**: +* Contributed to DG for non-functional requirements +* Contributed to DG for use cases regarding `delete` and `list` features +* Contributed to the DG for UML diagrams for `delete` feature +* Standardized some DG format for activity diagram +* Contributed to DG for manual test cases + +**Contribution to team-based tasks**: +* Keeping track of deadlines and objectives +* Monitored the TP dashboard to ensure the team met weekly goals + +**Review/mentoring contributions**: +* Conducted thorough reviews of pull requests and provided constructive comments + +**Contributions beyond the project team**: +* Participated in the PE Dry Run and gave feedback to the other team + +**Test Case Contributions**: +* Covered all the test cases for the commands that I have implemented. Contributed to the team's test case as well as AB3's test case. diff --git a/docs/team/madlamprey.md b/docs/team/madlamprey.md new file mode 100644 index 00000000000..de5863b337c --- /dev/null +++ b/docs/team/madlamprey.md @@ -0,0 +1,61 @@ +--- +layout: default.md +title: "Misra Aditya's Project Portfolio Page" +--- + +# Project: JABPRO +## Overview +JABPro aims to solve the problem of HR managers having to sort through tons of job applications. +It makes their life easier by allowing them to easily fetch important info about job applicants such as their contact details, application status etc. It serves as a one-stop addressbook for job applications. + +We aimed for JABPro to be a holistic applications management companion. This meant that its value must not be limited to the storage or filtering of applications - tasks that could be performed by a spreadsheet as well. I felt that there were many other struggles that hiring managers faced, a key one among them being - scheduling interviews. This enabled me to work on the `Events` feature. + +Additionally, for the application to be more centred around the needs of our target audience, we needed to modify `AddressBook` in a way that it was contextualised into the workflow of our target audience. Beyond simple addition, deletion and update of candidates [persons in AddressBook], for it to be a suitable tool for hiring managers, we tried to analyse what a hiring manager needs to thoroughly vet a candidate. Then came up the need to associate Linkedin or Github profiles with candidates' information. Since the use of APIs was not permitted, I chose to implement the redirect instead. + +## Summary of Contributions +### Code Contribution + +* [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=madlamprey&breakdown=true) showcases all code written by me for the JABPro codebase +* [PRs authored by me](https://github.com/AY2324S1-CS2103T-W09-4/tp/pulls?q=is%3Apr+author%3AMadLamprey) + +### Enhancements Implemented + +1. ***LinkedIn and Github*** +* Implemented the ability to add LinkedIn and Github usernames to details of candidates +* Implemented the redirection to the respective accounts on the default browser using the Java awt library +* Amended Person to include attributes of LinkedIn and Github + +2. ***Events*** +* Introduced Events to JABPro, which allows for addition of events relating to candidates +* Updated the UI to include an Events window to display the events, in order of their occurrence +* Implemented functionality to allow events to be stored as JSON files in an `eventBook` as well +* Updated implementations of existing commands - `delete` and `edit`, to allow for cascading deletion of events, and `clear`. + +### Contribution to the UG +* Updated the User Guide for + * Github/LinkedIn Feature: + * Adding LinkedIn/Github username + * Opening LinkedIn/Github profile + * Events Feature: + * Adding Events + * Viewing Events +* Added a UI breakdown to help explain what each part of the interface does/showcases + +### Contribution to DG +* Contributed to DG for "must-have" and "good-to-have" user-stories +* Contributed to DG for use cases related to linkedin/github feature or Events feature +* Contributed to DG for architecture diagram for UI, Storage and Model +* Contributed to DG for implementation details of LinkedIn/Github feature and Events feature +* Contributed to DG for test cases for `addL`, `addG`, `linkedin`, `github` and `event` commands, for Instructions on Manual Testing +* Contributed to DG for Redirection Implementation details as part of `Appendix - Effort` +* Contributed to DG for Activity and Sequence diagrams of `addL`, `addG` and `event`. + +### Contribution to team-based tasks +* Ensured testing as a whole meets coverage requirements + +### Review/mentoring contributions +* Reviewed few PR contributions of team members + +### Community +* Identified 10 bugs in fellow team's JAR file [Bug Reports](https://github.com/MadLamprey/ped/issues) +* Posted on forum [Issues](https://github.com/nus-cs2103-AY2324S1/forum/issues?q=is%3Aissue+author%3AMadLamprey+) diff --git a/docs/team/scarletblanks.md b/docs/team/scarletblanks.md new file mode 100644 index 00000000000..a21f7b9df2a --- /dev/null +++ b/docs/team/scarletblanks.md @@ -0,0 +1,50 @@ +--- +layout: default.md +title: "Peng Tat's Project Portfolio Page" +--- + +# Project: JABPRO +## Overview +JABPro aims to solve the problem of HR managers having to sort through tons of job applications. +It makes their life easier by allowing them to easily fetch important info about job applicants such as their contact details, application status etc. It serves as a one-stop addressbook for job applications. + +## Summary of Contributions +**Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&tabOpen=true&tabType=authorship&tabAuthor=ScarletBlanks&tabRepo=AY2324S1-CS2103T-W09-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +**Enhancement implemented**: +* Implementation of set feature +* Implementation of export feature + +**Contribution to the UG**: +* Key definitions and distinctions: Beginner & Advanced users +* Set feature usage & Examples +* Export feature usage & Examples +* Added set & Export to Command Summary + +**Contribution to DG**: +* Contributed to the section 'Planned enhancements' for export +* Contributed to the Appendix for 'Flexibility' +* Contributed to test cases for 'set' and 'export' +* Contributed to MSS for 'set' and 'export' +* Contributed to user stories relevant to 'set' and 'export' +* Contributed to the implementation section of 'set' and 'export' +* Contributed to the diagrams for 'set' and 'export' + +**Contribution to team-based tasks**: +* Code quality: Looks after code quality, ensures adherence to coding standards +* OOP Review: Review PRs and ensures adherence to Object-Oriented Programming standards +* Checks PRs for consistency between new code and previous code + +**Reviewing/mentoring contributions**: +* Reviewing of PRs +* Advising changes with regard to OOP and consistency with the way objects are used. + +**PRs reviewed** +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/79 (commented on OOP abstraction) +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/47 (commented on OOP & suggested using method overloading) +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/192 +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/190 +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/189 +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/168 +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/162 +* https://github.com/AY2324S1-CS2103T-W09-4/tp/pull/161 diff --git a/docs/team/sk2001git.md b/docs/team/sk2001git.md new file mode 100644 index 00000000000..77db3ce3a93 --- /dev/null +++ b/docs/team/sk2001git.md @@ -0,0 +1,561 @@ +--- +layout: default.md +title: "Sean Koh's Project Portfolio Page" +--- + +# Project: JABPRO + +## Overview +JABPro aims to solve the problem of HR managers having to manage and sort through tons of job applications. +The problem that we have identified included: +1. Visual Noise and Clutter (Through the use of Excel or other programmes where rows of data are displayed) +2. Lack of a way to easily compare candidates (Having to manually compare rows of data or need to search up statistical formulas) +The features that I have implemented to tackle these problems are: +1. `view` command to view a candidate's details in a separate panel (to reduce visual noise and clutter) whereby the candidate's details are displayed in full in a very organised and formatted manner. +This creates a very clean and organised view for the user to easily read and process the information stored for the viewed person. +2. `filter` command to filter candidates based on statistical metrics and values. Whereby the user can filter and display candidates whose value is greater than or equal to the specified value for the specified statistic metric. +This allows the user to easily compare candidates based on their performance for a particular tag. We use tag as a way to tag assessments that the candidate has taken. This allows the user to easily compare candidates based on their performance for a particular assessment. + +Thereby additional enhancement that I have implemented to support filter to support `Score` and `ScoreList` are: +1. Overloading the current `edit` command to edit the score of a candidate for a particular assessment. +2. Summary Statistic + +## Summary of Contributions +**Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=sk2001git&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&tabOpen=true&tabType=authorship&tabAuthor=sk2001git&tabRepo=AY2324S1-CS2103T-W09-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +**Enhancement implemented**: +* `remark`, `edit`(for scores portion) , `filter`, `view` command and features +* `Score`, `ScoreList`, `Remark` and its relevant storing and parsing of data +* Summary Statistic implementation +* UI for View Panel and Summary statistic panel + + + +**Contribution to the UG**: +* Updated the UG for the entire table of contents +* Updated the UG for `Overview of Main Features` for Introduction +* Updated UG for the `add`, `remark`, `filter`, `view`, `edit` command and `Summary Statistic` section + +**Contribution to DG**: +* Contributed to DG for writing User Stories + * 5 of the `* * *` Must-have features for user stories which links to the commands I have implemented +* Contributed to DG for target user profile + * 4 target user profiles characteristic +* Contributed to DG for non-functional requirements + * 3 NFRs +* Contributed to DG for use cases regarding `add`, `remark`, `view`, `Using scores to compare and filter people` +* Contributed to DG for architecture diagram for UI, Storage, Model +* Contributed to DG for feature implementation details for `view`. +* Contributed to DG for manual testing for `add`, `edit`, `view`, `filter`, `remark` features. + + +**Contribution to team-based tasks**: +* Keeping track of deadlines and objectives +* Separating tasks into workable units for team members each week +* Managing the team's progress and ensuring that everyone is on track +* In charge of overall UG and DG structure and formatting for the team + +**Review/mentoring contributions**: +* Reviewed PRs for team members +* Helped team members with their issues and bugs. Especially with design issues and understanding of the code base. + +**Contributions beyond the project team**: +* Participated in the PE Dry Run and gave feedback to the other team (About 9 bugs reported, out of which there was 1 major over-arching bug that was reported) + +**Test Case Contributions**: +* Covered all the test cases for the commands that I have implemented. Contributed to the team's test case as well as AB3's test case. + + +### Contributions to Developer Guide(Extracts) + + + +### View feature + +#### Implementation + +The view feature is implemented using the `ViewCommand` class. It extends `Command` and overrides the `execute()` method to display the person's details in full in a new window. + +Like every other command class, it involves a command `ViewCommand` class and a parser `ViewCommandParser`. `ViewCommand Parser` takes in the user input and returns a `ViewCommand` object. + +When executed, `ViewCommand` saves the index of the person to be viewed as `LastViewedPersonIndex` in the `Model` and returns a `CommandResult` object with `isView` property being true. + +By having a `isView` property in `CommandResult`, the `MainWindow` component is able to toggle the `UI` to the view the person of the `LastViewedPersonIndex` after the command has been executed. + + + + + +Given below is an example usage scenario and how the view feature behaves at each step. + +Step 1. The user launches the application. The `AddressBook` will be initialized with the current saved address book state + +User should see the UI as shown below. + +![Ui](../images/Ui.png) + +Step 2. The user wants to see the full information displayed for the first person in the displayed list. The user enters the command `view 1` to view the first person in the list. + +The following sequence diagram shows how the view operation works: + + + +**Note:** The lifeline for `RemarkCommand` and `RemarkCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +User should see the UI as shown below after entering `View 1` + +![View](../images/viewState.png) + +Step 3. The user can then read or process the information stored for the viewed person. + + + +**Note:** The view command can be most effectively used with `search` and `list`. Since the view index is dependent on the Index on the filtered list shown, the user can view the profile after filtering for specific properties in a person using `search` and sorting them using `list`. + +Alternatives considered + +Alternative 1 (Chosen): +The view feature is implemented using the `ViewCommand` class. It extends `Command` and overrides the `execute()` method to display the person's details in full in a new window. + +Pros: Follows the Software Design Patterns of Command. This is the same pattern used for all other commands thus creating consistency. + +Cons: Tougher to implement since other commands do not have the ability to trigger the `ViewCommand` in their execution. That is we specifically need to set the isView property to true if we want the `ViewCommand` to occur simultaneously with another command. + +Alternative 2 (Not Chosen): +The view feature is implemented using the `ViewCommand` class. It extends `Command` and overrides the `execute()` method to display the person's details in full in a new window. +Commands that involved viewing will extend `ViewCommand` instead of the `Command` class. All of them are returned as `ViewCommand` to ensure toggling of the UI after command is executed. + +Pros: Arguably a more OOP approach since all commands that trigger view IS-A `ViewCommand`. + +Cons: You cannot implement any command that does not involve viewing but inherits from any command that is a children of `ViewCommand`. +An example could be trying to create identical commands that does not toggle the UI after execution. This would require duplication of the exact same command code but inheriting from `Command` instead of `ViewCommand`. + + +* wants an organized way to keep track of candidates information +* wants to view and manage candidates information in a single place +* wants to attach a score to candidate performance over interview and assessments +* wants to be able to use scores in order to quantitatively compare candidates + +| Priority | As a …​ | I want to …​ | So that…​ | +|----------|----------------|------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `* * *` | Hiring Manager | add a candidate's contact information, including name, email, phone number | I can easily access and reach out to candidates when needed | +| `* * *` | Hiring Manager | add notes and comments to candidate profiles to document interview feedback and impressions | I can maintain a record of interactions and feedback | +| `* * *` | Hiring Manager | view a specific job applicant's resume or portfolio | I can check whether they meet the requirements requested by other department heads | +| `* * *` | Hiring Manager | record the score of the different activities such as interviewsor assessments that an applicant might go through | I can use them for effective comparison and filter the candidates easily | +| `* * *` | Hiring Manager | compare candidates using their performance in their assessments or interviews | I can choose the best candidates to move to the next stage of the hiring process and get the best performing candidates objectively | + +**The remaining not mentioned are the use cases for my features related and test cases for manual testing.** + + +### Contributions to User Guide(Extracts) + +### Introduction +1. Table of Contents +2. Overview of Main Features + +### Adding an applicant: `add` + + +Adds an applicant to JABPro. + +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/[CATEGORY] TAGNAME]…​` + +| Type | Prefix | Constraints | +|-----------|------------------------|-----------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `n/NAME` | `NAME` must be alphanumeric (Letters and numbers, no symbols allowed such as `/`, `,` ...). | +| Mandatory | `p/PHONE_NUMBER` | `PHONE_NUMBER` must contain numbers only and should be at-least 3 digits long. | +| Mandatory | `e/EMAIL` | `EMAIL` must be the standard email address format (There must be an email-prefix followed by `@` symbol and email domain). | +| Mandatory | `a/ADDRESS` | `ADDRESS` can be any value, including special characters such as `#`, `,` ... | +| Optional | `t/[CATEGORY] TAGNAME` | `TAGNAME` must be alphanumeric with no spaces. Any details after the space will be ignored. | + +**Notes regarding additional constraint on `add` command:** +* The uniqueness of the applicant is determined by the name only. This means that you cannot have 2 applicants with the same name in the application book. +* All other fields other than name can be identical between different people in JABPro. +* Applicants added using the `add` command will be added to the end of the list. + +**Notes on adding tags:** +* If you would like to tag a user with a tag that has not been categorised yet using the `create` command, + you can specify the category that you would like it to be categorised to in the `add` command. e.g. `...t/role swe` +* If you are using a tag that has not been categorised yet and you did not specify its category in the `add` command, + the tag would still be saved but it would be "uncategorised" by default. +* If you have multiple tags in different categories with the same name, you must specify the category when you want to + add one of these tags to the applicant you are adding. + + + +**Tip:** +* An applicant can have any number of tags (including 0)! + + +An example of the `add` command being successfully executed: +1. Enter the command `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/dept finance` +2. This is the result of the successful `add` command (Take note that command entered will not be shown in the result): + + ![Add-Success](../images/add-command-success.png) +
    + +**Error Handling Table for `add` command:** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-----------------------------------------------|-----------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| Missing add keyword: `add` | Unknown command | Follow the command format of `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAGNAME]…​` closely | +| Missing mandatory fields | Invalid command format! | Ensure that all mandatory fields are filled up. | +| Duplicate name | This person already exists in the address book | Ensure that the name of the applicant is unique. That is you cannot add the same name twice. Use some form of extra identification like a number | +| Invalid phone number | Phone numbers should only contain numbers, and it should be at least 3 digits long | Ensure that the phone number only contains number and should be at least 3 digits long | +| Invalid email | Emails should be of the format local-part@domain and adhere to the following constraints:| Ensure that the prefix and domain of the email is correct following the constraints stated by the error | +| Invalid tag name | Tag names should only contain alphanumeric characters and should not be blank | Ensure that the tag name only contains alphanumeric characters and should not be blank | +| Multiple prefixes of the same type being used | Multiple values specified for the following single-valued field(s): `prefix/` | Remove the duplicate prefix. The command should only have 1 of every prefix except for `t/` | + + + +**Tip:** To know if it is an error, the command entered will light up in red. It remains in the command box. +1. The error message will be displayed in the result display box. +2. Follow the error handling table for the command or use the suggested course of action in the result display to rectify the error. + + + + +### Adding a remark to an applicant: `remark` + + +Edits a remark of an existing applicant in JABPro. +Format: `remark INDEX r/REMARK` + +| Type | Prefix | Constraints | +|-----------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be an existing index in the displayed applicant list and it must not be greater than the total number of applicants in JABPro. | +| Optional | `r/ [REMARK]` | `REMARK` can be any value, including special characters such as `#`, `,` ... | + +**Notes regarding `remark` command:** +* The previous remark is not saved, and instead is replaced by the inputted remark. The command does not add to the existing remark. +* You can empty out a remark by inputting `r/` without any text after it or by omitting the `r/` prefix. +* You can get the remark previously inputted by using the **REMARK** keyword. It will be replaced with the previous remark. The keyword **REMARK** is case-sensitive. This means that `remark 1 r/**remark**` will just replace the remark with the word `**remark**`. +* You can use multiple prefix for `remark` but only the last prefix will be used. This means that `remark 1 r/remark r/remark2` will just replace the remark with `remark2`. + + +An example of the `remark` command being successfully executed: +1. Enter the command `remark 1 r/Great attitude, hardworking` +2. This is the result of the successful `remark` command (Take note that command entered will not be shown in the result): + +3. ![Remark-Success](../images/remark-command-success.png) +
    + +An example of the `remark` command being successfully executed with the **REMARK** keyword: +1. Enter the command `remark 1 r/**REMARK** furthermore he is great at teamwork` +2. This is the result of the successful `remark` command (Take note that command entered will not be shown in the result): + + ![Remark-Success](../images/remark-command-enhanced-success.png) +
    + +**Error Handling Table for `remark` command:** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-----------------------------------------------|---------------------------------------|-------------------------------------------------------------------------------------------------| +| Missing remark keyword: `remark` | Unknown command | Follow the command format of `remark INDEX r/[REMARK]` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | +| Negative or 0 Index | Invalid command format! | Ensure that the index is a positive integer and is also a number that is on the displayed applicant list. | + + + +**Tip:** To know if it is an error, the command entered will light up in red. It remains in the command box. +1. The error message will be displayed in the result display box. +2. Follow the error handling table for the command or use the suggested course of action in the result display to rectify the error. + + + +Additional Examples: +* `remark 1` Empties the remark of the 1st applicant. It is equivalent to `remark 1 r/`. + + +### Viewing a applicant's details: `view` + + +Creates a complete view for details of an applicant in the second main panel and summary statistics (if applicable) of an applicant in the third main panel. + +Format: `view INDEX` + +| Type | Prefix | Constraints | +|-----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be an existing index in the displayed applicant list and it must not be greater than the total number of applicant in JABPro. | + +**Notes regarding `view` command:** +* The index used will be the same index as the one shown in the displayed applicant list. +* Compatible with search and other features that changes the displayed applicant list. Index always follows the index shown in the displayed applicant list. +* Refer to the [Summary Statistics](#summary-statistics) section for more details on the summary statistics. + + + +**Tip:** Other operations that affect user's data will trigger a refresh of the view. +These include `add`, `edit`, `set`, `remark`, `addL`, `addG`. +This means that the view will be updated to reflect the latest changes to the data for that particular applicant. + + + +An example of the `view` command being successfully executed: +1. Enter the command `view 3` +2. This is the result of the successful `view` command (Take note that command entered will not be shown in the result): + + ![View-Success](../images/view-command-success.png) +
    + +An example of the `view` command being successfully executed for applicant with tags and score: +1. Enter the command `view 2` (**Applicant with tags and score**) +2. This is the result of the successful `view` command (Take note that command entered will not be shown in the result): + + ![View-Success](../images/view-command-with-stats-success.png) +
    + + +**Error Handling Table for `view` command:** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-----------------------------------------------|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| +| Missing view keyword: `view` | Unknown command | Follow the command format of `view INDEX` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid| Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | + + + +**Tip:** To know if it is an error, the command entered will light up in red. It remains in the command box. +1. The error message will be displayed in the result display box. +2. Follow the error handling table for the command or use the suggested course of action in the result display to rectify the error. + + + + + +### Editing a applicant: `edit` + + +Edits an existing applicant's detail in JABPro + +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAGNAME]…​ [sc/TAGNAME SCORE]` + +| Type | Prefix | Constraints | +|-----------|--------------------|-----------------------------------------------------------------------------------------------------------------------------| +| Mandatory | `INDEX` | `INDEX` must be a non-zero unsigned integer and must also not be greater than the total number of applicants in JABPro. | +| Optional | `n/NAME` | `NAME` must be alphanumeric (Letters and numbers, no symbols allowed such as `/`, `,` ...). | +| Optional | `p/PHONE_NUMBER` | `PHONE_NUMBER` must contain numbers only and should be at-least 3 digits long. | +| Optional | `e/EMAIL` | `EMAIL` must be the standard email address format (There must be an email-prefix followed by `@` symbol and email domain). | +| Optional | `a/ADDRESS` | `ADDRESS` can be any value, including special characters such as `#`, `,` ... | +| Optional | `t/TAGNAME` | `TAGNAME` must be alphanumeric with no spaces. Any details after the space will be ignored. | +| Optional | `sc/TAGNAME SCORE` | `TAGNAME` a tag that is being created or already exist for that applicant. `SCORE` must be a non-negative integer. | + +**Notes regarding `edit` command:** +* At least one of the optional fields must be provided. +* Existing attributes will be updated to the input values. +* There is a way to edit tags and their categories at the same time. Look at the notes for editing tags with categories `t/[CATEGORY] TAGNAME` for more details. + +**Notes on editing the tags of the specified applicant for `t/TAGNAME`**: +* When editing tags, the existing tags of the applicant will be removed i.e adding of tags is not cumulative. +* You can remove all the applicant’s tags by typing `t/` without + specifying any tags after it. +* There is no current way to keep the existing tags and add new tags to the applicant. You will have to re-tag the applicant with the existing tags and the new tags. + +**Notes on editing the score of the specified applicant for `sc/TAGNAME SCORE`**: +* The `TAG` in `sc/TAG SCORE` must be a tag of the category `assessment`. You cannot use the `sc/TAG SCORE` field for tags that are not of the `assessment` category. +* The `sc/TAG SCORE` field can only be used after the `t/TAG` field is used if the tag has not been created or the `TAG` already exist on the applicant. +* The `SCORE` in `sc/TAG SCORE` is non-negative, that is `SCORE` must be more than or equal to 0. +* To clear a tag's score, just re-tag it with the same tag name, but without using the `sc/TAG SCORE` field. + + +Notes on rules for `edit` command involving tags with categories for `t/[CATEGORY] TAGNAME`: +* Consequently, similar rules for `add` apply to the `edit` command involving tags: + * If you would like to tag a user with a tag that has not been categorised yet using the `create` command, + you can specify the category that you would like it to be categorised to in the `edit` command. e.g. `edit 1 t/role swe` + * If you are using a tag that has not been categorised yet and you did not specify its category in the `add` command, + the tag would still be saved but it would be "uncategorised" by default. + * If you have multiple tags in different categories with the same name, you must specify the category when you want to + tag the specified applicant with one of these tags. + + + +1. Editing an applicant's details will trigger a refresh of the view. This means that the view will be updated to reflect the latest changes to the data for that particular applicant. +2. We strongly recommend that you categorise tags using `create` before using `edit` to tag applicants. This is to reduce the confusion of having two ways to tag applicants. + + + +An example of the `edit` command being successfully executed: +1. Enter the command `edit 1 n/Alex Ho p/91234567` (**Edit name and phone number**) +2. This is the result of the successful `edit` command (Take note that command entered will not be shown in the result): + + ![Edit-Success](../images/edit-command-success.png) +
    + +An example of the `edit` command being successfully executed with tags and score: +1. Ensure that you have created a tag `Interview` under the `assessment` category using the `create` command. That is, enter the command `create t/assessment Interview` +2. Enter the command `edit 1 t/Interview sc/Interview 80` (**Edit tag and score**) +3. This is the result of the successful `edit` command (Take note that command entered will not be shown in the result): + + ![Edit-Success](../images/edit-command-with-stats-success.png) +
    + +An example of the `edit` command being successfully executed to clear a tags and score: +1. Enter the command `edit 1 t/` (**Clear all tags**) +2. This is the result of the successful `edit` command (Take note that command entered will not be shown in the result): + + ![Edit-Success](../images/edit-command-clear-tags-success.png) +
    + +**Error Handling Table for `edit` command:** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|-----------------------------------------------|------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Missing edit keyword: `edit` | Unknown command | Follow the command format of `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAGNAME]…​ [sc/TAGNAME SCORE]` closely | +| Missing Index | Invalid command format! | Ensure that the index is filled up. | +| Invalid Index | The person index provided is invalid | Ensure that the index is valid. That is it is a number that is on the displayed applicant list. | +| Missing at least one of the field | At least one field to edit must be provided. | Ensure that at least one of the field is filled up and to be changed. | +| Duplicate name | This person already exists in the address book | Ensure that the name of the applicant is unique. That is you cannot add the same name twice. Use some form of extra identification like a number | +| Invalid phone number | Phone numbers should only contain numbers, and it should be at least 3 digits long | Ensure that the phone number only contains number and should be at least 3 digits long | +| Invalid email | Emails should be of the format local-part@domain and adhere to the following constraints: | Ensure that the prefix and domain of the email is correct following the constraints stated by the error | +| Invalid tag name | Tag names should only contain alphanumeric characters and should not be blank | Ensure that the tag name only contains alphanumeric characters and should not be blank | +| Multiple prefixes of the same type being used | Multiple values specified for the following single-valued field(s): `prefix/` | Remove the duplicate prefix. The command should only have 1 of every prefix except for `t/` | +| Missing score for tag | Invalid score, score must be non-negative integer. | Ensure that the score is filled up and has a space from the `TAGNAME`. | +| Invalid tag to attach score | Invalid score tag, tag must a tag of the category assessment and must exist on the applicant | Ensure that the tag is of the category assessment and exist on the applicant. If its the wrong category, use `create`, if it is not tagged to the person use `edit` | + + + +**Tip:** To know if it is an error, the command entered will light up in red. It remains in the command box. +1. The error message will be displayed in the result display box. +2. Follow the error handling table for the command or use the suggested course of action in the result display to rectify the error. + + + + +### Filter job applicants by statistics: `filter` + + +Filters and display applicants in the current displayed applicant list using statistical metrics and values. + +Format:`filter t/TAGNAME met/METRIC val/VALUE` or `filter t/TAGNAME met/METRIC` + +| Type | Prefix | Constraints | +|-----------|--------------|--------------------------------------------------------------------------------------------------------------------| +| Mandatory | `t/TAGNAME` | `TAGNAME` must be a tag that is of the category `assessment`. | +| Mandatory | `met/METRIC` | `METRIC` must be either `score`, `percentile`, `mean`, `median`. | +| Optional | `val/VALUE` | Optional only for `mean` and `median`. Otherwise, `VALUE` must be a non-negative integer and is a mandatory field. | + +**Notes regarding `filter` command:** +* Filter works only on the current list of job applicants displayed. It is essential that you enter `list` before using `filter` to ensure that you are filtering the correct list of job applicants. +* It is strongly recommended that you use `filter` after you have tagged most of the job applicants with a tag that has a score. +* Filters and displays job applicants whose **value** is **greater than or equal** to the specified value for the specified statistic metric. +* For `METRIC` that is `mean` or `median`, the `VALUE` is optional. Specifying a `VALUE` here will be ignored accordingly. `filter t/TAGNAME met/METRIC` is equivalent to `filter t/TAGNAME met/METRIC val/X` where `X` is any positive integer. +* Filter does not edit, update or in any way change the data of the job applicants. It only filters and displays the job applicants. +* Filter does not trigger view, that is your view panels represent the previous applicant you viewed before filtering. +* To get back the **original list with all the applicants**, simply type `list` again. + +**Notes on the different metrics:** +Do look at the [Summary Statistics](#summary-statistics) section for more details on the summary statistics metrics. + + + +You should use `filter` after you have tagged most of the job applicants with a tag that has a score. +This is because some of the metrics such as `percentile`, `mean` and `median` require a certain number of scores to be considered meaningful. +Read more about this in the [Summary Statistics](#summary-statistics) section. + + + +Set up for examples when you first start JABPro with default data: +1. `list` +2. `create t/assessment interview` to create a tag `interview` under the `assessment` category. +3. `edit 1 t/interview sc/interview 80` +4. `edit 2 t/interview sc/interview 90` +5. `edit 3 t/interview sc/interview 70` +6. The result of the above commands should look like this: + + ![Filter-Setup](../images/filter-setup.png) +
    + +An example of the `filter` command being successfully executed: +1. Enter the command `list` +2. Enter the command `filter t/interview met/percentile val/80` (**Filter by percentile**) +3. This is the result of the successful `filter` command (Take note that command entered will not be shown in the result): + + ![Filter-Success](../images/filter-command-success.png) +
    + +An example of the `filter` command being successfully executed with `median`: +1. Enter the command `list` +2. Enter the command `filter t/interview met/median` (**Filter by median**) +3. This is the result of the successful `filter` command (Take note that command entered will not be shown in the result): + + ![Filter-Success](../images/filter-command-median-success.png) +
    + +**Error handling for `filter` command:** + +| Reason for Error | Error Message | Remedy / Suggested course of action | +|---------------------------------------------------------|--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Missing filter keyword: `filter` | Unknown command! | Follow the command format strictly of `filter t/TAGNAME met/METRIC val/VALUE` for score and percentile or `filter t/TAGNAME met/METRIC` for mean or median. | +| Missing parameters | Incomplete parameter inputs. t/TAG and met/SCORE are compulsory fields. | Enter the command again with the correct parameters. | +| Invalid tag as tag has wrong category or does not exist | Tag does not exist! | Check that the tag is of the category `assessment` and that the tag exists using `listT`. Use the `create` command if it does not. | +| Invalid metric | Invalid metric provided. Needs to be one of: score, mean, median, percentile | Check that the metric is one of the following: `score`, `mean`, `median`, `percentile` and that it is spelt correctly. Enter the command again with any of the 4 metric | +| Invalid value | Invalid value provided. Needs to be a non negative integer that is more than or equal to 0 | Check that the value is a non-negative integer that is more than or equal to 0. Enter the command again with the correct value. | +| Missing value | val/VALUE is missing, it is compulsory. | Enter a value for `val/VALUE` since the metric requires it. | +| Multiple prefixes of the same type being used | Multiple values specified for the following single-valued field(s): `prefix/` | Remove the duplicate prefix. The command should only have 1 of every prefix | + + + + +**Tip:** To know if it is an error, the command entered will light up in red. It remains in the command box. +1. The error message will be displayed in the result display box. +2. Follow the error handling table for the command or use the suggested course of action in the result display to rectify the error. + + + + +**Significance of using `filter` with the metrics `score`, `percentile`, `mean` and `median`:** +In essence, this allows you to find job applicants whose performance rating is above a certain percentile, score or mean/median score for that tag. +Ideally, this feature can then be used to find the best applicants easily and quickly without having to manually look through the list of applicants. + + +## Summary Statistics + +Summary Statistics is a table generated by JABPro that displays the following information about an applicant: +It is generated for tags that are categorised under the `assessment` category. + +| Statistic / Metric | Description | +|--------------------|-----------------------------------------------| +| `score` | The score of the applicant for the tag. | +| `mean` | The mean score of applicant with that tag. | +| `median` | The median score of applicant with that tag. | +| `minimum` | The minimum score of applicant with that tag. | +| `maximum` | The maximum score of applicant with that tag. | +| `percentile` | The percentile of the applicant for that tag. | + + + +You should ensure that you have **sufficient candidates of more than 20** with a score for the tag you are interested in, before using the summary statistics to make comparisons. + + +**Notes on why you should have sufficient applicants with a score for the tag you are interested in:** +1. This is due to the fact that these summary statistics rely on concepts such as mean, median and percentile, which are statistical concepts that require a sufficient sample size to be meaningful. +2. For example, if you have only assigned 5 out of 100 applicants, the summary statistics will not be representative of the actual mean, median and percentile for that tag. +3. In this case, you should assign more applicants with a score for that tag, before using the summary statistics to make comparisons. +4. If you have assigned a sufficient number of applicants with a score for that tag, you can use the summary statistics to make comparisons. For example, you want to check if an applicant's score for a tag is more than or equal to half of all the applicant who have a score for that tag, you can use the median to make this comparison. +* A **sufficient number** could be deemed as **any number that is more than 20**, but this is not a hard and fast rule. You should use your own discretion to determine if the number of applicant with a score for that tag is sufficient. + + + + +1. Use mostly `median` and `percentile` to make your judgement on the performance of an applicant. +2. `median` to find applicants who are the better performing half +3. `percentile` as where this applicant stands among all other applicants (treat it like a ranking system, the higher the percentile, the better the applicant is performing) + + + + +**Advanced users** +* Understand that `percentile` has limited functionality in some context. This is because if two applicants have the same score, they are `rank` the same. This means that the percentile of both applicants will be the same. + * If all applicants have the same score, their percentile will all be 0.0. This is because they are all `rank` the same. + * Additionally, when the spread of scores is small, the percentile will not be able to differentiate between applicants with similar scores. + + +**Formula used to calculate the summary statistics:** +**mean** is calculated by using the formula `sum of all scores with that tag / number of applicants with that tag` +**median** is calculated by using the formula `middle score of all scores with that tag` +**minimum** is calculated by using the formula `lowest score of all scores with that tag` +**maximum** is calculated by using the formula `highest score of all scores with that tag` +**percentile** is calculated by using the formula `number of applicants with a score strictly lower than the applicant / total number of applicants with that tag` + diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index d98f38982e7..8b18f27946b 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -1,8 +1,11 @@ --- -layout: page -title: "Tutorial: Adding a command" + layout: default.md + title: "Tutorial: Adding a command" + pageNav: 3 --- +# Tutorial: Adding a command + Let's walk you through the implementation of a new command — `remark`. This command allows users of the AddressBook application to add optional remarks to people in their address book and edit it if required. The command should have the following format: @@ -22,7 +25,7 @@ For now, let’s keep `RemarkCommand` as simple as possible and print some outpu **`RemarkCommand.java`:** -``` java +```java package seedu.address.logic.commands; import seedu.address.model.Model; @@ -57,13 +60,13 @@ Run `Main#main` and try out your new `RemarkCommand`. If everything went well, y While we have successfully printed a message to `ResultDisplay`, the command does not do what it is supposed to do. Let’s change the command to throw a `CommandException` to accurately reflect that our command is still a work in progress. -![The relationship between RemarkCommand and Command](../images/add-remark/RemarkCommandClass.png) + Following the convention in other commands, we add relevant messages as constants and use them. **`RemarkCommand.java`:** -``` java +```java public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the remark of the person identified " + "by the index number used in the last person listing. " @@ -90,7 +93,7 @@ Let’s change `RemarkCommand` to parse input from the user. We start by modifying the constructor of `RemarkCommand` to accept an `Index` and a `String`. While we are at it, let’s change the error message to echo the values. While this is not a replacement for tests, it is an obvious way to tell if our code is functioning as intended. -``` java +```java import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; //... public class RemarkCommand extends Command { @@ -142,13 +145,13 @@ Now let’s move on to writing a parser that will extract the index and remark f Create a `RemarkCommandParser` class in the `seedu.address.logic.parser` package. The class must extend the `Parser` interface. -![The relationship between Parser and RemarkCommandParser](../images/add-remark/RemarkCommandParserClass.png) + Thankfully, `ArgumentTokenizer#tokenize()` makes it trivial to parse user input. Let’s take a look at the JavaDoc provided for the function to understand what it does. **`ArgumentTokenizer.java`:** -``` java +```java /** * Tokenizes an arguments string and returns an {@code ArgumentMultimap} * object that maps prefixes to their respective argument values. Only the @@ -166,7 +169,7 @@ We can tell `ArgumentTokenizer#tokenize()` to look out for our new prefix `r/` a **`ArgumentMultimap.java`:** -``` java +```java /** * Returns the last value of {@code prefix}. */ @@ -181,7 +184,7 @@ This appears to be what we need to get a String of the remark. But what about th **`DeleteCommandParser.java`:** -``` java +```java Index index = ParserUtil.parseIndex(args); return new DeleteCommand(index); ``` @@ -192,7 +195,7 @@ Now that we have the know-how to extract the data that we need from the user’s **`RemarkCommandParser.java`:** -``` java +```java public RemarkCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, @@ -212,11 +215,11 @@ public RemarkCommand parse(String args) throws ParseException { } ``` -
    + -:information_source: Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`! +Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`! -
    + If you are stuck, check out the sample [here](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-8bf239e8e9529369b577701303ddd96af93178b4ed6735f91c2d8488b20c6b4a). @@ -244,7 +247,7 @@ Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/s **`PersonCard.java`:** -``` java +```java @FXML private Label remark; ``` @@ -276,11 +279,11 @@ We change the constructor of `Person` to take a `Remark`. We will also need to d Unfortunately, a change to `Person` will cause other commands to break, you will have to modify these commands to use the updated `Person`! -
    + -:bulb: Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands. +Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands. -
    + Refer to [this commit](https://github.com/se-edu/addressbook-level3/commit/ce998c37e65b92d35c91d28c7822cd139c2c0a5c) and check that you have got everything in order! @@ -291,11 +294,11 @@ AddressBook stores data by serializing `JsonAdaptedPerson` into `json` with the While the changes to code may be minimal, the test data will have to be updated as well. -
    + -:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! +You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! -
    + Check out [this commit](https://github.com/se-edu/addressbook-level3/commit/556cbd0e03ff224d7a68afba171ad2eb0ce56bbf) to see what the changes entail. @@ -308,7 +311,7 @@ Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/c **`PersonCard.java`:** -``` java +```java public PersonCard(Person person, int displayedIndex) { //... remark.setText(person.getRemark().value); @@ -328,7 +331,7 @@ save it with `Model#setPerson()`. **`RemarkCommand.java`:** -``` java +```java //... public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s"; public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s"; diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..c73bd379e5e 100644 --- a/docs/tutorials/RemovingFields.md +++ b/docs/tutorials/RemovingFields.md @@ -1,8 +1,11 @@ --- -layout: page -title: "Tutorial: Removing Fields" + layout: default.md + title: "Tutorial: Removing Fields" + pageNav: 3 --- +# Tutorial: Removing Fields + > Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. > > — Antoine de Saint-Exupery @@ -10,17 +13,17 @@ title: "Tutorial: Removing Fields" When working on an existing code base, you will most likely find that some features that are no longer necessary. This tutorial aims to give you some practice on such a code 'removal' activity by removing the `address` field from `Person` class. -
    + **If you have done the [Add `remark` command tutorial](AddRemark.html) already**, you should know where the code had to be updated to add the field `remark`. From that experience, you can deduce where the code needs to be changed to _remove_ that field too. The removing of the `address` field can be done similarly.

    However, if you have no such prior knowledge, removing a field can take a quite a bit of detective work. This tutorial takes you through that process. **At least have a read even if you don't actually do the steps yourself.** -
    + -* Table of Contents -{:toc} + + ## Safely deleting `Address` @@ -50,10 +53,10 @@ Let’s try removing references to `Address` in `EditPersonDescriptor`. 1. Remove the usages of `address` and select `Do refactor` when you are done. -
    + - :bulb: **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor. -
    + **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor. + 1. Repeat the steps for the remaining usages of `Address` @@ -71,7 +74,7 @@ A quick look at the `PersonCard` class and its `fxml` file quickly reveals why i **`PersonCard.java`** -``` java +```java ... @FXML private Label address; diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..2b1b0f2d6b7 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -1,26 +1,30 @@ --- -layout: page -title: "Tutorial: Tracing code" + layout: default.md + title: "Tutorial: Tracing code" + pageNav: 3 --- +# Tutorial: Tracing code + + > Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …​\[Therefore,\] making it easy to read makes it easier to write. > > — Robert C. Martin Clean Code: A Handbook of Agile Software Craftsmanship When trying to understand an unfamiliar code base, one common strategy used is to trace some representative execution path through the code base. One easy way to trace an execution path is to use a debugger to step through the code. In this tutorial, you will be using the IntelliJ IDEA’s debugger to trace the execution path of a specific user command. -* Table of Contents -{:toc} + + ## Before we start Before we jump into the code, it is useful to get an idea of the overall structure and the high-level behavior of the application. This is provided in the 'Architecture' section of the developer guide. In particular, the architecture diagram (reproduced below), tells us that the App consists of several components. -![ArchitectureDiagram](../images/ArchitectureDiagram.png) + It also has a sequence diagram (reproduced below) that tells us how a command propagates through the App. - + Note how the diagram shows only the execution flows _between_ the main components. That is, it does not show details of the execution path *inside* each component. By hiding those details, the diagram aims to inform the reader about the overall execution path of a command without overwhelming the reader with too much details. In this tutorial, you aim to find those omitted details so that you get a more in-depth understanding of how the code works. @@ -37,16 +41,16 @@ As you know, the first step of debugging is to put in a breakpoint where you wan In our case, we would want to begin the tracing at the very point where the App start processing user input (i.e., somewhere in the UI component), and then trace through how the execution proceeds through the UI component. However, the execution path through a GUI is often somewhat obscure due to various *event-driven mechanisms* used by GUI frameworks, which happens to be the case here too. Therefore, let us put the breakpoint where the `UI` transfers control to the `Logic` component. - + According to the sequence diagram you saw earlier (and repeated above for reference), the `UI` component yields control to the `Logic` component through a method named `execute`. Searching through the code base for an `execute()` method that belongs to the `Logic` component yields a promising candidate in `seedu.address.logic.Logic`. -
    + -:bulb: **Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`. -
    +**Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`. + A quick look at the `seedu.address.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for. @@ -67,14 +71,14 @@ public interface Logic { But apparently, this is an interface, not a concrete implementation. That should be fine because the [Architecture section of the Developer Guide](../DeveloperGuide.html#architecture) tells us that components interact through interfaces. Here's the relevant diagram: - + Next, let's find out which statement(s) in the `UI` code is calling this method, thus transferring control from the `UI` to the `Logic`. -
    + -:bulb: **Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used. -
    +**Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used. + ![`Find Usages` tool window. `Edit` \> `Find` \> `Find Usages`.](../images/tracing/FindUsages.png) @@ -87,10 +91,10 @@ Now let’s set the breakpoint. First, double-click the item to reach the corres Recall from the User Guide that the `edit` command has the format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` For this tutorial we will be issuing the command `edit 1 n/Alice Yeoh`. -
    + -:bulb: **Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component. -
    +**Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component. + 1. To start the debugging session, simply `Run` \> `Debug Main` @@ -110,7 +114,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ **LogicManager\#execute().** - ``` java + ```java @Override public CommandResult execute(String commandText) throws CommandException, ParseException { @@ -142,7 +146,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ![StepOver](../images/tracing/StepOver.png) 1. _Step into_ the line where user input in parsed from a String to a Command, which should bring you to the `AddressBookParser#parseCommand()` method (partial code given below): - ``` java + ```java public Command parseCommand(String userInput) throws ParseException { ... final String commandWord = matcher.group("commandWord"); @@ -157,7 +161,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Stepping through the `switch` block, we end up at a call to `EditCommandParser().parse()` as expected (because the command we typed is an edit command). - ``` java + ```java ... case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); @@ -166,8 +170,10 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Let’s see what `EditCommandParser#parse()` does by stepping into it. You might have to click the 'step into' button multiple times here because there are two method calls in that statement: `EditCommandParser()` and `parse()`. -
    :bulb: **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them! -
    + + + **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them! + 1. Stepping through the method shows that it calls `ArgumentTokenizer#tokenize()` and `ParserUtil#parseIndex()` to obtain the arguments and index required. @@ -175,17 +181,17 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ![EditCommand](../images/tracing/EditCommand.png) 1. As you just traced through some code involved in parsing a command, you can take a look at this class diagram to see where the various parsing-related classes you encountered fit into the design of the `Logic` component. - + 1. Let’s continue stepping through until we return to `LogicManager#execute()`. The sequence diagram below shows the details of the execution path through the Logic component. Does the execution path you traced in the code so far match the diagram?
    - ![Tracing an `edit` command through the Logic component](../images/tracing/LogicSequenceDiagram.png) + 1. Now, step over until you read the statement that calls the `execute()` method of the `EditCommand` object received, and step into that `execute()` method (partial code given below): **`EditCommand#execute()`:** - ``` java + ```java @Override public CommandResult execute(Model model) throws CommandException { ... @@ -205,25 +211,28 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
    FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
    To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked. -
    +
    * :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component) 1. As you step through the rest of the statements in the `EditCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
    Advancing the debugger by one more step should take you back to the middle of the `LogicManager#execute()` method.
    1. Given that you have already seen quite a few classes in the `Logic` component in action, see if you can identify in this partial class diagram some of the classes you've encountered so far, and see how they fit into the class structure of the `Logic` component: - + + * :bulb: This may be a good time to read through the [`Logic` component section of the DG](../DeveloperGuide.html#logic-component) 1. Similar to before, you can step over/into statements in the `LogicManager#execute()` method to examine how the control is transferred to the `Storage` component and what happens inside that component. -
    :bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into. -
    + + + **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into. + -1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability): +1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability): **`JsonSerializableAddressBook` constructor:** - ``` java + ```java /** * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. * @@ -243,7 +252,8 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ This is because regular Java objects need to go through an _adaptation_ for them to be suitable to be saved in JSON format. 1. While you are stepping through the classes in the `Storage` component, here is the component's class diagram to help you understand how those classes fit into the structure of the component.
    - + + * :bulb: This may be a good time to read through the [`Storage` component section of the DG](../DeveloperGuide.html#storage-component) 1. We can continue to step through until you reach the end of the `LogicManager#execute()` method and return to the `MainWindow#executeCommand()` method (the place where we put the original breakpoint). @@ -251,7 +261,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Stepping into `resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());`, we end up in: **`ResultDisplay#setFeedbackToUser()`** - ``` java + ```java public void setFeedbackToUser(String feedbackToUser) { requireNonNull(feedbackToUser); resultDisplay.setText(feedbackToUser); diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000000..386de816e07 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.6/userguide/multi_project_builds.html + */ + +rootProject.name = "tp" diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..4ff472a78ff 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -16,9 +16,11 @@ import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; import seedu.address.model.AddressBook; +import seedu.address.model.EventBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEventBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; import seedu.address.model.util.SampleDataUtil; @@ -28,6 +30,8 @@ import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; import seedu.address.storage.UserPrefsStorage; +import seedu.address.storage.event.EventBookStorage; +import seedu.address.storage.event.JsonEventBookStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; @@ -36,7 +40,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -58,7 +62,8 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); UserPrefs userPrefs = initPrefs(userPrefsStorage); AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + EventBookStorage eventBookStorage = new JsonEventBookStorage(userPrefs.getEventBookFilePath()); + storage = new StorageManager(addressBookStorage, userPrefsStorage, eventBookStorage); model = initModelManager(storage, userPrefs); @@ -74,23 +79,29 @@ public void init() throws Exception { */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { logger.info("Using data file : " + storage.getAddressBookFilePath()); + logger.info("Using data file : " + storage.getEventBookFilePath()); Optional addressBookOptional; + Optional eventBookOptional; ReadOnlyAddressBook initialData; + ReadOnlyEventBook initialEventData; try { + eventBookOptional = storage.readEventBook(); addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { logger.info("Creating a new data file " + storage.getAddressBookFilePath() + " populated with a sample AddressBook."); } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialEventData = eventBookOptional.orElse(new EventBook()); } catch (DataLoadingException e) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." + " Will be starting with an empty AddressBook."); initialData = new AddressBook(); + initialEventData = new EventBook(); } - return new ModelManager(initialData, userPrefs); + return new ModelManager(initialData, initialEventData, userPrefs); } private void initLogging(Config config) { diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/address/commons/core/index/Index.java index dd170d8b68d..96eff2b397c 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/address/commons/core/index/Index.java @@ -33,6 +33,26 @@ public int getOneBased() { return zeroBasedIndex + 1; } + /** + * Returns the default index. The default index is created with a value of + * Integer.MAX_VALUE. + * + * @return The default index with a value of Integer.MAX_VALUE. + */ + public static Index getDefaultIndex() { + return new Index((Integer.MAX_VALUE)); + } + + /** + * Checks if the index is present. An index is considered present if its value + * is not equal to Integer.MAX_VALUE. + * + * @return {@code true} if the index is present; {@code false} otherwise. + */ + public boolean isPresent() { + return zeroBasedIndex != Integer.MAX_VALUE; + } + /** * Creates a new {@code Index} using a zero-based index. */ diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..93e7922f2ab 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -7,6 +7,9 @@ import java.io.StringWriter; import java.util.Arrays; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + /** * Helper functions for handling strings. */ @@ -65,4 +68,62 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Returns true if {@code s} represents a non-negative integer + * @param s string to be checked + * @return true if {@code s} represents a non-negative integer + * @throws NullPointerException if {@code s} is null. + */ + public static boolean isNonNegativeInteger(String s) { + requireNonNull(s); + try { + int value = Integer.parseInt(s); + return value >= 0 && !s.startsWith("+"); // "+1" is successfully parsed by Integer#parseInt(String) + } catch (NumberFormatException nfe) { + return false; + } + } + + /** + * Uses escape quotations to specify a string as a string + * @param str + * @return + */ + public static String treatAsString(String str) { + return "\"" + str + "\""; + } + + /** + * Appends the persons to a StringBuilder + * @param model the model in use + * @return returns the StringBuilder containing all the data of Persons + */ + public static StringBuilder appendPersons(Model model) { + StringBuilder sb = new StringBuilder("Name,Phone,Email,Address,Tags,LinkedIn,Github,Remark,Status\n"); + + for (Person p : model.getAddressBook().getPersonList()) { + String name = treatAsString(p.getName().toString()); + String phone = treatAsString(p.getPhone().toString()); + String email = treatAsString(p.getEmail().toString()); + String address = treatAsString(p.getAddress().toString()); + String tags = treatAsString(p.getTags().toString()); + String linkedIn = treatAsString(p.getLinkedIn().toString()); + String github = treatAsString(p.getGithub().toString()); + String remark = treatAsString(p.getRemark().toString()); + String status = treatAsString(p.getStatus().toString()); + + sb.append(name) + .append(",").append(phone) + .append(",").append(email) + .append(",").append(address) + .append(",").append(tags) + .append(",").append(linkedIn) + .append(",").append(github) + .append(",").append(remark) + .append(",").append(status) + .append("\n"); + } + return sb; + } } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..8870f811218 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -4,11 +4,16 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEventBook; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; +import seedu.address.model.statistics.ReadOnlySummaryStatistic; +import seedu.address.model.tag.Tag; /** * API of the Logic component @@ -30,14 +35,29 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); + ReadOnlyEventBook getEventBook(); + + /** + * Returns the SummaryStatistic. + * @return SummaryStatistic + */ + ReadOnlySummaryStatistic getSummaryStatistic(); + /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + ObservableList getFilteredTagsList(); + + ObservableList getFilteredEventList(); + + /** * Returns the user prefs' address book file path. */ Path getAddressBookFilePath(); + Path getEventBookFilePath(); + /** * Returns the user prefs' GUI settings. */ @@ -47,4 +67,10 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the last viewed person index. + */ + Index getLastViewedPersonIndex(); + } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..b6e5bd354e5 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -8,6 +8,7 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; @@ -15,7 +16,11 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEventBook; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; +import seedu.address.model.statistics.ReadOnlySummaryStatistic; +import seedu.address.model.tag.Tag; import seedu.address.storage.Storage; /** @@ -27,6 +32,7 @@ public class LogicManager implements Logic { public static final String FILE_OPS_PERMISSION_ERROR_FORMAT = "Could not save data to file %s due to insufficient permissions to write to the file or the folder."; + private final Logger logger = LogsCenter.getLogger(LogicManager.class); private final Model model; @@ -50,8 +56,10 @@ public CommandResult execute(String commandText) throws CommandException, ParseE Command command = addressBookParser.parseCommand(commandText); commandResult = command.execute(model); + try { storage.saveAddressBook(model.getAddressBook()); + storage.saveEventBook(model.getEventBook()); } catch (AccessDeniedException e) { throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); } catch (IOException ioe) { @@ -66,16 +74,48 @@ public ReadOnlyAddressBook getAddressBook() { return model.getAddressBook(); } + @Override + public ReadOnlyEventBook getEventBook() { + return model.getEventBook(); + } + + @Override + public ReadOnlySummaryStatistic getSummaryStatistic() { + return model.getSummaryStatistic(); + } + @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredTagsList() { + return model.getFilteredTagList(); + } + + @Override + public ObservableList getFilteredEventList() { + return model.getFilteredEventList(); + } + + + + @Override + public Index getLastViewedPersonIndex() { + return model.getLastViewedPersonIndex(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); } + @Override + public Path getEventBookFilePath() { + return model.getEventBookFilePath(); + } + @Override public GuiSettings getGuiSettings() { return model.getGuiSettings(); @@ -85,4 +125,6 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + } diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..eb7b931965b 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -15,10 +15,18 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_TAG_DOES_NOT_EXIST = "The tag does not exist"; + public static final String MESSAGE_ILLEGAL_TAG_SCORE = "The tag does not exist, cannot attach a score to it"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; + + public static final String MESSAGE_INCORRECT_DATE_FORMAT = + "Date is not in correct format! \nRequired format: yyyy-MM-dd HH:mm"; + public static final String MESSAGE_INVALID_START = "End time must be after start time!"; + public static final String MESSAGE_INVALID_DATE = "Date entered is invalid!"; + /** * Returns an error message indicating the duplicate prefixes. */ diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..a7bfaaeddf8 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -7,6 +7,7 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; @@ -32,14 +33,17 @@ public class AddCommand extends Command { + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_TAG + "developer " + + PREFIX_TAG + "intern"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; private final Person toAdd; + private Index index; + + /** * Creates an AddCommand to add the specified {@code Person} */ @@ -57,7 +61,9 @@ public CommandResult execute(Model model) throws CommandException { } model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + index = Index.fromZeroBased(model.getFilteredPersonList().size() - 1); + model.setLastViewedPersonIndex(index); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd)), true); } @Override diff --git a/src/main/java/seedu/address/logic/commands/AddGCommand.java b/src/main/java/seedu/address/logic/commands/AddGCommand.java new file mode 100644 index 00000000000..97765654217 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddGCommand.java @@ -0,0 +1,83 @@ +package seedu.address.logic.commands; + +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Github; +import seedu.address.model.person.Person; + +/** + * Adds GitHub account to candidates existing details. + */ +public class AddGCommand extends Command { + public static final String COMMAND_WORD = "addG"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds Github to details of a candidate. " + + "Parameters: " + "[" + COMMAND_WORD + " u/]...\n" + + "where USERID must be a non-zero unsigned integer, and within bounds of list size\n" + + "Example: " + COMMAND_WORD + " 2 u/alexyeoh"; + + public static final String MESSAGE_SUCCESS = "Github account added for: %1$s"; + + private final Index index; + + private final Github username; + + /** + * Creates an AddG Command to add Github. + * @param index + * @param username + */ + public AddGCommand(Index index, Github username) { + this.index = index; + this.username = username; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + personToEdit.setGithub(username); + model.setLastViewedPersonIndex(index); + model.setPerson(personToEdit, personToEdit); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(generateSuccessMessage(personToEdit), true); + } + + /** + * Generates a command execution success message based on whether + * the username is added to or removed from + * {@code personToEdit}. + */ + private String generateSuccessMessage(Person personToEdit) { + String message = !username.value.isEmpty() ? MESSAGE_SUCCESS : ""; + return String.format(message, personToEdit.getName()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddGCommand)) { + return false; + } + + AddGCommand otherAddGCommand = (AddGCommand) other; + return this.index.equals(otherAddGCommand.index) && this.username.equals(otherAddGCommand.username); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/AddLCommand.java b/src/main/java/seedu/address/logic/commands/AddLCommand.java new file mode 100644 index 00000000000..5edc304e603 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddLCommand.java @@ -0,0 +1,83 @@ +package seedu.address.logic.commands; + +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.LinkedIn; +import seedu.address.model.person.Person; + +/** + * Adds LinkedIn account to candidate's existing details. + */ +public class AddLCommand extends Command { + public static final String COMMAND_WORD = "addL"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds LinkedIn to details of a candidate. " + + "Parameters: " + "[" + COMMAND_WORD + + " u/]...\nwhere USERID must be a non-zero unsigned integer, " + + "and within bounds of list size\n" + + "Example: " + COMMAND_WORD + " 2 u/alexyeoh"; + + public static final String MESSAGE_SUCCESS = "LinkedIn account added for: %1$s"; + + private final Index index; + + private final LinkedIn username; + + /** + * Creates an AddL command to add LinkedIn. + * @param index + * @param username + */ + public AddLCommand(Index index, LinkedIn username) { + this.index = index; + this.username = username; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + personToEdit.setLinkedIn(username); + model.setLastViewedPersonIndex(index); + model.setPerson(personToEdit, personToEdit); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(generateSuccessMessage(personToEdit), true); + } + + /** + * Generates a command execution success message based on whether + * the username is added to or removed from + * {@code personToEdit}. + */ + private String generateSuccessMessage(Person personToEdit) { + String message = !username.value.isEmpty() ? MESSAGE_SUCCESS : ""; + return String.format(message, personToEdit.getName()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddLCommand)) { + return false; + } + + AddLCommand otherAddLCommand = (AddLCommand) other; + return this.index.equals(otherAddLCommand.index) && this.username.equals(otherAddLCommand.username); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..22c94a6df07 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import seedu.address.model.AddressBook; +import seedu.address.model.EventBook; import seedu.address.model.Model; /** @@ -11,13 +12,14 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "JABPro has been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); + model.setEventBook(new EventBook()); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..62ee93ad1e2 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -19,13 +19,22 @@ public class CommandResult { /** The application should exit. */ private final boolean exit; + private final boolean isView; + + private final boolean showEvent; + private final boolean listTags; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean isView, + boolean showEvent, boolean listTags) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.isView = isView; + this.showEvent = showEvent; + this.listTags = listTags; } /** @@ -33,7 +42,16 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, false, false, false, false, false); + } + + /** + * Constructs a {@code CommandResult} with the specified fields. + * @param feedbackToUser feedback to user + * @param isView whether to show the view + */ + public CommandResult(String feedbackToUser, boolean isView) { + this(feedbackToUser, false, false, isView, false, false); } public String getFeedbackToUser() { @@ -48,6 +66,22 @@ public boolean isExit() { return exit; } + /** + * Returns true if the command result is to show the view. + * @return true if the command result is to show the view + */ + public boolean isView() { + return isView; + } + + public boolean isShowEvent() { + return showEvent; + } + + public boolean isListTags() { + return listTags; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -62,12 +96,15 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && isView == otherCommandResult.isView + && showEvent == otherCommandResult.showEvent + && listTags == otherCommandResult.listTags; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, isView, showEvent, listTags); } @Override @@ -76,6 +113,9 @@ public String toString() { .add("feedbackToUser", feedbackToUser) .add("showHelp", showHelp) .add("exit", exit) + .add("isView", isView) + .add("showEvent", showEvent) + .add("listTags", listTags) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/CreateTagCommand.java b/src/main/java/seedu/address/logic/commands/CreateTagCommand.java new file mode 100644 index 00000000000..4f4478ded43 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CreateTagCommand.java @@ -0,0 +1,124 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; + +/** + * Represents a command to create tags with specific categories. + */ +public class CreateTagCommand extends Command { + public static final String COMMAND_WORD = "create"; + public static final String MESSAGE_SUCCESS = "Tags created!"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Creates tags with specific categories. " + + "Parameters: " + + PREFIX_TAG + "ROLE TAG " + + "Example: " + COMMAND_WORD + " " + + PREFIX_TAG + "dept " + + "Tech " + + PREFIX_TAG + "role " + + "Analyst"; + public static final String MESSAGE_DUPLICATE_TAG = "This tag already exists in the address book!"; + public static final String MESSAGE_FAILURE = "Oops! You've reached the maximum limit for categories"; + + private final String[] tagParams; + + /** + * Constructs a CreateTagCommand with the specified tag parameters. + * + * @param tagParams An array of tag parameters, where each element represents a tag's category and name. + */ + public CreateTagCommand(String[] tagParams) { + requireNonNull(tagParams); + System.out.println(Arrays.toString(tagParams)); + this.tagParams = tagParams; + } + + /** + * Executes the CreateTagCommand to create tags with specific categories in the model. + * + * @param model The model in which the tags are created. + * @return A CommandResult indicating the success of the command. + * @throws CommandException If there is an error executing the command. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + UniqueTagList uniqueTagList = new UniqueTagList(); + for (String tagParam : tagParams) { + String[] categoryTag = tagParam.trim().split("\\s+"); + try { + String tagCategory = categoryTag[0]; + String tagName = categoryTag[1]; + Tag newTag = new Tag(tagName, tagCategory); + if (model.hasTag(newTag)) { + throw new CommandException(MESSAGE_DUPLICATE_TAG); + } + if (!uniqueTagList.containsTagCategory(tagCategory) + && countDistinctCategories(uniqueTagList.asUnmodifiableObservableList()) > 5) { + throw new CommandException(MESSAGE_FAILURE); + } + model.addTag(newTag); + } catch (IllegalValueException e) { + throw new RuntimeException(e); + } + } + return new CommandResult(MESSAGE_SUCCESS); + } + + private int countDistinctCategories(List tags) { + requireNonNull(tags); + Set distinctCategories = new HashSet<>(); + + for (Tag tag : tags) { + String tagCategory = tag.getTagCategory(); + if (!tagCategory.equals("uncategorised")) { + distinctCategories.add(tagCategory); + } + } + + return distinctCategories.size(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CreateTagCommand)) { + return false; + } + + CreateTagCommand otherCreateTagCommand = (CreateTagCommand) other; + boolean result = false; + String[] otherTagParams = otherCreateTagCommand.tagParams; + + if (tagParams.length != otherTagParams.length) { + return false; + } + + System.out.println(Arrays.toString(tagParams)); + System.out.println(Arrays.toString(otherTagParams)); + + for (int i = 0; i < tagParams.length; i++) { + result = tagParams[i].equals(otherTagParams[i]); + } + return result; + } + + + +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..0e228b46140 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,48 +1,108 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a person identified using its displayed index or tags from the address book. */ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; + + ": Deletes the person(s) identified by the index number, tags, or status " + + "used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer) or " + PREFIX_TAG + "TAG [MORE_TAGS]... or " + + PREFIX_STATUS + "STATUS\n" + + "Example (Delete by index): " + COMMAND_WORD + " 1\n" + + "Example (Delete by tags): " + COMMAND_WORD + " " + PREFIX_TAG + "manager " + PREFIX_TAG + "intern\n" + + "Example (Delete by status): " + COMMAND_WORD + " " + PREFIX_STATUS + "rejected\n" + + "Example (Delete by tags and status): " + COMMAND_WORD + " " + PREFIX_TAG + "softwareEngineer " + + PREFIX_STATUS + "interviewed"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_PERSONS_NOT_FOUND = "No persons with the specified tags or status found."; + public static final String MESSAGE_NO_TARGET_SPECIFIED = "You must specify either an index, tags, " + + "or status to delete."; - private final Index targetIndex; + private Index targetIndex = Index.getDefaultIndex(); + private List> predicatesList = new ArrayList<>(); public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; } + public DeleteCommand(List> predicatesList) { + this.predicatesList = predicatesList; + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); List lastShownList = model.getFilteredPersonList(); + List lastShownEventList = model.getFilteredEventList(); + + // Delete by index + if (targetIndex.isPresent()) { + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deletePerson(personToDelete); + + // Delete event(s) with deleted person(s) + List eventsToDelete = lastShownEventList.stream() + .filter(event -> event.getPerson().isSamePerson(personToDelete)) + .collect(Collectors.toList()); + for (Event event : eventsToDelete) { + model.deleteEvent(event); + } + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + } + + // Use predicates to filter the list of persons + List personsToDelete = lastShownList.stream() + .filter(person -> predicatesList.stream().allMatch(predicate -> predicate.test(person))) + .collect(Collectors.toList()); + + if (personsToDelete.isEmpty()) { + throw new CommandException(MESSAGE_PERSONS_NOT_FOUND); + } - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + for (Person person : personsToDelete) { + model.deletePerson(person); } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + // Delete event(s) with deleted person(s) + List eventsToDelete = lastShownEventList.stream() + .filter(event -> personsToDelete.stream() + .anyMatch(person -> event.getPerson().isSamePerson(person))) + .collect(Collectors.toList()); + + for (Event event : eventsToDelete) { + model.deleteEvent(event); + } + + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, + personsToDelete.size() == 1 ? Messages.format(personsToDelete.get(0)) + : personsToDelete.size() + " persons")); } @Override @@ -57,13 +117,21 @@ public boolean equals(Object other) { } DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); + return targetIndex.equals(otherDeleteCommand.targetIndex) + && predicatesList.equals(otherDeleteCommand.predicatesList); } @Override public String toString() { - return new ToStringBuilder(this) - .add("targetIndex", targetIndex) - .toString(); + ToStringBuilder stringBuilder = new ToStringBuilder(this); + if (targetIndex.isPresent()) { + return stringBuilder.add("targetIndex", targetIndex).toString(); + } + + if (!predicatesList.isEmpty()) { + return stringBuilder.add("predicateList", predicatesList).toString(); + } + + return stringBuilder.add("invalid", "No valid target specified").toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..b3a75a51d1b 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -5,7 +5,9 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; @@ -21,11 +23,18 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.event.Event; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Github; +import seedu.address.model.person.LinkedIn; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; +import seedu.address.model.person.Score; +import seedu.address.model.person.ScoreList; +import seedu.address.model.person.Status; import seedu.address.model.tag.Tag; /** @@ -43,7 +52,8 @@ public class EditCommand extends Command { + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_TAG + "TAG]... " + + "[" + PREFIX_SCORE + "TAG SCORE] \n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; @@ -71,21 +81,88 @@ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { public CommandResult execute(Model model) throws CommandException { requireNonNull(model); List lastShownList = model.getFilteredPersonList(); + List lastShownEventList = model.getFilteredEventList(); if (index.getZeroBased() >= lastShownList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + Person editedPersonWithoutFilteredScoreList = createEditedPerson(personToEdit, editPersonDescriptor); + + Person editedPerson = filterScoreList(editedPersonWithoutFilteredScoreList, personToEdit); if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + updateScoreList(personToEdit, editedPerson); model.setPerson(personToEdit, editedPerson); + model.setLastViewedPersonIndex(index); + model.loadSummaryStatistics(); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + + for (Event event : lastShownEventList) { + if (event.getPerson().isSamePerson(personToEdit)) { + Event updatedEvent = new Event(editedPerson, event.getDescription(), event.getStart_time(), + event.getEnd_time()); + model.setEvent(event, updatedEvent); + } + } + model.updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); + return new CommandResult( + String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)), true); + } + + private void updateScoreList(Person personToEdit, Person editedPerson) { + // If the tags are the same, then the score list can be updated through setting editedPerson + if (personToEdit.getTags().equals(editedPerson.getTags())) { + return; + } + // If there are no more tags, we should clean the score-list + ScoreList newScoreList = editedPerson.getScoreList(); + Set newTags = editedPerson.getTags(); + if (newTags.isEmpty()) { + editedPerson.setScoreList(new ScoreList()); + return; + } + + // If there is a difference in tags, delete all those that are not in current updated tags + for (Tag tag : newScoreList.getTagsWithScore()) { + if (!newTags.contains(tag)) { + newScoreList.removeScore(tag); + } + + } + editedPerson.setScoreList(newScoreList); + } + + /** + * Filters the score list of the person to edit, such that only the tags that are in the person's tag list + * @param editedPerson + * @param originalPerson + * @return + */ + private Person filterScoreList(Person editedPerson, Person originalPerson) throws CommandException { + Set currentTags = editedPerson.getTags(); + Set previousTags = originalPerson.getTags(); + if (currentTags.isEmpty()) { + return editedPerson; + } + + Set difference = new HashSet<>(currentTags); + difference.removeAll(previousTags); + + List tagsWithScore = editedPerson.getScoreList().getTagsWithScore(); + for (Tag tag : tagsWithScore) { + if (!currentTags.contains(tag) && !previousTags.contains(tag)) { + throw new CommandException(Messages.MESSAGE_ILLEGAL_TAG_SCORE); + } + if (!currentTags.contains(tag) && previousTags.contains(tag)) { + editedPerson.getScoreList().removeScore(tag); + } + } + return editedPerson; } /** @@ -99,9 +176,39 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Remark updatedRemark = personToEdit.getRemark(); + LinkedIn updatedLinkedIn = personToEdit.getLinkedIn(); + Github updatedGithub = personToEdit.getGithub(); + Status updatedStatus = personToEdit.getStatus(); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + Person editedPerson = new Person(updatedName, updatedPhone, + updatedEmail, updatedAddress, updatedRemark, updatedTags); + + + ScoreList updatedScoreList = createEditedScoreList(personToEdit.getScoreList(), + editPersonDescriptor.getScoreList()); + editedPerson.setScoreList(updatedScoreList); + editedPerson.setGithub(updatedGithub); + editedPerson.setLinkedIn(updatedLinkedIn); + editedPerson.setStatus(updatedStatus); + + return editedPerson; + } + + private static ScoreList createEditedScoreList( + ScoreList oldScoreList, + Optional editPersonDescriptorScoreList) { + Optional editedScoreList = editPersonDescriptorScoreList.filter(scoreList -> !scoreList.isEmpty()); + if (!editedScoreList.isPresent()) { + return oldScoreList; + } + ScoreList updatedScoreList = editPersonDescriptorScoreList.get(); + // The score list that is updated, only contains the update pair, which is 1 pair of tag score entry + Tag newTag = updatedScoreList.getTagsWithScore().get(0); + Score newScore = updatedScoreList.getScore(newTag); + oldScoreList.updateScoreList(newTag, newScore); + return oldScoreList; - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); } @Override @@ -137,6 +244,8 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; + + private ScoreList scoreList; private Set tags; public EditPersonDescriptor() {} @@ -151,13 +260,14 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setEmail(toCopy.email); setAddress(toCopy.address); setTags(toCopy.tags); + setScoreList(toCopy.scoreList); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, scoreList, tags); } public void setName(Name name) { @@ -192,6 +302,14 @@ public Optional
    getAddress() { return Optional.ofNullable(address); } + public void setScoreList(ScoreList scoreList) { + this.scoreList = (scoreList != null) ? scoreList : null; + } + + public Optional getScoreList() { + return this.scoreList == null ? Optional.empty() : Optional.ofNullable(scoreList); + } + /** * Sets {@code tags} to this object's {@code tags}. * A defensive copy of {@code tags} is used internally. @@ -225,6 +343,7 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(scoreList, otherEditPersonDescriptor.scoreList) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -235,6 +354,7 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("score-list", scoreList) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/EventCommand.java b/src/main/java/seedu/address/logic/commands/EventCommand.java new file mode 100644 index 00000000000..d5397e1f17b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EventCommand.java @@ -0,0 +1,75 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.person.Person; + +/** + * Adds an Event to JABPro. + */ +public class EventCommand extends Command { + + public static final String COMMAND_WORD = "event"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds Event relating to a candidate. " + + "Parameters: " + "[" + COMMAND_WORD + " d/ bt/ et/]...\n" + + "Example: " + COMMAND_WORD + " 2 d/Interview Round 1 bt/2023-10-22 09:00 et/2023-10-22 10:00"; + + public static final String MESSAGE_SUCCESS = "Event added: %1$s"; + + private final Event event; + + /** + * Creates an EventCommand to add event. + * @param event The event to be added. + */ + public EventCommand(Event event) { + requireNonNull(event); + this.event = event; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + Index targetIndex = event.getIndex(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person person = lastShownList.get(targetIndex.getZeroBased()); + Event event1 = new Event(person, event.getDescription(), event.getStart_time(), event.getEnd_time()); + model.addEvent(event1); + return new CommandResult(String.format(MESSAGE_SUCCESS, event1), + false, false, false, false, false); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EventCommand)) { + return false; + } + + EventCommand otherEventCommand = (EventCommand) other; + return event.equals(otherEventCommand.event); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("event", event) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..8ace7068750 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/ExportCommand.java b/src/main/java/seedu/address/logic/commands/ExportCommand.java new file mode 100644 index 00000000000..e1ec3f272f0 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.FileWriter; +import java.io.IOException; + +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + + +/** + * Exports the current dataset into Excel (.csv) format -- into the /data folder + */ +public class ExportCommand extends Command { + + public static final String COMMAND_WORD = "export"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Exports the current address book to Excel format " + + "Parameters: " + + "export"; + + public static final String MESSAGE_SUCCESS = "Sucessfully Exported"; + + private String defaulltPath = "data/export.csv"; + + /** + * Empty constructor, + */ + public ExportCommand() {} + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try (FileWriter writer = new FileWriter(defaulltPath)) { + writer.append(StringUtil.appendPersons(model)); + return new CommandResult(MESSAGE_SUCCESS); + } catch (IOException e) { + throw new CommandException("Error exporting data: " + e.getMessage()); + } + } + + /** + * Checks if the other command is an equivalent ExportCommand + * + * @param other the other object to be compared + */ + @Override + public boolean equals(Object other) { + return other instanceof ExportCommand; + } + + public String toString() { + return "ExportCommand"; + } +} diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..3df14eb14b2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.statistics.StatisticMetric; +import seedu.address.model.statistics.SummaryStatistic; +import seedu.address.model.tag.Tag; + +/** + * Filters the list of users by the given metric and value. + */ +public class FilterCommand extends Command { + public static final String COMMAND_WORD = "filter"; + public static final String MESSAGE_INVALID_TAG = "Invalid tag provided. Needs to be non-empty name"; + public static final String MESSAGE_INVALID_METRIC = "Invalid metric provided. Needs to be one of: " + + "score, mean, median, percentile"; + public static final String MESSAGE_INVALID_VALUE = "Invalid value provided. Needs to be a non negative integer " + + "that is more than or equal to 0"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters the list of users whose value is greater " + + "or equal to the value provided for the given metric and tag. \n" + + "Parameters: " + + "t/TAG (Tag must be a non-empty name of type assessment), " + + "met/METRIC (Metric must be one of: score, mean, median, percentile), " + + "val/VALUE (Value must be a non-negative integer, OPTIONAL for mean and median \n" + + "Example: " + COMMAND_WORD + " " + "t/Interview met/SCORE val/50" + + "Example: " + COMMAND_WORD + " " + "t/Interview met/MEAN"; + + private final StatisticMetric metric; + private final int value; + private final Tag tag; + + /** + * Creates a FilterCommand to filter the list of users by the given metric and value. + * @param tag + * @param metric + * @param value + */ + public FilterCommand(Tag tag, StatisticMetric metric, int value) { + requireAllNonNull(tag, metric, value); + this.tag = tag; + this.metric = metric; + this.value = value; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + SummaryStatistic summaryStatistic = new SummaryStatistic(model.getFilteredPersonList()); + if (summaryStatistic.getNumOfPeopleAssociatedWithTag(tag) == 0) { + throw new CommandException(Messages.MESSAGE_TAG_DOES_NOT_EXIST); + } + + List filteredPersonWithGreaterVal = summaryStatistic.filteredPersonList(tag, metric, value); + model.updateFilteredPersonList(person -> isFilteredPerson(filteredPersonWithGreaterVal, person)); + return new CommandResult(String.format(successMessage(model.getFilteredPersonList().size()))); + } + + /** + * Checks if the person is in the filtered list. + * @param filteredList + * @param person + * @return true if the person is in the filtered list, false otherwise. + */ + public boolean isFilteredPerson(List filteredList, Person person) { + if (filteredList.contains(person)) { + return true; + } + return false; + } + + /** + * Returns the success message for the filter command. + * @param numberOfPeople number of people in the filtered list + * @return success message for the filter command + */ + public String successMessage(int numberOfPeople) { + String message = String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, numberOfPeople) + + "\n" + + String.format("There are %d people with a value more than the value provided for the given metric %s", + numberOfPeople, metric); + return message; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof FilterCommand)) { + return false; + } + FilterCommand otherFilterCommand = (FilterCommand) other; + return this.tag.equals(otherFilterCommand.tag) + && this.metric.equals(otherFilterCommand.metric) + && this.value == otherFilterCommand.value; + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..d7b0cd88740 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -2,10 +2,13 @@ import static java.util.Objects.requireNonNull; +import java.util.List; +import java.util.function.Predicate; + import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. @@ -13,23 +16,29 @@ */ public class FindCommand extends Command { - public static final String COMMAND_WORD = "find"; + public static final String COMMAND_WORD = "search"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Searches users by categories (e.g. name, status, tags)" + + " whose details match the given keywords (case-insensitive) " + + "and displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + + "Example: " + COMMAND_WORD + " n/alex bernice st/interviewed t/softwareEngineer"; - private final NameContainsKeywordsPredicate predicate; + private final List> predicatesList; - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; + /** + * Creates an FindCommand to find the specified {@code Person} + */ + public FindCommand(List> predicatesList) { + this.predicatesList = predicatesList; } + @Override public CommandResult execute(Model model) { requireNonNull(model); - model.updateFilteredPersonList(predicate); + model.updateFilteredPersonList(predicatesList); + return new CommandResult( String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); } @@ -46,13 +55,23 @@ public boolean equals(Object other) { } FindCommand otherFindCommand = (FindCommand) other; - return predicate.equals(otherFindCommand.predicate); + Boolean isListEqual = false; + List> otherPredicates = otherFindCommand.predicatesList; + + if (predicatesList.size() != otherPredicates.size()) { + return false; + } + + for (int i = 0; i < predicatesList.size(); i++) { + isListEqual = predicatesList.get(i).equals(otherPredicates.get(i)); + } + return isListEqual; } @Override public String toString() { return new ToStringBuilder(this) - .add("predicate", predicate) + .add("predicates list", predicatesList) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/GithubCommand.java b/src/main/java/seedu/address/logic/commands/GithubCommand.java new file mode 100644 index 00000000000..09dbdd80e19 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/GithubCommand.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Redirects user to the Github account of a specific candidates. + */ +public class GithubCommand extends Command { + + public static final String COMMAND_WORD = "github"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Redirects to user's Github account. " + + "Parameters: " + "[" + COMMAND_WORD + " ]...\n" + + "where USERID must be a non-zero unsigned integer, and within bounds of list size\n" + + "Example: " + COMMAND_WORD + " 2"; + public static final String MESSAGE_SUCCESS = "Redirecting to Github ..."; + + private final Index index; + + public GithubCommand(Index index) { + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + String username = personToEdit.getGithub().value; + if (username.isEmpty()) { + throw new CommandException("No Github account has been added for this candidate."); + } + String githubUrl = "https://github.com/" + username; + try { + Desktop.getDesktop().browse(new URI(githubUrl)); + } catch (IOException | URISyntaxException e) { + throw new CommandException(e.getMessage()); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof GithubCommand)) { + return false; + } + + GithubCommand otherGithubCommand = (GithubCommand) other; + return this.index.equals(otherGithubCommand.index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..f4996722342 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/LinkedInCommand.java b/src/main/java/seedu/address/logic/commands/LinkedInCommand.java new file mode 100644 index 00000000000..6be4c2aea84 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LinkedInCommand.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Redirects the user to the LinkedIn account of a specific candidate. + */ +public class LinkedInCommand extends Command { + + public static final String COMMAND_WORD = "linkedin"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Redirects to user's LinkedIn account. " + + "Parameters: " + "[" + COMMAND_WORD + " ]...\n" + + "where USERID must be a non-zero unsigned integer, and within bounds of list size\n" + + "Example: " + COMMAND_WORD + " 2"; + public static final String MESSAGE_SUCCESS = "Redirecting to LinkedIn ..."; + + private final Index index; + + public LinkedInCommand(Index index) { + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + String username = personToEdit.getLinkedIn().value; + if (username.isEmpty()) { + throw new CommandException("No LinkedIn account has been added for this candidate."); + } + String linkedInUrl = "https://www.linkedin.com/in/" + username; + try { + Desktop.getDesktop().browse(new URI(linkedInUrl)); + } catch (IOException | URISyntaxException e) { + throw new CommandException(e.getMessage()); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LinkedInCommand)) { + return false; + } + + LinkedInCommand otherLinkedInCommand = (LinkedInCommand) other; + return this.index.equals(otherLinkedInCommand.index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..6ba7e673243 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,9 +1,13 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.Comparator; + import seedu.address.model.Model; +import seedu.address.model.person.Person; /** * Lists all persons in the address book to the user. @@ -14,10 +18,36 @@ public class ListCommand extends Command { public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all persons. " + + "Parameters: [so/ATTRIBUTE]\n" + + "Optional: ATTRIBUTE can be 'name' or other attributes for sorting.\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_SORT + "name"; + + public static final Comparator DEFAULT_COMPARATOR = (person1, person2) -> 0; // Comparator that does nothing + private final Comparator sortingComparator; + + /** + * Creates a ListCommand with the specified sorting comparator. + * + * @param sortingComparator The comparator to be used for sorting the person list. + */ + public ListCommand(Comparator sortingComparator) { + this.sortingComparator = sortingComparator; + } + + /** + * Gets the sorting comparator currently set for sorting the list of persons. + * + * @return The comparator used for sorting the list. + */ + public Comparator getSortingComparator() { + return sortingComparator; + } @Override public CommandResult execute(Model model) { requireNonNull(model); + model.sortPersonList(sortingComparator); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(MESSAGE_SUCCESS); } diff --git a/src/main/java/seedu/address/logic/commands/ListTCommand.java b/src/main/java/seedu/address/logic/commands/ListTCommand.java new file mode 100644 index 00000000000..04e8554f59f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListTCommand.java @@ -0,0 +1,33 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_TAGS; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Lists all tags in the address book. + */ +public class ListTCommand extends Command { + public static final String COMMAND_WORD = "listT"; + + public static final String MESSAGE_SUCCESS = "Listed all tags"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all tags. " + + "Example: " + COMMAND_WORD; + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.updateFilteredTagList(PREDICATE_SHOW_ALL_TAGS); + return new CommandResult(MESSAGE_SUCCESS, false, false, false, false, true); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RemarkCommand.java b/src/main/java/seedu/address/logic/commands/RemarkCommand.java new file mode 100644 index 00000000000..7deadfcec31 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemarkCommand.java @@ -0,0 +1,116 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.RemarkSyntaxHandler; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.Remark; + + + +/** + * Changes the remark of an existing person in the address book. + */ +public class RemarkCommand extends Command { + + public static final String COMMAND_WORD = "remark"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Edits the remark of the person identified " + + "by the index number used in the last person listing. " + + "Existing remark will be overwritten by the input.\n" + + "Parameters: INDEX (must be a positive integer) " + + "r/ [REMARK]\n" + + "Example: " + COMMAND_WORD + " 1 " + + "r/ Likes to swim."; + + public static final String MESSAGE_ARGUMENTS = "Index: %1$d, Remark: %2$s"; + public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s"; + public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s"; + + private final Index index; + private final Remark remark; + private final boolean isKeepRemark; + + /** + * @param index of the person in the filtered person list to edit the remark + * @param remark of the person to be updated to + */ + public RemarkCommand(Index index, Remark remark) { + requireAllNonNull(index, remark); + this.index = index; + this.remark = remark; + this.isKeepRemark = false; + } + + /** + * @param index of the person in the filtered person list to edit the remark + * @param remark of the person to be updated to + * @param isKeepRemark whether to keep the old remark + */ + public RemarkCommand(Index index, Remark remark, boolean isKeepRemark) { + requireAllNonNull(index, remark); + this.index = index; + this.remark = remark; + this.isKeepRemark = isKeepRemark; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + Remark newRemark = remark; + if (isKeepRemark) { + Remark currentRemark = personToEdit.getRemark(); + newRemark = RemarkSyntaxHandler.generateKeepRemarkCommand(remark.value, currentRemark); + } + + + + personToEdit.setRemark(newRemark); + + model.setLastViewedPersonIndex(index); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(generateSuccessMessage(personToEdit), true); + } + + /** + * Generates a command execution success message based on whether + * the remark is added to or removed from + * {@code personToEdit}. + */ + private String generateSuccessMessage(Person personToEdit) { + String message = !remark.value.isEmpty() ? MESSAGE_ADD_REMARK_SUCCESS : MESSAGE_DELETE_REMARK_SUCCESS; + return String.format(message, Messages.format(personToEdit)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof RemarkCommand)) { + return false; + } + + RemarkCommand e = (RemarkCommand) other; + return index.equals(e.index) + && remark.equals(e.remark) && isKeepRemark == e.isKeepRemark; + } +} diff --git a/src/main/java/seedu/address/logic/commands/ScheduleCommand.java b/src/main/java/seedu/address/logic/commands/ScheduleCommand.java new file mode 100644 index 00000000000..9044eea5037 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ScheduleCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands; + +import java.util.Comparator; + +import seedu.address.model.Model; +import seedu.address.model.event.Event; + +/** + * Format full help instructions for every command for display. + */ +public class ScheduleCommand extends Command { + + public static final String COMMAND_WORD = "schedule"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows events.\n" + + "Example: " + COMMAND_WORD; + + public static final String SHOWING_SCHEDULE_MESSAGE = "Opened events window."; + + @Override + public CommandResult execute(Model model) { + model.sortEventList(Comparator.comparing(Event::getStart_time)); + model.updateFilteredEventList(Model.PREDICATE_SHOW_ALL_EVENTS); + return new CommandResult(SHOWING_SCHEDULE_MESSAGE, false, false, false, true, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SetCommand.java b/src/main/java/seedu/address/logic/commands/SetCommand.java new file mode 100644 index 00000000000..30895c3e869 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SetCommand.java @@ -0,0 +1,79 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.StatusTypes; + +/** + * Sets the status of a person in the address book. + */ + +public class SetCommand extends Command { + public static final String MESSAGE_SET_PERSON_SUCCESS = "SET Person: %1$s"; + + public static final String COMMAND_WORD = "set"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Sets the status of a person. " + "Parameters: " + + "[" + COMMAND_WORD + " ]...\n" + "Example: " + + COMMAND_WORD + " 5 Interviewed"; + + public static final String MESSAGE_SUCCESS = "Status set for: %1$s"; + + private final StatusTypes newStatus; + + private final Index index; + + /** + * Creates a SetCommand to set the status of the person at the specified target index. + * + * @param index The index of the person to set the status. + * @param newStatus The new status to set. + */ + + public SetCommand(Index index, StatusTypes newStatus) { + requireNonNull(index); + requireNonNull(newStatus); + this.index = index; + this.newStatus = newStatus; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + personToEdit.getStatus().setStatusType(newStatus); + model.setLastViewedPersonIndex(index); + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_SET_PERSON_SUCCESS, Messages.format(personToEdit)), true); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", index) + .add("newStatus", newStatus) + .toString(); + } + + public boolean equals(SetCommand this, SetCommand other) { + return this.index.equals(other.index) && this.newStatus.equals(other.newStatus); + } +} + + diff --git a/src/main/java/seedu/address/logic/commands/ViewCommand.java b/src/main/java/seedu/address/logic/commands/ViewCommand.java new file mode 100644 index 00000000000..48817057ede --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewCommand.java @@ -0,0 +1,72 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Views a person identified using it's last displayed index from the address book. + */ +public class ViewCommand extends Command { + public static final String COMMAND_WORD = "view"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Views the details of the person identified " + + "by the index number used in the last person listing. \n" + + "Parameters: INDEX (must be a positive integer) \n" + + "Example: " + COMMAND_WORD + " 1 "; + + public static final String MESSAGE_ARGUMENTS = "Index: %1$d"; + public static final String MESSAGE_SUCCESSFUL_VIEW = "Full-view shown for Person: %1$s"; + private final Index index; + + /** + * Constructor for ViewCommand + * @param index of the person in the filtered person list to view + */ + public ViewCommand(Index index) { + requireAllNonNull(index); + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + model.setLastViewedPersonIndex(index); + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + model.loadSummaryStatistics(); + Person personToView = lastShownList.get(index.getZeroBased()); + return new CommandResult(generateSuccessMessage(personToView), true); + } + + /** + * Generates a command execution success message for viewing {@code personToView}. + */ + private String generateSuccessMessage(Person personToView) { + return String.format(MESSAGE_SUCCESSFUL_VIEW, Messages.format(personToView)); + } + + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + // instanceof handles nulls + if (!(other instanceof ViewCommand)) { + return false; + } + + ViewCommand v = (ViewCommand) other; + return index.equals(v.index); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..4bdbd2a1d13 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -17,6 +17,7 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; import seedu.address.model.tag.Tag; /** @@ -43,9 +44,10 @@ public AddCommand parse(String args) throws ParseException { Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Remark remark = new Remark(""); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, remark, tagList); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddGCommandParser.java b/src/main/java/seedu/address/logic/parser/AddGCommandParser.java new file mode 100644 index 00000000000..b2f7b5f73aa --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddGCommandParser.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_USERNAME; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddGCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Github; + +/** + * Parses input arguments and creates a new AddGCommand object + */ +public class AddGCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddGCommand + * and returns an AddGCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddGCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultiMap = ArgumentTokenizer.tokenize(args, PREFIX_USERNAME); + + if (!arePrefixesPresent(argMultiMap, PREFIX_USERNAME)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddGCommand.MESSAGE_USAGE)); + } + + Index index; + + try { + index = ParserUtil.parseIndex(argMultiMap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddGCommand.MESSAGE_USAGE)); + } + + index = ParserUtil.parseIndex(argMultiMap.getPreamble()); + String username = argMultiMap.getValue(PREFIX_USERNAME).orElse(""); + + return new AddGCommand(index, new Github(username)); + } + + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddLCommandParser.java b/src/main/java/seedu/address/logic/parser/AddLCommandParser.java new file mode 100644 index 00000000000..5fda485dde3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddLCommandParser.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_USERNAME; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddLCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.LinkedIn; + +/** + * Parses input arguments and creates a new AddLCommand object + */ +public class AddLCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddLCommand + * and returns an AddLCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddLCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultiMap = ArgumentTokenizer.tokenize(args, PREFIX_USERNAME); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultiMap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddLCommand.MESSAGE_USAGE)); + } + + if (!arePrefixesPresent(argMultiMap, PREFIX_USERNAME)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddLCommand.MESSAGE_USAGE)); + } + + index = ParserUtil.parseIndex(argMultiMap.getPreamble()); + String username = argMultiMap.getValue(PREFIX_USERNAME).orElse(""); + + return new AddLCommand(index, new LinkedIn(username)); + } + + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..3dc9987b3ed 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,27 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddGCommand; +import seedu.address.logic.commands.AddLCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CreateTagCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EventCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportCommand; +import seedu.address.logic.commands.FilterCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.GithubCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.LinkedInCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListTCommand; +import seedu.address.logic.commands.RemarkCommand; +import seedu.address.logic.commands.ScheduleCommand; +import seedu.address.logic.commands.SetCommand; +import seedu.address.logic.commands.ViewCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -68,8 +81,14 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: - return new ListCommand(); + return new ListCommandParser().parse(arguments); + + case ListTCommand.COMMAND_WORD: + return new ListTCommand(); case ExitCommand.COMMAND_WORD: return new ExitCommand(); @@ -77,6 +96,39 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case RemarkCommand.COMMAND_WORD: + return new RemarkCommandParser().parse(arguments); + + case ViewCommand.COMMAND_WORD: + return new ViewCommandParser().parse(arguments); + + case AddLCommand.COMMAND_WORD: + return new AddLCommandParser().parse(arguments); + + case AddGCommand.COMMAND_WORD: + return new AddGCommandParser().parse(arguments); + + case LinkedInCommand.COMMAND_WORD: + return new LinkedInCommandParser().parse(arguments); + + case GithubCommand.COMMAND_WORD: + return new GithubCommandParser().parse(arguments); + + case SetCommand.COMMAND_WORD: + return new SetCommandParser().parse(arguments); + + case EventCommand.COMMAND_WORD: + return new EventCommandParser().parse(arguments); + + case ScheduleCommand.COMMAND_WORD: + return new ScheduleCommand(); + + case ExportCommand.COMMAND_WORD: + return new ExportCommandParser().parse(arguments); + + case CreateTagCommand.COMMAND_WORD: + return new CreateTagCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..85a911c1b82 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -10,6 +10,18 @@ public class CliSyntax { public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_REMARK = new Prefix("r/"); + + public static final Prefix PREFIX_VIEW = new Prefix("v/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_SORT = new Prefix("so/"); + public static final Prefix PREFIX_STATUS = new Prefix("st/"); + public static final Prefix PREFIX_USERNAME = new Prefix("u/"); + public static final Prefix PREFIX_DESCRIPTION = new Prefix("d/"); + public static final Prefix PREFIX_STARTTIME = new Prefix("bt/"); + public static final Prefix PREFIX_ENDTIME = new Prefix("et/"); + public static final Prefix PREFIX_SCORE = new Prefix("sc/"); + public static final Prefix PREFIX_METRIC = new Prefix("met/"); + public static final Prefix PREFIX_VALUE = new Prefix("val/"); } diff --git a/src/main/java/seedu/address/logic/parser/CreateTagCommandParser.java b/src/main/java/seedu/address/logic/parser/CreateTagCommandParser.java new file mode 100644 index 00000000000..49a4c11d7f0 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CreateTagCommandParser.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.CreateTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses user input and creates a new CreateTagCommand. + */ +public class CreateTagCommandParser implements Parser { + + /** + * Parses the given user input and creates a CreateTagCommand. + * + * @param args The user input arguments. + * @return A CreateTagCommand based on the parsed input. + * @throws ParseException If the input does not conform to the expected format. + */ + @Override + public CreateTagCommand parse(String args) throws ParseException { + requireNonNull(args); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_TAG) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateTagCommand.MESSAGE_USAGE)); + } + + String[] tagParams = ParserUtil.parseTagCategories(argMultimap.getAllValues(PREFIX_TAG)); + + return new CreateTagCommand(tagParams); + + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..6e6b3f4c843 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,10 +1,20 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; +import seedu.address.model.person.StatusContainsKeywordsPredicate; +import seedu.address.model.person.TagContainsKeywordsPredicate; /** * Parses input arguments and creates a new DeleteCommand object @@ -17,13 +27,75 @@ public class DeleteCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { + // Try index-based deletion first try { Index index = ParserUtil.parseIndex(args); return new DeleteCommand(index); } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + // Ignore parsing exception and continue + } + + // If not delete by index + List statusKeywords = new ArrayList<>(); + List tagKeywords = new ArrayList<>(); + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_STATUS, PREFIX_TAG); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_STATUS, PREFIX_TAG); + + if (!(arePrefixesPresent(argMultimap, PREFIX_STATUS) || arePrefixesPresent(argMultimap, PREFIX_TAG)) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); } + + if (arePrefixesPresent(argMultimap, PREFIX_STATUS)) { + setKeywords(statusKeywords, tagKeywords, argMultimap, PREFIX_STATUS); + } + if (arePrefixesPresent(argMultimap, PREFIX_TAG)) { + setKeywords(statusKeywords, tagKeywords, argMultimap, PREFIX_TAG); + } + + StatusContainsKeywordsPredicate statusPredicate = new StatusContainsKeywordsPredicate(statusKeywords); + TagContainsKeywordsPredicate tagPredicate = new TagContainsKeywordsPredicate(tagKeywords); + List> predicateList = getPredicatesList(statusKeywords, tagKeywords, statusPredicate, + tagPredicate); + return new DeleteCommand(predicateList); } + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + private List> getPredicatesList(List statusKeywords, + List tagKeywords, + StatusContainsKeywordsPredicate statusPredicate, + TagContainsKeywordsPredicate tagPredicate) { + List> predicatesList = new ArrayList<>() {{ + if (!statusKeywords.isEmpty()) { + add(statusPredicate); + } + if (!tagKeywords.isEmpty()) { + add(tagPredicate); + } + }}; + + return predicatesList; + } + + private void setKeywords(List statusKeywords, + List tagKeywords, + ArgumentMultimap argMultimap, + Prefix prefix) throws ParseException { + if (prefix.equals(PREFIX_STATUS)) { + List keywords = ParserUtil.parseSinglePrefixStatus(argMultimap.getAllValues(PREFIX_STATUS)); + for (String keyword : keywords) { + statusKeywords.add(keyword); + } + } + if (prefix.equals(PREFIX_TAG)) { + List keywords = ParserUtil.parseSinglePrefixTags(argMultimap.getAllValues(PREFIX_TAG)); + for (String keyword : keywords) { + tagKeywords.add(keyword); + } + } + } } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..793fee43385 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -6,6 +6,7 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCORE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; @@ -13,10 +14,13 @@ import java.util.Optional; import java.util.Set; +import javafx.util.Pair; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Score; +import seedu.address.model.person.ScoreList; import seedu.address.model.tag.Tag; /** @@ -32,7 +36,8 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_SCORE, PREFIX_TAG); Index index; @@ -42,7 +47,7 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_SCORE); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -58,12 +63,22 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_SCORE).isPresent()) { + ScoreList scoreList = createScoreListForParse(argMultimap.getValue(PREFIX_SCORE).get()); + editPersonDescriptor.setScoreList(scoreList); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } + // Return empty score list if not edited, just like tags, scorelist is optional + if (!editPersonDescriptor.getScoreList().isPresent()) { + editPersonDescriptor.setScoreList(new ScoreList()); + } + return new EditCommand(index, editPersonDescriptor); } @@ -74,7 +89,6 @@ public EditCommand parse(String args) throws ParseException { */ private Optional> parseTagsForEdit(Collection tags) throws ParseException { assert tags != null; - if (tags.isEmpty()) { return Optional.empty(); } @@ -82,4 +96,10 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + private ScoreList createScoreListForParse(String scoreWithTag) throws ParseException { + ScoreList scoreList = new ScoreList(); + Pair tagScorePair = ParserUtil.parseTagScore(scoreWithTag); + scoreList.updateScoreList(tagScorePair.getKey(), tagScorePair.getValue()); + return scoreList; + } } diff --git a/src/main/java/seedu/address/logic/parser/EventCommandParser.java b/src/main/java/seedu/address/logic/parser/EventCommandParser.java new file mode 100644 index 00000000000..e685a7623b8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EventCommandParser.java @@ -0,0 +1,73 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INCORRECT_DATE_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_INVALID_DATE; +import static seedu.address.logic.Messages.MESSAGE_INVALID_START; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ENDTIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STARTTIME; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EventCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.Event; + +/** + * Parses input arguments and returns an EventCommand object. + */ +public class EventCommandParser implements Parser { + + /** + * Parses the given {@code String} in the context of an EventCommand + * and returns an EventCommand object for execution. + */ + public EventCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DESCRIPTION, + PREFIX_STARTTIME, PREFIX_ENDTIME); + + if (!arePrefixesPresent(argMultimap, PREFIX_DESCRIPTION, PREFIX_STARTTIME, PREFIX_ENDTIME)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EventCommand.MESSAGE_USAGE)); + } + LocalDateTime startTime; + LocalDateTime endTime; + + String description = argMultimap.getValue(PREFIX_DESCRIPTION).get(); + try { + startTime = LocalDateTime.parse(argMultimap.getValue(PREFIX_STARTTIME).get(), + DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm").withResolverStyle(ResolverStyle.STRICT)); + endTime = LocalDateTime.parse(argMultimap.getValue(PREFIX_ENDTIME).get(), + DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm").withResolverStyle(ResolverStyle.STRICT)); + } catch (DateTimeException e) { + String s = e.getMessage(); + if (s.contains("Invalid")) { + throw new ParseException(MESSAGE_INVALID_DATE); + } else { + throw new ParseException(MESSAGE_INCORRECT_DATE_FORMAT); + } + } + + if (!(endTime.isAfter(startTime))) { + throw new ParseException(MESSAGE_INVALID_START); + } + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EventCommand.MESSAGE_USAGE), pe); + } + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + Event event = new Event(index, description, startTime, endTime); + return new EventCommand(event); + } + + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ExportCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java new file mode 100644 index 00000000000..57e2452d745 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java @@ -0,0 +1,24 @@ +package seedu.address.logic.parser; + + +import seedu.address.logic.commands.ExportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditCommand object + */ + +public class ExportCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SetCommand + * and returns a SetCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public ExportCommand parse(String args) throws ParseException { + return new ExportCommand(); + } + +} + diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..d6bb1f19c12 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java @@ -0,0 +1,78 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_METRIC; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_VALUE; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Score; +import seedu.address.model.statistics.StatisticMetric; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; + + +/** + * Parses input arguments and creates a new FilterCommand object + */ +public class FilterCommandParser implements Parser { + private static Logger logger = LogsCenter.getLogger(FilterCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the FilterCommand + * @param args String of arguments + * @return FilterCommand object + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + requireNonNull(args); + UniqueTagList uniqueTagList = new UniqueTagList(); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG, PREFIX_METRIC, PREFIX_VALUE); + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_TAG, PREFIX_METRIC, PREFIX_VALUE); + + if (!(argMultimap.getValue(PREFIX_TAG).isPresent() && argMultimap.getValue(PREFIX_METRIC).isPresent())) { + throw new ParseException(String.format("Incomplete parameter inputs. t/TAG and met/SCORE are compulsory" + + "fields. \n" + FilterCommand.MESSAGE_USAGE)); + } + String tagName = argMultimap.getValue(PREFIX_TAG).orElse(""); + if (!(Tag.isValidTagName(tagName) + && uniqueTagList.contains(new Tag(tagName, "assessment")))) { + throw new ParseException(String.format(FilterCommand.MESSAGE_INVALID_TAG)); + } + + + if (!StatisticMetric.isValidMetric(argMultimap.getValue(PREFIX_METRIC).orElse(""))) { + throw new ParseException(String.format(FilterCommand.MESSAGE_INVALID_METRIC)); + } + Tag tag = new Tag(argMultimap.getValue(PREFIX_TAG).get(), "assessment"); + StatisticMetric metric = StatisticMetric.valueOf(argMultimap.getValue(PREFIX_METRIC).get().toUpperCase()); + + if (!StatisticMetric.isScoreRelevant(argMultimap.getValue(PREFIX_METRIC).orElse(""))) { + return new FilterCommand(tag, metric, 0); + } + + if (!argMultimap.getValue(PREFIX_VALUE).isPresent()) { + throw new ParseException(String.format("val/VALUE is missing, it is compulsory. \n" + + FilterCommand.MESSAGE_USAGE)); + } + + try { + int value = Integer.parseInt(argMultimap.getValue(PREFIX_VALUE).orElse("0")); + if (!Score.isValidScoreValue(value)) { + throw new ParseException(String.format(FilterCommand.MESSAGE_INVALID_VALUE)); + } + } catch (NumberFormatException e) { + throw new ParseException(String.format(FilterCommand.MESSAGE_INVALID_VALUE)); + } + + int value = Integer.parseInt(argMultimap.getValue(PREFIX_VALUE).get()); + + return new FilterCommand(tag, metric, value); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..9e1747001ec 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,12 +1,22 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.person.StatusContainsKeywordsPredicate; +import seedu.address.model.person.TagContainsKeywordsPredicate; + /** * Parses input arguments and creates a new FindCommand object @@ -19,15 +29,84 @@ public class FindCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + List nameKeywords = new ArrayList<>(); + List statusKeywords = new ArrayList<>(); + List tagKeywords = new ArrayList<>(); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_STATUS, PREFIX_TAG); + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_STATUS, PREFIX_TAG); + if (!(arePrefixesPresent(argMultimap, PREFIX_NAME) || arePrefixesPresent(argMultimap, PREFIX_STATUS) + || arePrefixesPresent(argMultimap, PREFIX_TAG)) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + if (arePrefixesPresent(argMultimap, PREFIX_NAME)) { + setKeywords(nameKeywords, statusKeywords, tagKeywords, argMultimap, PREFIX_NAME); } + if (arePrefixesPresent(argMultimap, PREFIX_STATUS)) { + setKeywords(nameKeywords, statusKeywords, tagKeywords, argMultimap, PREFIX_STATUS); + } + if (arePrefixesPresent(argMultimap, PREFIX_TAG)) { + setKeywords(nameKeywords, statusKeywords, tagKeywords, argMultimap, PREFIX_TAG); + } + + NameContainsKeywordsPredicate namePredicate = new NameContainsKeywordsPredicate(nameKeywords); + StatusContainsKeywordsPredicate statusPredicate = new StatusContainsKeywordsPredicate(statusKeywords); + TagContainsKeywordsPredicate tagPredicate = new TagContainsKeywordsPredicate(tagKeywords); + List> predicateList = getPredicatesList(nameKeywords, statusKeywords, tagKeywords, + namePredicate, statusPredicate, tagPredicate); + return new FindCommand(predicateList); + } + + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } - String[] nameKeywords = trimmedArgs.split("\\s+"); + private List> getPredicatesList(List nameKeywords, + List statusKeywords, + List tagKeywords, + NameContainsKeywordsPredicate namePredicate, + StatusContainsKeywordsPredicate statusPredicate, + TagContainsKeywordsPredicate tagPredicate) { + List> predicatesList = new ArrayList<>() {{ + if (!nameKeywords.isEmpty()) { + add(namePredicate); + } + if (!statusKeywords.isEmpty()) { + add(statusPredicate); + } + if (!tagKeywords.isEmpty()) { + add(tagPredicate); + } + }}; - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + return predicatesList; } + private void setKeywords(List nameKeywords, + List statusKeywords, + List tagKeywords, + ArgumentMultimap argMultimap, + Prefix prefix) throws ParseException { + if (prefix.equals(PREFIX_NAME)) { + List keywords = ParserUtil.parseSinglePrefixName(argMultimap.getAllValues(PREFIX_NAME)); + for (String keyword : keywords) { + nameKeywords.add(keyword); + } + } + if (prefix.equals(PREFIX_STATUS)) { + List keywords = ParserUtil.parseSinglePrefixStatus(argMultimap.getAllValues(PREFIX_STATUS)); + for (String keyword : keywords) { + statusKeywords.add(keyword); + } + } + if (prefix.equals(PREFIX_TAG)) { + List keywords = ParserUtil.parseSinglePrefixTags(argMultimap.getAllValues(PREFIX_TAG)); + for (String keyword : keywords) { + tagKeywords.add(keyword); + } + } + } } diff --git a/src/main/java/seedu/address/logic/parser/GithubCommandParser.java b/src/main/java/seedu/address/logic/parser/GithubCommandParser.java new file mode 100644 index 00000000000..cbf29935fc9 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GithubCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.GithubCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new GithubCommand object. + */ +public class GithubCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the GithubCommand + * and returns a GithubCommand object. + * @throws ParseException if the user input does not conform the expected format. + */ + public GithubCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new GithubCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, GithubCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/LinkedInCommandParser.java b/src/main/java/seedu/address/logic/parser/LinkedInCommandParser.java new file mode 100644 index 00000000000..3ce11e1342c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LinkedInCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.LinkedInCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new LinkedInCommand object. + */ +public class LinkedInCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the LinkedInCommand + * and returns a LinkedInCommand object. + * @throws ParseException if the user input does not conform the expected format. + */ + public LinkedInCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new LinkedInCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, LinkedInCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ListCommandParser.java b/src/main/java/seedu/address/logic/parser/ListCommandParser.java new file mode 100644 index 00000000000..1819b22cadd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ListCommandParser.java @@ -0,0 +1,52 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Comparator; + +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; + +/** + * Parses input arguments and creates a new ListCommand object with sorting options. + */ +public class ListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ListCommand + * and returns an ListCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public ListCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, CliSyntax.PREFIX_SORT); + + if (!argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE)); + } + + String sortingAttribute = argMultimap.getValue(CliSyntax.PREFIX_SORT).orElse(null); + + // Create the sorting comparator based on the sorting attribute + Comparator sortingComparator = createSortingComparator(sortingAttribute); + + return new ListCommand(sortingComparator); + } + + /** + * Creates a sorting comparator based on the specified sorting attribute. + * + * @param sortingAttribute The attribute by which the list should be sorted (e.g., "name" or "email"). + * @return A comparator for sorting based on the specified attribute. + */ + private Comparator createSortingComparator(String sortingAttribute) { + if ("name".equalsIgnoreCase(sortingAttribute)) { + return Comparator.comparing(Person::getName); + } else if ("email".equalsIgnoreCase(sortingAttribute)) { + return Comparator.comparing(Person::getEmail); + } else { + return ListCommand.DEFAULT_COMPARATOR; // A comparator that does nothing + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..2d3e7509787 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,19 +1,31 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; +import javafx.util.Pair; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.CreateTagCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Score; +import seedu.address.model.person.ScoreList; +import seedu.address.model.person.Status; +import seedu.address.model.person.StatusTypes; import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; + /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -22,9 +34,11 @@ public class ParserUtil { public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. + * * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { @@ -50,6 +64,32 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** + * Parses a {@code String statusType} into a {@code StatusType}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code statusType} is invalid. + */ + public static StatusTypes parseStatusType(String statusType) throws ParseException { + requireNonNull(statusType); + String trimmedStatus = statusType.trim().toLowerCase(); + if (!StatusTypes.isValidStatusType(trimmedStatus)) { + throw new ParseException("PLACEHOLDER: PARSE_EXCEPTION STATUS TYPE"); + } + switch (trimmedStatus) { + case "interviewed": + return StatusTypes.INTERVIEWED; + case "offered": + return StatusTypes.OFFERED; + case "rejected": + return StatusTypes.REJECTED; + case "preliminary": + return StatusTypes.PRELIMINARY; + default: + throw new ParseException("e"); + } + } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -101,13 +141,18 @@ public static Email parseEmail(String email) throws ParseException { * * @throws ParseException if the given {@code tag} is invalid. */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); + public static Tag parseTag(String tagName, String tagCategory) throws ParseException { + requireNonNull(tagName); + requireNonNull(tagCategory); + UniqueTagList uniqueTagList = new UniqueTagList(); + String trimmedTag = tagName.trim(); if (!Tag.isValidTagName(trimmedTag)) { throw new ParseException(Tag.MESSAGE_CONSTRAINTS); } - return new Tag(trimmedTag); + if (uniqueTagList.contains(new Tag(trimmedTag, "assessment"))) { + return uniqueTagList.getTag(trimmedTag, "assessment"); + } + return uniqueTagList.getTag(trimmedTag, tagCategory); } /** @@ -116,9 +161,188 @@ public static Tag parseTag(String tag) throws ParseException { public static Set parseTags(Collection tags) throws ParseException { requireNonNull(tags); final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); + if (tags.size() == 0) { + return tagSet; + } + String listTags = tags.toString(); + String cleanedList = listTags.replaceAll("[\\[\\]]", ""); + String[] tagNameCategoryPairs = cleanedList.split(","); + + for (String tagNameCategory : tagNameCategoryPairs) { + tagNameCategory = tagNameCategory.trim(); + if (tagNameCategory.split("\\s+").length > 1) { + String[] nameCategory = tagNameCategory.split("\\s+"); + // category specified + String tagName = nameCategory[1]; + String tagCategory = nameCategory[0]; + tagSet.add(parseTag(tagName, tagCategory)); + } else { + // category not specified + tagSet.add(parseTag(tagNameCategory, "")); + } } return tagSet; } + + /** + * Parses a collection of tag strings into an array of tag categories. + * + * @param tags A collection of tag strings to be parsed. + * @return An array of tag categories extracted from the provided collection of tag strings. + */ + public static String[] parseTagCategories(Collection tags) throws ParseException { + requireNonNull(tags); + String listTags = tags.toString(); + String cleanedList = listTags.replaceAll("[\\[\\]]", ""); + String[] tagNameCategoryPairs = cleanedList.split(","); + for (String tagNameCategory : tagNameCategoryPairs) { + tagNameCategory = tagNameCategory.trim(); + if (tagNameCategory.split("\\s+").length > 1) { + if (!Tag.isValidTagName(tagNameCategory.split("\\s+")[1])) { + throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + } + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateTagCommand.MESSAGE_USAGE)); + } + } + return tagNameCategoryPairs; + } + + /** + * Parses a list of keywords into an array of strings. + * + * @param keywordsList A list of keywords, where each element may contain multiple words. + * @return An array of strings where each element represents an individual keyword. + * @throws ParseException if any of the search parameters contain non-alphanumeric characters e.g. commas + * + * The method first converts the list of keywords into a string representation, + * e.g., [Alex, Yeoh] (including square brackets). It then removes the square brackets + * from the string representation, resulting in a cleaned string, e.g., Alex, Yeoh (no square brackets). + * Finally, the cleaned string is split into an array of strings, where each word separated + * by a whitespace or comma is considered a single element. + * + * Example: + * If keywordsList is ["John Doe"], the returned array will be ["John", "Doe"]. + */ + private static String[] parseSinglePrefixParams(Collection keywordsList) { + String list = keywordsList.toString(); + String cleanedList = list.replaceAll("[\\[\\]]", ""); + String[] singlePrefixParams = cleanedList.split("\\s+"); + return singlePrefixParams; + } + + /** + * Parses {@code Collection status parameters} into a {@code List of status}. + */ + public static List parseSinglePrefixStatus(Collection statuses) + throws ParseException { + requireNonNull(statuses); + String[] statusArr = parseSinglePrefixParams(statuses); + final List statusList = new ArrayList<>(); + for (String status : statusArr) { + status = status.trim(); + if (!StatusTypes.isValidStatusType(status.toLowerCase())) { + throw new ParseException(Status.MESSAGE_CONSTRAINTS); + } + checkArgument(StatusTypes.isValidStatusType(status.toLowerCase()), Status.MESSAGE_CONSTRAINTS); + statusList.add(status); + } + return statusList; + } + + /** + * Parses {@code Collection name parameters} into a {@code List of names}. + */ + public static List parseSinglePrefixName(Collection names) + throws ParseException { + requireNonNull(names); + String[] nameArr = parseSinglePrefixParams(names); + final List nameList = new ArrayList<>(); + for (String name : nameArr) { + name = name.trim(); + if (!Name.isValidName(name)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + checkArgument(Name.isValidName(name), Name.MESSAGE_CONSTRAINTS); + nameList.add(name); + } + return nameList; + } + + /** + * Parses {@code Collection tag parameters} into a {@code List of tags}. + */ + public static List parseSinglePrefixTags(Collection tags) + throws ParseException { + requireNonNull(tags); + UniqueTagList uniqueTagList = new UniqueTagList(); + String[] tagArr = parseSinglePrefixParams(tags); + final List tagList = new ArrayList<>(); + List nonExistingTags = new ArrayList<>(); + + for (String tag : tagArr) { + tag = tag.trim(); + + // Check if the tag is valid + if (!Tag.isValidTagName(tag)) { + throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + } + + // Check if the tag already exists in the uniqueTagList + if (!uniqueTagList.containsTagName(tag)) { + nonExistingTags.add(tag); + } else { + tagList.add(tag); + } + } + + if (!nonExistingTags.isEmpty()) { + // Throw an exception with a message specifying all non-existing tags + throw new ParseException(Tag.MESSAGE_TAG_DOES_NOT_EXIST + String.join(", ", nonExistingTags)); + } + + return tagList; + } + + + /** + * Parses a {@code String score} into a {@code Score}. + * + * @param score String to be parsed + * @return Score object + * @throws ParseException if the given {@code score} is invalid. + */ + public static Score parseScore(String score) throws ParseException { + requireNonNull(score); + String trimmedScore = score.trim(); + if (!StringUtil.isNonNegativeInteger(trimmedScore)) { + throw new ParseException(Score.MESSAGE_CONSTRAINTS); + } + return new Score(Integer.parseInt(trimmedScore)); + } + + /** + * Parses a Tag Score string input and turns it into a Pair with head as Tag and tail as Score. + * @param tagScore String to be parsed + * @return Pair with head as Tag and tail as Score + * @throws ParseException if the given {@code tagScorePair} is invalid. + */ + public static Pair parseTagScore(String tagScore) throws ParseException { + requireNonNull(tagScore); + String trimmedTagScorePair = tagScore.trim(); + String[] tagScorePairArr = trimmedTagScorePair.split(" "); + if (tagScorePairArr.length != 2) { + throw new ParseException("Invalid score, score must be non-negative integer."); + } + + + Tag tag = parseTag(tagScorePairArr[0], ""); + if (!ScoreList.isValidScoreTag(tag)) { + throw new ParseException("Invalid score tag, tag must a tag of the category " + + "assessment and must exist on the applicant" + " (made using " + + "the create command."); + } + Score score = parseScore(tagScorePairArr[1]); + return new Pair<>(tag, score); + } } diff --git a/src/main/java/seedu/address/logic/parser/RemarkCommandParser.java b/src/main/java/seedu/address/logic/parser/RemarkCommandParser.java new file mode 100644 index 00000000000..92975bf02ac --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemarkCommandParser.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REMARK; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.RemarkCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Remark; + +/** + * Parses input arguments and creates a new {@code RemarkCommand} object + */ +public class RemarkCommandParser implements Parser { + private static final Logger logger = LogsCenter.getLogger(RemarkCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the {@code RemarkCommand} + * and returns a {@code RemarkCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemarkCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_REMARK); + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemarkCommand.MESSAGE_USAGE), pe); + } + + String remarkText = argMultimap.getValue(PREFIX_REMARK).orElse(""); + Remark remark = new Remark(remarkText); + + if (RemarkSyntaxHandler.isKeepRemark(args)) { + logger.fine("Keep remark detected, replacing relevant syntax with old remark"); + return new RemarkCommand(index, remark, true); + } + + return new RemarkCommand(index, remark); + } + + + +} diff --git a/src/main/java/seedu/address/logic/parser/RemarkSyntaxHandler.java b/src/main/java/seedu/address/logic/parser/RemarkSyntaxHandler.java new file mode 100644 index 00000000000..824697e4497 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemarkSyntaxHandler.java @@ -0,0 +1,44 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.model.person.Remark; + + +/** + * Parses special input arguments and creates a new {@code RemarkCommand} object + */ +public class RemarkSyntaxHandler { + public static final String REGEX_KEEP_REMARK = "\\*\\*REMARK\\*\\*"; + + /** + * Parses the given {@code String} of arguments in the context of the {@code RemarkCommand} + * @param args the string to be parsed + * @return true if the string contains the special syntax for keeping the remark + */ + public static boolean isKeepRemark(String args) { + requireNonNull(args); + Pattern pattern = Pattern.compile(REGEX_KEEP_REMARK); + Matcher matcher = pattern.matcher(args); + if (matcher.find()) { + return true; + } + return false; + } + + /** + * Generates a new remark based on the current remark and the special syntax **remark** + * @param args the string to be parsed + * @param currentRemark the current remark of the person + * @return the new remark where **remark** is replaced by the current remark + */ + public static Remark generateKeepRemarkCommand(String args, Remark currentRemark) { + requireNonNull(args); + String resultString = args.replaceAll(REGEX_KEEP_REMARK, currentRemark.value); + return new Remark(resultString); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/SetCommandParser.java b/src/main/java/seedu/address/logic/parser/SetCommandParser.java new file mode 100644 index 00000000000..cf7e9eee497 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SetCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SetCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.StatusTypes; + +/** + * Parses input arguments and creates a new EditCommand object + */ + +public class SetCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SetCommand + * and returns a SetCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public SetCommand parse(String args) throws ParseException { + try { + String[] splitArgs = args.trim().split("\\s+"); + + if (splitArgs.length < 2) { + throw new ParseException(SetCommand.MESSAGE_USAGE); + } + + Index index = ParserUtil.parseIndex(splitArgs[0]); + StatusTypes newStatusType = ParserUtil.parseStatusType((splitArgs[1])); + + return new SetCommand(index, newStatusType); + + } catch (ParseException e) { + throw new ParseException(SetCommand.MESSAGE_USAGE, e); + } + } +} + diff --git a/src/main/java/seedu/address/logic/parser/ViewCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java new file mode 100644 index 00000000000..33f13b48a7b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java @@ -0,0 +1,36 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_VIEW; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ViewCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code ViewCommand} object + * + */ +public class ViewCommandParser implements Parser { + + + /** + * Parses the given {@code String} of arguments in the context of the {@code ViewCommand} + * and returns a {@code ViewCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ViewCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_VIEW); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE), pe); + } + + return new ViewCommand(index); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..e26c182a056 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,12 +2,15 @@ import static java.util.Objects.requireNonNull; +import java.util.Comparator; import java.util.List; import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; /** * Wraps all data at the address-book level @@ -16,6 +19,7 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final UniqueTagList tags; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -26,6 +30,7 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + tags = new UniqueTagList(); } public AddressBook() {} @@ -94,6 +99,44 @@ public void removePerson(Person key) { persons.remove(key); } + /** + * Sorts the address book using the provided comparator. + * + * @param comparator The comparator used to determine the sorting order. + */ + public void sortAddressBook(Comparator comparator) { + ObservableList personObservableList = getPersonList(); + List sortedList = personObservableList.sorted(comparator); + setPersons(sortedList); + } + + //// tag-level operations + + /** + * Adds a tag to the address book. + * The tag must not already exist in the address book. + */ + public void addTag(Tag tag) { + this.tags.add(tag); + } + + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + public boolean hasTag(Tag tag) { + requireNonNull(tag); + return tags.contains(tag); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeTag(Tag key) { + tags.remove(key); + } + + //// util methods @Override @@ -108,6 +151,11 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getTagList() { + return tags.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -127,4 +175,5 @@ public boolean equals(Object other) { public int hashCode() { return persons.hashCode(); } + } diff --git a/src/main/java/seedu/address/model/EventBook.java b/src/main/java/seedu/address/model/EventBook.java new file mode 100644 index 00000000000..4cc7f05ccb9 --- /dev/null +++ b/src/main/java/seedu/address/model/EventBook.java @@ -0,0 +1,117 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; + +import java.util.Comparator; +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.event.Event; +import seedu.address.model.event.UniqueEventList; + +/** + * Wraps all data at the event-book level + */ +public class EventBook implements ReadOnlyEventBook { + private final UniqueEventList events; + + /* + * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + { + events = new UniqueEventList(); + } + + public EventBook() {} + + /** + * Creates an EventBook using the Events in the {@code toBeCopied} + */ + public EventBook(ReadOnlyEventBook toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the event list with {@code events}. + */ + public void setEvents(List events) { + this.events.setEvents(events); + } + + /** + * Resets the existing data of this {@code EventBook} with {@code newData}. + */ + public void resetData(ReadOnlyEventBook newData) { + requireNonNull(newData); + setEvents(newData.getEventList()); + } + + //// event-level operations + public void addEvent(Event e) { + events.add(e); + } + + public void setEvent(Event target, Event editedEvent) { + events.setEvent(target, editedEvent); + } + + /** + * Removes {@code key} from this {@code EventBook}. + * {@code key} must exist in the event book. + */ + public void removeEvent(Event key) { + events.remove(key); + } + + public boolean hasEvent(Event event) { + return events.contains(event); + } + + /** + * Sorts the event book using the provided comparator. + * + * @param comparator The comparator used to determine the sorting order. + */ + public void sortEventBook(Comparator comparator) { + ObservableList eventObservableList = getEventList(); + List sortedList = eventObservableList.sorted(comparator); + setEvents(sortedList); + } + + //// util methods + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("events", events) + .toString(); + } + + @Override + public ObservableList getEventList() { + return events.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EventBook)) { + return false; + } + + EventBook otherEventBook = (EventBook) other; + return events.equals(otherEventBook.events); + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..f0a7d875349 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,11 +1,18 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; +import seedu.address.model.statistics.ReadOnlySummaryStatistic; +import seedu.address.model.tag.Tag; /** * The API of the Model component. @@ -14,6 +21,10 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate PREDICATE_SHOW_ALL_TAGS = unused -> true; + + Predicate PREDICATE_SHOW_ALL_EVENTS = unused -> true; + /** * Replaces user prefs data with the data in {@code userPrefs}. */ @@ -39,19 +50,29 @@ public interface Model { */ Path getAddressBookFilePath(); + Path getEventBookFilePath(); + /** * Sets the user prefs' address book file path. */ void setAddressBookFilePath(Path addressBookFilePath); + void setEventBookFilePath(Path eventBookFilePath); + /** * Replaces address book data with the data in {@code addressBook}. */ void setAddressBook(ReadOnlyAddressBook addressBook); + void setEventBook(ReadOnlyEventBook eventBook); + /** Returns the AddressBook */ ReadOnlyAddressBook getAddressBook(); + ReadOnlyEventBook getEventBook(); + + ReadOnlySummaryStatistic getSummaryStatistic(); + /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ @@ -63,11 +84,14 @@ public interface Model { */ void deletePerson(Person target); + void deleteEvent(Event event); + /** * Adds the given person. * {@code person} must not already exist in the address book. */ void addPerson(Person person); + void addEvent(Event event); /** * Replaces the given person {@code target} with {@code editedPerson}. @@ -75,13 +99,59 @@ public interface Model { * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. */ void setPerson(Person target, Person editedPerson); + void setEvent(Event target, Event editedEvent); /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + ObservableList getFilteredTagList(); + ObservableList getFilteredEventList(); /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Updates the filter of the filtered person list to filter by all the given {@code predicates}. + * @throws NullPointerException if any of the {@code predicatesList} is null. + */ + void updateFilteredPersonList(List> predicatesList); + + void updateFilteredTagList(Predicate predicate); + + void updateFilteredEventList(Predicate predicate); + + void updateFilteredEventList(List> predicateList); + + /** + * Sorts the list of persons using the provided comparator. + * + * @param comparator The comparator used to determine the sorting order. + */ + void sortPersonList(Comparator comparator); + + void sortEventList(Comparator comparator); + + + /** + * Returns the Index of the last view command called. + */ + Index getLastViewedPersonIndex(); + + /** + * Sets the Index of the last view command called. + */ + void setLastViewedPersonIndex(Index index); + + + void addTag(Tag tag) throws IllegalValueException; + + boolean hasTag(Tag tag); + /** + * Loads the summary statistics based on the current Address Book + */ + void loadSummaryStatistics(); + + } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..fe38f7025e8 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,8 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,7 +13,12 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; +import seedu.address.model.statistics.ReadOnlySummaryStatistic; +import seedu.address.model.statistics.SummaryStatistic; +import seedu.address.model.tag.Tag; /** * Represents the in-memory model of the address book data. @@ -20,24 +27,35 @@ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); private final AddressBook addressBook; + private final SummaryStatistic summaryStatistic; + private final EventBook eventBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredTags; + private final FilteredList filteredEvents; + + private Index lastViewedPersonIndex; + /** * Initializes a ModelManager with the given addressBook and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { + public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyEventBook eventBook, ReadOnlyUserPrefs userPrefs) { requireAllNonNull(addressBook, userPrefs); logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); this.addressBook = new AddressBook(addressBook); + this.eventBook = new EventBook(eventBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredTags = new FilteredList<>(this.addressBook.getTagList()); + filteredEvents = new FilteredList<>(this.eventBook.getEventList()); + summaryStatistic = new SummaryStatistic(this.addressBook.getPersonList()); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new AddressBook(), new EventBook(), new UserPrefs()); } //=========== UserPrefs ================================================================================== @@ -69,12 +87,23 @@ public Path getAddressBookFilePath() { return userPrefs.getAddressBookFilePath(); } + @Override + public Path getEventBookFilePath() { + return userPrefs.getEventBookFilePath(); + } + @Override public void setAddressBookFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); userPrefs.setAddressBookFilePath(addressBookFilePath); } + @Override + public void setEventBookFilePath(Path eventBookFilePath) { + requireNonNull(eventBookFilePath); + userPrefs.setEventBookFilePath(eventBookFilePath); + } + //=========== AddressBook ================================================================================ @Override @@ -82,11 +111,26 @@ public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); } + @Override + public void setEventBook(ReadOnlyEventBook eventBook) { + this.eventBook.resetData(eventBook); + } + @Override public ReadOnlyAddressBook getAddressBook() { return addressBook; } + @Override + public ReadOnlyEventBook getEventBook() { + return eventBook; + } + + @Override + public ReadOnlySummaryStatistic getSummaryStatistic() { + return summaryStatistic; + } + @Override public boolean hasPerson(Person person) { requireNonNull(person); @@ -98,19 +142,36 @@ public void deletePerson(Person target) { addressBook.removePerson(target); } + @Override + public void deleteEvent(Event target) { + eventBook.removeEvent(target); + } + @Override public void addPerson(Person person) { addressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } + @Override + public void addEvent(Event event) { + eventBook.addEvent(event); + updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); + } + @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - addressBook.setPerson(target, editedPerson); } + @Override + public void setEvent(Event target, Event editedEvent) { + eventBook.setEvent(target, editedEvent); + } + + + //=========== Filtered Person List Accessors ============================================================= /** @@ -128,6 +189,82 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + @Override + public void updateFilteredPersonList(List> predicatesList) { + requireNonNull(predicatesList); + Predicate combinedPredicate = predicatesList.stream() + .reduce(Predicate::and) + .orElse(person -> true); + filteredPersons.setPredicate(combinedPredicate); + } + + @Override + public ObservableList getFilteredTagList() { + return filteredTags; + } + @Override + public void updateFilteredTagList(Predicate predicate) { + filteredTags.setPredicate(predicate); + } + + @Override + public ObservableList getFilteredEventList() { + return filteredEvents; + } + + @Override + public void updateFilteredEventList(Predicate predicate) { + requireNonNull(predicate); + filteredEvents.setPredicate(predicate); + } + + @Override + public void updateFilteredEventList(List> predicatesList) { + requireNonNull(predicatesList); + Predicate combinedPredicate = predicatesList.stream() + .reduce(Predicate::and) + .orElse(event -> true); + filteredEvents.setPredicate(combinedPredicate); + } + + @Override + public void sortPersonList(Comparator comparator) { + requireNonNull(comparator); + addressBook.sortAddressBook(comparator); + } + + @Override + public void sortEventList(Comparator comparator) { + requireNonNull(comparator); + eventBook.sortEventBook(comparator); + } + + @Override + public void setLastViewedPersonIndex(Index index) { + requireNonNull(index); + lastViewedPersonIndex = index; + } + + @Override + public Index getLastViewedPersonIndex() { + return lastViewedPersonIndex; + } + + @Override + public boolean hasTag(Tag tag) { + requireNonNull(tag); + return addressBook.hasTag(tag); + } + @Override + public void addTag(Tag tag) { + addressBook.addTag(tag); + } + + public void loadSummaryStatistics() { + summaryStatistic.updatePersonData(addressBook.getPersonList()); + } + + @Override public boolean equals(Object other) { if (other == this) { @@ -142,7 +279,8 @@ public boolean equals(Object other) { ModelManager otherModelManager = (ModelManager) other; return addressBook.equals(otherModelManager.addressBook) && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + && filteredPersons.equals(otherModelManager.filteredPersons) + && filteredEvents.equals(otherModelManager.filteredEvents); } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..a0a3a561075 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -2,6 +2,7 @@ import javafx.collections.ObservableList; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * Unmodifiable view of an address book @@ -14,4 +15,5 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + ObservableList getTagList(); } diff --git a/src/main/java/seedu/address/model/ReadOnlyEventBook.java b/src/main/java/seedu/address/model/ReadOnlyEventBook.java new file mode 100644 index 00000000000..ccc2008ab61 --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyEventBook.java @@ -0,0 +1,11 @@ +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.event.Event; + +/** + * Unmodifiable view of an event book + */ +public interface ReadOnlyEventBook { + ObservableList getEventList(); +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java index befd58a4c73..addf88be42d 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java @@ -13,4 +13,6 @@ public interface ReadOnlyUserPrefs { Path getAddressBookFilePath(); + Path getEventBookFilePath(); + } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 6be655fb4c7..f8366e09b58 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -15,6 +15,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path eventBookFilePath = Paths.get("data", "eventbook.json"); /** * Creates a {@code UserPrefs} with default values. @@ -36,6 +37,7 @@ public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setEventBookFilePath(newUserPrefs.getEventBookFilePath()); } public GuiSettings getGuiSettings() { @@ -51,11 +53,20 @@ public Path getAddressBookFilePath() { return addressBookFilePath; } + public Path getEventBookFilePath() { + return eventBookFilePath; + } + public void setAddressBookFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); this.addressBookFilePath = addressBookFilePath; } + public void setEventBookFilePath(Path eventBookFilePath) { + requireNonNull(eventBookFilePath); + this.eventBookFilePath = eventBookFilePath; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -69,12 +80,13 @@ public boolean equals(Object other) { UserPrefs otherUserPrefs = (UserPrefs) other; return guiSettings.equals(otherUserPrefs.guiSettings) - && addressBookFilePath.equals(otherUserPrefs.addressBookFilePath); + && addressBookFilePath.equals(otherUserPrefs.addressBookFilePath) + && eventBookFilePath.equals(otherUserPrefs.eventBookFilePath); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings, addressBookFilePath, eventBookFilePath); } @Override @@ -82,6 +94,7 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings); sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nLocal data file location : " + eventBookFilePath); return sb.toString(); } diff --git a/src/main/java/seedu/address/model/event/Event.java b/src/main/java/seedu/address/model/event/Event.java new file mode 100644 index 00000000000..a4ab49c0f76 --- /dev/null +++ b/src/main/java/seedu/address/model/event/Event.java @@ -0,0 +1,105 @@ +package seedu.address.model.event; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; + +/** + * Represents an Event in JABPro. + */ +public class Event { + private final Person person; + private final Index index; + private final LocalDateTime startTime; + private final LocalDateTime endTime; + private final String description; + + /** + * Initialises an Event object. + */ + public Event(Person person, String description, LocalDateTime startTime, LocalDateTime endTime) { + this.person = person; + this.description = description; + this.startTime = startTime; + this.endTime = endTime; + this.index = null; + } + + /** + * Initialises an Event object. + */ + public Event(Index index, String description, LocalDateTime startTime, LocalDateTime endTime) { + this.index = index; + this.description = description; + this.startTime = startTime; + this.endTime = endTime; + this.person = null; + } + + public Person getPerson() { + return person; + } + + public String getDescription() { + return description; + } + + public LocalDateTime getStart_time() { + return startTime; + } + + public LocalDateTime getEnd_time() { + return endTime; + } + + public Index getIndex() { + return index; + } + + /** + * Returns true if both events are associated with the same person, and have the same description. + * This defines a weaker notion of equality between two events. + */ + public boolean isSameEvent(Event otherEvent) { + if (otherEvent == this) { + return true; + } + + return otherEvent != null && otherEvent.getPerson().isSamePerson(getPerson()) + && otherEvent.getDescription().equalsIgnoreCase(getDescription()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Event)) { + return false; + } + + Event otherEvent = (Event) other; + if (person == null) { + return index.equals(otherEvent.index) + && description.equals(otherEvent.description); + } + return person.isSamePerson(otherEvent.person) + && description.equals(otherEvent.description); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this.getClass().getSimpleName()); + builder.add("name", person.getName().fullName) + .add("description", description) + .add("startTime", startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))) + .add("endTime", endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + + return builder.toString(); + } +} diff --git a/src/main/java/seedu/address/model/event/UniqueEventList.java b/src/main/java/seedu/address/model/event/UniqueEventList.java new file mode 100644 index 00000000000..c5b345501f2 --- /dev/null +++ b/src/main/java/seedu/address/model/event/UniqueEventList.java @@ -0,0 +1,110 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.event.exceptions.EventNotFoundException; + +/** + * A list of events that does not allow nulls. + * Supports a minimal set of list operations. + * + */ +public class UniqueEventList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent event as the given argument. + */ + public boolean contains(Event toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameEvent); + } + + /** + * Adds an event to the list. + */ + public void add(Event toAdd) { + requireNonNull(toAdd); + internalList.add(toAdd); + } + + /** + * Replaces the event {@code target} in the list with {@code editedEvent}. + * {@code target} must exist in the list. + */ + public void setEvent(Event target, Event editedEvent) { + requireAllNonNull(target, editedEvent); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new EventNotFoundException(); + } + + internalList.set(index, editedEvent); + } + + /** + * Removes the equivalent event from the list. + * The event must exist in the list. + */ + public void remove(Event toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new EventNotFoundException(); + } + } + + public void setEvents(UniqueEventList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code events}. + */ + public void setEvents(List events) { + requireAllNonNull(events); + internalList.setAll(events); + } + + + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UniqueEventList)) { + return false; + } + + UniqueEventList otherUniqueEventList = (UniqueEventList) other; + return internalList.equals(otherUniqueEventList.internalList); + } + + @Override + public String toString() { + return internalList.toString(); + } +} diff --git a/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java new file mode 100644 index 00000000000..5117db006ea --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.event.exceptions; + +/** + * Signals that the operation is unable to find the specified event. + */ +public class EventNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..98bded05494 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -7,7 +7,7 @@ * Represents a Person's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ -public class Email { +public class Email implements Comparable { private static final String SPECIAL_CHARACTERS = "+_.-"; public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " @@ -76,4 +76,9 @@ public int hashCode() { return value.hashCode(); } + @Override + public int compareTo(Email other) { + return this.value.compareTo(other.value); + } + } diff --git a/src/main/java/seedu/address/model/person/Github.java b/src/main/java/seedu/address/model/person/Github.java new file mode 100644 index 00000000000..2b6647c9d54 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Github.java @@ -0,0 +1,37 @@ +package seedu.address.model.person; + +/** + * Represents a person's Github details in JABPro. + */ +public class Github { + + public final String value; + + /** + * Constructs a {@code Github}. + * @param github A valid Github username + */ + public Github(String github) { + value = github; + } + + @Override + public String toString() { + return value != null ? value : ""; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Github)) { + return false; + } + + Github otherGithub = (Github) other; + return value.equals(otherGithub.value); + } +} diff --git a/src/main/java/seedu/address/model/person/LinkedIn.java b/src/main/java/seedu/address/model/person/LinkedIn.java new file mode 100644 index 00000000000..2b50bc69d44 --- /dev/null +++ b/src/main/java/seedu/address/model/person/LinkedIn.java @@ -0,0 +1,37 @@ +package seedu.address.model.person; + +/** + * Represents a person's LinkedIn details in JABPro. + */ +public class LinkedIn { + + public final String value; + + /** + * Constructs a {@code LinkedIn}. + * @param linkedin + */ + public LinkedIn(String linkedin) { + value = linkedin; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LinkedIn)) { + return false; + } + + LinkedIn otherLinkedIn = (LinkedIn) other; + return value.equals(otherLinkedIn.value); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..f65aafbae68 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -7,7 +7,7 @@ * Represents a Person's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ -public class Name { +public class Name implements Comparable { public static final String MESSAGE_CONSTRAINTS = "Names should only contain alphanumeric characters and spaces, and it should not be blank"; @@ -64,4 +64,9 @@ public int hashCode() { return fullName.hashCode(); } + @Override + public int compareTo(Name other) { + return this.fullName.compareTo(other.fullName); + } + } diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..17ed2026082 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -23,18 +23,29 @@ public class Person { // Data fields private final Address address; + private final Set tags = new HashSet<>(); + private LinkedIn linkedIn = new LinkedIn(""); + private Github github = new Github(""); + private Remark remark; + private Status currentStatus = new Status(); + + private ScoreList scoreList = new ScoreList(); + + + /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, Remark remark, Set tags) { + requireAllNonNull(name, phone, email, address, tags, remark); this.name = name; this.phone = phone; this.email = email; this.address = address; this.tags.addAll(tags); + this.remark = remark; } public Name getName() { @@ -53,6 +64,17 @@ public Address getAddress() { return address; } + public Remark getRemark() { + return remark; + } + + + public Status getStatus() { + return currentStatus; + } + + + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -61,6 +83,54 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + public LinkedIn getLinkedIn() { + return linkedIn; + } + + public Github getGithub() { + return github; + } + + + public Score getScoreForTag(Tag tag) { + return scoreList.getScore(tag); + } + + public ScoreList getScoreList() { + return scoreList; + } + + public void setLinkedIn(LinkedIn linkedIn) { + this.linkedIn = linkedIn; + } + + public void setGithub(Github github) { + this.github = github; + } + + /** + * Sets the score list of the person to the given score list. + * This is ONLY recommended for use in Person Builder. Strongly discouraged otherwise. + * @param scoreList the score list to set to + */ + public void setScoreList(ScoreList scoreList) { + this.scoreList = scoreList; + } + + public void setScoreForTag(Tag tag, Score score) { + requireAllNonNull(tag, score); + scoreList.updateScoreList(tag, score); + } + + public void setStatus(Status newStatus) { + this.currentStatus = newStatus; + } + + public void setRemark(Remark remark) { + this.remark = remark; + } + + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -70,8 +140,7 @@ public boolean isSamePerson(Person otherPerson) { return true; } - return otherPerson != null - && otherPerson.getName().equals(getName()); + return otherPerson != null && otherPerson.getName().equals(getName()); } /** @@ -90,28 +159,44 @@ public boolean equals(Object other) { } Person otherPerson = (Person) other; + return name.equals(otherPerson.name) && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) + && remark.equals(otherPerson.remark) + && scoreList.equals(otherPerson.scoreList) && tags.equals(otherPerson.tags); + } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, tags, remark); } @Override public String toString() { - return new ToStringBuilder(this) - .add("name", name) + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("name", name) .add("phone", phone) .add("email", email) .add("address", address) .add("tags", tags) - .toString(); + .add("remark", remark) + .add("status", currentStatus) + .add("score-list", scoreList); + + if (!linkedIn.value.isEmpty()) { + builder.add("linkedin", linkedIn); + } + + if (!github.value.isEmpty()) { + builder.add("github", github); + } + + return builder.toString(); } } diff --git a/src/main/java/seedu/address/model/person/Remark.java b/src/main/java/seedu/address/model/person/Remark.java new file mode 100644 index 00000000000..43924e383dd --- /dev/null +++ b/src/main/java/seedu/address/model/person/Remark.java @@ -0,0 +1,37 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a Person's remark in the address book. + * Guarantees: immutable; is always valid + */ +public class Remark { + public final String value; + + /** + * Constructs a {@code Remark}. + * @param remark A valid remark. + */ + public Remark(String remark) { + requireNonNull(remark); + value = remark; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Remark // instanceof handles nulls + && value.equals(((Remark) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Score.java b/src/main/java/seedu/address/model/person/Score.java new file mode 100644 index 00000000000..43d9e01601e --- /dev/null +++ b/src/main/java/seedu/address/model/person/Score.java @@ -0,0 +1,67 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's score in JABPro. + */ +public class Score implements Comparable { + public static final String MESSAGE_CONSTRAINTS = "Score should be a non-negative integer"; + public final int value; + + + /** + * Constructs a {@code Score}. + * @param value + */ + public Score(int value) { + requireNonNull(value); + checkArgument(isValidScoreValue(value), MESSAGE_CONSTRAINTS); + this.value = value; + } + + /** + * Returns if a given integer is a valid score. + * @param test integer to be tested + * @return true if test is a non-negative integer + */ + public static boolean isValidScoreValue(int test) { + return test >= 0; + } + + public static boolean isValidScore(Score score) { + return isValidScoreValue(score.value); + } + + @Override + public String toString() { + return Integer.toString(value); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Score)) { + return false; + } + + Score otherScore = (Score) other; + return value == otherScore.value; + } + + @Override + public int hashCode() { + return Integer.hashCode(value); + } + + @Override + public int compareTo(Score other) { + return Integer.compare(this.value, other.value); + } +} + diff --git a/src/main/java/seedu/address/model/person/ScoreList.java b/src/main/java/seedu/address/model/person/ScoreList.java new file mode 100644 index 00000000000..b72eb0fa1b2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/ScoreList.java @@ -0,0 +1,153 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import seedu.address.model.tag.Tag; + +/** + * Represents the score list of a person. + */ +public class ScoreList { + private static final String MESSAGE_CONSTRAINTS = "Score tag should start with assessment"; + private static final String MESSAGE_MISSING_TAG = "Tag does not exist in score list"; + private final HashMap scoreList; + + public ScoreList() { + scoreList = new HashMap(); + } + + public ScoreList(HashMap scoreList) { + this.scoreList = scoreList; + } + + + /** + * Updates the score list with the new score. + * @param tag tag + * @param score score + */ + public void updateScoreList(Tag tag, Score score) { + requireAllNonNull(tag, score); + isValidScoreTag(tag); + checkArgument(isValidScoreTag(tag), MESSAGE_CONSTRAINTS); + checkArgument(Score.isValidScore(score), Score.MESSAGE_CONSTRAINTS); + scoreList.put(tag.tagName, score); + } + + /** + * Returns the score associated with the tag. + * @param tag tag + * @return score associated with the tag + */ + public Score getScore(Tag tag) { + checkArgument(scoreList.containsKey(tag.tagName), MESSAGE_MISSING_TAG); + return scoreList.get(tag.tagName); + } + + /** + * Removes the score associated with the tag. Does nothing if the tag does not exist. + * @param tag tag + */ + public void removeScore(Tag tag) { + if (hasTag(tag)) { + scoreList.remove(tag.tagName); + } + } + + public HashMap getScoreList() { + HashMap copy = new HashMap<>(); + for (Map.Entry entry : scoreList.entrySet()) { + copy.put(entry.getKey(), entry.getValue()); + } + return copy; + } + + /** + * Returns true if the score list contains the tag. + * @param tag tag + * @return true if the score list contains the tag + */ + public boolean hasTag(Tag tag) { + return scoreList.containsKey(tag.tagName); + } + + /** + * Gets the List of tags with score. + * @return List of tags with score + */ + public List getTagsWithScore() { + ArrayList result = new ArrayList(); + if (scoreList.isEmpty()) { + return result; + } + Set tags = scoreList.keySet(); + + for (String tag : tags) { + result.add(new Tag(tag, "assessment")); + } + return result; + } + + /** + * Returns true if a given tag is a valid score tag. + * @param tag tag + * @return true if a given tag is a valid score tag + */ + public static boolean isValidScoreTag(Tag tag) { + return tag.tagCategory.toLowerCase().contains("assessment"); + } + + + /** + * Returns true if the score list is empty. + * @return true if the score list is empty + */ + public boolean isEmpty() { + return scoreList.isEmpty(); + } + + + + /** + * Returns true if both score lists have the same score list. + * @param other other score list + * @return true if both score lists have the same score list + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof ScoreList)) { + return false; + } + ScoreList otherScoreList = (ScoreList) other; + return scoreList.equals(otherScoreList.scoreList); + } + + /** + * Returns the string representation of the score list. + * @return + */ + @Override + public String toString() { + return scoreList.toString(); + } + + /** + * Returns the hashcode of the score list. + * @return hashcode of the score list + */ + @Override + public int hashCode() { + return scoreList.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Status.java b/src/main/java/seedu/address/model/person/Status.java new file mode 100644 index 00000000000..e65e7dffe87 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Status.java @@ -0,0 +1,91 @@ +package seedu.address.model.person; + +/** + * Represents a Status in the Person class + */ +public class Status { + public static final String MESSAGE_CONSTRAINTS = "Status should be either one of the following: 'Preliminary'," + + "'Interviewed', 'Offered', 'Rejected' and it should not be blank"; + private String value; + private StatusTypes statusType; + + + /** + * Creates a default Status - Preliminary + */ + public Status() { + this.statusType = StatusTypes.PRELIMINARY; + this.value = "Preliminary"; + } + + /** + * Creates a Status from String + * @param value The status type string to used + */ + public Status(String value) { + this.value = value; + switch (value.toLowerCase()) { + case "offered": + this.statusType = StatusTypes.OFFERED; + break; + case "rejected": + this.statusType = StatusTypes.REJECTED; + break; + case "interviewed": + this.statusType = StatusTypes.INTERVIEWED; + break; + case "preliminary": + this.statusType = StatusTypes.PRELIMINARY; + break; + default: + break; + } + } + + /** + * Creates a Status from a StatusType + * @param statusType The StatusType to be used + */ + public Status(StatusTypes statusType) { + this.value = statusType.toString(); + this.statusType = statusType; + } + + public StatusTypes getStatusType() { + return this.statusType; + } + + public void setStatusType(StatusTypes newStatus) { + this.statusType = newStatus; + this.value = newStatus.toString(); + } + + + @Override + public String toString() { + return statusType.toString(); + } + + public String getValue() { + return this.value; + } + + /** + * Checks the other object is equals to this one + * + * @param other the other Object + * @return {@code true} if equals, {@code false} otherwise. + */ + public boolean equals(Object other) { + if (this == other) { + return true; + } else { + if (other instanceof Status) { + return this.statusType == ((Status) other).statusType; + } else { + return false; + } + } + } + +} diff --git a/src/main/java/seedu/address/model/person/StatusContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/StatusContainsKeywordsPredicate.java new file mode 100644 index 00000000000..d860d6e3c80 --- /dev/null +++ b/src/main/java/seedu/address/model/person/StatusContainsKeywordsPredicate.java @@ -0,0 +1,44 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class StatusContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public StatusContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getStatus().toString(), keyword)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof StatusContainsKeywordsPredicate)) { + return false; + } + + StatusContainsKeywordsPredicate otherStatusContainsKeywordsPredicate = (StatusContainsKeywordsPredicate) other; + return keywords.equals(otherStatusContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/StatusTypes.java b/src/main/java/seedu/address/model/person/StatusTypes.java new file mode 100644 index 00000000000..a61aa7cea78 --- /dev/null +++ b/src/main/java/seedu/address/model/person/StatusTypes.java @@ -0,0 +1,34 @@ +package seedu.address.model.person; +/** + * Represents a StatusType in the Status class. + */ +public enum StatusTypes { + PRELIMINARY("Preliminary"), + INTERVIEWED("Interviewed"), + REJECTED("Rejected"), + OFFERED("Offered"); + + private final String statusName; + + StatusTypes(String statusName) { + this.statusName = statusName; + } + + @Override + public String toString() { + return this.statusName; + } + + /** + * Checks if the provided status is a valid status. + * + * @param input The string status to be checked for validity. + * @return {@code true} if the status is valid, {@code false} otherwise. + */ + public static boolean isValidStatusType(String input) { + return input.equals("interviewed") || input.equals("preliminary") + || input.equals("rejected") + || input.equals("offered"); + } + +} diff --git a/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java new file mode 100644 index 00000000000..82d0d30221b --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java @@ -0,0 +1,46 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class TagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> person.getTags().stream() + .anyMatch(tag -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword)) + ); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TagContainsKeywordsPredicate)) { + return false; + } + + TagContainsKeywordsPredicate otherTagContainsKeywordsPredicate = (TagContainsKeywordsPredicate) other; + return keywords.equals(otherTagContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/statistics/ReadOnlySummaryStatistic.java b/src/main/java/seedu/address/model/statistics/ReadOnlySummaryStatistic.java new file mode 100644 index 00000000000..1e02d09bcff --- /dev/null +++ b/src/main/java/seedu/address/model/statistics/ReadOnlySummaryStatistic.java @@ -0,0 +1,53 @@ +package seedu.address.model.statistics; + +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + + +/** + * Represents a ReadOnlySummaryStatistic, that is no update can be done to the summary statistic. + */ +public interface ReadOnlySummaryStatistic { + /** + * Returns the number of people in the address book associated with that tag. + * @return number of people in the address book associated with that tag. + */ + public int getNumOfPeopleAssociatedWithTag(Tag tag); + + /** + * Generate the Median of the score of the people associated with that tag. + * @param tag tag to be associated with + * @return median of the score of the people associated with that tag. + */ + public int generateMedianWithTag(Tag tag); + + /** + * Generate the Mean of the score of the people associated with that tag. + * @param tag tag to be associated with + * @return mean of the score of the people associated with that tag. + */ + public int generateMeanWithTag(Tag tag); + + /** + * Generate the Max score of the people associated with that tag. + * @param person person to be associated with + * @param tag tag to be associated with + * @return max score of the people associated with that tag. + */ + public double generatePercentileWithTag(Person person, Tag tag); + + /** + * Generate the Min score of the people associated with that tag. + * @param tag tag to be associated with + * @return min score of the people associated with that tag. + */ + public int generateMaxScoreValueWithTag(Tag tag); + + /** + * Generate the Min score of the people associated with that tag. + * @param tag tag to be associated with + * @return min score of the people associated with that tag. + */ + public int generateMinScoreValueWithTag(Tag tag); + +} diff --git a/src/main/java/seedu/address/model/statistics/StatisticMetric.java b/src/main/java/seedu/address/model/statistics/StatisticMetric.java new file mode 100644 index 00000000000..9c5cf953f3e --- /dev/null +++ b/src/main/java/seedu/address/model/statistics/StatisticMetric.java @@ -0,0 +1,41 @@ +package seedu.address.model.statistics; + +/** + * Represents the different metrics that can be used to calculate the statistics. + */ +public enum StatisticMetric { + + SCORE("SCORE"), + MEAN("MEAN"), + MEDIAN("MEDIAN"), + PERCENTILE("PERCENTILE"); + private final String metricName; + StatisticMetric(String metricName) { + this.metricName = metricName; + } + @Override + public String toString() { + return this.metricName; + } + /** + * Checks if the provided metric is a valid metric. + * + * @param input The string metric to be checked for validity. + * @return {@code true} if the metric is valid, {@code false} otherwise. + */ + public static boolean isValidMetric(String input) { + String inputUpperCase = input.toUpperCase(); + return inputUpperCase.equals("SCORE") || inputUpperCase.equals("MEAN") || inputUpperCase.equals("MEDIAN") + || inputUpperCase.equals("PERCENTILE"); + } + + /** + * Checks if a score is needed for the metric. + * @param input The string metricF to be checked. + * @return {@code true} if the metric needs a score, {@code false} otherwise. + */ + public static boolean isScoreRelevant(String input) { + String inputUpperCase = input.toUpperCase(); + return inputUpperCase.equals("SCORE") || inputUpperCase.equals("PERCENTILE"); + } +} diff --git a/src/main/java/seedu/address/model/statistics/SummaryStatistic.java b/src/main/java/seedu/address/model/statistics/SummaryStatistic.java new file mode 100644 index 00000000000..780625104c9 --- /dev/null +++ b/src/main/java/seedu/address/model/statistics/SummaryStatistic.java @@ -0,0 +1,268 @@ +package seedu.address.model.statistics; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Person; +import seedu.address.model.person.Score; +import seedu.address.model.person.ScoreList; +import seedu.address.model.tag.Tag; + +/** + * Represents the summary statistic of the address book. + * Using terminal operations on Stream like count() consumes the stream and closes it. + * This can throw an IllegalStateException if you try to use the stream after it is closed. + */ +public class SummaryStatistic implements ReadOnlySummaryStatistic { + private static final Logger logger = LogsCenter.getLogger(SummaryStatistic.class); + private ObservableList personData; + + /** + * Initializes a SummaryStatistic with the observable list of given person data. + * @param persons person data + */ + public SummaryStatistic(ObservableList persons) { + requireAllNonNull(persons); + personData = persons; + } + + /** + * Updates the person data in the summary statistic. + * @param persons person data + */ + public void updatePersonData(ObservableList persons) { + personData = persons; + } + + + /** + * Returns the number of people in the address book associated with that tag. + * @param tag tag to be associated with + * @return number of people in the address book associated with that tag. + */ + public int getNumOfPeopleAssociatedWithTag(Tag tag) { + Stream filteredStream = personData.stream().filter(person -> person.getTags().contains(tag)); + return (int) filteredStream.count(); + } + + /** + * Returns the sorted stream of score value in the address book associated with that tag. + * @param tag tag to be associated with + * @return sorted stream of score value + */ + private Stream getSortedScoreValueStream(Tag tag) { + Stream filteredStream = filteredPersonsWithScoreTag(tag); + Stream scoreListStream = filteredStream.map(person -> person.getScoreList()); + Stream scoreStream = scoreListStream.map(scoreList -> scoreList.getScore(tag)); + Stream scoreValueStream = scoreStream.map(score -> score.value); + Stream sortedScoreValueStream = scoreValueStream.sorted(); + return sortedScoreValueStream; + } + + /** + * Calculate the median of the score value stream. + * @param sortedScoreValueStream sorted score value stream + * @return median of the score value stream + */ + public int calculateMedian(Stream sortedScoreValueStream) { + List sortedValueList = sortedScoreValueStream.collect(Collectors.toList()); + int numOfPeople = sortedValueList.size(); + int median = 0; + if (numOfPeople <= 0) { + logger.warning("No people in the list, median will be left as default of 0"); + return median; + } + if (numOfPeople == 1) { + median = sortedValueList.get(0); + return median; + } + + if (numOfPeople % 2 == 0) { + int medianIndex = numOfPeople / 2; + median = (sortedValueList.get(medianIndex - 1) + sortedValueList.get(medianIndex)) / 2; + return median; + } else { + int medianIndex = (numOfPeople + 1) / 2; + median = sortedValueList.get(medianIndex - 1); + return median; + } + } + + /** + * Generate the Median of the score of the people associated with that tag. + * @param tag tag to be associated with + * @return median of the score of the people associated with that tag. + */ + public int generateMedianWithTag(Tag tag) { + Stream sortedScoreValueStream = getSortedScoreValueStream(tag); + int median = calculateMedian(sortedScoreValueStream); + return median; + } + + /** + * Calculate the mean of the score value stream. + * @param sortedScoreValueStream sorted score value stream + * @return mean of the score value stream + */ + public int calculateMean(Stream sortedScoreValueStream) { + List sortedValueList = sortedScoreValueStream.collect(Collectors.toList()); + int numOfPeople = sortedValueList.size(); + int mean = 0; + if (numOfPeople <= 0) { + logger.warning("No people in the list, mean will be left as default of 0"); + return mean; + } + if (numOfPeople == 1) { + Integer ans = sortedValueList.get(0); + return ans; + } + for (Integer value : sortedValueList) { + mean += value; + } + mean /= numOfPeople; + return mean; + } + + /** + * Generate the Mean of the score of the people associated with that tag. + * @param tag tag to be associated with + * @return mean of the score of the people associated with that tag. + */ + public int generateMeanWithTag(Tag tag) { + Stream sortedScoreValueStream = getSortedScoreValueStream(tag); + int mean = calculateMean(sortedScoreValueStream); + return mean; + } + + /** + * Generate the percentile of the score of the person associated with that tag. + * @param person person to be associated with + * @param tag tag to be associated with + * @return percentile of the score of the person associated with that tag. + */ + public double generatePercentileWithTag(Person person, Tag tag) { + List filteredList = getSortedScoreValueStream(tag).collect(Collectors.toList()); + if (filteredList.size() <= 0) { + logger.warning("No people in the list, percentile will be left as default of 0"); + return 0; + } else if (filteredList.size() == 1) { + return 100; + } + Stream filteredStreamWithScoreValue = filteredPersonsWithScoreTag(tag) + .filter(personInList -> personInList.getScoreForTag(tag).compareTo(person.getScoreForTag(tag)) < 0); + List filteredListWithScoreValue = filteredStreamWithScoreValue.collect(Collectors.toList()); + double percentile = 0; + + int sameScoreIndividuals = filteredPersonsWithScoreTag(tag) + .filter(personInList -> personInList.getScoreForTag(tag).compareTo(person.getScoreForTag(tag)) == 0) + .collect(Collectors.toList()).size(); + int trueListSize = filteredList.size() - sameScoreIndividuals; + if (filteredListWithScoreValue.size() <= 0) { + logger.warning("No people in the list, percentile will be left as default of 0"); + return percentile; + } + percentile = Math.ceil((double) filteredListWithScoreValue.size() / trueListSize * 100); + return percentile; + } + /** + * Generate the Max score of the people associated with that tag. + * @param tag tag to be associated with + * @return max score of the people associated with that tag. + */ + public int generateMaxScoreValueWithTag(Tag tag) { + Stream sortedScoreValueStream = getSortedScoreValueStream(tag); + List sortedList = sortedScoreValueStream.collect(Collectors.toList()); + if (sortedList.size() <= 0) { + logger.warning("No people in the list, max score will be left as default of 0"); + return 0; + } + if (sortedList.size() == 1) { + return sortedList.get(0); + } + int maxScore = sortedList.get(sortedList.size() - 1); + return maxScore; + } + + /** + * Generate the Min score of the people associated with that tag. + * @param tag tag to be associated with + * @return min score of the people associated with that tag. + */ + public int generateMinScoreValueWithTag(Tag tag) { + Stream sortedScoreValueStream = getSortedScoreValueStream(tag); + List sortedList = sortedScoreValueStream.collect(Collectors.toList()); + if (sortedList.size() <= 0) { + logger.warning("No people in the list, min score will be left as default of 0"); + return 0; + } + if (sortedList.size() == 1) { + return sortedList.get(0); + } + int minScore = sortedList.get(0); + return minScore; + } + + /** + * Returns the list of people in the address book associated with that tag and has a value greater than + * or equal to the metric and value provided by the user. + * @param tag tag to be associated with + * @param metric metric to be associated with + * @param value value to be associated with + * @return + */ + public List filteredPersonList(Tag tag, StatisticMetric metric, int value) { + switch (metric) { + case SCORE: + Stream scoreValueStream = getSortedScoreValueStream(tag); + Stream filteredPersonStream = scoreValueStream.filter(scoreValue -> scoreValue >= value) + .flatMap(scoreValue -> personData.stream() + .filter(person -> person.getTags().contains(tag) + && person.getScoreList().hasTag(tag)) + .filter(person -> person.getScoreForTag(tag).value >= scoreValue)).distinct(); + List filteredPersonList = filteredPersonStream.collect(Collectors.toList()); + return filteredPersonList; + case MEAN: + Stream sortedScoreValueStream = getSortedScoreValueStream(tag); + int mean = calculateMean(sortedScoreValueStream); + Stream filteredPersonStreamWithMean = filteredPersonsWithScoreTag(tag) + .filter(person -> person.getScoreForTag(tag).value >= mean); + List filteredPersonListWithMean = filteredPersonStreamWithMean.collect(Collectors.toList()); + return filteredPersonListWithMean; + case MEDIAN: + Stream sortedScoreValueStreamWithMedian = getSortedScoreValueStream(tag); + int median = calculateMedian(sortedScoreValueStreamWithMedian); + Stream filteredPersonStreamWithMedian = filteredPersonsWithScoreTag(tag) + .filter(person -> person.getScoreForTag(tag).value >= median); + List filteredPersonListWithMedian = filteredPersonStreamWithMedian.collect(Collectors.toList()); + return filteredPersonListWithMedian; + case PERCENTILE: + Stream filteredStreamWithPercentile = filteredPersonsWithScoreTag(tag) + .filter(person -> generatePercentileWithTag(person, tag) >= value); + List filteredPersonListWithPercentile = filteredStreamWithPercentile.collect(Collectors.toList()); + return filteredPersonListWithPercentile; + default: + assert false : "Invalid metric"; + return List.of(); + } + } + + /** + * Returns the stream of people in JABPro associated with that tag and has a value greater than + * @param tag tag to be associated with + * @return stream of people in JABPro associated with that tag and has a value greater than + */ + public Stream filteredPersonsWithScoreTag(Tag tag) { + Stream filteredStream = personData.stream().filter(person -> person.getTags().contains(tag) + && person.getScoreList().hasTag(tag)); + return filteredStream; + } + + +} + diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..c5f5b3fc761 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -10,19 +10,23 @@ public class Tag { public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; + public static final String MESSAGE_TAG_DOES_NOT_EXIST = "The following tag(s) do not exist: "; public static final String VALIDATION_REGEX = "\\p{Alnum}+"; public final String tagName; + public final String tagCategory; /** * Constructs a {@code Tag}. * * @param tagName A valid tag name. */ - public Tag(String tagName) { + public Tag(String tagName, String tagCategory) { requireNonNull(tagName); + requireNonNull(tagCategory); checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); this.tagName = tagName; + this.tagCategory = tagCategory; } /** @@ -32,6 +36,14 @@ public static boolean isValidTagName(String test) { return test.matches(VALIDATION_REGEX); } + public String getTagName() { + return tagName; + } + + public String getTagCategory() { + return tagCategory; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -44,7 +56,7 @@ public boolean equals(Object other) { } Tag otherTag = (Tag) other; - return tagName.equals(otherTag.tagName); + return tagName.equals(otherTag.tagName) && tagCategory.equals(otherTag.tagCategory); } @Override diff --git a/src/main/java/seedu/address/model/tag/UniqueTagList.java b/src/main/java/seedu/address/model/tag/UniqueTagList.java new file mode 100644 index 00000000000..fc9105c5be3 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/UniqueTagList.java @@ -0,0 +1,181 @@ +package seedu.address.model.tag; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.CreateTagCommand.MESSAGE_FAILURE; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.exceptions.DuplicateTagException; +import seedu.address.model.tag.exceptions.TagNotFoundException; + +/** + * Represents a list of unique tags. This class provides methods to manage a collection of tags, + * ensuring that no duplicate tags are allowed in the list. + */ +public class UniqueTagList { + + private static final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Checks if the list contains a specific tag. + * + * @param toCheck The tag to check for in the list. + * @return true if the tag is in the list, false otherwise. + */ + public boolean contains(Tag toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::equals); + } + + /** + * Checks if the list contains a tag with the specified category. + * + * @param categoryToCheck The category to check for in the list. Must not be null. + * @return True if a tag with the specified category is found in the list, false otherwise. + */ + public boolean containsTagCategory(String categoryToCheck) { + requireNonNull(categoryToCheck); + for (Tag tag : internalList) { + if (tag.tagCategory.equals(categoryToCheck)) { + return true; + } + } + return false; + } + + /** + * Checks if the list contains a tag with the specified name. + * + * @param nameToCheck The category to check for in the list. Must not be null. + * @return True if a tag with the specified name is found in the list, false otherwise. + */ + public boolean containsTagName(String nameToCheck) { + requireNonNull(nameToCheck); + for (Tag tag : internalList) { + if (tag.tagName.equals(nameToCheck)) { + return true; + } + } + return false; + } + + + + /** + * Adds a tag to the list. + * + * @param toAdd The tag to add. + * @throws DuplicateTagException If the tag to add already exists in the list. + */ + public void add(Tag toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateTagException(); + } + internalList.add(toAdd); + } + + /** + * Retrieves a tag by its name. + * + * @param tagName The name of the tag to retrieve. + * @return The tag with the specified name. + * @throws ParseException If the tag is not found in the list. + */ + public Tag getTag(String tagName, String tagCategory) throws ParseException { + Optional foundTag = internalList.stream() + .filter(tag -> tag.tagName.equals(tagName) && tag.tagCategory.contains(tagCategory)) + .findFirst(); + + if (!tagCategory.isEmpty()) { + // tag category is specified + for (Tag tag : internalList) { + if (tag.tagName.equals(tagName) && tag.tagCategory.equals(tagCategory)) { + return tag; + } + } + if (!containsTagCategory(tagCategory) + && countDistinctCategories(internalList) > 5) { + throw new ParseException(MESSAGE_FAILURE); + } + Tag tag = new Tag(tagName, tagCategory); + add(tag); + return tag; + } else if (foundTag.isPresent()) { + // tag category not specified + long occurrence = internalList.stream() + .filter(tag -> tag.tagName.equals(tagName) && tag.tagCategory.contains(tagCategory)) + .count(); + + + // if tag occurs more than once in tag list + if (occurrence > 1) { + throw new ParseException("Multiple tags exists with the same name! " + + "Specify the category of the tag when adding it to a person e.g. edit 1 t/experience 3"); + } + return foundTag.get(); + } + // tag is uncategorised + Tag uncategorisedTag = new Tag(tagName, "uncategorised"); + this.add(uncategorisedTag); // add uncategorised tag to unique tag list + return uncategorisedTag; + } + + private int countDistinctCategories(List tags) { + requireNonNull(tags); + Set distinctCategories = new HashSet<>(); + + for (Tag tag : tags) { + String tagCategory = tag.getTagCategory(); + if (!tagCategory.equals("uncategorised")) { + distinctCategories.add(tagCategory); + } + } + + return distinctCategories.size(); + } + + /** + * Returns an unmodifiable view of the internal tag list. + * + * @return An unmodifiable ObservableList of tags. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + /** + * Removes a tag from the list. + * + * @param toRemove The tag to remove. + * @throws TagNotFoundException If the tag to remove is not found in the list. + */ + public void remove(Tag toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new TagNotFoundException(); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof UniqueTagList)) { + return false; + } + + UniqueTagList otherUniqueTagList = (UniqueTagList) other; + return internalList.equals(otherUniqueTagList.internalList); + } +} diff --git a/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java b/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java new file mode 100644 index 00000000000..0b1a40c783b --- /dev/null +++ b/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java @@ -0,0 +1,15 @@ +package seedu.address.model.tag.exceptions; + +/** + * Represents an exception that is thrown when an operation would result in duplicate tags. + */ +public class DuplicateTagException extends RuntimeException { + + /** + * Constructs a DuplicateTagException with a default error message. + * The error message indicates that the operation would result in duplicate tags. + */ + public DuplicateTagException() { + super("Operation would result in duplicate tags"); + } +} diff --git a/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java b/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java new file mode 100644 index 00000000000..a5c5c4b800a --- /dev/null +++ b/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java @@ -0,0 +1,15 @@ +package seedu.address.model.tag.exceptions; + +/** + * Represents an exception that is thrown when a specified tag is not found in the tag manager. + */ +public class TagNotFoundException extends RuntimeException { + + /** + * Constructs a TagNotFoundException with a default error message. + * The error message indicates that the tag was not found and suggests creating the tag before using it. + */ + public TagNotFoundException() { + super("Tag not found! Create tag before tagging a person with it!"); + } +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..855622a1656 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -11,32 +11,38 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; import seedu.address.model.tag.Tag; /** * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + public static final Remark EMPTY_REMARK = new Remark(""); + public static Person[] getSamplePersons() { return new Person[] { new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), + new Address("Blk 30 Geylang Street 29, #06-40"), EMPTY_REMARK, + getTagSet("dept marketing", "employment intern")), new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), EMPTY_REMARK, + getTagSet("dept software", "role developer", "employment intern")), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), EMPTY_REMARK, + getTagSet("dept marketing", "employment intern")), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), EMPTY_REMARK, + getTagSet("dept marketing", "employment intern")) + }; + } + + public static Tag[] getSampleTags() { + return new Tag[] { + new Tag("intern", "employment"), + new Tag("developer", "role"), + new Tag("marketing", "dept"), + new Tag("software", "dept") }; } @@ -45,6 +51,9 @@ public static ReadOnlyAddressBook getSampleAddressBook() { for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } + for (Tag sampleTag : getSampleTags()) { + sampleAb.addTag(sampleTag); + } return sampleAb; } @@ -53,7 +62,17 @@ public static ReadOnlyAddressBook getSampleAddressBook() { */ public static Set getTagSet(String... strings) { return Arrays.stream(strings) - .map(Tag::new) + .map(x -> { + String[] tagNameCategory = x.split("\\s+"); + if (tagNameCategory.length > 1) { + String tagName = tagNameCategory[1]; + String tagCategory = tagNameCategory[0]; + return new Tag(tagName, tagCategory); + } else { + return new Tag(x, "uncategorised"); + } + + }) .collect(Collectors.toSet()); } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..98d2c0ed21f 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -1,10 +1,11 @@ package seedu.address.storage; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -12,9 +13,14 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Github; +import seedu.address.model.person.LinkedIn; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; +import seedu.address.model.person.ScoreList; +import seedu.address.model.person.Status; import seedu.address.model.tag.Tag; /** @@ -28,7 +34,15 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; - private final List tags = new ArrayList<>(); + private final List> tags = new ArrayList<>(); + private final JsonAdaptedScoreList scoreList; + private final String linkedIn; + private final String github; + + private final String remark; + private final String status; + + /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,7 +50,11 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("tags") List> tags, + @JsonProperty("scoreList") JsonAdaptedScoreList scoreList, + @JsonProperty("linkedIn") String linkedIn, + @JsonProperty("github") String github, + @JsonProperty("remark") String remark, @JsonProperty("status") String status) { this.name = name; this.phone = phone; this.email = email; @@ -44,6 +62,11 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone if (tags != null) { this.tags.addAll(tags); } + this.linkedIn = linkedIn; + this.github = github; + this.remark = remark; + this.status = status; + this.scoreList = scoreList; } /** @@ -54,9 +77,18 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; - tags.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); + Set personTags = source.getTags(); + for (Tag tag : personTags) { + Map map = new HashMap<>(); + map.put("tagCategory", tag.tagCategory); + map.put("tagName", tag.tagName); + this.tags.add(map); + } + scoreList = new JsonAdaptedScoreList(source.getScoreList()); + linkedIn = source.getLinkedIn().value; + github = source.getGithub().value; + remark = source.getRemark().value; + status = source.getStatus().getValue(); } /** @@ -66,10 +98,15 @@ public JsonAdaptedPerson(Person source) { */ public Person toModelType() throws IllegalValueException { final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tags) { - personTags.add(tag.toModelType()); - } + for (Map tagData : tags) { + String tagCategory = tagData.get("tagCategory"); + String tagName = tagData.get("tagName"); + if (!Tag.isValidTagName(tagName)) { + throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); + } + personTags.add(new Tag(tagName, tagCategory)); + } if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -101,9 +138,25 @@ public Person toModelType() throws IllegalValueException { throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); } final Address modelAddress = new Address(address); + if (remark == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Remark.class.getSimpleName())); + } + final Remark modelRemark = new Remark(remark); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } + Person p = new Person(modelName, modelPhone, modelEmail, modelAddress, modelRemark, modelTags); + if (linkedIn != null) { + p.setLinkedIn(new LinkedIn(linkedIn)); + } + if (github != null) { + p.setGithub(new Github(github)); + } + if (status != null) { + p.setStatus(new Status(status)); + } + ScoreList modelScoreList = (scoreList != null) ? scoreList.toModelType() : new ScoreList(); + p.setScoreList(modelScoreList); + return p; + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedScoreList.java b/src/main/java/seedu/address/storage/JsonAdaptedScoreList.java new file mode 100644 index 00000000000..97c340b1204 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedScoreList.java @@ -0,0 +1,62 @@ +package seedu.address.storage; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.model.person.Score; +import seedu.address.model.person.ScoreList; +import seedu.address.model.tag.Tag; + + +/** + * Jackson-friendly version of {@link ScoreList}. + */ +public class JsonAdaptedScoreList { + private Map scores; + + /** + * @param scoreList + */ + public JsonAdaptedScoreList(ScoreList scoreList) { + scores = new HashMap<>(); + for (Map.Entry entry : scoreList.getScoreList().entrySet()) { + scores.put(entry.getKey(), entry.getValue().value); + } + } + + /** + * Constructs a {@code JsonAdaptedScoreList} with the given score details. + * @param scores + */ + @JsonCreator + public JsonAdaptedScoreList(Map scores) { + this.scores = scores; + } + + /** + * Gets the score list. + * @return score list + */ + @JsonValue + public Map getScoreList() { + return scores; + } + + /** + * Converts this Jackson-friendly adapted score list object into the model's {@code ScoreList} object. + * @return ScoreList + */ + public ScoreList toModelType() { + ScoreList scoreList = new ScoreList(); + if (scores == null) { + return scoreList; + } + for (Map.Entry entry : scores.entrySet()) { + scoreList.updateScoreList(new Tag(entry.getKey(), "assessment"), new Score(entry.getValue())); + } + return scoreList; + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java deleted file mode 100644 index 0df22bdb754..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -package seedu.address.storage; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Tag}. - */ -class JsonAdaptedTag { - - private final String tagName; - - /** - * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. - */ - @JsonCreator - public JsonAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given {@code Tag} into this class for Jackson use. - */ - public JsonAdaptedTag(Tag source) { - tagName = source.tagName; - } - - @JsonValue - public String getTagName() { - return tagName; - } - - /** - * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted tag. - */ - public Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..a257d132275 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -1,17 +1,21 @@ package seedu.address.storage; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonRootName; +import javafx.collections.ObservableList; import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * An Immutable AddressBook that is serializable to JSON format. @@ -21,16 +25,23 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_TAG = "Tags list contains duplicate tag(s)."; + private final List persons = new ArrayList<>(); + private final List> tagList = new ArrayList<>(); + /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and tags. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("tagList") List> tags) { this.persons.addAll(persons); + this.tagList.addAll(tags); } + /** * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. * @@ -38,6 +49,13 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List internalTagList = source.getTagList(); + for (Tag tag : internalTagList) { + Map map = new HashMap<>(); + map.put("tagCategory", tag.tagCategory); + map.put("tagName", tag.tagName); + this.tagList.add(map); + } } /** @@ -54,6 +72,16 @@ public AddressBook toModelType() throws IllegalValueException { } addressBook.addPerson(person); } + for (Map tagData : tagList) { + String tagCategory = tagData.get("tagCategory"); + String tagName = tagData.get("tagName"); + + Tag tag = new Tag(tagName, tagCategory); + + if (!addressBook.hasTag(tag)) { + addressBook.addTag(tag); + } + } return addressBook; } diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 9fba0c7a1d6..758264cbfb6 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -6,13 +6,15 @@ import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEventBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.storage.event.EventBookStorage; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends AddressBookStorage, UserPrefsStorage, EventBookStorage { @Override Optional readUserPrefs() throws DataLoadingException; @@ -23,10 +25,19 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override Path getAddressBookFilePath(); + @Override + Path getEventBookFilePath(); + @Override Optional readAddressBook() throws DataLoadingException; + @Override + Optional readEventBook() throws DataLoadingException; + @Override void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + @Override + void saveEventBook(ReadOnlyEventBook eventBook) throws IOException; + } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 8b84a9024d5..259be6a6e66 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -8,8 +8,10 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEventBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.storage.event.EventBookStorage; /** * Manages storage of AddressBook data in local storage. @@ -18,14 +20,17 @@ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); private AddressBookStorage addressBookStorage; + private EventBookStorage eventBookStorage; private UserPrefsStorage userPrefsStorage; /** * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. */ - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage, + EventBookStorage eventBookStorage) { this.addressBookStorage = addressBookStorage; this.userPrefsStorage = userPrefsStorage; + this.eventBookStorage = eventBookStorage; } // ================ UserPrefs methods ============================== @@ -53,6 +58,11 @@ public Path getAddressBookFilePath() { return addressBookStorage.getAddressBookFilePath(); } + @Override + public Path getEventBookFilePath() { + return eventBookStorage.getEventBookFilePath(); + } + @Override public Optional readAddressBook() throws DataLoadingException { return readAddressBook(addressBookStorage.getAddressBookFilePath()); @@ -64,6 +74,17 @@ public Optional readAddressBook(Path filePath) throws DataL return addressBookStorage.readAddressBook(filePath); } + @Override + public Optional readEventBook() throws DataLoadingException { + return readEventBook(eventBookStorage.getEventBookFilePath()); + } + + @Override + public Optional readEventBook(Path filePath) throws DataLoadingException { + logger.fine("Attempting to read data from file: " + filePath); + return eventBookStorage.readEventBook(filePath); + } + @Override public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); @@ -75,4 +96,14 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro addressBookStorage.saveAddressBook(addressBook, filePath); } + @Override + public void saveEventBook(ReadOnlyEventBook eventBook) throws IOException { + saveEventBook(eventBook, eventBookStorage.getEventBookFilePath()); + } + + @Override + public void saveEventBook(ReadOnlyEventBook eventBook, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + eventBookStorage.saveEventBook(eventBook, filePath); + } } diff --git a/src/main/java/seedu/address/storage/event/EventBookStorage.java b/src/main/java/seedu/address/storage/event/EventBookStorage.java new file mode 100644 index 00000000000..2e6be1c36e9 --- /dev/null +++ b/src/main/java/seedu/address/storage/event/EventBookStorage.java @@ -0,0 +1,44 @@ +package seedu.address.storage.event; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.ReadOnlyEventBook; + +/** + * Represents a storage for {@link seedu.address.model.EventBook}. + */ +public interface EventBookStorage { + + /** + * Returns the file path of the data file. + */ + Path getEventBookFilePath(); + + /** + * Returns EventBook data as a {@link ReadOnlyEventBook}. + * Returns {@code Optional.empty()} if storage file is not found. + * + * @throws DataLoadingException if loading the data from storage failed. + */ + Optional readEventBook() throws DataLoadingException; + + /** + * @see #getEventBookFilePath() + */ + Optional readEventBook(Path filePath) throws DataLoadingException; + + /** + * Saves the given {@link ReadOnlyEventBook} to the storage. + * @throws IOException if there was any problem writing to the file. + */ + void saveEventBook(ReadOnlyEventBook eventBook) throws IOException; + + /** + * + */ + void saveEventBook(ReadOnlyEventBook eventBook, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/event/JsonAdaptedEvent.java b/src/main/java/seedu/address/storage/event/JsonAdaptedEvent.java new file mode 100644 index 00000000000..718390c81a2 --- /dev/null +++ b/src/main/java/seedu/address/storage/event/JsonAdaptedEvent.java @@ -0,0 +1,112 @@ +package seedu.address.storage.event; + +import static seedu.address.logic.Messages.MESSAGE_INCORRECT_DATE_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_INVALID_DATE; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; +import java.util.HashSet; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.Event; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; + +/** + * Jackson-friendly version of {@link Event}. + */ +class JsonAdaptedEvent { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Event's %s field is missing!"; + private final String name; + private final String description; + private final String startTime; + private final String endTime; + + /** + * Constructs a {@code JsonAdaptedEvent} with the given event details. + */ + @JsonCreator + public JsonAdaptedEvent(@JsonProperty("name") String name, @JsonProperty("description") String description, + @JsonProperty("startTime") String startTime, @JsonProperty("endTime") String endTime) { + this.name = name; + this.description = description; + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Converts a given {@code Event} into this class for Jackson use. + */ + public JsonAdaptedEvent(Event source) { + name = source.getPerson().getName().fullName; + description = source.getDescription(); + startTime = source.getStart_time().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + endTime = source.getEnd_time().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + } + + /** + * Converts this Jackson-friendly adapted event object into the model's {@code Event} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted event. + */ + public Event toModelType() throws IllegalValueException { + + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "name")); + } + + if (description == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "description")); + } + + LocalDateTime modelStartTime; + if (startTime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "startTime")); + } else { + try { + modelStartTime = LocalDateTime.parse(startTime, + DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm").withResolverStyle(ResolverStyle.STRICT)); + } catch (DateTimeException e) { + String s = e.getMessage(); + if (s.contains("Invalid")) { + throw new ParseException(MESSAGE_INVALID_DATE); + } else { + throw new ParseException(MESSAGE_INCORRECT_DATE_FORMAT); + } + } + } + + LocalDateTime modelEndTime; + if (endTime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "endTime")); + } else { + try { + modelEndTime = LocalDateTime.parse(endTime, + DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm").withResolverStyle(ResolverStyle.STRICT)); + } catch (DateTimeException e) { + String s = e.getMessage(); + if (s.contains("Invalid")) { + throw new ParseException(MESSAGE_INVALID_DATE); + } else { + throw new ParseException(MESSAGE_INCORRECT_DATE_FORMAT); + } + } + } + + Person person = new Person(new Name(name), new Phone("00000"), new Email("filler@email.com"), + new Address("36 College Avenue East"), new Remark(""), new HashSet<>()); + Event e = new Event(person, description, modelStartTime, modelEndTime); + return e; + } +} diff --git a/src/main/java/seedu/address/storage/event/JsonEventBookStorage.java b/src/main/java/seedu/address/storage/event/JsonEventBookStorage.java new file mode 100644 index 00000000000..5ca8bb25004 --- /dev/null +++ b/src/main/java/seedu/address/storage/event/JsonEventBookStorage.java @@ -0,0 +1,76 @@ +package seedu.address.storage.event; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.ReadOnlyEventBook; + +/** + * A class to access EventBook data stored as a json file on the hard disk. + */ +public class JsonEventBookStorage implements EventBookStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonEventBookStorage.class); + + private Path filePath; + + public JsonEventBookStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getEventBookFilePath() { + return filePath; + } + + @Override + public Optional readEventBook() throws DataLoadingException { + return readEventBook(filePath); + } + + /** + * @param filePath location of the data. Cannot be null. + * @throws DataLoadingException if loading the data from storage failed. + */ + public Optional readEventBook(Path filePath) throws DataLoadingException { + requireNonNull(filePath); + + Optional jsonEventBook = JsonUtil.readJsonFile( + filePath, JsonSerializableEventBook.class); + if (!jsonEventBook.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonEventBook.get().toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataLoadingException(ive); + } + } + + @Override + public void saveEventBook(ReadOnlyEventBook eventBook) throws IOException { + saveEventBook(eventBook, filePath); + } + + /** + * @param filePath location of the data. Cannot be null. + */ + public void saveEventBook(ReadOnlyEventBook eventBook, Path filePath) throws IOException { + requireNonNull(eventBook); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableEventBook(eventBook), filePath); + } + +} diff --git a/src/main/java/seedu/address/storage/event/JsonSerializableEventBook.java b/src/main/java/seedu/address/storage/event/JsonSerializableEventBook.java new file mode 100644 index 00000000000..9bb16aadf44 --- /dev/null +++ b/src/main/java/seedu/address/storage/event/JsonSerializableEventBook.java @@ -0,0 +1,55 @@ +package seedu.address.storage.event; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.EventBook; +import seedu.address.model.ReadOnlyEventBook; +import seedu.address.model.event.Event; + +/** + * An Immutable EventBook that is serializable to JSON format. + */ +@JsonRootName(value = "eventbook") +class JsonSerializableEventBook { + + private final List events = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableEventBook} with the given events. + */ + @JsonCreator + public JsonSerializableEventBook(@JsonProperty("events") List events) { + this.events.addAll(events); + } + + /** + * Converts a given {@code ReadOnlyEventBook} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableEventBook}. + */ + public JsonSerializableEventBook(ReadOnlyEventBook source) { + events.addAll(source.getEventList().stream().map(JsonAdaptedEvent::new).collect(Collectors.toList())); + } + + /** + * Converts this event book into the model's {@code EventBook} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public EventBook toModelType() throws IllegalValueException { + EventBook eventBook = new EventBook(); + for (JsonAdaptedEvent jsonAdaptedEvent : events) { + Event event = jsonAdaptedEvent.toModelType(); + eventBook.addEvent(event); + } + return eventBook; + } + +} diff --git a/src/main/java/seedu/address/ui/EventCard.java b/src/main/java/seedu/address/ui/EventCard.java new file mode 100644 index 00000000000..b8ea1cdb8ad --- /dev/null +++ b/src/main/java/seedu/address/ui/EventCard.java @@ -0,0 +1,55 @@ +package seedu.address.ui; + +import java.time.format.DateTimeFormatter; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.event.Event; + +/** + * An UI component that displays information of a {@code Person}. + */ +public class EventCard extends UiPart { + + private static final String FXML = "EventListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + public final Event event; + + @FXML + private Label description; + @FXML + private HBox cardPane; + @FXML + private Label index; + @FXML + private Label id; + @FXML + private Label startTime; + @FXML + private Label endTime; + @FXML + private Label name; + + + /** + * Creates a {@code PersonCode} with the given {@code Person} and index to display. + */ + public EventCard(Event event) { + super(FXML); + this.event = event; + description.setText("Event: " + event.getDescription()); + name.setText("Candidate: " + event.getPerson().getName().fullName); + startTime.setText("Start Time: " + + event.getStart_time().format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"))); + endTime.setText("End Time: " + event.getEnd_time().format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"))); + } +} diff --git a/src/main/java/seedu/address/ui/EventListPanel.java b/src/main/java/seedu/address/ui/EventListPanel.java new file mode 100644 index 00000000000..5f3c82f5492 --- /dev/null +++ b/src/main/java/seedu/address/ui/EventListPanel.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.event.Event; + +/** + * Panel containing the list of persons. + */ +public class EventListPanel extends UiPart { + private static final String FXML = "EventListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(EventListPanel.class); + + @FXML + private ListView eventListView; + + /** + * Creates a {@code PersonListPanel} with the given {@code ObservableList}. + */ + public EventListPanel(ObservableList eventList) { + super(FXML); + eventListView.setItems(eventList); + eventListView.setCellFactory(listView -> new EventListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. + */ + class EventListViewCell extends ListCell { + @Override + protected void updateItem(Event event, boolean empty) { + super.updateItem(event, empty); + + if (empty || event == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new EventCard(event).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/EventWindow.java b/src/main/java/seedu/address/ui/EventWindow.java new file mode 100644 index 00000000000..6a80e1e6032 --- /dev/null +++ b/src/main/java/seedu/address/ui/EventWindow.java @@ -0,0 +1,51 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Logic; + +/** + * Controller for events window + */ +public class EventWindow extends UiPart { + private static final String FXML = "EventWindow.fxml"; + + private static final Logger logger = LogsCenter.getLogger(EventWindow.class); + + private EventListPanel eventListPanel; + + @FXML + private StackPane eventListPanelPlaceholder; + + /** + * Initialises an EventWindow object + */ + public EventWindow(Stage root, Logic logic) { + super(FXML, root); + eventListPanel = new EventListPanel(logic.getFilteredEventList()); + eventListPanelPlaceholder.getChildren().add(eventListPanel.getRoot()); + } + + /** + * Shows the event window. + */ + public void show() { + logger.fine("Showing events page."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + public boolean isShowing() { + return getRoot().isShowing(); + } + + public void focus() { + getRoot().requestFocus(); + } + + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..1711111376d 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s1-cs2103t-w09-4.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..dda90198bc6 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -12,10 +12,13 @@ import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; import seedu.address.logic.Logic; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; +import seedu.address.model.person.ScoreList; /** * The Main Window. Provides the basic application layout containing @@ -28,13 +31,23 @@ public class MainWindow extends UiPart { private final Logger logger = LogsCenter.getLogger(getClass()); private Stage primaryStage; + private Logic logic; // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private EventListPanel eventListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private EventWindow eventWindow; + + private TagListWindow tagListWindow; + + private PersonInformationPanel personInformationPanel; + + private SummaryStatisticScreen summaryStatisticScreen; + @FXML private StackPane commandBoxPlaceholder; @@ -43,10 +56,18 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane eventListPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; + @FXML + private StackPane personInformationPanelPlaceholder; + + @FXML + private StackPane summaryStatisticScreenPlaceholder; + @FXML private StackPane statusbarPlaceholder; @@ -66,6 +87,8 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + eventWindow = new EventWindow(new Stage(), logic); + tagListWindow = new TagListWindow(new Stage(), logic); } public Stage getPrimaryStage() { @@ -110,6 +133,7 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { + personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); @@ -147,6 +171,30 @@ public void handleHelp() { } } + /** + * Opens the events window. + */ + @FXML + public void handleEvent() { + if (!eventWindow.isShowing()) { + eventWindow.show(); + } else { + eventWindow.focus(); + } + } + + /** + * Opens the tag list window. + */ + @FXML + public void handleListTags() { + if (!tagListWindow.isShowing()) { + tagListWindow.show(); + } else { + tagListWindow.focus(); + } + } + void show() { primaryStage.show(); } @@ -163,10 +211,43 @@ private void handleExit() { primaryStage.hide(); } + /** + * Updates the personListPanel based on last View Command entered + */ + @FXML + private void handleView() { + Index index = logic.getLastViewedPersonIndex(); + Person personToView = logic.getFilteredPersonList().get(index.getZeroBased()); + + personInformationPanel = new PersonInformationPanel(personToView); + personInformationPanelPlaceholder.getChildren().add(personInformationPanel.getRoot()); + + ScoreList scoreList = personToView.getScoreList(); + if (scoreList.isEmpty()) { + logger.info("No score list detected"); + summaryStatisticScreenPlaceholder.getChildren().clear(); + return; + } + + logger.info("Score list detected"); + + + Person personToView2 = logic.getFilteredPersonList().get(index.getZeroBased()); + + + summaryStatisticScreen = new SummaryStatisticScreen(logic.getSummaryStatistic(), personToView2); + summaryStatisticScreenPlaceholder.getChildren().add(summaryStatisticScreen.getRoot()); + } + + public PersonListPanel getPersonListPanel() { return personListPanel; } + public EventListPanel getEventListPanel() { + return eventListPanel; + } + /** * Executes the command and returns the result. * @@ -178,6 +259,7 @@ private CommandResult executeCommand(String commandText) throws CommandException logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + if (commandResult.isShowHelp()) { handleHelp(); } @@ -186,6 +268,19 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + if (commandResult.isView()) { + logger.fine("View command detected"); + handleView(); + } + + if (commandResult.isShowEvent()) { + handleEvent(); + } + + if (commandResult.isListTags()) { + handleListTags(); + } + return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..077919d70a6 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,6 +1,8 @@ package seedu.address.ui; -import java.util.Comparator; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -8,6 +10,8 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; /** * An UI component that displays information of a {@code Person}. @@ -40,6 +44,14 @@ public class PersonCard extends UiPart { private Label email; @FXML private FlowPane tags; + @FXML + private Label linkedIn; + @FXML + private Label github; + + @FXML + private Label score; + /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -52,8 +64,44 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + linkedIn.setText(person.getLinkedIn().value); + github.setText(person.getGithub().value); + setTagLabel(person.getTags()); + } + + private void setTagLabel(Set tagsSet) { + List tagCategories = new ArrayList<>(); + UniqueTagList uniqueTagList = new UniqueTagList(); + for (Tag tag : uniqueTagList.asUnmodifiableObservableList()) { + if (!tagCategories.contains(tag.tagCategory) && !tag.tagCategory.equals("assessment") + && !tag.tagCategory.equals("uncategorised")) { + tagCategories.add(tag.tagCategory); + } + } + + for (Tag tag : tagsSet) { + Label label = new Label(tag.tagName); + + if (tag.tagCategory.equals("assessment")) { //assessment tag + label.getStyleClass().add("label7"); + } + + if (tagCategories.indexOf(tag.tagCategory) == 0) { + label.getStyleClass().add("label2"); + } else if (tagCategories.indexOf(tag.tagCategory) == 1) { + label.getStyleClass().add("label3"); + } else if (tagCategories.indexOf(tag.tagCategory) == 2) { + label.getStyleClass().add("label4"); + } else if (tagCategories.indexOf(tag.tagCategory) == 3) { + label.getStyleClass().add("label5"); + } else if (tagCategories.indexOf(tag.tagCategory) == 4) { + label.getStyleClass().add("label6"); + } else if (tagCategories.indexOf(tag.tagCategory) == 5) { + label.getStyleClass().add("label7"); + } else { // uncategorised + label.getStyleClass().add("label1"); + } + tags.getChildren().add(label); + } } } diff --git a/src/main/java/seedu/address/ui/PersonInformationPanel.java b/src/main/java/seedu/address/ui/PersonInformationPanel.java new file mode 100644 index 00000000000..eebfb99295e --- /dev/null +++ b/src/main/java/seedu/address/ui/PersonInformationPanel.java @@ -0,0 +1,150 @@ +package seedu.address.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleButton; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Person; +import seedu.address.model.person.Status; +import seedu.address.model.person.StatusTypes; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; + +/** + * An UI component that displays information of a {@code Person} + */ +public class PersonInformationPanel extends UiPart { + private static final String FXML = "PersonInformationPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); + private final Person person; + + @FXML + private Label name; + @FXML + private FlowPane tags; + @FXML + private Label phone; + @FXML + private Label address; + @FXML + private Label email; + @FXML + private Label remark; + @FXML + private ToggleButton preliminary; + @FXML + private ToggleButton interviewed; + @FXML + private ToggleButton status; + @FXML + private Label linkedIn; + @FXML + private Label github; + + + /** + * Creates a {@code PersonInformationPanel} with the given {@code Person}. + * @param person the person to display + */ + public PersonInformationPanel(Person person) { + super(FXML); + this.person = person; + name.setText(person.getName().fullName); + phone.setText(person.getPhone().value); + address.setText(person.getAddress().value); + email.setText(person.getEmail().value); + remark.setText(person.getRemark().value); + setTagLabel(person.getTags()); + linkedIn.setText(person.getLinkedIn().value); + github.setText(person.getGithub().value); + setResultButton(person.getStatus(), status); + setButton(person.getStatus()); + } + + private void setTagLabel(Set tagsSet) { + List tagCategories = new ArrayList<>(); + UniqueTagList uniqueTagList = new UniqueTagList(); + for (Tag tag : uniqueTagList.asUnmodifiableObservableList()) { + if (!tagCategories.contains(tag.tagCategory) && !tag.tagCategory.equals("assessment") + && !tag.tagCategory.equals("uncategorised")) { + tagCategories.add(tag.tagCategory); + } + + } + for (Tag tag : tagsSet) { + Label label = new Label(tag.tagName); + + if (tag.tagCategory.equals("assessment")) { //assessment tag + label.getStyleClass().add("label7"); + } + + if (tagCategories.indexOf(tag.tagCategory) == 0) { + label.getStyleClass().add("label2"); + } else if (tagCategories.indexOf(tag.tagCategory) == 1) { + label.getStyleClass().add("label3"); + } else if (tagCategories.indexOf(tag.tagCategory) == 2) { + label.getStyleClass().add("label4"); + } else if (tagCategories.indexOf(tag.tagCategory) == 3) { + label.getStyleClass().add("label5"); + } else if (tagCategories.indexOf(tag.tagCategory) == 4) { + label.getStyleClass().add("label6"); + } else if (tagCategories.indexOf(tag.tagCategory) == 5) { + label.getStyleClass().add("label7"); + } else { // uncategorised + label.getStyleClass().add("label1"); + } + tags.getChildren().add(label); + } + } + + private static void setResultButton(Status status, ToggleButton statusButton) { + StatusTypes statusType = status.getStatusType(); + if (statusType == StatusTypes.OFFERED) { + statusButton.setText("Offered"); + } else if (statusType == StatusTypes.REJECTED) { + statusButton.setText("Rejected"); + } else { + statusButton.setText("Pending"); + } + } + + private void setButton(Status currentStatus) { + StatusTypes statusType = currentStatus.getStatusType(); + if (statusType == StatusTypes.OFFERED) { + status.getStyleClass().clear(); + status.getStyleClass().add("offered-button"); + + preliminary.getStyleClass().clear();; + preliminary.getStyleClass().add("offered-button"); + + interviewed.getStyleClass().clear(); + interviewed.getStyleClass().add("offered-button"); + } else if (statusType == StatusTypes.REJECTED) { + status.getStyleClass().clear(); + status.getStyleClass().add("rejected-button"); + } else if (statusType == StatusTypes.INTERVIEWED) { + preliminary.getStyleClass().clear();; + preliminary.getStyleClass().add("offered-button"); + + interviewed.getStyleClass().clear(); + interviewed.getStyleClass().add("offered-button"); + } else { + preliminary.getStyleClass().clear();; + preliminary.getStyleClass().add("offered-button"); + + } + } + + + + + + +} diff --git a/src/main/java/seedu/address/ui/SummaryStatisticScreen.java b/src/main/java/seedu/address/ui/SummaryStatisticScreen.java new file mode 100644 index 00000000000..46cafa2e039 --- /dev/null +++ b/src/main/java/seedu/address/ui/SummaryStatisticScreen.java @@ -0,0 +1,130 @@ +package seedu.address.ui; + +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.TableView; +import javafx.scene.layout.Region; +import seedu.address.model.person.Person; +import seedu.address.model.statistics.ReadOnlySummaryStatistic; +import seedu.address.model.tag.Tag; + +/** + * Controller for a Summary Statistic page + */ +public class SummaryStatisticScreen extends UiPart { + private static final String FXML = "SummaryStatisticScreen.fxml"; + private final ReadOnlySummaryStatistic summaryStatistic; + private final Person person; + + @FXML + private TableView tableView; + + /** + * Constructor for summary statistic screen. + * @param summaryStatistic summary statistic + * @param person person + */ + public SummaryStatisticScreen(ReadOnlySummaryStatistic summaryStatistic, Person person) { + super(FXML); + this.summaryStatistic = summaryStatistic; + this.person = person; + updateTableView(); + } + + /** + * Updates the table view with the statistic data. + */ + public void updateTableView() { + ObservableList statisticData = FXCollections.observableArrayList(); + statisticData = fillStatisticDataForPerson(statisticData); + tableView.setItems(statisticData); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + } + + /** + * Fills the statistic data for the person. + * @return statistic data + */ + public ObservableList fillStatisticDataForPerson(ObservableList statisticData) { + + + List tags = person.getScoreList().getTagsWithScore(); + + for (Tag tag : tags) { + int mean = summaryStatistic.generateMeanWithTag(tag); + int median = summaryStatistic.generateMedianWithTag(tag); + int min = summaryStatistic.generateMinScoreValueWithTag(tag); + int max = summaryStatistic.generateMaxScoreValueWithTag(tag); + double percentile = summaryStatistic.generatePercentileWithTag(person, tag); + int currentScore = person.getScoreList().getScore(tag).value; + + statisticData.add(new StatisticData(tag.tagName, mean, median, min, max, percentile, currentScore)); + + } + return statisticData; + } + + + /** + * Represents the statistic data. + */ + public static class StatisticData { + private final String tag; + private final int mean; + private final int median; + private final int max; + private final int min; + private final double percentile; + + private final int currentScore; + + /** + * Constructor for statistic data. + * @param tag tag + * @param mean mean + * @param median median + * @param max max + * @param min min + * @param percentile percentile + */ + public StatisticData(String tag, int mean, int median, int min, int max, double percentile, int currentScore) { + this.tag = tag; + this.mean = mean; + this.median = median; + this.min = min; + this.max = max; + this.percentile = percentile; + this.currentScore = currentScore; + } + public String getTag() { + return tag; + } + + public int getMean() { + return mean; + } + + public int getMedian() { + return median; + } + + public int getMin() { + return min; + } + + public int getMax() { + return max; + } + + public double getPercentile() { + return percentile; + } + + public int getCurrentScore() { + return currentScore; + } + } +} diff --git a/src/main/java/seedu/address/ui/TagCard.java b/src/main/java/seedu/address/ui/TagCard.java new file mode 100644 index 00000000000..1c5886c5c2f --- /dev/null +++ b/src/main/java/seedu/address/ui/TagCard.java @@ -0,0 +1,42 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.tag.Tag; + +/** + * An UI component that displays information of a {@code Tag}. + */ +public class TagCard extends UiPart { + + private static final String FXML = "TagListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + public final Tag tag; + + @FXML + private Label tagName; + @FXML + private Label tagCategory; + @FXML + private HBox cardPane; + + + /** + * Creates a {@code PersonCode} with the given {@code Person} and index to display. + */ + public TagCard(Tag tag) { + super(FXML); + this.tag = tag; + tagName.setText("Tag Name: " + tag.getTagName()); + tagCategory.setText("Category: " + tag.getTagCategory()); + } +} diff --git a/src/main/java/seedu/address/ui/TagListPanel.java b/src/main/java/seedu/address/ui/TagListPanel.java new file mode 100644 index 00000000000..c1f3e9ca494 --- /dev/null +++ b/src/main/java/seedu/address/ui/TagListPanel.java @@ -0,0 +1,48 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.tag.Tag; + +/** + * Panel containing the list of tags. + */ +public class TagListPanel extends UiPart { + private static final String FXML = "TagListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(TagListPanel.class); + + @FXML + private ListView tagListView; + + /** + * Creates a {@code TagListPanel} with the given {@code ObservableList}. + */ + public TagListPanel(ObservableList tagList) { + super(FXML); + tagListView.setItems(tagList); + tagListView.setCellFactory(listView -> new TagListPanel.TagListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Tag} using a {@code TagCard}. + */ + class TagListViewCell extends ListCell { + @Override + protected void updateItem(Tag tag, boolean empty) { + super.updateItem(tag, empty); + + if (empty || tag == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new TagCard(tag).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/TagListWindow.java b/src/main/java/seedu/address/ui/TagListWindow.java new file mode 100644 index 00000000000..d458e519549 --- /dev/null +++ b/src/main/java/seedu/address/ui/TagListWindow.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Logic; + +/** + * The window for displaying the list of tags. + */ +public class TagListWindow extends UiPart { + private static final String FXML = "TagListWindow.fxml"; + + private static final Logger logger = LogsCenter.getLogger(TagListWindow.class); + + private TagListPanel tagListPanel; + + @FXML + private StackPane tagListPanelPlaceholder; + + /** + * Initialises an TagListWindow object + */ + public TagListWindow(Stage root, Logic logic) { + super(FXML, root); + tagListPanel = new TagListPanel(logic.getFilteredTagsList()); + tagListPanelPlaceholder.getChildren().add(tagListPanel.getRoot()); + } + + /** + * Shows the tag list window. + */ + public void show() { + logger.fine("Showing tag list page."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + public boolean isShowing() { + return getRoot().isShowing(); + } + + public void focus() { + getRoot().requestFocus(); + } +} diff --git a/src/main/resources/images/jabpro.png b/src/main/resources/images/jabpro.png new file mode 100644 index 00000000000..f6d535e953f Binary files /dev/null and b/src/main/resources/images/jabpro.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..31bcfb3f024 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -132,6 +132,12 @@ -fx-text-fill: #010504; } +.event_cell_small_label { + -fx-font-family: "Segoe UI Semibold"; + -fx-font-size: 18px; + -fx-text-fill: #010504; +} + .stack-pane { -fx-background-color: derive(#1d1d1d, 20%); } @@ -342,11 +348,67 @@ -fx-vgap: 3; } -#tags .label { +.label1 { -fx-text-fill: white; - -fx-background-color: #3e7b91; + -fx-background-color: #3e7b91; // blue -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; -fx-font-size: 11; } + +.label2 { + -fx-text-fill: white; + -fx-background-color: #bc8650; // orange + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +.label3 { + -fx-text-fill: white; + -fx-background-color: #008E8B; // greenish blue + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +.label4 { + -fx-text-fill: white; + -fx-background-color: #114D77; // dark blue + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +.label5 { + -fx-text-fill: white; + -fx-background-color: #bc5150; // weird green + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +.label6 { + -fx-text-fill: white; + -fx-background-color: #9B3B6A; // pink magenta + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +.label7 { + -fx-text-fill: white; + -fx-background-color: #005910; // green + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + + diff --git a/src/main/resources/view/EventListCard.fxml b/src/main/resources/view/EventListCard.fxml new file mode 100644 index 00000000000..4916b477ae2 --- /dev/null +++ b/src/main/resources/view/EventListCard.fxml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/EventListPanel.fxml b/src/main/resources/view/EventListPanel.fxml new file mode 100644 index 00000000000..e3366a5542d --- /dev/null +++ b/src/main/resources/view/EventListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/EventWindow.fxml b/src/main/resources/view/EventWindow.fxml new file mode 100644 index 00000000000..0f21f0f283f --- /dev/null +++ b/src/main/resources/view/EventWindow.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/Form.css b/src/main/resources/view/Form.css new file mode 100644 index 00000000000..4e14064fa1e --- /dev/null +++ b/src/main/resources/view/Form.css @@ -0,0 +1,78 @@ +.white-box { + -fx-background-color: #fff; + -fx-border-color: #ccc; + -fx-padding: 20px; + -fx-pref-width: 300px; + -fx-margin: 0; + -fx-effect: drop shadow(three-pass-box, #888, 2, 2, 5, 0); + -fx-background-radius: 5px; +} + +.text-field { + -fx-background-color: #333333; + -fx-border-color: #fff; + -fx-padding: 3px; + -fx-margin: 10px; + -fx-background-radius: 3px; + -fx-font-family: "Segoe UI"; + -fx-font-size: 12px; + -fx-text-fill: #fff; +} + +.box { + -fx-padding: 10px; + -fx-background-color: #0074d9; + -fx-text-fill: #fff; + -fx-border-width: 0; + -fx-cursor: hand; + -fx-background-radius: 3px; +} + +.box:hover { + -fx-background-color: #0056b3; +} + +.stage-button { + -fx-background-color: #ccc; + -fx-text-fill: black; + -fx-font-weight: bold; + -fx-padding: 5px 10px; + -fx-background-radius: 50; +} + +.offered-button { + -fx-background-color: #005910; + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-padding: 5px 10px; + -fx-background-radius: 50; +} + +.rejected-button { + -fx-background-color: #BC5150; + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-padding: 5px 10px; + -fx-background-radius: 50; +} + +.connection-line { + -fx-border-style: solid; + -fx-border-color: grey; + -fx-border-width: 1px; +} + +.connection-box { + -fx-padding: 5 0; +} + +.connection-line { + -fx-fill: #ccc; + -fx-stroke: #ccc; + -fx-stroke-width: 4px; + -fx-min-width: 100px; + -fx-max-width: 200px; + -fx-min-height: 2px; + -fx-max-height: 2px; +} + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..5b79e08df25 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -11,16 +11,18 @@ + + title="JABPro" minWidth="1000" minHeight="800" onCloseRequest="#handleExit"> - + + @@ -31,6 +33,9 @@ + + + @@ -46,12 +51,34 @@ - + - - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonInformationPanel.fxml b/src/main/resources/view/PersonInformationPanel.fxml new file mode 100644 index 00000000000..81ec8394f99 --- /dev/null +++ b/src/main/resources/view/PersonInformationPanel.fxml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..9ea0cd5d945 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -31,6 +31,8 @@