diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 00000000..36c923bc
Binary files /dev/null and b/.DS_Store differ
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
new file mode 100644
index 00000000..d857d7ec
--- /dev/null
+++ b/.github/workflows/npm-publish.yml
@@ -0,0 +1,24 @@
+# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
+
+name: Node.js Package
+
+on:
+ release:
+ types: [created]
+
+jobs:
+ publish-npm:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ registry-url: https://registry.npmjs.org/
+ - run: npm ci
+ - run: npm publish
+ env:
+ NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
diff --git a/.gitignore b/.gitignore
index a61bf99c..6ff3778d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
-node_modules
-page-components/course-alignment-ctnt.html
-page-components/introduction-ctnt.html
-page-components/list-of-experiments-ctnt.html
-page-components/objective-ctnt.html
-page-components/target-audience-ctnt.html
+node_modules
+page-components
+expbuilds
+exprepos
+.vscode/*
+plugins/
+dist/
+*.log
+.npmrc
diff --git a/KT.org b/KT.org
index 24074e50..4cd714b5 100644
--- a/KT.org
+++ b/KT.org
@@ -1,85 +1,85 @@
-#+TITLE: KT on Phase 3 Lab Processes Automation
-
-* Scope of the Project
-
- This project aims to provide a central interface for performing
- routine tasks related to Phase 3 labs hosted by VLEAD. A command
- line untility is provided that automates the following tasks:
-
- 1. Lab Pages Genreration
- 2. Lab Pages Release Management
- 3. Lab Pages Hosting
- 4. Experiment Generation
- 5. Experiment Hosting
-
- Each of the above topics are described in detail in this document.
-
-** TODO Move the contents of this document to proper places
- This document should only contain meeting notes. All other
- contents should be somewhere else (where?).
-
-
-* Session 1
-
-** Detail
-
- - Date :: [2020-08-31 Mon]
- - Time :: 12:30 PM
- - Participants ::
- - Ojas :: Current Project Maintainer
- - Balamma :: New Project Maintainer (onboarding)
-
-** Agenda
-
- *Introduction to the project* : In this session we will be
- discussion the high level concepts and scope of the project. Ojas
- will demonstrate the operation of the main command line utility
- while explaining what underlying processes are performed.
-
-** Topics
-
-*** Steps for Development Installation
-
- - Repo link :: https://github.com/virtual-labs/Phase-3-Lab-Template.git
- - Branch :: feature-json-validate
-
-*** DONE Lab Descriptor
- - schema can be found here: [[file:labDescSchema.json][schema]]
- - The descriptor should be in the lab repo
- - Found issue in the README documentation. The init command is
- not working. Please copy the descriptor manually. (TO BE FIXED
- BY : Ojas)
-
-*** DONE Lab Repository
- - created a [[https://github.com/virtual-labs/trial-lab-src][new repository]] for demo of lab sources generation.
-
-*** DONE Demo
- - init :: command didn't work. Need to fix
- - all :: generation, deployment and release is working.
- Demonstrated generation and deployment of a sample lab
-
- Found out that links to experiments are not working on
- the local system after changing the links to remove lab
- name. This was working on Pavan's machine. (NEED TO
- CHECK but Balamma knows what to do for this.)
-
-*** Types of Experiments
-
-**** IIITH format
- - ds1 and ds2
- - phase 2 labs
-
-**** IITB format
- - all others
-
-*** TODO Next Step
-
- - Balamma to go through the README and try the installation and
- generation process.
- - Next session on Thursday.
-
-* Session 2
-
-- [2020-09-07 Mon]
-
-
+#+TITLE: KT on Phase 3 Lab Processes Automation
+
+* Scope of the Project
+
+ This project aims to provide a central interface for performing
+ routine tasks related to Phase 3 labs hosted by VLEAD. A command
+ line untility is provided that automates the following tasks:
+
+ 1. Lab Pages Genreration
+ 2. Lab Pages Release Management
+ 3. Lab Pages Hosting
+ 4. Experiment Generation
+ 5. Experiment Hosting
+
+ Each of the above topics are described in detail in this document.
+
+** TODO Move the contents of this document to proper places
+ This document should only contain meeting notes. All other
+ contents should be somewhere else (where?).
+
+
+* Session 1
+
+** Detail
+
+ - Date :: [2020-08-31 Mon]
+ - Time :: 12:30 PM
+ - Participants ::
+ - Ojas :: Current Project Maintainer
+ - Balamma :: New Project Maintainer (onboarding)
+
+** Agenda
+
+ *Introduction to the project* : In this session we will be
+ discussion the high level concepts and scope of the project. Ojas
+ will demonstrate the operation of the main command line utility
+ while explaining what underlying processes are performed.
+
+** Topics
+
+*** Steps for Development Installation
+
+ - Repo link :: https://github.com/virtual-labs/Phase-3-Lab-Template.git
+ - Branch :: feature-json-validate
+
+*** DONE Lab Descriptor
+ - schema can be found here: [[file:labDescSchema.json][schema]]
+ - The descriptor should be in the lab repo
+ - Found issue in the README documentation. The init command is
+ not working. Please copy the descriptor manually. (TO BE FIXED
+ BY : Ojas)
+
+*** DONE Lab Repository
+ - created a [[https://github.com/virtual-labs/trial-lab-src][new repository]] for demo of lab sources generation.
+
+*** DONE Demo
+ - init :: command didn't work. Need to fix
+ - all :: generation, deployment and release is working.
+ Demonstrated generation and deployment of a sample lab
+
+ Found out that links to experiments are not working on
+ the local system after changing the links to remove lab
+ name. This was working on Pavan's machine. (NEED TO
+ CHECK but Balamma knows what to do for this.)
+
+*** Types of Experiments
+
+**** IIITH format
+ - ds1 and ds2
+ - phase 2 labs
+
+**** IITB format
+ - all others
+
+*** TODO Next Step
+
+ - Balamma to go through the README and try the installation and
+ generation process.
+ - Next session on Thursday.
+
+* Session 2
+
+- [2020-09-07 Mon]
+
+
diff --git a/README.org b/README.org
index 75b3682a..fbf2305b 100644
--- a/README.org
+++ b/README.org
@@ -1,206 +1,181 @@
-#+title: Phase 3 Lab and Experiment Operations
-#+setupfile: ./org-html-themes/setup/theme-readtheorg-local.setup
-#+export_file_name: ./docs/index.html
-
-* Introduction
-
- A Virtual Labs /Experiment/ is a systematic collection of learning
- resources for Engineering students to learn about a single topic in
- a subject. Each experiment consists of some theory and reading
- material, interactive simulations and quizes or tests. The
- authoring process may be different for each institute but the end
- result is a collection of web-pages that are published together as
- an /Experiment/.
-
- Each experiment is a part of of /Lab/. A /Lab/ is a conceptually a
- collection of /Experiments/ related to a subject. A /Lab/ is
- published as a collection of web-pages that provide an entry point
- for a user to learn about that subject. The list of experiments
- included in a lab provides links to all the available experiments.
-
- Creating a publishable lab along with all it's experiments, is a
- process that involves a lot of manual effort that does not add any
- value to the lab itself. *Labgen* is a command-line tool that helps
- in avoiding all the repeatative effort involved in creating a lab
- from individual experiments and other lab content.
-
- The following processes are supported by *labgen*:
- 1. Generation of Lab pages.
- 2. Deployment of Lab
- 3. Build and Deployment of Experiments
-
- The automated build and deployment of experiments is supported only
- for experiments generated using the /Phase 3 experiment authoring
- process/ (link?) defined by IIT-B.
-
-
-* Requirements
-
- 1. *node 12.12.x*
- - Download link: [[https://nodejs.org/en/download/][node]]
- - recommended to install via [[https://github.com/nvm-sh/nvm][nvm]]
- 2. *npm 6.11.x*
-
-
-* Setup
-
-** Install Dependencies
-
- #+BEGIN_SRC bash
-
- npm install
-
- #+END_SRC
-
-
-* Generating Lab Pages
-
-
-** Create Lab Repository
-
- Create a new lab repository on [[https://github.com/virtual-labs][Github]] or clone and existing one.
- The repository should be empty.
-
-** Initialize Lab Descriptor
-
- To start with the process of lab generation, you need a /Lab
- Descriptor/.
-
- /Lab Descriptor/ is a json file the contains data for lab pages and
- other information for lab and experiment deployment. [[file:lab-descriptor.org][This]] document
- describes the structure of a lab descriptor file.
-
- Run the following command to initialize a lab descriptor file that
- contains all the required fields, without any values.
-
- #+NAME: labgen-init
- #+BEGIN_SRC bash
- npm run labgen -- init
- #+END_SRC
-
- In the above command == should be replaced with
- the actual path to the local lab repository mentioned in the first
- step.
-
-*** Init Example
-
- For example, if https://github.com/virtual-labs/myNewLab is your
- repository and you clone it on your local system at location
- =/home/myuser/virtual-labs/=, then you would do run the following
- commands -
-
- #+NAME: labgen-init-example
- #+BEGIN_SRC bash
- npm run labgen -- init /home/myuser/virtual-labs/myNewLab
- #+END_SRC
-
- or
-
- #+NAME: labgen-init-example-rel-path
- #+BEGIN_SRC bash
- npm run labgen -- init ~/virtual-labs/myNewLab
- #+END_SRC
-
- This will generate the file
- =~/virtual-labs/myNewLab/lab-descriptor.json=.
-
-** Complete and Verify Lab Descriptor
-
- Fill all the values in the =/lab-descriptor.json=
- file generated above and create a pull-request to get the
- lab-descriptor approved.
-
-
-*** LD Verification Example
-
- Continuing the [[Init Example][init]] example, lets see how to submit the completed
- lab-descriptor for verification.
-
- In the lab repository, switch to a new branch and edit the json.
-
- #+BEGIN_SRC bash
- cd ~/virtual-labs/myNewLab
- git checkout -b lab-descriptor
- editor lab-descriptor.json
- #+END_SRC
-
- After filling the json push the file to remote and create a pull
- request.
-
- #+BEGIN_SRC bash
- git add lab-descriptor.json
- git commit -m "lab-descriptor for verification"
- git push origin lab-descriptor
- #+END_SRC
-
- Once this pull request is merged to master, do not make any new
- changes to the lab-descriptor.
-
-
-** Run All
-
- From the /Phase-3-Lab-Template/ repository, run the following.
-
- #+NAME: labgen-generate
- #+BEGIN_SRC bash
- npm run labgen --
- #+END_SRC
-
-* Updates
-
-** Reporting
-
- The timestamp, verion number and status (success or failure) of the
- run is updated to a google sheet.
-
-** Release management of Lab Sources
-
- After a lab (including all its experiments) is deployed, a new tag
- is created for the lab using the master branch. The tag number
- follows semantic versioning and the next version number is derived
- from the previous version with the help of input provided by the
- user regarding type of the release (major, minor or patch). By
- default a minor version is generated.
-
- The lab descriptor is updated with the current release number.
-
-** Automated JSON Validation
-
- The lab descriptor is validated using a [[file:labDescSchema.json][schema]]. If anything is
- invalid in the lab descriptor, the error is displayed and process
- aborts.
-
-** Deployment Process
-
- 1. Stage all experiments
- 2. Stage Lab
- 3. Deploy Experiments
- 4. Deploy Lab
-
-*** Staging
-
- - /var/www/html/
- - /
- - stage/
- - all lab sources here.
- - exp/
- - all experiments here.
- - contents of the stage/ directory are copied here after
- staging is complete.
-
- If anything fails, you need to start the process again.
-
-** TODO
-
- - [-] move release type to pre run option.
- - [-] log to file
- - [X] remove lab name from path to experiment
- - experiment is unit of development
- - Current url pattern: https://de-iitr.vlabs.ac.in/digital-electronics-iitr/exp/truth-table-gates/
- - New url pattern: https://de-iitr.vlabs.ac.in/exp/truth-table-gates/
- - [X] +symlink for compatibility+
- - [X] test on a lab that is not linked
- - KT to Balamma
- - [-] session 1 scheduled on <2020-08-31 Mon>. Find the [[file:KT.org][minutes]]
- here.
-
+#+title: Phase 3 Lab and Experiment Operations
+#+setupfile: ./org-html-themes/setup/theme-readtheorg-local.setup
+#+export_file_name: ./docs/index.html
+
+* Introduction
+
+ A Virtual Labs /Experiment/ is a systematic collection of learning
+ resources for Engineering students to learn about a single topic in
+ a subject. Each experiment consists of some theory and reading
+ material, interactive simulations and quizes or tests. The
+ authoring process may be different for each institute but the end
+ result is a collection of web-pages that are published together as
+ an /Experiment/.
+
+ Each experiment is a part of of /Lab/. A /Lab/ is a conceptually a
+ collection of /Experiments/ related to a subject. A /Lab/ is
+ published as a collection of web-pages that provide an entry point
+ for a user to learn about that subject. The list of experiments
+ included in a lab provides links to all the available experiments.
+
+ Creating a publishable lab along with all it's experiments, is a
+ process that involves a lot of manual effort that does not add any
+ value to the lab itself. *Labgen* is a command-line tool that helps
+ in avoiding all the repeatative effort involved in creating a lab
+ from individual experiments and other lab content.
+
+ The following processes are supported by *labgen*:
+ 1. Generation of Lab pages.
+ 2. Deployment of Lab
+ 3. Build and Deployment of Experiments
+
+ The automated build and deployment of experiments is supported only
+ for experiments generated using the /Phase 3 experiment authoring
+ process/ (link?) defined by IIT-B.
+
+
+* Requirements
+
+ 1. *node 12.12.x*
+ - Download link: [[https://nodejs.org/en/download/][node]]
+ - recommended to install via [[https://github.com/nvm-sh/nvm][nvm]]
+ 2. *npm 6.11.x*
+
+
+* Setup
+
+** Install Dependencies
+
+ #+BEGIN_SRC bash
+
+ npm install
+
+ #+END_SRC
+
+
+* Generating Lab Pages
+
+** Create Lab Repository
+
+ Create a new lab repository on [[https://github.com/virtual-labs][Github]] or clone and existing one.
+ The repository should be empty.
+
+** Initialize Lab Descriptor
+
+ To start with the process of lab generation, you need a /Lab
+ Descriptor/.
+
+ /Lab Descriptor/ is a json file the contains data for lab pages and
+ other information for lab and experiment deployment. [[file:lab-descriptor.org][This]] document
+ describes the structure of a lab descriptor file.
+
+
+** Complete and Verify Lab Descriptor
+
+ Fill all the values in the =/lab-descriptor.json=
+ file generated above and create a pull-request to get the
+ lab-descriptor approved.
+
+
+*** LD Verification Example
+
+ Continuing the [[Init Example][init]] example, lets see how to submit the completed
+ lab-descriptor for verification.
+
+ In the lab repository, switch to a new branch and edit the json.
+
+ #+BEGIN_SRC bash
+ cd ~/virtual-labs/myNewLab
+ git checkout -b lab-descriptor
+ editor lab-descriptor.json
+ #+END_SRC
+
+ After filling the json push the file to remote and create a pull
+ request.
+
+ #+BEGIN_SRC bash
+ git add lab-descriptor.json
+ git commit -m "lab-descriptor for verification"
+ git push origin lab-descriptor
+ #+END_SRC
+
+ Once this pull request is merged to master, do not make any new
+ changes to the lab-descriptor.
+
+
+** Run All
+
+ From the /Phase-3-Lab-Template/ repository, run the following.
+
+ #+NAME: labgen-generate
+ #+BEGIN_SRC bash
+ npm run labgen -- --release minor
+ #+END_SRC
+
+
+* Building an Experiment
+
+To build an individual experiment first clone the experiment and run
+the following:
+
+
+#+BEGIN_SRC sh
+cd
+git clone https://github.com/virtual-labs/ph3-lab-mgmt/
+cd ph3-lab-mgmt
+npm install
+npm run build-exp ../
+#+END_SRC
+
+Or, from with this repository:
+
+#+BEGIN_SRC sh
+npm run build-exp
+#+END_SRC
+
+The first method is useful when running the script from the github
+actions pipeline.
+
+The experiment can be found in the =build= directory with the
+experiment repository location.
+
+* DOCS
+
+|--------------------------+---------------------------------------------------------------------------|
+| DOCUMENT | DESCRIPTION |
+|--------------------------+---------------------------------------------------------------------------|
+| [[file:docs/lab-build-process.org][Lab Build Process]] | This document describes the lab build process. |
+| [[file:docs/lab-descriptor.org][Lab Descriptor]] | This document describes the structure and functions of the lab-descriptor |
+| [[file:docs/exp-build-process.md][Experiment Build Process]] | Document describing the experiment build process. |
+| [[file:docs/analytics.org][Analytics]] | Lab and Experiment Analytics details. |
+| [[docs/units.org][Units]] | Defines all types of units in an experiment |
+| [[docs/plugins.org][Proposed Plugins]] | Proposal for moving towards a plugin based architecture. |
+|--------------------------+---------------------------------------------------------------------------|
+
+
+* TODO
+
+- DOCS:
+ + Lab Build Process
+ + Lab descriptor - DONE
+ + templates - TODO
+ + staging and deployment process - DONE
+
+ + Experiment build process
+ - experiment descriptor - DONE
+ - experiment model - DONE
+
+ + Analytics - DONE
+
+ + script usage - DONE
+
+- quiz - TODO
+
+- exp process integration and testing - DONE
+
+------------
+
+- identify modules - WIP
+- FAQ - WIP
+
+- label -> backend => build-process - DONE
diff --git a/lab-structure/src/themes/green-icon/css/bootstrap.min.css b/assets/css/bootstrap.min.css
similarity index 100%
rename from lab-structure/src/themes/green-icon/css/bootstrap.min.css
rename to assets/css/bootstrap.min.css
diff --git a/assets/css/common-styles-responsive.css b/assets/css/common-styles-responsive.css
new file mode 100644
index 00000000..2798de90
--- /dev/null
+++ b/assets/css/common-styles-responsive.css
@@ -0,0 +1,97 @@
+.slidecontainer {
+ text-align: center;
+}
+
+.slider {
+ width: 10%;
+}
+
+.text-box {
+ padding: 7px 20px;
+ margin: 8px 0;
+ box-sizing: border-box;
+ width: 14%;
+}
+
+.legend { list-style: none; }
+.legend li { padding-bottom : 1.5vw; width: 20vw; }
+.legend span { border: 0.1vw solid black; float: left; border-radius: 50%;}
+.legend .grey { background-color: grey; }
+.legend .green { background-color: #a4c652; }
+.legend .black { background-color: black; }
+
+.button-input {
+ border-radius: 50vw;
+ background-color: #288ec8;
+ border: none;
+ color: white;
+ padding: 1%;
+ margin-left: 1%;
+ margin-right: 1%;
+ padding-bottom: 1%;
+ padding-top: 1%;
+ padding-left: 2%;
+ padding-right: 2%;
+}
+
+.button-input:hover {
+ background-color:gray;
+ cursor:pointer;
+}
+
+.comment-box {
+ position: relative;
+ padding: 1vw;
+ width: 30vw;
+ text-align: center;
+}
+
+.instruction-box {
+ position: relative;
+ width: 100%;
+ transition: width 0.2s ease-out;
+ border: 0.1vw solid grey;
+ z-index : 10;
+}
+
+.collapsible {
+ background-color: Transparent;
+ color: "grey";
+ cursor: pointer;
+ width: 100%;
+ border: none;
+ text-align: center;
+ outline: none;
+ font-weight: bold;
+ padding-top: 1%;
+ padding-bottom: 1%;
+}
+
+.collapsible::-moz-focus-inner {
+ border: 0;
+}
+
+.active, .collapsible:hover {
+ background-color: "white";
+}
+
+/*The unicode \25BE is for ▾ (Dropdown arrow) */
+.collapsible:after {
+ content: "\25BE";
+ color: "grey";
+ font-weight: bold;
+ float: right;
+ margin-left: 5px;
+}
+
+.active:after {
+ content: "\25B4";
+}
+
+.content {
+ padding: 0 1.8vw;
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.2s ease-out;
+ background-color: "white";
+}
diff --git a/assets/css/common-styles.css b/assets/css/common-styles.css
new file mode 100644
index 00000000..a2f6d804
--- /dev/null
+++ b/assets/css/common-styles.css
@@ -0,0 +1,104 @@
+.slidecontainer {
+ text-align: center;
+}
+
+.slider {
+ width: 10%;
+}
+
+.text-box {
+ padding: 7px 20px;
+ margin: 8px 0;
+ box-sizing: border-box;
+ width: 14%;
+}
+
+.legend{
+ font-size: 1.4vw;
+}
+.legend { list-style: none; }
+.legend li { padding-bottom : 1.5vw; width: 20vw; }
+.legend span { border: 0.1vw solid black; float: left; width: 2vw; height: 2vw; margin-right : 0.5vw; border-radius: 50%;}
+.legend .grey { background-color: grey; }
+.legend .green { background-color: #a4c652; }
+.legend .black { background-color: black; }
+
+.button-input {
+ border-radius: 50vw;
+ background-color: #288ec8;
+ border: none;
+ color: white;
+ padding: 1%;
+ font-size: 1.3vw;
+ margin-left: 1%;
+ margin-right: 1%;
+ padding-bottom: 1%;
+ padding-top: 1%;
+ padding-left: 2%;
+ padding-right: 2%;
+}
+
+.button-input:hover {
+ background-color:gray;
+ cursor:pointer;
+}
+
+.comment-box {
+ position: relative;
+ padding: 1vw;
+ width: 30vw;
+ font-size: 1.5vw;
+ text-align: center;
+}
+
+.instruction-box {
+ position: relative;
+ width: 100%;
+ transition: width 0.2s ease-out;
+ border: 0.1vw solid grey;
+ font-size: 1.5vw;
+ z-index : 10;
+}
+
+.collapsible {
+ background-color: Transparent;
+ color: "grey";
+ cursor: pointer;
+ width: 100%;
+ border: none;
+ text-align: center;
+ outline: none;
+ font-size: 1.5vw;
+ font-weight: bold;
+ padding-top: 1%;
+ padding-bottom: 1%;
+}
+
+.collapsible::-moz-focus-inner {
+ border: 0;
+}
+
+.active, .collapsible:hover {
+ background-color: "white";
+}
+
+.collapsible:after {
+ content: '\25BE';
+ color: "grey";
+ font-weight: bold;
+ float: right;
+ margin-left: 5px;
+}
+
+.active:after {
+ content: "\25B4";
+}
+
+.content {
+ padding: 0 1.8vw;
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.2s ease-out;
+ background-color: "white";
+}
+
diff --git a/ph3-beta-to-ui3.0-conv/ui3template/assets/css/fontawesome.min.css b/assets/css/fontawesome.min.css
similarity index 100%
rename from ph3-beta-to-ui3.0-conv/ui3template/assets/css/fontawesome.min.css
rename to assets/css/fontawesome.min.css
diff --git a/ph3-beta-to-ui3.0-conv/ui3template/assets/css/github-markdown.min.css b/assets/css/github-markdown.min.css
similarity index 100%
rename from ph3-beta-to-ui3.0-conv/ui3template/assets/css/github-markdown.min.css
rename to assets/css/github-markdown.min.css
diff --git a/assets/css/katex.min.css b/assets/css/katex.min.css
new file mode 100644
index 00000000..678802eb
--- /dev/null
+++ b/assets/css/katex.min.css
@@ -0,0 +1 @@
+@font-face{font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")}.katex{text-rendering:auto;font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.16.3"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .hbox{width:100%}.katex .hbox,.katex .thinbox{display:inline-flex;flex-direction:row}.katex .thinbox{max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{border:0 solid;display:inline-block;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}
diff --git a/assets/css/toast.css b/assets/css/toast.css
new file mode 100644
index 00000000..85e04757
--- /dev/null
+++ b/assets/css/toast.css
@@ -0,0 +1,16 @@
+/* assets/css/toast.css */
+.custom-toast-popup {
+ border-radius: 10px; /* Rounded corners */
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); /* Custom shadow */
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: fixed; /* Fixed positioning */
+ top: 50%; /* Center vertically */
+ left: 50%; /* Center horizontally */
+ transform: translate(-50%, -50%); /* Adjust for exact centering */
+ z-index: 1000; /* Ensure it appears above other elements */
+ padding: 10px;
+ color: #fff;
+ font-size: 1rem;
+ }
\ No newline at end of file
diff --git a/assets/css/vlabs-style.css b/assets/css/vlabs-style.css
new file mode 100644
index 00000000..47cfdf51
--- /dev/null
+++ b/assets/css/vlabs-style.css
@@ -0,0 +1,444 @@
+html {
+ overflow-y: auto;
+}
+
+.svc-rating-display{
+ display:flex;
+ flex-direction: column;
+ margin-right: 40px ;
+ margin-bottom: 17px;
+ align-items: center ;
+}
+
+.vl-rating-display {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ padding: 0px 10px;
+ margin-top: -15px; /* Adjust this value as needed */
+}
+.list-of-experiments-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: left;
+ align-items: center;
+ overflow: hidden;
+ flex: 0 0 1%; /*Adjust this value to decrease the width*/
+}
+
+.list-of-experiments-container > div{
+ margin: 1px;
+}
+.list-of-experiments-display-rating {
+ position: relative;
+ top: -10px;
+ left: 20px;
+}
+
+.vlabs-page {
+ height: 100vh;
+ overflow-x: hidden;
+}
+
+.vlabs-header {
+ border-bottom: 8px solid #ff6600;
+ font-family: "Raleway", sans-serif;
+}
+
+.vlabs-page-main {
+ font-size: 1rem;
+ font-family: "Open Sans", sans-serif;
+}
+
+.vlabs-lab-name {
+ font-size: 1.7rem;
+}
+
+.vlabs-footer {
+ font-size: 14px;
+ background: rgb(17, 17, 17);
+ font-family: "Raleway", sans-serif;
+}
+
+.vlabs-footer-sect-name {
+ border-width: 0.15rem;
+ border-style: solid;
+ border-image: linear-gradient(to right, #3ec1d5 20%, #555 0%) 0% 0% 100% 0%;
+}
+
+.vlabs-lab-name {
+ color: #2c99ce;
+}
+
+.vlabs-page-content {
+ font-size: 1.2rem;
+ overflow-y: hidden;
+ font-family: "Raleway", sans-serif;
+}
+
+.social-links > a {
+ color: #fff;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+}
+
+.nav-menu a,
+.simulation-header .dropdown .nav-menu a {
+ color: #3e6389;
+}
+
+.nav-menu .tasks a,
+.simulation-header .dropdown .nav-menu .tasks a {
+ color: #5c5c5c;
+}
+
+.nav-menu a.current-item,
+.simulation-header .dropdown .nav-menu a.current-item {
+ color: #ff6600;
+}
+
+.nav-menu .tasks,
+.simulation-header .dropdown .nav-menu .tasks {
+ font-size: 1rem;
+}
+
+.nav-menu,
+.simulation-header .dropdown .nav-menu {
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+.nav-menu-body {
+ justify-content: center;
+}
+
+.sidebar .nav-menu-body {
+ justify-content: start;
+}
+
+.sidebar {
+ /* border-right: 2px dotted #89a7c4; */
+ border-right: thin solid #e8e8e8;
+ overflow: hidden;
+ box-sizing: content-box;
+}
+
+.popupmenu .vlabs-logo {
+ height: 2.5rem;
+}
+
+@media (max-width: 991px) {
+ .sidebar {
+ max-height: 6000px;
+ transition: max-height 1s ease-in;
+ flex-wrap: nowrap;
+ overflow-y: auto;
+ }
+ .vlabs-logo {
+ height: 3rem;
+ }
+
+ .simulation-header {
+ display: none !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .vlabs-hidden {
+ display: none !important;
+ }
+ #toggle-menu-float-button {
+ display: none;
+ }
+ .bug-report-mobile {
+ display: none;
+ }
+}
+
+.vlabs-header a {
+ margin: 0 1rem;
+ padding: 0.5rem 1rem;
+ color: #2c98cd;
+}
+
+.vlabs-header #headerNavbar a:hover {
+ background: #77bb41;
+ color: #fff;
+ border-radius: 10px;
+}
+
+.vlabs-header #headerNavbar a {
+ border-radius: 10px;
+ transition: 0.3s;
+}
+
+.breadcrumbs,
+.breadcrumbs a,
+.breadcrumbs span {
+ font-size: 1.6rem;
+ color: #337ab7;
+}
+
+.page-name {
+ color: #337ab7;
+}
+
+/*pre-test and post-test page styling fix*/
+
+.answers {
+ font-size: 1rem;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 1rem;
+}
+
+.question {
+ font-weight: 900;
+}
+
+/* feedback */
+#feedback-btn {
+ color: #2c99ce;
+ border-color: #2c99ce;
+}
+
+/* to override markdown styling */
+.markdown-body {
+ font-family: "Raleway", sans-serif;
+ color: #000000;
+ text-align: justify;
+}
+
+/* to fix the extended lines*/
+.markdown-body table tr {
+ border-top: 0;
+}
+
+h1,
+h2,
+h3 {
+ color: #2c99ce;
+}
+
+h2 {
+ padding-top: 2rem;
+ padding-bottom: 1rem;
+}
+
+h3 {
+ font-size: 1.1rem;
+ color: #333333;
+ padding-top: 1rem;
+ text-decoration: underline;
+}
+
+/* for ds experiments - video iframes */
+iframe {
+ width: 100%;
+ height: calc(100vw / 3);
+}
+
+.simulation-container {
+ padding: 0px;
+ height: 100vh;
+ width: 100vw;
+ overflow: hidden;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+}
+
+.responsive-iframe {
+ flex: 1;
+}
+
+/* Style the buttons that are used to open and close the accordion panel */
+.accordion {
+ display: none;
+ margin-left: 20px;
+ color: #337ab7;
+ text-decoration: underline;
+ text-align: right;
+}
+
+/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
+/* .active,
+.accordion:hover {
+} */
+
+/* Style the accordion panel. Note: hidden by default */
+.panel {
+ display: none;
+ background-color: #eee;
+ color: #444;
+ padding: 18px;
+ width: 100%;
+ text-align: left;
+ border: none;
+ outline: none;
+ transition: 0.4s;
+}
+
+/* Customize the label (the container) */
+.container {
+ display: block;
+ position: relative;
+ padding-left: 35px;
+ margin-bottom: 12px;
+ cursor: pointer;
+ font-size: 22px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+/* .form-check input[type="checkbox"] {
+ width: 1rem;
+ height: 1rem;
+ margin: 5px 10px 0px 0px;
+} */
+
+/* Responsive layout - makes the three columns stack on top of each other instead of next to each other */
+@media screen and (max-width: 600px) {
+ .column {
+ width: 100%;
+ }
+}
+
+#difficulty-container,
+.form-check {
+ display: flex;
+ align-items: center;
+}
+
+.form-check input[type="checkbox"] {
+ margin: 0.5em;
+ transform: scale(1.5);
+}
+
+.fix-spacing > * {
+ padding-top: 0;
+ margin-top: 1em;
+}
+
+#toggle-menu-float-button {
+ position: absolute;
+ bottom: 20px;
+ left: 20px;
+ z-index: 1;
+ padding: 0.8em 1em;
+ background: rgba(255, 255, 255, 0.9);
+ cursor: pointer;
+ border-radius: 0.5em;
+ color: #fff;
+ border: 3px solid rgba(0, 174, 255, 0.274);
+ transform: scale(0.9);
+}
+
+.toggle-menu-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+ height: 30px;
+ width: 30px;
+}
+
+.btn-close {
+ box-sizing: content-box;
+ width: 1em;
+ height: 1em;
+ padding: 0.25em 0.25em;
+ color: #000;
+ background: transparent
+ url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e")
+ center/1em auto no-repeat;
+ border: 0;
+ border-radius: 0.25rem;
+ opacity: 0.5;
+}
+
+.tools {
+ top: 50%;
+ transform: translate(0%, 25%);
+}
+
+.simulation-header .navbar-brand .vlabs-logo {
+ height: 3rem;
+}
+
+.simulation-header h1,
+.simulation-header h2 {
+ border: none;
+ font-size: 1.5rem;
+ padding: 0;
+ flex: 2;
+ text-align: center;
+
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ overflow-wrap: break-word;
+}
+
+.simulation-header .dropdown {
+ position: relative;
+ display: inline-block;
+}
+
+.simulation-header .dropdown .backdrop {
+ position: absolute;
+ width: 100vw;
+ background-color: rgba(0, 0, 0, 0.2);
+ height: 100vh;
+}
+
+.simulation-header .dropdown .nav-menu {
+ width: fit-content;
+ background-color: #fff;
+ margin-top: 8px;
+ padding: 10px;
+ box-shadow: 0px 8px 14px 0px rgba(0, 0, 0, 0.2);
+ font-family: "Open Sans", sans-serif;
+ max-height: 80%;
+ overflow: auto;
+}
+
+.simulation-header .dropdown .nav-menu-body {
+ justify-content: flex-start;
+}
+
+.simulation-header .dropdown .vlabs-hidden {
+ display: none;
+}
+
+.expand-1 {
+ flex: 1;
+}
+
+/* Add Button style from virtual styles */
+.v-button {
+ border: none;
+ color: #ffffff;
+ background-color: #288ec8;
+ text-align: center;
+ font-size: 1.05rem;
+ border-radius: 1em;
+ padding: 0.6em 1.2em;
+ cursor: pointer;
+}
+
+.v-button:hover {
+ background-color: #a9a9a9;
+}
+
+.v-button:disabled {
+ background-color: #a9a9a9;
+ cursor: not-allowed;
+}
\ No newline at end of file
diff --git a/lab-structure/src/themes/green-icon/css/font-awesome.min.css b/assets/fonts/font-awesome-4.7.0/css/font-awesome.min.css
old mode 100755
new mode 100644
similarity index 99%
rename from lab-structure/src/themes/green-icon/css/font-awesome.min.css
rename to assets/fonts/font-awesome-4.7.0/css/font-awesome.min.css
index 5578ea5d..540440ce
--- a/lab-structure/src/themes/green-icon/css/font-awesome.min.css
+++ b/assets/fonts/font-awesome-4.7.0/css/font-awesome.min.css
@@ -1,4 +1,4 @@
-/*!
- * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
- * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
- */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
+/*!
+ * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
diff --git a/lab-structure/src/themes/green-icon/fonts/fonts/FontAwesome.otf b/assets/fonts/font-awesome-4.7.0/fonts/FontAwesome.otf
old mode 100755
new mode 100644
similarity index 100%
rename from lab-structure/src/themes/green-icon/fonts/fonts/FontAwesome.otf
rename to assets/fonts/font-awesome-4.7.0/fonts/FontAwesome.otf
diff --git a/lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.eot b/assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.eot
old mode 100755
new mode 100644
similarity index 100%
rename from lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.eot
rename to assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.eot
diff --git a/lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.svg b/assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.svg
old mode 100755
new mode 100644
similarity index 99%
rename from lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.svg
rename to assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.svg
index d7534c97..855c845e
--- a/lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.svg
+++ b/assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.svg
@@ -1,2671 +1,2671 @@
-
-
-
+
+
+
diff --git a/lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.ttf b/assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf
old mode 100755
new mode 100644
similarity index 100%
rename from lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.ttf
rename to assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf
diff --git a/lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.woff b/assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.woff
old mode 100755
new mode 100644
similarity index 100%
rename from lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.woff
rename to assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.woff
diff --git a/lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.woff2 b/assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2
old mode 100755
new mode 100644
similarity index 100%
rename from lab-structure/src/themes/green-icon/fonts/fonts/fontawesome-webfont.woff2
rename to assets/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2
diff --git a/lab-structure/src/themes/green-icon/images/favicon.ico b/assets/images/favicon.ico
similarity index 100%
rename from lab-structure/src/themes/green-icon/images/favicon.ico
rename to assets/images/favicon.ico
diff --git a/lab-structure/src/themes/green-icon/images/favicon.png b/assets/images/favicon.png
similarity index 100%
rename from lab-structure/src/themes/green-icon/images/favicon.png
rename to assets/images/favicon.png
diff --git a/lab-structure/src/themes/green-icon/images/logo-new.png b/assets/images/logo-new.png
similarity index 100%
rename from lab-structure/src/themes/green-icon/images/logo-new.png
rename to assets/images/logo-new.png
diff --git a/ph3-beta-to-ui3.0-conv/ui3template/assets/images/logo.png b/assets/images/logo.png
similarity index 100%
rename from ph3-beta-to-ui3.0-conv/ui3template/assets/images/logo.png
rename to assets/images/logo.png
diff --git a/ph3-beta-to-ui3.0-conv/ui3template/assets/images/popout.png b/assets/images/popout.png
old mode 100755
new mode 100644
similarity index 100%
rename from ph3-beta-to-ui3.0-conv/ui3template/assets/images/popout.png
rename to assets/images/popout.png
diff --git a/assets/images/vlabs-color-small-moe.jpg b/assets/images/vlabs-color-small-moe.jpg
new file mode 100644
index 00000000..f4129424
Binary files /dev/null and b/assets/images/vlabs-color-small-moe.jpg differ
diff --git a/assets/js/assessment.js b/assets/js/assessment.js
new file mode 100644
index 00000000..09b735b0
--- /dev/null
+++ b/assets/js/assessment.js
@@ -0,0 +1,53 @@
+const quizContainer = document.getElementById("quiz");
+const resultsContainer = document.getElementById("results");
+const submitButton = document.getElementById("submit");
+
+
+function showResults() {
+ // gather answer containers from our quiz
+ const answerContainers = quizContainer.querySelectorAll(".answers");
+ answerContainers.forEach(e => e.style.color = "black");
+
+ // keep track of user's answers
+ let numCorrect = 0;
+
+ // for each question...
+ myQuestions.forEach((currentQuestion, questionNumber) => {
+ // find selected answer
+ const answerContainer = answerContainers[questionNumber];
+ const selector = `input[name=question${questionNumber}]:checked`;
+ const userAnswerElement = answerContainer.querySelector(selector);
+ const userAnswer = userAnswerElement ? userAnswerElement.value : undefined;
+
+ // Log the value and type of userAnswer
+ //console.log(`Question ${questionNumber + 1}: userAnswer =`, userAnswer, `, type =`, typeof userAnswer);
+
+ // if answer is correct
+ if (userAnswer === currentQuestion.correctAnswer) {
+ // add to the number of correct answers
+ numCorrect++;
+
+ // color the selected answer green
+ if (userAnswerElement) {
+ userAnswerElement.parentElement.style.color = "lightgreen";
+ }
+ }
+ // if answer is blank
+ else if (userAnswer === undefined) {
+ // color the answers black
+ answerContainers[questionNumber].style.color = "black";
+ }
+ // if answer is wrong
+ else {
+ // color the answers red
+ if (userAnswerElement) {
+ userAnswerElement.parentElement.style.color = "red";
+ }
+ }
+ });
+ // show number of correct answers out of total
+ resultsContainer.innerHTML = `${numCorrect} out of ${myQuestions.length}`;
+}
+
+
+submitButton.addEventListener("click", showResults);
diff --git a/assets/js/assessment_v2.js b/assets/js/assessment_v2.js
new file mode 100644
index 00000000..7c30e47d
--- /dev/null
+++ b/assets/js/assessment_v2.js
@@ -0,0 +1,185 @@
+"use strict";
+
+const quizContainer = document.getElementById("quiz");
+const resultsContainer = document.getElementById("results");
+const submitButton = document.getElementById("submit");
+const difficultyLevels = ["beginner", "intermediate", "advanced"];
+
+let difficulty = [];
+let questions = { all: myQuestions };
+
+const addEventListener_explanations = () => {
+ let accordions = document.getElementsByClassName("accordion");
+ Array.from(accordions).forEach((accordion) => {
+ accordion.addEventListener("click", function () {
+ /* Toggle between adding and removing the "active" class,
+ to highlight the button that controls the panel */
+ accordion.classList.toggle("active");
+
+ /* Toggle between hiding and showing the active panel */
+ let panel = accordion.parentElement.nextElementSibling;
+ if (panel.style.display === "block") {
+ panel.style.display = "none";
+ } else {
+ panel.style.display = "block";
+ }
+ });
+ });
+};
+
+const addEventListener_checkbox = () => {
+ difficulty.forEach((diff) => {
+ let cBox = document.getElementById(diff);
+ cBox.addEventListener("change", function () {
+ if (cBox.checked) {
+ difficulty.push(diff);
+ } else {
+ difficulty.splice(difficulty.indexOf(diff), 1);
+ }
+ updateQuestions();
+ });
+ });
+};
+
+const populateQuestions = () => {
+ let num = 0;
+ myQuestions.forEach((currentQuestion) => {
+ if (difficultyLevels.indexOf(currentQuestion.difficulty) === -1) {
+ currentQuestion.difficulty = "beginner";
+ }
+ if (!(currentQuestion.difficulty in questions)) {
+ questions[currentQuestion.difficulty] = [];
+ }
+ questions[currentQuestion.difficulty].push(currentQuestion);
+
+ currentQuestion.num = num;
+ num += 1;
+ });
+
+ if (Object.keys(questions).length > 2) {
+ document.getElementById("difficulty-label").style.display = "flex";
+ difficultyLevels.forEach((diff) => {
+ if (!(diff in questions)) {
+ return;
+ }
+ difficulty.push(diff);
+ let checkbox = document.getElementById(diff);
+ checkbox.checked = true;
+ checkbox.parentElement.style.display = "flex";
+ });
+ } else {
+ difficultyLevels.forEach((diff) => {
+ if (!(diff in questions)) {
+ return;
+ }
+ difficulty.push(diff);
+ });
+ }
+};
+
+const checkDifficulties = (classlist) => {
+ if (difficulty.length === Object.keys(questions).length - 1) return true;
+ for (let i in difficulty) {
+ if (classlist.contains(difficulty[i])) return true;
+ }
+ // If beginner is checked list the unlisted question as beginner
+ for (let i in difficultyLevels) {
+ if (classlist.contains(difficultyLevels[i])) return false;
+ }
+ if (difficulty.indexOf("beginner") > -1) {
+ return true;
+ }
+};
+
+function updateQuestions() {
+ const quiz = document.getElementById("quiz");
+ const qquestions = quiz.getElementsByClassName("question");
+ for (let i = 0; i < qquestions.length; i += 1) {
+ if (!checkDifficulties(qquestions[i].classList)) {
+ qquestions[i].style.display = "none";
+ qquestions[i].nextElementSibling.style.display = "none";
+ } else {
+ qquestions[i].style.display = "block";
+ qquestions[i].nextElementSibling.style.display = "flex";
+ }
+ }
+}
+
+function showResults() {
+ // gather answer containers from our quiz
+ const answerContainers = quizContainer.querySelectorAll(".answers");
+ // keep track of user's answers
+ let numCorrect = 0;
+ let totalNum = 0;
+
+ // for each question...
+ myQuestions.forEach((currentQuestion) => {
+ // find selected answer
+ if (
+ difficulty.indexOf(currentQuestion.difficulty) === -1 &&
+ difficulty.length !== Object.keys(questions).length - 1
+ )
+ return;
+ let questionNumber = currentQuestion.num;
+ const answerContainer = answerContainers[questionNumber];
+ const selector = `input[name=question${questionNumber}]:checked`;
+ const userAnswer = (answerContainer.querySelector(selector) || {}).value;
+ // Add to total
+ totalNum++;
+
+ // if answer is correct
+ if (userAnswer === currentQuestion.correctAnswer) {
+ // Color the correct answer lightgreen
+ const correctAnswerElement = document.getElementById(
+ "answer" + questionNumber.toString() + userAnswer
+ );
+ correctAnswerElement.style.color = "lightgreen";
+
+ // add to the number of correct answers
+ numCorrect++;
+
+ // Show all explanations
+ if (currentQuestion.explanations) {
+ for (let answer in currentQuestion.answers) {
+ let explanation = currentQuestion.explanations[answer];
+ let explanationButton = document.getElementById(
+ "explanation" + questionNumber.toString() + answer
+ );
+ if (explanation) {
+ explanationButton.parentElement.nextElementSibling.innerHTML = explanation;
+ explanationButton.style.display = "inline-block";
+ } else {
+ explanationButton.style.display = "none";
+ }
+ }
+ }
+ } else if (userAnswer) {
+ // if answer is wrong
+ document.getElementById(
+ "answer" + questionNumber.toString() + userAnswer
+ ).style.color = "red";
+
+ // Show explanation for the selected answer
+ if (currentQuestion.explanations && userAnswer) {
+ let explanation = currentQuestion.explanations[userAnswer];
+ let explanationButton = document.getElementById(
+ "explanation" + questionNumber.toString() + userAnswer
+ );
+ if (explanation) {
+ explanationButton.parentElement.nextElementSibling.innerHTML = explanation;
+ explanationButton.style.display = "inline-block";
+ } else {
+ explanationButton.style.display = "none";
+ }
+ }
+ }
+ });
+
+ // show number of correct answers out of total
+ resultsContainer.innerHTML = `Score: ${numCorrect} out of ${totalNum}`;
+}
+
+populateQuestions();
+addEventListener_explanations();
+addEventListener_checkbox();
+submitButton.addEventListener("click", showResults);
\ No newline at end of file
diff --git a/assets/js/event-handler.js b/assets/js/event-handler.js
new file mode 100644
index 00000000..324cc788
--- /dev/null
+++ b/assets/js/event-handler.js
@@ -0,0 +1,76 @@
+"use-strict";
+
+const Toast = Swal.mixin({
+ toast: true,
+ position: 'center',
+ showConfirmButton: false,
+ timer: 3000,
+ timerProgressBar: true,
+ didOpen: (toast) => {
+ toast.addEventListener('mouseenter', Swal.stopTimer)
+ toast.addEventListener('mouseleave', Swal.resumeTimer)
+ }
+})
+
+document.getElementById('bug-report').addEventListener('vl-bug-report', (event) => {
+ //console.log('Received vl-bug-report event:', event);
+ if (event.detail.status === 200 || event.detail.status === 201) {
+ const learningUnit = document.head.querySelector('meta[name="learning-unit"]').content;
+ const task = document.head.querySelector('meta[name="task-name"]').content;
+ if (window.dataLayer) {
+ window.dataLayer.push({
+ event: "vl-bug-report",
+ "bug-type": event.detail.issues,
+ "learning-unit": learningUnit ? learningUnit : "",
+ "task-name": task ? task : ""
+ })
+ }
+ Toast.fire({
+ icon: 'success',
+ iconColor: "white",
+ background: "#a5dc86",
+ title: 'Bug Reported Successfully',
+ })
+ } else {
+ const error = event.detail.error;
+ console.log('Error details:', error);
+ Toast.fire({
+ icon: 'error',
+ iconColor: "white",
+ color: "white",
+ background: "#f27474",
+ timer: 5000,
+ title: 'Bug Report Failed',
+ text: 'Please try again later',
+ //text: 'Please try again later. Error: ' + error,
+ });
+ }
+});
+
+// Function to handle the rating submit logic
+function handleRatingSubmit(e) {
+ const learningUnit = document.head.querySelector('meta[name="learning-unit"]').content;
+ const task = document.head.querySelector('meta[name="task-name"]').content;
+ if (window.dataLayer) {
+ window.dataLayer.push({
+ event: "vl-rating-submit",
+ "rating": e.detail.rating,
+ "rating-value": e.detail.rating,
+ "learning-unit": learningUnit ? learningUnit : "",
+ "task-name": task ? task : ""
+ });
+ }
+ Toast.fire({
+ icon: 'success',
+ iconColor: "white",
+ background: "#a5dc86",
+ title: 'Rating Submitted Successfully',
+ });
+}
+
+const ratingSubmitElement = document.querySelector('rating-submit');
+if (ratingSubmitElement) {
+ // Wait for the 'vl-rating-submit' event before adding the event listener
+ ratingSubmitElement.addEventListener('vl-rating-submit', handleRatingSubmit);
+}
+
diff --git a/assets/js/iframeResize.js b/assets/js/iframeResize.js
new file mode 100644
index 00000000..fc116cbf
--- /dev/null
+++ b/assets/js/iframeResize.js
@@ -0,0 +1,29 @@
+const sendPostMessage = (mutationList, ob) => {
+ if (mutationList && mutationList.length > 0) {
+ let height = document.scrollingElement.scrollHeight;
+ window.parent.postMessage({
+ frameHeight: height
+ }, '*');
+ }
+}
+
+window.onresize = () => sendPostMessage();
+
+const config = { attributes: true, childList: true, subtree: true };
+
+const observer = new MutationObserver(sendPostMessage);
+observer.observe(document.body, config);
+
+
+
+/* This is only needed when there is a nested iframe, and
+will work only if this script is manualy inserted in the embedded iframe page.
+*/
+window.onmessage = (e) => {
+ if (e.data.hasOwnProperty("frameHeight")) {
+ var iframeDiv = document.querySelector("iframe");
+ if (iframeDiv) {
+ iframeDiv.style["padding-top"] = `${e.data.frameHeight}px`;
+ }
+ }
+};
diff --git a/assets/js/instruction-box.js b/assets/js/instruction-box.js
new file mode 100644
index 00000000..f8152c85
--- /dev/null
+++ b/assets/js/instruction-box.js
@@ -0,0 +1,11 @@
+var collapsibleEl = document.getElementsByClassName("collapsible")[0];
+collapsibleEl.addEventListener("click", function() {
+ this.classList.toggle("active");
+ var content = this.nextElementSibling;
+ if (content.style.maxHeight){
+ content.style.maxHeight = null;
+ } else {
+ content.style.maxHeight = content.scrollHeight + "px";
+ }
+});
+
diff --git a/lab-structure/src/themes/green-icon/js/jquery-3.4.1.slim.min.js b/assets/js/jquery-3.4.1.slim.min.js
similarity index 100%
rename from lab-structure/src/themes/green-icon/js/jquery-3.4.1.slim.min.js
rename to assets/js/jquery-3.4.1.slim.min.js
diff --git a/assets/js/toggleSidebar.js b/assets/js/toggleSidebar.js
new file mode 100644
index 00000000..4059d064
--- /dev/null
+++ b/assets/js/toggleSidebar.js
@@ -0,0 +1,37 @@
+const sidebar = document.querySelector(".sidebar");
+const myModal = new bootstrap.Modal(document.getElementById("popupMenu"));
+const breakpointLg = 992;
+
+function toggle() {
+ const w = $(this).width();
+ if (w < breakpointLg) {
+ myModal.toggle();
+ } else {
+ if (sidebar.classList.contains("vlabs-hidden")) {
+ sidebar.classList.remove("vlabs-hidden");
+ } else {
+ sidebar.classList.add("vlabs-hidden");
+ }
+ }
+}
+
+function simulationHeaderToggle() {
+ const dropdown = document.querySelector(
+ ".simulation-header .dropdown .backdrop"
+ );
+ if (dropdown.classList.contains("vlabs-hidden")) {
+ dropdown.classList.remove("vlabs-hidden");
+ } else {
+ dropdown.classList.add("vlabs-hidden");
+ }
+}
+
+if (document.querySelector(".simulation-header")) {
+ window.addEventListener("click", ({ target }) => {
+ if (!(target.closest(".navbar-toggler") || target.closest(".nav-menu"))) {
+ document
+ .querySelector(".simulation-header .dropdown .backdrop")
+ .classList.add("vlabs-hidden");
+ }
+ });
+}
diff --git a/ph3-beta-to-ui3.0-conv/ui3template/assets/js/webcomponents-loader.min.js b/assets/js/webcomponents-loader.min.js
similarity index 100%
rename from ph3-beta-to-ui3.0-conv/ui3template/assets/js/webcomponents-loader.min.js
rename to assets/js/webcomponents-loader.min.js
diff --git a/ph3-beta-to-ui3.0-conv/ui3template/assets/js/zero-md.min.js b/assets/js/zero-md.min.js
similarity index 100%
rename from ph3-beta-to-ui3.0-conv/ui3template/assets/js/zero-md.min.js
rename to assets/js/zero-md.min.js
diff --git a/assets_plugins/json/bug-report-questions.js b/assets_plugins/json/bug-report-questions.js
new file mode 100644
index 00000000..7467fbaf
--- /dev/null
+++ b/assets_plugins/json/bug-report-questions.js
@@ -0,0 +1,24 @@
+const { ContentTypes } = require("../../enums");
+
+const issues = {
+ [ContentTypes.TEXT]: ["Insufficient Content"],
+ [ContentTypes.VIDEO]: ["Insufficient Content"],
+ [ContentTypes.ASSESMENT]: [
+ "Incorrect Options",
+ "Incorrect Answer",
+ "Incorrect Question",
+ ],
+ [ContentTypes.ASSESSMENT]: [
+ "Incorrect Options",
+ "Incorrect Answer",
+ "Incorrect Question",
+ ],
+ [ContentTypes.SIMULATION]: [
+ "Simulation Not Working",
+ "Incorrect Results/Observations",
+ "Insufficient/Incorrect Instructions",
+ ],
+ DEFAULT: ["Page Not Loading", "Content Not Visible", "Incorrect Content"],
+};
+
+module.exports = issues;
diff --git a/config.js b/config.js
new file mode 100644
index 00000000..338f97f5
--- /dev/null
+++ b/config.js
@@ -0,0 +1,114 @@
+const path = require("path");
+const log = require("./logger.js");
+
+const Lab = {
+ descriptor_name: "lab-descriptor",
+ build_dir: "build",
+ exp_build_dir: "exprepos",
+ exp_deploy_dir: "exp",
+ deployment_dest: "/var/www/html",
+ stage_dir: "stage",
+ ui_template_name: "templates",
+ static_content_dir: "static_content",
+ partials: [
+ ["analytics_head", "lab-analytics-head"],
+ ["analytics_body", "lab-analytics-body"],
+ ["validation_body", "lab-validation-body"],
+ ["commons", "commons"],
+ ["header", "header"],
+ ["breadcrumbs", "lab-breadcrumbs"],
+ ["side_menu", "sidemenu"],
+ ["content", "content"],
+ ["footer", "footer"],
+ ["popup_menu", "popupmenu"],
+ ["simulation_header", "simulation-header"],
+ ["bug_report_mobile", "bug-report-mobile"],
+ ["svc_rating_display", "svc-rating-display"],
+ ["svc_rating_submit", "svc-rating-submit"],
+ ["nav_menu_items", "nav-menu-items"],
+ ],
+ pages: [
+ {
+
+ }
+ ]
+};
+
+const Experiment = {
+ descriptor_name: "experiment-descriptor.json",
+ default_descriptor: "default-experiment-descriptor.json",
+ build_dir: "build",
+ exp_dir: "experiment",
+ ui_template_name: "templates",
+ static_content_dir: "static_content",
+ partials: [
+ ["analytics_head", "analytics-head"],
+ ["analytics_body", "analytics-body"],
+ ["validation_body", "validation-body"],
+ ["meta", "meta"],
+ ["commons", "commons"],
+ ["header", "header"],
+ ["breadcrumbs", "breadcrumbs"],
+ ["tools", "tools"],
+ ["side_menu", "sidemenu"],
+ ["content", "content"],
+ ["footer", "footer"],
+ ["popup_menu", "popupmenu"],
+ ["simulation_header", "simulation-header"],
+ ["bug_report_mobile", "bug-report-mobile"],
+ ["svc_rating_display", "svc-rating-display"],
+ ["svc_rating_submit", "svc-rating-submit"],
+ ["nav_menu_items", "nav-menu-items"],
+ ],
+ optional_pages: [
+ "Observations",
+ "Assignment",
+ "Quiz"
+ ]
+};
+
+const PROJECT_ROOT = path.resolve(__dirname);
+
+
+/*
+Experiment build path
+-------------------------
+This is here to avoid circular dependency between
+Experiment moudle and Task module.
+*/
+function build_path(src) {
+ return path.resolve(src, Experiment.build_dir, path.basename(path.resolve(src)));
+}
+
+function assets_path() {
+ return path.resolve(__dirname, "assets");
+}
+
+function isURL(source) {
+ try {
+ new URL(source);
+ return true;
+ } catch (e) {
+ log.debug(`${source} is not a valid URL`);
+ return false;
+ }
+}
+
+const VLAB_INFRA_CONFIG = {
+ "production": {
+ "url": "https://cdn.vlabs.ac.in/config/vlabs-config.js",
+ "urlAlt": "https://cdn.vlabs.ac.in/config/vlabs-config.umd.cjs"
+ },
+ "testing": {
+ "url": "https://cdn.vlabs.ac.in/config/vlabs-config.js",
+ "urlAlt": "https://cdn.vlabs.ac.in/config/vlabs-config.umd.cjs"
+ }
+};
+
+module.exports.Experiment = Experiment;
+module.exports.Lab = Lab;
+module.exports.build_path = build_path;
+module.exports.assets_path = assets_path;
+module.exports.isURL = isURL;
+module.exports.PROJECT_ROOT = PROJECT_ROOT;
+module.exports.VLAB_INFRA_CONFIG = VLAB_INFRA_CONFIG;
diff --git a/default-code-assessment.json b/default-code-assessment.json
new file mode 100644
index 00000000..0b1ab54e
--- /dev/null
+++ b/default-code-assessment.json
@@ -0,0 +1,22 @@
+{
+ "version": 1,
+ "experiment name": "Experiment Name",
+ "problems": [
+ {
+ "problem name": "Basic Bubble Sort",
+ "description": "Implement basic Bubble Sort algorithm for the given input array",
+ "inputs": [
+ [64,66,20,49,11,79]
+ ],
+ "expected": ["11,20,49,64,66,79"]
+ },
+ {
+ "problem name": "Optimized Bubble Sort",
+ "description": "Implement Optimized Bubble Sort algorithm for the given input array",
+ "inputs": [
+ [64,66,20,49,11,79]
+ ],
+ "expected": ["11,20,49,64,66,79"]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/default-experiment-descriptor.json b/default-experiment-descriptor.json
new file mode 100644
index 00000000..8dd38760
--- /dev/null
+++ b/default-experiment-descriptor.json
@@ -0,0 +1,59 @@
+{
+ "unit-type": "lu",
+ "label": "",
+ "basedir": ".",
+ "LaTeXinMD": false,
+ "code-assessment": {
+ "include": false,
+ "languages": ["javascript"],
+ "position": 5
+ },
+ "service-worker": "/path/to/service-worker.js",
+ "units": [
+ {
+ "unit-type": "aim"
+ },
+ {
+ "target": "theory.html",
+ "source": "theory.md",
+ "label": "Theory",
+ "unit-type": "task",
+ "content-type": "text"
+ },
+ {
+ "target": "pretest.html",
+ "source": "pretest.json",
+ "label": "Pretest",
+ "unit-type": "task",
+ "content-type": "assesment"
+ },
+ {
+ "target": "procedure.html",
+ "source": "procedure.md",
+ "label": "Procedure",
+ "unit-type": "task",
+ "content-type": "text"
+ },
+ {
+ "target": "simulation.html",
+ "source": "simulation/index.html",
+ "label": "Simulation",
+ "unit-type": "task",
+ "content-type": "simulation"
+ },
+ {
+ "target": "posttest.html",
+ "source": "posttest.json",
+ "label": "Posttest",
+ "unit-type": "task",
+ "content-type": "assesment"
+ },
+ {
+ "target": "references.html",
+ "source": "references.md",
+ "label": "References",
+ "unit-type": "task",
+ "content-type": "text"
+ }
+ ]
+}
diff --git a/docs/analytics.org b/docs/analytics.org
new file mode 100644
index 00000000..70cfeb46
--- /dev/null
+++ b/docs/analytics.org
@@ -0,0 +1,98 @@
+#+TITLE: Analytics
+
+* Introduction
+This document describes the analytics information included in a lab
+and experiment page.
+
+The Lab and Experiment pages are tracked for analytics information
+using the google analytics service. This service keeps track usage
+information for each page in a given domain. Currently we collect
+analytics for labs hosted in the [[vlabs.ac.in][vlabs.ac.in]] domain.
+
+The reporting of analytics is done using googld data studio reports
+and is publically available [[https://datastudio.google.com/u/0/reporting/1bVjKkAw-e617LmNE1v_WPdIByVRz2waa/page/5fLPB][here]].
+
+The setup for tracking analytics is done using google tag manager.
+
+*Tag Manager Container: GTM-W59SWTR*
+
+* Mechanism for Collecting Analytics
+There are two types of information collected for any page by google
+analytics:
+
+- predefined variable :: Predefined/inbuilt variables are things like
+ page url, timestamp etc which is common to all webpages. No
+ configuration is required for these variables.
+
+- user defined variables :: These variables are used to store
+ additional information related to the page. For example, in an
+ experiment level page, the experiment name is stored in a user
+ defined variable. The [[https://support.google.com/tagmanager/answer/6164391?hl=en][Datalayer]] facility of google tag manager
+ is used to create these variables.
+
+In the next two sections we discuss the datalayer variables for Lab
+and Experiment level pages.
+
+* Analytics Setup in pages
+
+In order to track any page using google analytics, we need to add two
+scripts in the page.
+
+** Header Script
+The header script contains some javascript code provided by google tag
+manager along with the =dataLayer= object.
+
+Refer to the Html templates for header scripts in [[../page-templates/head.handlebars][lab]] and [[../templates/partials/analytics-head.handlebars][experiment]]
+pages for details.
+
+*NOTE*: The code in the header scripts include the GTM container ID.
+It has been hardcoded in the templates and will need to be changed in
+the unlikely event that the container ID changes.
+
+** Body Snippet
+The body snippet needs to be added at the top of the body (first child
+of the body element).
+
+This snippet also contains the GTM container ID and will have to
+change when the container ID changes.
+
+The body snippet for the lab pages is included in the
+[[../skeleton-new.html][skeleton-new.html]] document which is the common template for all lab
+pages.
+
+The body snippet for the experiment pages is included in the
+[[../templates/partials/analytics-body.handlebars][analytics-body.handlebars]] template.
+
+
+* Lab Analytics
+
+|------------+---------------------------|
+| VARIABLE | DESCRIPTION |
+|------------+---------------------------|
+| labName | Name of the Lab |
+| discipline | Subject Domain of the Lab |
+| college | College Code |
+| phase | Phase Number of the Lab |
+|------------+---------------------------|
+
+All of these fields are given in the lab-descriptor document.
+
+
+* Experiment Analytics
+
+|--------------+---------------------------|
+| VARIABLE | DESCRIPTION |
+|--------------+---------------------------|
+| labName | Name of the Lab |
+| discipline | Subject Domain of the Lab |
+| college | College Code |
+| phase | Phase Number of the Lab |
+| expName | Experiment Name |
+| expShortName | Experiment Short Name |
+|--------------+---------------------------|
+
+All of these fields are given in the lab-descriptor.
+
+
+*NOTE*: *Please get all the fields verified before building a lab.*
+
diff --git a/docs/content-validation.md b/docs/content-validation.md
new file mode 100644
index 00000000..eb395c75
--- /dev/null
+++ b/docs/content-validation.md
@@ -0,0 +1,41 @@
+# Content Validation
+---
+title: "Content Validation"
+summary: "Tools for code and content validation"
+audience: "Experiment Developer"
+date: 2023-05-11T13:49:21.348Z
+tags: []
+phase: "Ext Phase 3"
+author: ["Shreyash Jain","Aditya Malhotra"]
+---
+
+## Introduction
+There is a need for a tool that can validate the code and content of the experiment. This tool will help in catching errors early and since these tools are available directly to the developers they can validate the files, thus it saves time. We have added 4 validation tools, and a dedicated UI webpage to view the logs. The UI is user-friendly which allows the developer to debug easily. These tools are:
+1. Eslint
+This will ensure consistency in the code, will point out bugs and will raise warnings when the coding standards are not followed.
+2. Validate Experiment Descriptor
+The experiment descriptor is the primary file that contains information on how the build directory is going to be created, thus validating it is of high importance.
+3. Validate code-assessment.json
+The `code-assessment.json` file contains the Code assessment problems along with their inputs and
+expected outputs. The component needs the json to be in prescribed format for the data to be parsed. Thus validating it becomes significant.
+4. Validating Assessment Files
+In an experiment, a developer wants to create quizzes. Virtual labs have a standard template for rendering quizzes, thus a developer needs to write a JSON file which will be converted to a quiz. These JSON files are validated via this tool
+5. Checking for HTTP links
+These tools check whether the build directory contains any HTTP links or not.
+The HTTP links bring security concerns and hosting issues.
+
+## How to use
+After the experiment has been build and deployed there will be button on the top named `validation-tool` (as shown in the figure below). On clicking it, a new page will open that contains all the logs of the validation tools.
+![validation_tool1](./images/validation-tool1.jpg)
+
+The page will look like this:
+![validation_tool2](./images/validation-tool2.jpg)
+
+On the left side, there will be a list of all the validation tools. You can select multiple tools, there is an option to choose severity as well. On the right side the logs will be displayed, you will get the file name, to see the details of the error click on the file name. The details will be displayed below the file name.
+
+## How to validate
+Check the build logs, if there are any errors in the build logs then the validation tool will not work. If there are no errors in the build logs then you can use the validation tool.
+
+## Important Points
+1. The validation tool will only work if there are no major errors in the build process. You can check the build logs to see if there are any errors or not.
+2. The validation tool can be viewed in the deployed experiment.
diff --git a/docs/contributors.md b/docs/contributors.md
new file mode 100644
index 00000000..f972fb77
--- /dev/null
+++ b/docs/contributors.md
@@ -0,0 +1,47 @@
+---
+title: contributors
+author: "Shreyash Jain and Aditya Malhotra"
+audience: "Experiment Developer"
+date: 2023-05-11T13:49:21.348Z
+phase: "Ext Phase 3"
+summary: "This document describes the how to enable the contributors page in your experiment."
+tags: []
+---
+
+## Introduction
+We at Virtual Labs have always believed in giving credit where its due. Developers and SMEs now have a dedicated page to get the credit for their contributions in the experiment.
+
+## How to use
+The contributors.md file will look like this:
+```markdown
+EMPTY
+
+## Subject Matter Experts
+| SNo. | Name | Email | Institute | ID |
+| :---: | :---: | :---: | :---: | :---: |
+| 1 | name | email | institute | id |
+
+## Developers
+| SNo. | Name | Email | Institute | ID |
+| :---: | :---: | :---: | :---: | :---: |
+| 1 | name | email | institute | id |
+| 2 | name | email | institute | id |
+```
+This file will be present in the experiment directory. You can add the details of the contributors in the table.
+You will need to remove the `EMPTY` line before adding the details.
+If the file is not present in the experiment directory, you can copy the template above and add it to your experiment directory.
+
+
+## How to verify
+You can verify the contributors page by deploying the experiment. The contributors page will be available in the experiment in the sidebar.
+The page will look like this:
+![Contributors](/docs/images/Contributors.png)
+
+
+## Important Points
+1. The contributors.md file should be present in the experiment directory.
+2. The contributors.md file should be in the above format.
+3. The contributors page will not be rendered if you dont remove the `EMPTY` line.
+
+
+
diff --git a/docs/custom-modules.md b/docs/custom-modules.md
new file mode 100644
index 00000000..4c7ccb63
--- /dev/null
+++ b/docs/custom-modules.md
@@ -0,0 +1,39 @@
+---
+title: Custom Modules
+author: "Shreyash Jain and Aditya Malhotra"
+audience: "Experiment Developer"
+date: 2023-05-11T13:49:21.348Z
+phase: "Ext Phase 3"
+summary: "This document describes how to add custom modules to the experiment."
+tags: []
+---
+
+## Introduction
+In an experiment, there are several theoretical pages like procedure, theory etc. The developer is supposed to write the contents of those pages in a markdown file, later these markdown files are rendered. To render the build script uses the virtual labs CSS and JS. There might be cases that a developer wants to add his own CSS and JS modules to add further functionality to their experiments.
+The new build script allows developers to add their own CSS/JS either as a file or a CDN link.
+
+
+## How to Use
+The developer needs to specify the CSS/JS files in the experiment-descriptor.json file. The developer can specify the CSS/JS files in the following way:
+
+```json
+{
+ "target": "procedure.html",
+ "source": "procedure.md",
+ "js_modules" : ["procedure.js","https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/katex.min.js"],
+ "css_modules" : ["procedure.css","https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/katex.min.css"],
+ "label": "Procedure",
+ "unit-type": "task",
+ "content-type": "text"
+}
+```
+
+As shown in the above snippet the js modules and the css modules will be added to the procedure.html page. The developer can specify the modules for every page in the experiment-descriptor.json file.
+
+## How to verify
+To verify, deploy the experiment and check if the CSS/JS modules are added to the page or not.
+
+## Important points to note
+1. The developer can specify the CSS/JS files in the experiment-descriptor.json file.
+2. The developer can specify the CSS/JS files either as a file or a CDN link.
+3. The developer can specify the CSS/JS files for every page in the experiment-descriptor.json file as long as the unit-type is task.
\ No newline at end of file
diff --git a/docs/exp-build-process.md b/docs/exp-build-process.md
new file mode 100644
index 00000000..276edda6
--- /dev/null
+++ b/docs/exp-build-process.md
@@ -0,0 +1,388 @@
+# Experiment Build Process
+
+## Introduction
+This document describes the Experiment UI build process.
+
+## Conceptual Model of an Experiment
+An experiment is divided into several /learning units/ and /tasks/.
+Learning Units and Tasks are logical units of an experiment that we
+discuss in the next few sections.
+
+## Learning Unit
+
+### Concept of a Learning Unit
+A Learning Unit is a pedagogical term used to denote a set of learning
+material including lessons, exercises and evaluations aimed at
+teaching a particular subset of topics to be covered in a course.
+
+The students have to perform a set of activities of various kind in
+order to achieve the learning objectives of the learning unit. We
+refer to these activities as /Tasks/.
+
+### Structure and Properties of a Learning Unit
+Learning Units have the following properties:
+
+- AIM :: A learning unit always has an aim/learning objective. This
+ aim outlines the topics and concepts that the students are
+ expected to learn from a given learning unit.
+
+- Sub Learning Units :: A learning unit can be further divided into
+ more than one learning units. This is useful when a topic is
+ huge and consists of several sub-topics that require detailed
+ study.
+
+- Tasks :: There can be several tasks in a learning unit. Each task
+ corresponds to a learning activity that the students need
+ to perform.
+
+
+## Task
+### Concept of a Task
+A Learning Unit can be divided into several smaller Learning Units and
+Tasks. Currently, Tasks are the lowest logical units in any
+experiment and cannot be further divided. A task corresponds to a
+single activity that the students need to perform.
+
+### Structure and Properties of a Task
+
+- Content Type :: Each Task has one of the following content types:
+ + Text
+ + Video
+ + Assesment
+ + Simulation
+
+- Source :: File used to build the page related to that task. The
+ nature of this source document depends on the content type
+ and is discussed in the sections on the relevant content
+ types.
+
+- Target :: The Html page to be generated for this task. Each task
+ has a corresponding Html page related to it that contains
+ the content related to this task.
+
+
+
+* Experiment Webpages
+A Virtual Labs Experiment is a collection of webpages containing the
+learning material for the topics covered in the Experiment. These
+webpages are built using content provided by the subject matter
+experts and it can be in several formats. The resulting page and it's
+build process depends on the nature of the content.
+
+### Relation between Learning Units, Tasks and Pages
+
+- Learning Unit :: A learning unit itself does not have a page
+ associated with it. A learning unit has a set of
+ sub-units and tasks.
+
+- Task :: Each task has exactly one page associated with it. The kind
+ of page that is generated for a task depends on the content
+ type of the task.
+
+### Contents supported for Task Pages
+Each task has a corresponding webpage associated with it. This page
+is generated as a result of the build process. The build process of a
+page depends on the content type of the task.
+
+In the following sections we discuss the different content types and
+build process details.
+
+## Text
+A Task with the "text" content type contains reading meterial for the
+learner. It contains mostly textual information and may include
+images and hyperlinks.
+
+**NOTE** : **text content may not contain videos.**
+
+- Source :: A page of text type requires a markdown document
+ containing the source of text. This markdown document is
+ used to build the final page for the task.
+
+- Target :: The target page is build using the markdown source. The
+ markdown content is processed throught a markdown parser
+ and the resulting html is added to the common page
+ template.
+
+
+## Video
+
+A Task with "video" content type contains at least one video in the
+task page. It can also have additional information in the form of
+text, images and hyperlinks.
+
+All the processing for video content type is similar to "text" type.
+
+### What is the difference between text and video content types?
+
+- For Authors ::
+ + Text content cannot have embedded video.
+ + Video content must have at least one embedded video.
+
+- For Developers ::
+ + Embedded videos in "video" type pages need to be processing with
+ analytics information.
+ + If there are embedded videos in a "text" source document, the
+ processing should fail with appropriate error message.
+ + If there is no embedded video in a "video" source document, the
+ processing should fail with appropriate error message.
+
+## Implementation Status
+
+Video and text content types are processed the same way. The
+analytics injection in embedded video pages is to be implemented.
+
+
+## Simulation
+
+A Simulation type page contains interactive simulations that
+demonstrate the concepts learned in the reading material/ videos. A
+simulation is an html document along with all it's assets (css, js,
+images, etc.) required to run the simulation. These pages are
+embedded in the corresponding task page using iframes.
+
+- Source :: The html document that contains the simulation.
+- Target :: The task page is built by inserting an iframe in the
+ common template. This iframe points to the source html
+ document.
+
+One task page can only contain a single simulation.
+
+
+### Assesment
+Assesment type page contains multiple-choice questions related to the
+material in the learning unit.
+
+- Source :: The questions in an assesment are written in a js module
+ with a predefined format that contains questions along
+ with assesment code. The source should provide link to
+ this document.
+- Target :: Page corresponding to the assesment task. The source is
+ included as a script in the task page.
+
+
+## URL Scheme for a page
+~~~
+- github pages: https://virtual-labs.github.io/
+- vlab website: https://.vlabs.ac.in//exp/
+~~~
+/lab-code/ refers to the 2-3 letter code used for the sub-domain of
+the lab. For example: /de-iitr/ for IITR Digital Electronics Labs,
+/ds1/ and /ds2/ for IIITH data structures labs.
+
+Each learning unit has a different directory for all its content. So,
+the filepath for any page is /.
+
+
+## The Feedback Page
+
+
+# Experiment Descriptor
+
+The experiment descriptor is a json document that provides information
+about all the learning units and tasks in an experiment, that is
+required to build an experiment as well as configurations for components like
+code assessment.
+
+The =experiment-descriptor.json= needs to be provided by the
+experiment authors in the root of the experiment repository. If it is
+not present then a default-experiment-descriptor.json is used.
+
+## Elements of a descriptor
+### Task Object
+A Task Object describes a task. For example:
+
+~~~
+{
+ "unit-type": "task",
+ "label": "Overview",
+ "LaTeXinMD": "boolean",
+ "content-type": "video",
+ "source": "overview.md",
+ "target": "overview.html"
+}
+~~~
+
+
+The above json object represents a task. It is identified as a task
+by the field =unit-type=.
+
+The =label= field is used as a label for the unit's link in the
+sidemenu.
+
+The =content-type= field describes the type of content in the target
+html page generated for this unit.
+
+The =source= field gives a relative link to the source document
+required to build the page to be generated for the task. The path of
+the page to be generated is given in the =target= field.
+
+### Learning Unit Object
+A Learning Unit object describes a learning unit. For example:
+
+~~~
+{
+ "unit-type": "lu",
+ "label": "Bubble Sort",
+ "basedir": "bubble-sort",
+ "code-assessment": {
+ "include": false,
+ "languages": ["javascript"],
+ "position": 5
+ },
+ "units": [...]
+
+}
+~~~
+
+The above object describes a learning unit. Observe that a learning
+unit does not have source and target fields. This is because no page
+is generated for a learning unit. Pages are only generated for tasks
+in a learning unit.
+
+Some additional fields:
+
+- basedir :: This field gives relative path to the directory that
+ contains all the contents (tasks and units) in this unit.
+ All the tasks and units use this directory as root and
+ relative paths are computed accordingly.
+
+- LaTeXinMD :: This feild gives LaTeX support into the markdown using KaTeX integration. The feature is controlled by a flag which needs to put on the experiment descriptor.
+
+- code-assessment :: Configuration for the code-assessment component. It includes options like languages to choose from, position/index in the List of all Units.
+
+- units :: This is a list of units (Tasks and LUs) that are contained
+ within this learning unit.
+
+### Aim Object
+
+Each Learning Unit needs to have an Aim. This aim describes the
+learning objectives of a unit. This Aim Object denotes the fact that
+an Aim is required in this learning unit.
+
+*Each learning unit should have the aim object as the first object in
+its list of units.* (This is not enforced as of now, but it is a good
+practice.)
+
+The aim object looks as follows:
+
+~~~
+{
+ "unit-type": "aim"
+}
+~~~
+
+An aim object is a special case of task, and is similar to the
+following task:
+
+~~~
+{
+ "unit-type": "task",
+ "label": "Aim",
+ "content-type": "text",
+ "source": "aim.md",
+ "target": "index.html"
+}
+~~~
+
+
+## Default Descriptor
+
+- assumptions
+
+- the experiment authors follow the experiment structure prescribed by
+ the template.
+ - The directory structure is preserved.
+ - The file names remain the same and are used for the intended
+ purpose.
+ - Refer to the [default descriptor](https://github.com/virtual-labs/ph3-lab-mgmt/blob/master/default-experiment-descriptor.json) for details.
+
+- failure scenarios
+ - The main reason for why the experiment build process fails is
+ missing files.
+
+ - *How does the build script know which files to look for?*
+
+ - The experiment build process depends on the experiment
+ descriptor for all the experiment structure related information.
+ The experiment descriptor is a json document that lists all the
+ learning units and tasks in an experiment. It contains
+ information regarding the html pages to build for each unit and
+ their dependencies in the form of relative file paths. The
+ build script looks for each of these dependencies in the given
+ path in the experiment repository and it exits with failure if
+ any of the dependent files are missing. The experiment
+ descriptor is written by the authors but it is optional and
+ usually not needed. If the authors do not write the experiment
+ descriptor then a default descriptor is used.
+
+ - There are two possible scenarios ::
+ 1. Custom Experiment Descriptor - If the authors have provided an
+ experiment descriptor in the root directory of the experiment
+ repository. This is done when a different experiment structure
+ is required from the one given in the template.
+
+ 2. Default Experiment Descriptor - This is the most common
+ scenario. The authors do not provide any experiment descriptor
+ and follow the prescribed experiment template. In this case
+ the default experiment descriptor is used.
+
+## Structure of an experiment descriptor
+
+The experiment descriptor contains a single Learning Unit object that
+acts as the root unit for the experiment.
+
+See [this](https://github.com/virtual-labs/exp-bubble-sort-iiith/blob/main/experiment-descriptor.json) descriptor for an example
+
+### Problems and Limitations of the experiment descriptor
+The experiment descriptor is intended to provided a mechanism to make
+the experiment structure more flexible. The authors get the freedom
+to define their own directory structure and file names and divide
+their experiment into several learning units. However, the current
+implementation supports this under certain limits. In this section we
+didcuss the limitations of the functionalities supported by the
+experiment descriptor.
+
+#### No support for Nested Learning Units
+The experiment descriptor is designed with flexibility in mind. It
+allows for a recursive structure where each learning unit can contain
+several nested units. But, the current implementation does not
+support this. It assumes the following:
+
+- The top level learning unit that represents the experiment contains
+ all the learning units listed as its direct children in the "units"
+ field.
+
+- The inner learning units contain only tasks, no learning units.
+
+The main challenge in supporting nested learning units is the
+structure and functioning of the side menu. The side menu is a list
+of links to all the units in the experiment that is presented at the
+left side of each page in the experiment. The details of the side
+menu are discussed [here](https://github.com/virtual-labs/ph3-lab-mgmt/blob/master/docs/exp-side-menu.org). The side menu contains a drawer for each
+learning unit that contains links to all the units contained in that
+unit.
+
+The challenge is to design the user interface that facilitates easy
+navigation with nested drawers for nested learning units. The drawers
+a separated from each other visually by a horizontal left margin that
+represents its level in the heirarchy and when you go deeper in the
+nested structure it might be difficult to accomodate the spacing.
+
+
+#### The Label field in the top level unit object
+The top level object is a learning unit that represents the entire
+experiment. A learning unit object has a *label* field which is used
+as a label for the side menu drawer for that unit. Because the side
+menu does not have a drawer for the experiment unit this is field is
+not used anywhere.
+
+This can cause confusion to the authors who might assume that this
+field is used for the name of the experiment whereas the experiment
+name comes from the "experiment-name.md" document.
+
+
+## Unit Types
+
+- lu :: Learning Unit
+- task :: Task
+- aim :: Aim
diff --git a/docs/exp-side-menu.md b/docs/exp-side-menu.md
new file mode 100644
index 00000000..37290ce4
--- /dev/null
+++ b/docs/exp-side-menu.md
@@ -0,0 +1,33 @@
+# Experiment UI - Side Menu
+
+## INTRODUCTION
+This document describes the side menu of the experiment user interface.
+
+## PURPOSE OF THE SIDE MENU
+The side menu is intended as a navigation tool for the user. It allows the user to navigate different parts/units of the experiment that are divided into different pages. The side menu highlights the current page to make it easy for the user to track their location in the experiment.
+
+## FEATURES
+The side menu provides the following features:
+
+### Links to all the pages in the experiment
+The side menu provides links to all the pages in the experiment. The default pages currently supported are:
+1. Aim - aim.md
+2. Theory - theory.md
+3. Pretest - pretest.json - [Instructions](https://github.com/virtual-labs/ph3-exp-template/blob/main/experiment/README.md) to populate this file
+4. Procedure - procedure.md
+5. Simulation - populate simulation directory
+6. Posttest - posttest.json - [Instructions](https://github.com/virtual-labs/ph3-exp-template/blob/main/experiment/README.md) to populate this file
+7. Contributors - contributors.md - Captures the names and contact information of SME's and the developers of the experiment. If required the developer may change the presentation template and also add a Credit section to this file.
+8. References/Further Reading - references.md
+9. Feedback - It is populated by the script and does not need any inputs from the developer
+
+This menu can be customized by making changes to the experiment-descriptor.json as detailed [here](https://github.com/virtual-labs/ph3-lab-mgmt/blob/master/docs/exp-build-process.md#experiment-descriptor).
+
+### Highlights the link to the current page
+To facilitate tracking the current page, the side menu highlights the link to the current page in a different text color.
+
+### Drawers for links to nested units
+There are three kinds of units: Learning Unit, Task, and Aim. Out of these units, only Task and Aim units have a page. Learning units do not have a page. This difference is reflected in the side menu as follows:
+
+1. TASK AND AIM : For Task and Aim units, the side menu provides links to the corresponding pages.
+2. LEARNING UNIT : Learning units do not have pages associated with them. The side menu has a drawer in place of a link for each learning unit. The drawer groups all links to the units within the learning unit and the user can open/close the drawer to access the links contained within.
\ No newline at end of file
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 00000000..56743f26
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,147 @@
+# Frequently Asked Questions
+
+## What is a Lab?
+
+A Lab is a collection of virtual experiments on a particular topic
+provided to the students in the form of webpages and can be found at
+[www.vlab.co.in](https://www.vlab.co.in/).
+
+
+## What do you mean by the Lab build process?
+
+A Lab is published in the form of a collection of webpages consisting
+of all the learning material divided into experiments. The lab build
+process is responsible for building these webpages using the source
+material created by the lab authors. The experiment build process is
+a part of the lab build process and is responsible for building the
+webpages for a particular experiment in the lab.
+
+Details about the complete lab build process can be found [here](./lab-build-process.org).
+
+
+## What is the difference between developing a lab and building a lab?
+
+### Lab Development
+
+A lab is developed by subject matter experts at different
+universities. The lab development process involves writing the
+necessary information about the lab as well as creating learning
+material for each experiment (which includes creating reading
+material, assesment quizzes, instructional videos, and most
+importantly the interactive simulations).
+
+The details of lab related information to be provided can be found
+here.
+
+After development of a lab is complete the development team has to
+create a hosting request to get the lab published. The details of the
+process to get a lab published can be found here.
+
+All labs are published by the VLEAD Team at IIIT Hyderabad. To
+publish a lab, the lab sources (including the experiments) have to be
+first converted into webpages. This is done by the lab build process.
+
+
+### Lab Building
+
+Upon recieving a hosting request for a lab, the hosting team at VLEAD
+takes the lab information and experiment sources, and generates the
+webpages for the lab related information and for each experiment.
+
+In short the sequence of processes is as follows:
+
+LAB DEVELOPMENT -> HOSTING REQUEST -> LAB BUILD -> PUBLISH
+
+
+## Who develops a lab?
+
+The subject matter experts on a given topic are responsible for
+developing a lab. These are usually faculty members at universities
+affiliated with Virtual Labs.
+
+
+## Who builds a Lab?
+
+The VLEAD hosting team builds and publishes a lab when requested by
+the developers.
+
+
+## What is a Lab Descriptor?
+
+A Lab Descriptor is a document that contains all the relevant
+information about a lab. It can be found in the repository of a lab.
+Details of lab descriptor can be found
+[here](docs/lab-descriptor.org).
+
+
+## What is the format of a Lab Descriptor?
+
+A lab descriptor is a json document containing an object with key
+value pairs.
+
+## What is the structure of a Lab Descriptor?
+
+The structure of a lab descriptor can be found in
+[this](docs/lab-descriptor.org) document. Please refer to the
+[schema](../labDescSchema.json) for a formal definition.
+
+## Who writes a Lab Descriptor?
+
+A lab descriptor is created by the hosting engineer at VLEAD. This is
+because VLEAD is responsible for creating labs. The different team
+that work on developing experiments do not create lab.
+
+## Is it necessary to write a Lab Descriptor?
+
+Yes, lab descriptor must be created and verified before the lab is
+built. The build process checks if the descriptor is present and
+valid.
+
+## What is an Experiment?
+An experiment is a collection of webpages that contain learning
+resources for a particular topic of interest.
+
+
+# What is the process of building an experiment from the sources?
+The process of building an individual experiment from its sources
+provided by the authors, it described in [this](exp-build-process.org)
+document.
+
+
+## Who develops an experiment?
+An experiment is developed by subject matter experts at various
+insitutes. The development teams follow the given
+[template](https://github.com/virtual-labs/ph3-exp-template/) to
+develop experiments.
+
+## Who builds an experiment?
+The experiment is built in two environment:
+1. Testing: The teams can test their experiments while developing
+ them. This happens automatically using the experiment build
+ scripts and github actions.
+
+2. Production: This is done as a part of the lab build process by the
+ hosting engineer.
+
+
+## What is an Experiment Descriptor?
+An experiment descriptor is used to as a map for building the
+experiments. It describes all the elements that go into an experiment
+and where to find the sources for each page.
+
+## What is the format of an Experiment Descriptor?
+The experiment descriptor is a json document. Find a sample
+experiment descriptor
+[here](https://github.com/virtual-labs/exp-bubble-sort-iiith/blob/main/experiment-descriptor.json).
+
+## What is the structure of an Experiment Descriptor?
+Details of the experiment descriptor can be found
+[here](exp-build-process.org).
+
+## Who wirtes an Experiment Descriptor?
+The experiment descriptor is written by the developers of the
+experiment.
+
+## Is it necessary to write an experiment descriptor?
+No. The experiment descriptor is needed when you want a structure
+that is different from the one prescribed by the template.
diff --git a/docs/images/Contributors.png b/docs/images/Contributors.png
new file mode 100644
index 00000000..e9d633e7
Binary files /dev/null and b/docs/images/Contributors.png differ
diff --git a/docs/images/validation-tool1.jpg b/docs/images/validation-tool1.jpg
new file mode 100644
index 00000000..a35ecf1b
Binary files /dev/null and b/docs/images/validation-tool1.jpg differ
diff --git a/docs/images/validation-tool2.jpg b/docs/images/validation-tool2.jpg
new file mode 100644
index 00000000..36a6c379
Binary files /dev/null and b/docs/images/validation-tool2.jpg differ
diff --git a/docs/index.html b/docs/index.html
deleted file mode 100644
index 13d7d49c..00000000
--- a/docs/index.html
+++ /dev/null
@@ -1,525 +0,0 @@
-
-
-
-
-
-
-
-Phase 3 Lab and Experiment Operations
-
-
-
-
-
-
-
-
-
-
-
-
-
-A Virtual Labs Experiment is a systematic collection of learning
-resources for Engineering students to learn about a single topic in
-a subject. Each experiment consists of some theory and reading
-material, interactive simulations and quizes or tests. The
-authoring process may be different for each institute but the end
-result is a collection of web-pages that are published together as
-an Experiment.
-
-
-
-Each experiment is a part of of Lab. A Lab is a conceptually a
-collection of Experiments related to a subject. A Lab is
-published as a collection of web-pages that provide an entry point
-for a user to learn about that subject. The list of experiments
-included in a lab provides links to all the available experiments.
-
-
-
-Creating a publishable lab along with all it's experiments, is a
-process that involves a lot of manual effort that does not add any
-value to the lab itself. Labgen is a command-line tool that helps
-in avoiding all the repeatative effort involved in creating a lab
-from individual experiments and other lab content.
-
-
-
-The following processes are supported by labgen:
-
-
-
Generation of Lab pages.
-
Deployment of Lab
-
Build and Deployment of Experiments
-
-
-
-The automated build and deployment of experiments is supported only
-for experiments generated using the Phase 3 experiment authoring
-process (link?) defined by IIT-B.
-
-Create a new lab repository on Github or clone and existing one.
-The repository should be empty.
-
-
-
-
-
-
4.2 Initialize Lab Descriptor
-
-
-To start with the process of lab generation, you need a Lab
-Descriptor.
-
-
-
-Lab Descriptor is a json file the contains data for lab pages and
-other information for lab and experiment deployment. This document
-describes the structure of a lab descriptor file.
-
-
-
-Run the following command to initialize a lab descriptor file that
-contains all the required fields, without any values.
-
-
-
-
npm run labgen -- init <path/to/lab/repo>
-
-
-
-
-In the above command <path/to/lab/repo> should be replaced with
-the actual path to the local lab repository mentioned in the first
-step.
-
-
-
-
-
4.2.1 Init Example
-
-
-For example, if https://github.com/virtual-labs/myNewLab is your
-repository and you clone it on your local system at location
-/home/myuser/virtual-labs/, then you would do run the following
-commands -
-
-This will generate the file
-~/virtual-labs/myNewLab/lab-descriptor.json.
-
-
-
-
-
-
-
4.3 Complete and Verify Lab Descriptor
-
-
-Fill all the values in the <path/to/lab/repo>/lab-descriptor.json
-file generated above and create a pull-request to get the
-lab-descriptor approved.
-
-
-
-
-
-
4.3.1 LD Verification Example
-
-
-Continuing the init example, lets see how to submit the completed
-lab-descriptor for verification.
-
-
-
-In the lab repository, switch to a new branch and edit the json.
-
-
-
-
cd ~/virtual-labs/myNewLab
-git checkout -b lab-descriptor
-editor lab-descriptor.json
-
-
-
-
-After filling the json push the file to remote and create a pull
-request.
-
-Once this pull request is merged to master, do not make any new
-changes to the lab-descriptor.
-
-
-
-
-
-
-
4.4 Generate Lab
-
-
-From the Phase-3-Lab-Template repository, run the following.
-
-
-
-
npm run labgen -- generate <path/to/lab/repo>
-
-
-
-
-This will generate the lab pages using the verified
-lab-descriptor.json from the lab repository and push the
-generated content to remote.
-
-
-
-
-
-
4.5 Deploy Lab and Experiments
-
-
-From the Phase-3-Lab-Template repository, run the following.
-
-
-
-
npm run labgen -- deploy <path/to/lab/repo>
-
-
-
-
-This will copy the lab pages to /var/www/html/<lab-name>, and
-then build and copy experiments listed in the lab-descriptor to
-/var/www/html/<lab-name>/<exp>.
-
-
-
diff --git a/process.org b/docs/lab-build-process.org
similarity index 63%
rename from process.org
rename to docs/lab-build-process.org
index ccbc416d..0206397d 100644
--- a/process.org
+++ b/docs/lab-build-process.org
@@ -1,161 +1,135 @@
-#+TITLE: Process
-
-* Introduction
-
- This document describes the page generation process.
-
-* Process
-
-** Generate Pages
-
- [[file:page-generation.png][file:page-generation.png]]
-
- =generate= function is reponsible for generating the lab pages. It
- takes the labpath and generates all the html pages required for the
- lab.
-
-
-*** Dependencies
- - Template :: The template html file that defines the common
- skeleton for all pages.
- - Config :: This contains information about which pages are
- generated using which templates.
- - Lab Descriptor :: This is a json document that contains all the
- lab related data. This should be present in
- the lab repository, on the master branch.
- - Page Templates :: Each page in the lab is generated using the
- top level skeleton and a page specific
- template. The data for the page specific
- template comes from the lab descriptor. All
- the page templates are placed in a directory
- called =page-templates=.
-
-*** Generating Components
- The components are generated using templates. Templates are found
- in the [[file:page-templates][page-templates]] directory.
-
- The =page-templates= directory contains [[https://handlebarsjs.com/][handlebarsjs]] templates.
- Each of these templates is one of two types:
- - Page Specific Template :: Page specific templates are defined
- for components that are required in a single page.
- - Common Template :: Common Templates are used to generate
- components like headers, footers etc. that are placed in each
- page.
-
-**** Difference Between IIITH and Other Lab pages
- The IIITH lab pages are similar to all other lab pages, except
- for the List of Experiments Page. The list of experiments page
- has different template for the two types of experiments.
-
- - Template for IIITH Labs :: nested-list-of-experiments-ctnt.handlebars
- - Template for Other Labs :: list-of-experiments-ctnt.handlebars
-
- As the templates are different, the data also comes from
- different fields in the lab-descriptor.
-
- - IIITH Lab List of Experiments Field :: =experiments=
- - Other Lab List of Experiments Field :: =nested-experiments=
-
- Only one of these fields should be present in a lab-descriptor.
-
- *NOTE*: If both fields are present, the lab pages will not be
- generated.
-
-*** Generate Lab
-
- Once the pages are generated (generated pages are present in
- =page-components=) the lab is generated using the following:
-
- - Lab Structure :: The =lab-structure= directory contains the
- files and directories needed to make the pages
- work. Like the needed css and js files are
- present here. This directory also contains a
- makefile that contains the recipe for building
- the lab using the sources.
- - Page Components :: Page Components are the html pages generated
- using the data and templates.
-
- The lab structure directory is copied to the =labpath= directory
- (that contains the lab-descriptor) and then the generated pages
- are copied there.
-
- Finally, the makefile is executed in the =labpath/src= directory.
-
-** Build and Stage Experiments
-
- [[file:deployment-process.jpg][file:deployment-process.jpg]]
-
- After the lab pages are built, the experiments need to built and
- staged.
-
- The =deployExperiments= function handles this process.
-
- Here again, we make a distinction between IIITH Experiments and
- Other experiments. The type of experiments is decided based on the
- =collegeName= field in the lab-descriptor. If the =collegeName= is
- =IIITH= then all experiments are identified as =IIITH= experiments.
-
- The reason we make this distinction, is because these two types of
- experiments have a different build process.
-
-*** IIITH Experiments
- The IIITH Experiments are designed in such a way that there are
- three repositories that contain different content for each
- experiments.
-
- - content-html :: the main pages.
- - artefacts :: the artefacts like images, simulations etc.
- - ? ::
-
- The build process requires us the clone the =content-html=
- repository and run the makefile. This makefile takes care of the
- entire build process.
-
-*** Other Experiments
- For all experiments that are developed using the =IITB Development
- Process=, the build process is different than that of =IIITH=
- experiments. The experiment content is authored in markdown and
- the simulations are embedded in iframes.
-
- The scripts for building and hosting these experiments is
- available in a separate [[https://github.com/virtual-labs/ph3-beta-to-ui3.0-conv][repository]]. We provide the repo URL and
- the version of the sources to use for our deployment. Our scripts
- then clones the repo, and executes the scripts on our behalf.
-
-** Stage Lab
-
- If the build and staging process of experiments is successful, we
- move on to the process of staging the lab.
-
- Here we basically just copy the contents of the =build= directory
- from the labpath to the staging location, which is a directory on
- the server.
-
-** Deploy
-
- If the staging of experiments and lab sources succeeds the last
- step is to place the lab sources and the experiments to their final
- location on the server.
-
- *NOTE*: We assume that is process is not prone to any known errors.
-
-** Release Management
-
- After each successful lab generation and deployment process, the
- lab sources are pushed to its repository along with the timestamp
- in its commit message.
-
- A new release of the sources is made by incrementing the previous
- release number. The numbering scheme follows semantic versioning.
- The type of release determines the version number.
-
-
-*** Version scheme
-
- Every release follows the scheme : =v..=. Where X, Y, and
- Z are non-negative integers.
-
- Types of releases:
- - Major :: X is incremented and Y, Z become 0.
- - Minor :: X stays the same. Y is incremented and Z becomes 0.
- - Patch :: X and Y remain the same. Z is incremented.
+#+TITLE: Lab Build Process
+
+* Introduction
+This document describes the lab build process.
+
+* Process
+
+** Generate Pages
+
+ [[file:page-generation.png][file:page-generation.png]]
+
+*** Assumptions
+- There exists a git repository for the lab.
+- The lab repository is available on the machine that runs this
+ script.
+- The lab repository has a remote (usually on
+ https://github.com/virtual-labs/)
+- The lab repository has a =master= branch.
+- There exists a =lab-descriptor.json= in the lab repository.
+- Experiment repositories (and tags) exist and follow the provided
+ experiment [[https://github.com/virtual-labs/ph3-exp-template/][template]].
+
+
+*** Dependencies
+ - Template :: The template html file that defines the common
+ skeleton for all pages.
+ - Config :: This contains information about which pages are
+ generated using which templates.
+ - Lab Descriptor :: This is a json document that contains all the
+ lab related data. This should be present in
+ the lab repository, on the master branch.
+ - Page Templates :: Each page in the lab is generated using the
+ top level skeleton and a page specific
+ template. The data for the page specific
+ template comes from the lab descriptor. All
+ the page templates are placed in a directory
+ called =page-templates=.
+
+*** Generating Components
+ The components are generated using templates. Templates are found
+ in the [[file:page-templates][page-templates]] directory.
+
+ The =page-templates= directory contains [[https://handlebarsjs.com/][handlebarsjs]] templates.
+ Each of these templates is one of two types:
+ - Page Specific Template :: Page specific templates are defined
+ for components that are required in a single page.
+ - Common Template :: Common Templates are used to generate
+ components like headers, footers etc. that are placed in each
+ page.
+
+**** Difference Between IIITH and Other Lab pages
+ The IIITH lab pages are similar to all other lab pages, except
+ for the List of Experiments Page. The list of experiments page
+ has different template for the two types of experiments.
+
+ - Template for IIITH Labs :: nested-list-of-experiments-ctnt.handlebars
+ - Template for Other Labs :: list-of-experiments-ctnt.handlebars
+
+ As the templates are different, the data also comes from
+ different fields in the lab-descriptor.
+
+ - IIITH Lab List of Experiments Field :: =experiments=
+ - Other Lab List of Experiments Field :: =nested-experiments=
+
+ Only one of these fields should be present in a lab-descriptor.
+
+ *NOTE*: If both fields are present, the lab pages will not be
+ generated.
+
+*** Generate Lab
+
+ Once the pages are generated (generated pages are present in
+ =page-components=) the lab is generated using the following:
+
+ - Page Components :: Page Components are the html pages generated
+ using the data and templates.
+
+ - assets :: the css, js, images and fonts are taken from
+ =templates/assets= directory.
+
+** Build and Stage Experiments
+
+ [[file:deployment-process.jpg][file:deployment-process.jpg]]
+
+ After the lab pages are built, the experiments need to built and
+ staged.
+
+** Stage Lab
+
+ If the build and staging process of experiments is successful, we
+ move on to the process of staging the lab.
+
+ Here we basically just copy the contents of the =build= directory
+ from the labpath to the staging location, which is a directory on
+ the server.
+
+** Deploy
+
+ If the staging of experiments and lab sources succeeds the last
+ step is to place the lab sources and the experiments to their final
+ location on the server.
+
+ *NOTE*: We assume that is process is not prone to any known errors.
+
+** Release Management
+
+ After each successful lab generation and deployment process, the
+ lab sources are pushed to its repository along with the timestamp
+ in its commit message.
+
+ A new release of the sources is made by incrementing the previous
+ release number. The numbering scheme follows semantic versioning.
+ The type of release determines the version number.
+
+
+*** Version scheme
+
+ Every release follows the scheme : =v..=. Where X, Y, and
+ Z are non-negative integers.
+
+ Types of releases:
+ - Major :: X is incremented and Y, Z become 0.
+ - Minor :: X stays the same. Y is incremented and Z becomes 0.
+ - Patch :: X and Y remain the same. Z is incremented.
+
+* Reporting
+
+ The timestamp, verion number and status (success or failure) of the
+ run is updated to a [[https://docs.google.com/spreadsheets/d/1Z-acT5GKrna_JyHanxq3mXKocRPUb1KPL8vY7mKa5KA/edit#gid=0][google sheet]].
+
+* Descriptor Validation
+
+ The lab descriptor is validated using a [[file:~/iiith/vlead/Phase-3-Lab-Template/labDescSchema.json][schema]]. If anything is
+ invalid in the lab descriptor, the error is displayed and process
+ aborts.
diff --git a/docs/lab-descriptor.html b/docs/lab-descriptor.html
deleted file mode 100644
index ea9cc05c..00000000
--- a/docs/lab-descriptor.html
+++ /dev/null
@@ -1,722 +0,0 @@
-
-
-
-
-
-
-
-Lab Descriptor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/lab-descriptor.org b/docs/lab-descriptor.org
similarity index 95%
rename from lab-descriptor.org
rename to docs/lab-descriptor.org
index 5bbbc278..35af71a7 100644
--- a/lab-descriptor.org
+++ b/docs/lab-descriptor.org
@@ -1,188 +1,192 @@
-#+title: Lab Descriptor
-#+setupfile: ./org-html-themes/setup/theme-readtheorg-local.setup
-#+export_file_name: ./docs/lab-descriptor.html
-
-* Introduction
-
- Lab Descriptor is a json file that contains information about
- generation, and deployment of lab pages and related experiments.
-
-* Lab Descriptor Structure
-
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | Field | Sub Field | Type | Description |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *boradArea* | /name/ | =String= | Broad Area (Domain) name |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | | /link/ | =URL= | Link to the page on vlab.co.in that |
- | | | | lists all labs in the domain. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *baseUrl* | | =URL= | A URL has =host= and =url-path=. baseUrl |
- | | | | contains the =host= part of the lab. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *lab* | | =String= | Lab name as displayed on lab pages. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *deployLab* | | =Boolean= | Should the lab be deployed? |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *phase* | | =number= | Virtual Labs Phase |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *collegeName* | | =String= | Name of authoring college. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *introduction* | | =String= | Contents of the =Introduction.html= page. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *experiments* | | =List Experiment= | List of Experiments. Each item in this list |
- | | | | is an =Experiment= object. =Experiment= object |
- | | | | is described [[Experiment][here]]. This list is used for |
- | | | | populating the =List Of Experiments.html= page |
- | | | | and for deployment of experiments. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *experiment-sections* | /sect-name/ | =String= | This is required when list of experiments is a |
- | | | | nested list. (ex: Data Structures Lab). Otherwise |
- | | | | just use *experiments* and ommit |
- | | | | *experiment-sections*. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | | /experiments/ | =List Experiment= | List of experiments in a section. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *targetAudience* | /UG/ | =List String= | List of UG degrees, that the lab is targeted |
- | | | | towards. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | | /PG/ | =List String= | List of PG degrees that the lab is targeted |
- | | | | towards. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *objective* [optional] | | =String= | Content for =Objective.html= page. This field is |
- | | | | optional. If this is omitted then =Objective.html= |
- | | | | page is not generated. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | *courseAlignment* | /description/ | =String= | Content for =Course Alignment.html= page. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
- | | /universities/ | =List String= | Content for =Course Alignment.html= page. |
- |------------------------+----------------+-------------------+-----------------------------------------------------|
-
-* Experiment
-
- Each experiment record should contain the following information:
-
- |--------------+-----------+------------------+------------------------------------------|
- | Field | Type | Source | Description |
- |--------------+-----------+------------------+------------------------------------------|
- | *name* | =String= | Hosting Request | Name of the Experiment as displayed on |
- | | | | the Experiment pages. |
- |--------------+-----------+------------------+------------------------------------------|
- | *short-name* | =String= | Hosting Engineer | Unique short name for the experiment. |
- | | | | Used in the url path for the experiment. |
- |--------------+-----------+------------------+------------------------------------------|
- | *repo* | =URL= | Hosting Request | URL of the remote repository for the |
- | | | | experiment sources. |
- |--------------+-----------+------------------+------------------------------------------|
- | *tag* | =String= | Hosting Request | Tag of the repository to be used. |
- |--------------+-----------+------------------+------------------------------------------|
- | *deploy* | =Boolean= | Hosting Engineer | Should the experiment be deployed. |
- |--------------+-----------+------------------+------------------------------------------|
-
- The information comes from two sources: Hosting Request and Hosting
- Engineer. The hosting engineer has to provide a unique =short-name=
- for the experiment which is used for the experiment deployment path.
- All other values should be taken from the Hosting Request.
-
-
-** Example
-
- #+BEGIN_SRC js
-
- {
- "name": "Performance Characteristics of Centrifugal Pump",
- "short-name": "centrifugal-pump",
- "repo": "http://vlabs.ac.in/gitlab/central-hosting/fluid/centrifugal-pump-nitk.git",
- "tag": "v1.0.1",
- "deploy": true
- }
-
- #+END_SRC
-
-* Empty Lab descriptor
-
- Following is a lab descriptor file which has not been filled. Use
- this as a starting point to create the lab descriptor for your lab.
-
- #+BEGIN_SRC js :eval no :noweb yes :mkdirp yes :tangle ./lab-descriptor.json
- {
- "broadArea": {
- "name": "",
- "link": ""
- },
- "lab": "",
- "deployLab": ,
- "phase": ,
- "collegeName": "",
- "baseUrl": "",
- "introduction": "",
- "experiments": [],
- "targetAudience": {
- "UG": [],
- "PG": []
- },
- "objective": "",
- "courseAlignment": {
- "description": "",
- "universities": []
- }
- }
- #+END_SRC
-
-* Sample Lab Descriptor
-
- Following lab descriptor is used for testing.
-
- #+BEGIN_SRC js :eval no :noweb yes :mkdirp yes :tangle ./sample-lab-descriptor.json
-
- {
- "broadArea": {
- "name": "Civil Engineering",
- "link": "http://vlab.co.in/broad-area-civil-engineering"
- },
- "lab": "Fluid Machinery",
- "deployLab":true,
- "phase": 3,
- "collegeName": "NITS",
- "baseUrl": "http://localhost",
- "introduction": "Welcome to fluid macninery lab!",
- "experiments": [
- {
- "name": "Performance Characteristics of Centrifugal Pump",
- "short-name": "centrifugal-pump",
- "repo": "http://vlabs.iitb.ac.in/gitlab/vlabs-dev-central-hosting/fluid-machinery-nitk/performance-characteristics-centrifugal-pump-nitk.git",
- "tag": "v1.0.0",
- "deploy": true
- },
- {
- "name": "Performance Characteristics of Hydraulic Ram",
- "short-name": "hydraulic-ram",
- "repo": "http://vlabs.iitb.ac.in/gitlab/vlabs-dev-central-hosting/fluid-machinery-nitk/performance-characteristics-hydraulic-ram-nitk.git",
- "tag": "v1.0.0",
- "deploy": true
- }
- ],
- "targetAudience": {
- "UG": [
- "B. Tech./ B.E in Civil Engineering"
- ],
- "PG": [
- "MS/Ph. D. Beginners in Civil Engineering and related topics"
- ]
- },
- "objective": "Hello World.",
- "courseAlignment": {
- "description": "course are listed here:",
- "universities": [
- "Visvesvaraya Technological University Karnataka",
- "National Institute of Technology Karnataka Karnataka",
- "Anna University Tamil Nadu",
- "SRM University Delhi - NCR Sonepat Haryana",
- "University of Kerala Kerala",
- "APJ Abdul Kalam Technological University Kerala",
- "CUSAT Kerala"
- ]
- }
- }
-
- #+END_SRC
+#+title: Lab Descriptor
+#+setupfile: ./org-html-themes/setup/theme-readtheorg-local.setup
+#+export_file_name: ./docs/lab-descriptor.html
+
+* Introduction
+
+ Lab Descriptor is a json file that contains information about
+ generation, and deployment of lab pages and related experiments.
+
+* Lab Descriptor Structure
+
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | Field | Sub Field | Type | Description |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *boradArea* | /name/ | =String= | Broad Area (Domain) name |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | | /link/ | =URL= | Link to the page on vlab.co.in that |
+ | | | | lists all labs in the domain. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *baseUrl* | | =URL= | A URL has =host= and =url-path=. baseUrl |
+ | | | | contains the =host= part of the lab. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *lab* | | =String= | Lab name used in google analytics. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *lab_display_name* | | =String= | Lab name as displayed on lab pages. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *deployLab* | | =Boolean= | Should the lab be deployed? |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *phase* | | =number= | Virtual Labs Phase |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *collegeName* | | =String= | Name of authoring college. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *introduction* | | =String= | Contents of the =Introduction.html= page. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *experiments* | | =List Experiment= | List of Experiments. Each item in this list |
+ | | | | is an =Experiment= object. =Experiment= object |
+ | | | | is described [[Experiment][here]]. This list is used for |
+ | | | | populating the =List Of Experiments.html= page |
+ | | | | and for deployment of experiments. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *experiment-sections* | /sect-name/ | =String= | This is required when list of experiments is a |
+ | | | | nested list. (ex: Data Structures Lab). Otherwise |
+ | | | | just use *experiments* and ommit |
+ | | | | *experiment-sections*. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | | /experiments/ | =List Experiment= | List of experiments in a section. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *targetAudience* | /UG/ | =List String= | List of UG degrees, that the lab is targeted |
+ | | | | towards. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | | /PG/ | =List String= | List of PG degrees that the lab is targeted |
+ | | | | towards. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *objective* [optional] | | =String= | Content for =Objective.html= page. This field is |
+ | | | | optional. If this is omitted then =Objective.html= |
+ | | | | page is not generated. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | *courseAlignment* | /description/ | =String= | Content for =Course Alignment.html= page. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+ | | /universities/ | =List String= | Content for =Course Alignment.html= page. |
+ |------------------------+----------------+-------------------+-----------------------------------------------------|
+
+* Experiment
+
+ Each experiment record should contain the following information:
+
+ |--------------+-----------+------------------+------------------------------------------|
+ | Field | Type | Source | Description |
+ |--------------+-----------+------------------+------------------------------------------|
+ | *name* | =String= | Hosting Request | Name of the Experiment as displayed on |
+ | | | | the Experiment pages. |
+ |--------------+-----------+------------------+------------------------------------------|
+ | *short-name* | =String= | Hosting Engineer | Unique short name for the experiment. |
+ | | | | Used in the url path for the experiment. |
+ |--------------+-----------+------------------+------------------------------------------|
+ | *repo* | =URL= | Hosting Request | URL of the remote repository for the |
+ | | | | experiment sources. |
+ |--------------+-----------+------------------+------------------------------------------|
+ | *tag* | =String= | Hosting Request | Tag of the repository to be used. |
+ |--------------+-----------+------------------+------------------------------------------|
+ | *deploy* | =Boolean= | Hosting Engineer | Should the experiment be deployed. |
+ |--------------+-----------+------------------+------------------------------------------|
+
+ The information comes from two sources: Hosting Request and Hosting
+ Engineer. The hosting engineer has to provide a unique =short-name=
+ for the experiment which is used for the experiment deployment path.
+ All other values should be taken from the Hosting Request.
+
+
+** Example
+
+ #+BEGIN_SRC js
+
+ {
+ "name": "Performance Characteristics of Centrifugal Pump",
+ "short-name": "centrifugal-pump",
+ "repo": "http://vlabs.ac.in/gitlab/central-hosting/fluid/centrifugal-pump-nitk.git",
+ "tag": "v1.0.1",
+ "deploy": true
+ }
+
+ #+END_SRC
+
+* Empty Lab descriptor
+
+ Following is a lab descriptor file which has not been filled. Use
+ this as a starting point to create the lab descriptor for your lab.
+
+ #+BEGIN_SRC js :eval no :noweb yes :mkdirp yes :tangle ./lab-descriptor.json
+ {
+ "broadArea": {
+ "name": "",
+ "link": ""
+ },
+ "lab": "",
+ "lab_display_name": "",
+ "deployLab": ,
+ "phase": ,
+ "collegeName": "",
+ "baseUrl": "",
+ "introduction": "",
+ "experiments": [],
+ "targetAudience": {
+ "UG": [],
+ "PG": []
+ },
+ "objective": "",
+ "courseAlignment": {
+ "description": "",
+ "universities": []
+ }
+ }
+ #+END_SRC
+
+* Sample Lab Descriptor
+
+ Following lab descriptor is used for testing.
+
+ #+BEGIN_SRC js :eval no :noweb yes :mkdirp yes :tangle ./sample-lab-descriptor.json
+
+ {
+ "broadArea": {
+ "name": "Civil Engineering",
+ "link": "http://vlab.co.in/broad-area-civil-engineering"
+ },
+ "lab": "Fluid Machinery",
+ "lab_display_name": "Fluid Machinery",
+ "deployLab":true,
+ "phase": 3,
+ "collegeName": "NITS",
+ "baseUrl": "http://localhost",
+ "introduction": "Welcome to fluid macninery lab!",
+ "experiments": [
+ {
+ "name": "Performance Characteristics of Centrifugal Pump",
+ "short-name": "centrifugal-pump",
+ "repo": "http://vlabs.iitb.ac.in/gitlab/vlabs-dev-central-hosting/fluid-machinery-nitk/performance-characteristics-centrifugal-pump-nitk.git",
+ "tag": "v1.0.0",
+ "deploy": true
+ },
+ {
+ "name": "Performance Characteristics of Hydraulic Ram",
+ "short-name": "hydraulic-ram",
+ "repo": "http://vlabs.iitb.ac.in/gitlab/vlabs-dev-central-hosting/fluid-machinery-nitk/performance-characteristics-hydraulic-ram-nitk.git",
+ "tag": "v1.0.0",
+ "deploy": true
+ }
+ ],
+ "targetAudience": {
+ "UG": [
+ "B. Tech./ B.E in Civil Engineering"
+ ],
+ "PG": [
+ "MS/Ph. D. Beginners in Civil Engineering and related topics"
+ ]
+ },
+ "objective": "Hello World.",
+ "courseAlignment": {
+ "description": "course are listed here:",
+ "universities": [
+ "Visvesvaraya Technological University Karnataka",
+ "National Institute of Technology Karnataka Karnataka",
+ "Anna University Tamil Nadu",
+ "SRM University Delhi - NCR Sonepat Haryana",
+ "University of Kerala Kerala",
+ "APJ Abdul Kalam Technological University Kerala",
+ "CUSAT Kerala"
+ ]
+ }
+ }
+
+ #+END_SRC
diff --git a/docs/latex.md b/docs/latex.md
new file mode 100644
index 00000000..769d82e4
--- /dev/null
+++ b/docs/latex.md
@@ -0,0 +1,43 @@
+---
+title: latex
+author: "Shreyash Jain and Aditya Malhotra"
+audience: "Experiment Developer"
+date: 2023-05-11T13:49:21.348Z
+phase: "Ext Phase 3"
+summary: "This document describes the how to enable the latex support in your experiment."
+tags: []
+---
+
+## Introduction
+We have incorporated the support for rendering math following the commonly used KaTeX syntax into our build pipeline.
+You can write KaTeX syntax in both Markdown as well as Assessment(JSON) files. Which will be rendered into maths automatically by our tool.
+
+## How to use
+To enable LaTeX rendering in your experiment, you need to add the following line in your experiment descriptor file:
+```json
+"LaTeXinMD": true
+```
+
+## How to verify
+To verify that the LaTeX is working properly, you can add the following line in your Markdown file:
+```markdown
+$$
+\frac{1}{2}
+$$
+```
+This will render the following equation:
+$$
+\frac{1}{2}
+$$
+
+You can view the rendered equation in the deployed experiment.
+
+## Important Points
+1. You can use LaTeX in both Markdown and Assessment(JSON) files.
+2. You need to add the following line in your experiment descriptor file to enable LaTeX in Markdown files:
+```json
+"LaTeXinMD": true
+```
+3. LaTeX is enabled by default in Assessment(JSON) files.
+
+
diff --git a/docs/plugins.org b/docs/plugins.org
new file mode 100644
index 00000000..12641370
--- /dev/null
+++ b/docs/plugins.org
@@ -0,0 +1,76 @@
+#+TITLE: PLUGINS
+
+* Introduction
+This document describes the components of the lab and experiment
+architecture that can be redesigned as independent systems and
+included in the the lab architecture
+
+
+* Plugin Arcitecture
+The plugin architecture is based on the idea of separating the core
+application from additional features in order to achieve the
+following:
+
+- Ease of Extension :: When the application is modelled as a
+ composition of several features put together as independent
+ plugins, it becomes easy to add/remove/modify the features as and
+ when needed.
+
+- Plugins can be indenpendently developed and maintained :: Plugins
+ can be devloped by as separate projects that can be simply
+ imported and used by the core system.
+
+There are two main components in a plugin based system: The Core and
+Extensions.
+
+
+** Core
+The core is the basic application. This core is kept as simple as
+possible and only conatains business logic. All the features are
+separated into plugins.
+
+In case of the lab build process, the core of the application consists
+of the lab and experiment related data (lab name, broad area, list of
+experiments etc.) and logic (processing pipeline, url schemes,
+configurations etc.).
+
+
+** Extensions/Plugins
+Any feature that is not related to the application domain, can be
+separated as a plugin. In the Lab and Experiment build processes the
+following features can be separated as plugins.
+
+- Page
+- Content Rendering
+- Content Dependency Validation
+- Analytics
+- Feedback
+- Discussion Forum
+- Ratings
+- Component Units like code assessment
+
+*** Analytics
+Analytics feature tracks the usage of lab and experiment webpages.
+This feature is not unique to the Virtual Labs. It is widely used by
+websites to measure the activities of visiting users. As a result
+there are several tools available for performing website analytics.
+Currently we at Virtual Labs use Google Analytics, but it is not the
+only option avaiable.
+
+Additionally, there are scenarios where analytics is not needed. For
+example, while developing and testing an experiment/lab, analytics is
+not required. So, we need the ability to enable/disable analytics.
+
+The details of analytics are provided in [[file:analytics.org][this]] document.
+
+As of now, the process to add analytics to any page is included in the
+page building process which in turn depends of the kind of page (is it
+a lab page or an experiment page).
+
+The first step towards creating a plugin for analytics requires
+separating the page building process from the core.
+
+* References
+
+https://medium.com/omarelgabrys-blog/plug-in-architecture-dec207291800
+
diff --git a/docs/quiz.md b/docs/quiz.md
new file mode 100644
index 00000000..8d5b0b4c
--- /dev/null
+++ b/docs/quiz.md
@@ -0,0 +1,233 @@
+# Quiz
+### 1. Introduction
+The quiz section is designed as part of the experiment to assess students' learning and understanding. It allows for the creation of multiple-choice, single-answer quizzes. The quiz can be used for different purposes:
+
+* Pretest: To assess prerequisite knowledge before starting.
+* Posttest: To test understanding after completing the module.
+* Learning Unit Quizzes: To evaluate the knowledge gained from specific sections of the content.
+
+The quiz implementation format is explained below.
+
+### 2. Target Audience
+This guide is intended for developers building experiments the virtual lab and aiming to include a quiz section in the experiment.
+
+### 3. Structure of quiz
+The quiz is structured in a JSON file format. The quiz questions must be represented as an array of objects. Each object corresponds to a question. Below are the specifications for the quiz format :
+
+### 3.1 Array of Questions
+Each question in the quiz is defined with the following attributes:
+* **question:** This represents the question to be asked.
+* **answers:** This contains the possible answer choices, each labeled (for example, as a, b, c, and d).
+* **correctAnswer:** The correct option for the question, specified by the corresponding letter.
+
+Example:
+
+```
+"questions" : [
+ {
+ "question" : "What is 1+2 ?",
+ "answers" :
+ {
+ "a" : 1,
+ "b" : 2,
+ "c" : 3,
+ "d" : 4
+ },
+ "correctAnswer" : c
+ }
+]
+```
+### 3.2 Explanation of Keys:
+
+* "question": The text to be displayed as the question.
+* "answers": An object of key-value pairs where the key is a label (a, b, c, d), and the value is the corresponding answer choice.
+* "correctAnswer": The correct answer's label (e.g., a, b, c, or d).
+
+### 3.3 Best Practices for Writing Questions:
+* Design the questions clear and unambiguous.
+* Ensure that only one answer is correct.
+* Provide distinct and not confusing answer choices.
+* The correct answer key should always correspond to the correct option in the answers field.
+
+### 4. Quiz V2.0 (Enhancements done)
+The new format of quiz has multiple new additions. The details for which have been described below.
+The format of json would be as linked [here](./pretest.json)
+
+First we will look at the additional fields added
+
+### 4.1 Fields
+* Mandatory Fields
+ * [version](#42-version) - Ensures the enhanced quiz is rendered correctly.
+ * [levels](#44-levels) - Adds difficulty levels to each question, allowing for filtering.
+
+* Optional Fields
+ * [explanations](#43-explanations) - Adds an explanation to each answer. If wrong answer is choosen, only it's explanation pops up. If correct answer is choosen, all available explanations pop up.
+
+### 4.2 Version
+The very first field is absolutely necessary. This ensures that the quiz supports the new features.
+```
+"version": 2.0
+```
+
+### 4.3 Explanations
+The explanations section shows up after a question is answered. It is optional and can be omitted entirely. Below are examples of how to structure the explanations:
+
+1. All answers have explanations
+```
+"explanations": {
+ "a" : "Explanation 1",
+ "b" : "Explanation 2",
+ "c" : "Explanation 3",
+ "d" : "Explanation 4"
+},
+```
+2. Some answers have explanations
+```
+"explanations": {
+ "a" : "Explanation 1",
+ "d" : "Explanation 4"
+},
+```
+
+3. No answers have explanations
+```
+/* Can be excluded from json */
+```
+
+### 4.4 Levels
+The difficulty level for each question is mandatory and allows filtering. The available difficulty levels are:
+```
+['beginner', 'intermediate', 'advanced']
+```
+Using any other will not work. The format for the same:
+```
+"difficulty" : "beginner"
+```
+
+### 5. Tips
+1. **Rich Text in explanations**
+Explanations can include HTML-formatted rich text, allowing features like hyperlinks and text formatting.
+```
+"explanations": {
+ "a" : "Explanation 1 here",
+ "b" : "Explanation 2"
+},
+```
+[Example](https://github.com/virtual-labs/exp-stacks-queues-iiith/blob/main/experiment/pretest.json)
+
+2. **Multi Correct Questions**
+To mimic multi-correct questions, you can structure the options within the question itself and offer options like:
+```
+ "answers" :
+ {
+ "a" : "both i and ii",
+ "b" : "All i, ii, iii, iv",
+ "c" : "Only i",
+ "d" : "None of the above"
+ }
+```
+[Example](https://github.com/virtual-labs/exp-Effect-field-gradient-iiith/blob/main/experiment/pretest.json)
+
+3. **Image Support**
+Images can be added to both question and answers.
+
+* **Image in question** :
+```
+"questions" : [
+ {
+ "question": "$\\\\$ ",
+ "answers" :
+ {
+ "a" : 1,
+ "b" : 2,
+ "c" : 3,
+ "d" : 4
+ },
+ "correctAnswer" : c,
+ "explanations": {},
+ "difficulty": "intermediate"
+ }
+]
+```
+
+* **Image and Text in question** :
+```
+"questions" : [
+ {
+ "question": "This is an example question $\\\\$ ",
+ "answers" :
+ {
+ "a" : 1,
+ "b" : 2,
+ "c" : 3,
+ "d" : 4
+ },
+ "correctAnswer" : c,
+ "explanations": {},
+ "difficulty": "advanced"
+ }
+]
+```
+The same two cases apply for answers too.
+[Example](https://github.com/virtual-labs/exp-convolutional-codes-iiith/blob/dev/experiment/posttest.json)
+
+* **Multiple lines in the Text in question** :
+```
+"questions" : [
+ {
+ "question": "This is an example question Text line 1 Text line 2",
+ "answers" :
+ {
+ "a" : 1,
+ "b" : 2,
+ "c" : 3,
+ "d" : 4
+ },
+ "correctAnswer" : d,
+ "explanations": {},
+ "difficulty": "beginner"
+ }
+]
+```
+[Example](https://github.com/virtual-labs/exp-bubble-sort-iiith/blob/main/experiment/posttest.json)
+
+4. **Debugging through JSON validator**
+Please consider running your JSON files through a JSON validator like https://jsonlint.com/ for smoother debugging.
+
+**Make sure the image aspect ratio remains constant and good to maintain the structure**
+
+### 6. Manual Validation of Quiz Json (wrt version 2.0)
+Until automatic validation is set up, manually check:
+* The first field has to be "version" with 2 or 2.0 as value.
+* The questions field needs to be an array of objects containing questions.
+* Each question object should have
+ * question : Should be a string
+ * answer : Should be an object containing options, and each option should be a string.
+ * difficulty : Should be a string and should have values from ["beginner", "intermerdiate", "advanced"]
+ * correctAnswer : Should be a string and it's value should be present in keys of one of the answer.
+* If explanation is present, it has to be an object and needs to follow the description of answer object.
+
+### 7. Test Cases
+- [x] Using the mentioned quiz format
+- [x] Using the old quiz json format
+- [ ] Not including the version in json
+- [ ] Including incorrect version in json
+- [ ] Including correct version but following old format
+- [x] Difficulty not mentioned
+- [x] Incorrect difficulty level mentioned
+- [x] Explanation not provided for all options
+- [x] Explanation not mentioned
+- [x] Explanation object not defined
+- [x] HTML in question (tags like hyper links, bold etc)
+- [x] HTML in answer (tags like hyper links, bold etc)
+- [x] HTML in explanation (tags like hyper links, bold etc)
+- [x] On wrong answer, the corresponding answer text is colored red
+- [x] On correct answer, the corresponding answer text is colored green
+- [x] Combination of filters working properly
+- [x] If all questions have same difficulty, filter option should be hidden.
+- [x] When questions are answered after filtering, marks should be counted out of filtered questions, not total.
+- [x] On wrong answer, only explanation of wrong answer is shown
+- [x] On correct answer, all available explanations are shown
+
+### 8. TODO
+* Add automatic schema validation
\ No newline at end of file
diff --git a/docs/templates.org b/docs/templates.org
new file mode 100644
index 00000000..d9ab03b3
--- /dev/null
+++ b/docs/templates.org
@@ -0,0 +1,8 @@
+#+TITLE: TEMPLATES
+
+This document describes the templates used for building experiment and
+lab pages.
+
+* LAB TEMPLATES
+
+* EXPERIMEMT TEMPLATES
diff --git a/docs/units.dia b/docs/units.dia
new file mode 100644
index 00000000..6e92189f
Binary files /dev/null and b/docs/units.dia differ
diff --git a/docs/units.org b/docs/units.org
new file mode 100644
index 00000000..4550f761
--- /dev/null
+++ b/docs/units.org
@@ -0,0 +1,107 @@
+#+TITLE: UNITS - DESIGN
+
+* Introduction
+This document describes the classes used for implementing the
+experiment model.
+
+* Enums
+Enums (Enumerated types) are used here to define the datatypes for
+different elements.
+
+These are used for two reasons:
+- Central place for defining all the types
+- Validation of inputs (descriptors)
+
+The module that implements these enums also defines the validation
+function for each of then. The validation functions are used to
+verify the input (string). The input strings have to match exactly
+with the given string representations, otherwise the validation fails.
+
+*Note for future enhancement:* /This should be moved to a json
+schema. The schema already has features to validate inputs. See the
+following issue for details: [[https://gitlab.com/vlead/task-allocation/-/issues/95][95]]/
+
+
+** UnitTypes
+The =UnitTypes= record defines the valid unit types.
+
+|------+-----------------------|
+| Type | String Representation |
+|------+-----------------------|
+| LU | "lu" |
+| TASK | "task" |
+| AIM | "aim" |
+|------+-----------------------|
+
+** ContentTypes
+Content Types represent the types of content available for building
+pages.
+
+|------------+-----------------------|
+| Type | String Representation |
+|------------+-----------------------|
+| TEXT | "text" |
+| VIDEO | "video" |
+| SIMULATION | "simulation" |
+| ASSESMENT | "assesment" |
+| COMPONENT | "component" |
+|------------+-----------------------|
+
+** BuildEnvs
+The build environments used for configuring the build process.
+
+|------------+-----------------------|
+| Type | String Representation |
+|------------+-----------------------|
+| PRODUCTION | "production" |
+| TESTING | "testing" |
+| LOCAL | "local" |
+|------------+-----------------------|
+
+The local mode is not yet implemented, it's just an idea.
+
+The main difference between testing and production environments:
+
+|-------------------------------+----------+------------|
+| Feature | Testing | Production |
+|-------------------------------+----------+------------|
+| Analytics | DISABLED | ENABLED |
+| Breadcrumbs with links to lab | DISABLED | ENABLED |
+|-------------------------------+----------+------------|
+
+The reason why breadcrums are disabled in testing mode is because the
+testing mode was initially designed to be used with individual
+experiment build process. When the experiments are built
+individually, the lab level information is not available, so the links
+to lab pages are not generated.
+
+** Future Improvement
+The analytics and breadcrumbs features are coupled together. There
+should be a configuration option that allows the developers to
+enable/disable features for each mode.
+
+This can only be properly achieved when we move to the plugin based
+architecture.
+
+* Unit
+
+=Unit= is the base class for all types of units in an experiment. It
+defines the common attributes in all units.
+
+* LearningUnit
+The =LearningUnit= class inherits from the =Unit= class. It has an
+additional field called =units=. This field defines the list of units
+contained in this unit.
+
+* Task
+Defines the =Task= class. Inherits from =Unit=. Difference between
+=Task= and =LearningUnit= classes:
+
+- Tasks have a corresponding =source= and =target (html)= page.
+- Learning units can contains other units.
+
+
+* Aim
+=Aim= inherits from Task. It is a special kind of task which acts as
+the entry point for each learning unit. Each learning unit should
+have an aim.
diff --git a/docs/units.png b/docs/units.png
new file mode 100644
index 00000000..36dbbf94
Binary files /dev/null and b/docs/units.png differ
diff --git a/docs/usage.org b/docs/usage.org
new file mode 100644
index 00000000..1a0ddab0
--- /dev/null
+++ b/docs/usage.org
@@ -0,0 +1,14 @@
+#+TITLE: USAGE GUIDE
+
+* INTRODUCTION
+
+This document describes the various tools provided in this repository
+and how to use them.
+
+* TOOLS
+
+- lab descriptor verification ::
+
+- building a lab ::
+
+- building an experiment ::
diff --git a/docs/using-node.md b/docs/using-node.md
new file mode 100644
index 00000000..a439c516
--- /dev/null
+++ b/docs/using-node.md
@@ -0,0 +1,65 @@
+# Run the build script using node
+
+1. To run the build script using nodejs, you have to clone this repository in your experiment project folder.
+2. Then, you have to install the dependencies using the following command:
+```bash
+npm install
+```
+3. After that, you can run the build script using the following commands and options:
+```bash
+Usage: node main.js [mode] [options]
+Modes:
+ build Build the experiment
+ validate Validate the code and content
+ clean Clean the build and plugins
+ deploy Deploy the experiment locally
+ buildLab Build the lab
+ deployLab Deploy the lab locally
+
+
+Common Options:
+ --src path to the experiment, default is parent directory
+ --debug enable debug mode
+ --help display help for command
+
+
+Mode: build
+Usage: build [options]
+Options:
+ --clean clean build folder
+ --validateEslint validate the code using eslint
+ --validateExpDesc validate the experiment description and assessment files
+ --disablePlugin disable the plugins
+ --deploy deploy the experiment locally
+ --env environment to build the experiment
+
+
+Mode: Validate
+Usage: validate [options]
+Options:
+ --validateEslint validate the code using eslint
+ --validateExpDesc validate the experiment description and assessment files
+
+
+Mode: clean
+Usage: clean
+
+
+Mode: deploy
+Usage: deploy
+
+
+Mode: buildLab
+Usage: buildLab [options]
+Options:
+ --src path to the lab, default is parent directory
+ --deploy deploy the lab locally
+ --release release type of the lab, default is minor
+
+
+Mode: deployLab
+Usage: deployLab [options]
+Options:
+ --src path to the lab, default is parent directory
+ --release release type of the lab, default is minor
+```
diff --git a/docs/using_npm_package.md b/docs/using_npm_package.md
new file mode 100644
index 00000000..cc1926e7
--- /dev/null
+++ b/docs/using_npm_package.md
@@ -0,0 +1,23 @@
+# Run Build Script using npm package
+
+1. To run the build script using the npm package you need to make sure that the experiment has package.json file in the root directory. If you don't have one, you can create it by running the following command in the root directory of your experiment:
+```bash
+npm init -y
+```
+2. After initialising the experiment as a node project, you can run the build script by running the following command:
+```bash
+Usage: npx @virtual-labs/buildexp [command]
+Options:
+Commands:
+ build-exp Build the experiment
+ build-exp-deploy Build the experiment and deploy locally
+ build-exp-noplugin Build the experiment without using any plugins
+ clean-build-exp Clean and build the experiment
+ validate Validate the code and content
+ clean Clean the build and plugins
+ deploy Deploy the experiment locally
+ build-lab Build the lab
+ deploy-lab Deploy the lab locally
+ build-and-deploy-lab Build and deploy the lab locally
+ help Display help for command
+```
diff --git a/enums.js b/enums.js
new file mode 100644
index 00000000..70782400
--- /dev/null
+++ b/enums.js
@@ -0,0 +1,76 @@
+const UnitTypes = {
+ LU: "lu",
+ TASK: "task",
+ AIM: "aim",
+};
+
+const ContentTypes = {
+ TEXT: "text",
+ VIDEO: "video",
+ SIMULATION: "simulation",
+ ASSESMENT: "assesment",
+ ASSESSMENT: "assessment",
+ COMPONENT: "component",
+};
+
+const BuildEnvs = {
+ PRODUCTION: 'production',
+ TESTING: 'testing',
+ LOCAL: 'local'
+};
+
+const PluginScope = {
+ EXPERIMENT: 'experiment',
+ PAGE: 'page',
+ POSTBUILD: 'postbuild'
+};
+
+const PluginConfig = {
+ Default: {
+ JS_MODULE: 'js/main.js',
+ CSS_MODULE: 'css/main.css',
+ },
+};
+
+const DeveloperInstitutes = ["AMRT", "COEP", "DLBG", "IIITH", "IITB", "IITD", "IITG", "IITK", "IITKGP", "IITR", "NITK"];
+
+function validType(t, v) {
+ return Object.values(t).includes(v);
+}
+
+function validUnitType(ut) {
+ if (validType(UnitTypes, ut)) {
+ return ut;
+ } else {
+ throw new Error("Invalid unit type");
+ }
+}
+
+function validContentType(ct) {
+ if (validType(ContentTypes, ct)) {
+ return ct;
+ } else {
+ throw new Error("Invalid content type");
+ }
+}
+
+function validBuildEnv(e) {
+ if (validType(BuildEnvs, e)) {
+ return e;
+ } else {
+ throw new Error("Invalid build environment");
+ }
+}
+
+module.exports = {
+ UnitTypes,
+ ContentTypes,
+ BuildEnvs,
+ PluginScope,
+ PluginConfig,
+ DeveloperInstitutes,
+ validType,
+ validContentType,
+ validUnitType,
+ validBuildEnv
+};
diff --git a/exp_build/.eslintrc.js b/exp_build/.eslintrc.js
new file mode 100644
index 00000000..7444e52f
--- /dev/null
+++ b/exp_build/.eslintrc.js
@@ -0,0 +1,14 @@
+module.exports = {
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "rules": {
+ },
+ "plugins": ["only-warn"]
+}
diff --git a/exp_build/aim.js b/exp_build/aim.js
new file mode 100644
index 00000000..26338af7
--- /dev/null
+++ b/exp_build/aim.js
@@ -0,0 +1,26 @@
+const { Task } = require("./task.js");
+const {
+ UnitTypes,
+ ContentTypes,
+ validType,
+ validContentType,
+} = require("../enums.js");
+
+class Aim extends Task {
+ constructor(basedir, lu_label, exp_path) {
+ super(
+ UnitTypes.AIM,
+ "Aim",
+ ContentTypes.TEXT,
+ exp_path,
+ basedir,
+ "aim.md",
+ "index.html",
+ [],
+ [],
+ lu_label
+ );
+ }
+}
+
+module.exports = { Aim };
diff --git a/exp_build/exp_gen.js b/exp_build/exp_gen.js
new file mode 100644
index 00000000..5a7f647e
--- /dev/null
+++ b/exp_build/exp_gen.js
@@ -0,0 +1,95 @@
+const fs = require("fs");
+const path = require("path");
+const Handlebars = require("handlebars");
+const shell = require("shelljs");
+
+const { Experiment } = require("./experiment.js");
+const Config = require("../config.js");
+const { Plugin } = require("./plugin.js");
+const { PluginScope } = require("../enums.js");
+const log = require("../logger.js");
+
+
+function copyFileSync(src, dest, options = {}) {
+
+ try {
+ fs.copyFileSync(src, dest);
+ } catch (e) {
+ log.error(`Error while copying ${src} to ${dest}::${e}`);
+ return 1;
+ }
+ return 0;
+}
+
+function run(src, lab_data, build_options) {
+ log.debug(`Running build process at ${src}`);
+ // if the experiment repo does not contain experiment descriptor we will add the default descriptor.
+ if (!shell.test("-f", Experiment.descriptorPath(src))) {
+ // shell.cp(
+ copyFileSync(
+ path.resolve(Config.PROJECT_ROOT, Config.Experiment.default_descriptor),
+ path.resolve(this.src, Experiment.descriptorPath(src))
+ );
+ }
+
+ const exp = new Experiment(src);
+ const pluginConfigFile = Plugin.getConfigFileName(build_options.env);
+ const pluginConfig = require(pluginConfigFile);
+
+ const pageScopePlugins = pluginConfig.filter(
+ (p) => p.scope === PluginScope.PAGE
+ );
+
+ pageScopePlugins.forEach((plugin) => {
+ if (plugin.id === "svc-rating") {
+ plugin.attributes.columnValue = lab_data.exp_short_name;
+ }
+ });
+
+ const code_assessment = exp.descriptor["code-assessment"];
+
+ // Include code-assessment.json if the code editor is included
+ if (code_assessment && code_assessment.include) {
+ log.info("Code Editor included");
+ build_options.codeditor = true;
+ build_options.code_assessment = code_assessment;
+ exp.includeCodeEditor(code_assessment.position);
+ }
+ else {
+ build_options.codeditor = false;
+ log.info("Code Editor Not included");
+ }
+
+ const services = exp.descriptor["services"];
+
+ if (services && services.length > 0) {
+ build_options.services = services;
+ }
+
+ exp.init(Handlebars);
+ // Validation
+ if (build_options.isValidate) {
+ exp.validate(build_options);
+ }
+
+ // if the experiment repo contains contributors.md file we will add its lu to the descriptor.
+ if (shell.test("-f", Experiment.contributorsPath(src))) {
+ if (
+ shell
+ .head({ "-n": 1 }, Experiment.contributorsPath(src))
+ .includes("EMPTY")
+ ) {
+ log.warn("Contributors.md file is empty, please add contributors to the file.");
+ } else {
+ exp.includeContributors();
+ }
+ }
+ else {
+ log.warn("Contributors.md file is not present, please add contributors to the experiment.");
+ }
+ exp.includeFeedback();
+ exp.build(Handlebars, lab_data, build_options);
+}
+
+module.exports.run = run;
+module.exports.build_experiment = run;
\ No newline at end of file
diff --git a/exp_build/experiment.js b/exp_build/experiment.js
new file mode 100644
index 00000000..1503e8c0
--- /dev/null
+++ b/exp_build/experiment.js
@@ -0,0 +1,304 @@
+const path = require("path");
+const fs = require("fs");
+const { renderMarkdown } = require("./renderer.js");
+const process = require("process");
+const shell = require("shelljs");
+
+const Config = require("../config.js");
+const { LearningUnit } = require("./learning_unit.js");
+const { Task } = require("./task.js");
+const {
+ UnitTypes,
+ ContentTypes,
+ BuildEnvs,
+ PluginScope,
+} = require("../enums.js");
+const { Plugin } = require("./plugin.js");
+const log = require("../logger.js");
+
+function getAssessmentPath(src, units) {
+ let assessmentPath = [];
+ units.forEach((unit) => {
+ if (unit["unit-type"] === "lu") {
+ const nextSrc = path.resolve(src, unit.basedir);
+ let paths = getAssessmentPath(nextSrc, unit.units);
+ assessmentPath.push(...paths);
+ }
+ if (unit["content-type"] === ContentTypes.ASSESMENT || unit["content-type"] === ContentTypes.ASSESSMENT) {
+ const quiz = path.resolve(src, unit.source);
+ assessmentPath.push(quiz);
+ }
+ });
+ return assessmentPath;
+}
+
+class Experiment {
+ constructor(src) {
+ this.src = src;
+ this.descriptor = require(Experiment.descriptorPath(src));
+ }
+
+ static ui_template_path = path.resolve(
+ __dirname,
+ Config.Experiment.ui_template_name
+ );
+
+ static static_content_path = path.resolve(
+ __dirname,
+ Config.Experiment.static_content_dir
+ );
+
+ static descriptorPath(src) {
+ return path.resolve(src, Config.Experiment.descriptor_name);
+ }
+
+ static codeAssessmentPath(src) {
+ return path.resolve(`${src}/experiment`, "code-assessment.json");
+ }
+
+ static contributorsPath(src) {
+ return path.resolve(`${src}/experiment`, "contributors.md");
+ }
+
+ static registerPartials(hb) {
+ Config.Experiment.partials.forEach(([name, file]) => {
+ const partial_content = fs.readFileSync(
+ path.resolve(
+ Experiment.ui_template_path,
+ "partials",
+ `${file}.handlebars`
+ )
+ );
+ hb.registerPartial(name, partial_content.toString());
+ });
+ }
+
+ init(hb) {
+ try {
+ log.debug("Initializing experiment");
+ const bp = Config.build_path(this.src);
+ shell.mkdir(path.resolve(this.src, Config.Experiment.build_dir));
+ shell.cp("-R", path.resolve(this.src, Config.Experiment.exp_dir), bp);
+ shell.cp("-R", Config.assets_path(), bp);
+
+ // Copy the Katex CSS and fonts to the build directory in assets/katex_assets
+ log.debug("Moving Katex assets");
+ shell.mkdir(path.resolve(bp, "assets", "katex_assets"));
+ const pathtoKatex = require.resolve("katex")
+ const katex_assets_path = path.dirname(pathtoKatex);
+ shell.cp(
+ "-R",
+ path.resolve(katex_assets_path, "katex.min.css"),
+ path.resolve(bp, "assets", "katex_assets")
+ );
+ shell.cp(
+ "-R",
+ path.resolve(katex_assets_path, "fonts"),
+ path.resolve(bp, "assets", "katex_assets")
+ );
+
+ log.debug("Moving feedback file");
+ shell.cp(
+ "-R",
+ path.resolve(Experiment.static_content_path, "feedback.md"),
+ bp
+ );
+
+ log.debug("Linking with Handlebars");
+ Experiment.registerPartials(hb);
+ } catch (e) {
+ log.error("Error initializing experiment", e);
+ log.error("Exiting Build Process");
+ process.exit();
+ }
+ }
+
+ validate(build_options) {
+ log.debug("Validating experiment");
+ const buildPath = Config.build_path(this.src);
+ const expPath = path.resolve(this.src, Config.Experiment.exp_dir);
+ if (build_options.isESLINT) {
+ try {
+ log.debug("Validating with eslint");
+ shell.exec(`npx eslint -c ${__dirname}/.eslintrc.js ${expPath} > ${buildPath}/eslint.log`);
+ } catch (e) {
+ log.error("Error validating with eslint", e);
+ }
+ }
+ if (build_options.isExpDesc) {
+ const descriptorPath = Experiment.descriptorPath(this.src);
+ const descriptor = require(descriptorPath);
+ const pathToValidator = path.resolve(__dirname, "../validation/validate.js");
+ try {
+ log.debug("Validating experiment descriptor");
+ shell.exec(`node ${pathToValidator} -f ${descriptorPath} >> ${buildPath}/validate.log`);
+ } catch (e) {
+ log.error("Error validating experiment descriptor", e);
+ }
+ // loop through the units and validate the content
+ try {
+ log.debug("Validating Assessment files");
+ const assessmentPath = getAssessmentPath(expPath, descriptor.units);
+ assessmentPath.forEach((file) => {
+ if (fs.existsSync(file)) {
+ // trim ep from file
+ const fileName = file.replace(expPath, "");
+ shell.exec(`echo =${fileName} >> ${buildPath}/assesment.log`);
+ shell.exec(
+ `node ${pathToValidator} -f ${file} -c assessment >> ${buildPath}/assesment.log`
+ );
+ } else {
+ log.error(`Assessment file ${path} does not exist`);
+ }
+ });
+ } catch (e) {
+ log.error("Error validating Assessment files", e);
+ }
+ }
+ if (build_options.codeditor) {
+ try {
+ log.debug("Validating with Code Assessment");
+ const codeAssessmentPath = path.resolve(expPath, './code-assessment.json');
+ const pathToValidator = path.resolve(__dirname, "../validation/validate.js");
+ shell.exec(`node ${pathToValidator} -f ${codeAssessmentPath} >> ${buildPath}/code-assessment.log`);
+ } catch (e) {
+ log.error("Error validating with eslint", e);
+ }
+ }
+ }
+ name() {
+ const name_file = fs.readFileSync(
+ path.resolve(Config.build_path(this.src), "experiment-name.md")
+ );
+ return renderMarkdown(name_file.toString());
+ }
+
+ generateServiceWorker(buildPath) {
+ const { generateSW } = require("@virtual-labs/service_worker");
+ const expPath = path.resolve(this.src, Config.Experiment.exp_dir);
+ let inputPath = "";
+ // check if the user has provided a service worker file path and if so, check if it exists
+ if (this.descriptor['service-worker']) {
+ const swPath = path.resolve(expPath, this.descriptor['service-worker']);
+ if (fs.existsSync(swPath)) {
+ inputPath = swPath;
+ } else {
+ log.warn(`Service worker file ${swPath} does not exist`);
+ }
+ } else {
+ log.warn("No service worker file provided");
+ }
+ const swDest = path.resolve(buildPath, "sw.js");
+ generateSW(inputPath, buildPath, swDest);
+ }
+
+ build(hb, lab_data, options) {
+ /*
+ here we are assuming that the descriptor contains a simgle object
+ that represents the learning unit corresponding to the experiment.
+ */
+ log.debug(`Building experiment`);
+ const explu = LearningUnit.fromRecord(this.descriptor, this.src);
+ const exp_info = {
+ name: this.name(),
+ menu: explu.units,
+ src: this.src,
+ bp: Config.build_path(this.src) + "/",
+ };
+
+ if(options.codeditor) {
+ const [codeditor_id, div_id, js_modules, css_modules] = Plugin.loadCodeAssessment(options);
+ exp_info.codeditor_id = codeditor_id;
+ exp_info.codeditor_div_id = div_id;
+ exp_info.codeassessment_js_modules = js_modules;
+ exp_info.codeassessment_css_modules = css_modules;
+ exp_info.codeassessment_languages = options.code_assessment.languages;
+ }
+
+ if (options.plugins) {
+ Plugin.loadAllPlugins(options);
+ exp_info.plugins = Plugin.processExpScopePlugins(
+ exp_info,
+ hb,
+ lab_data,
+ options
+ );
+ }
+ explu.build(exp_info, lab_data, options);
+ // post build
+ if (options.plugins) {
+ Plugin.processPostBuildPlugins(exp_info, options);
+ }
+
+ // generate service worker
+ this.generateServiceWorker(exp_info.bp);
+ /*
+ This "tmp" directory is needed because when you have a sub-directory
+ with the same name, it can cause issue. So, we assume that there should
+ not be any sub-directory with "tmp" name, and first move the contents to tmp
+ before moving the contents to the top level directory.
+ */
+ const tmp_dir = path.resolve(this.src, Config.Experiment.build_dir, "tmp");
+ shell.mv(path.resolve(Config.build_path(this.src)), tmp_dir);
+ shell.mv(
+ path.resolve(tmp_dir, "*"),
+ path.resolve(this.src, Config.Experiment.build_dir)
+ );
+ shell.rm("-rf", tmp_dir);
+ }
+
+ includeFeedback() {
+ const feedbackLU = {
+ "unit-type": "task",
+ label: "Feedback",
+ "content-type": "text",
+ source: "feedback.md",
+ target: "feedback.html",
+ };
+
+ this.descriptor.units.push(feedbackLU);
+ }
+
+ includeContributors() {
+ const contributors = {
+ "unit-type": "task",
+ label: "Contributors",
+ "content-type": "text",
+ source: "contributors.md",
+ target: "contributors.html",
+ };
+ this.descriptor.units.push(contributors);
+ }
+
+ includeCodeEditor(position = 5) {
+ const codeditor = {
+ "target": "code-assessment.html",
+ "label": "Code Assessment",
+ "source": "code-assessment.json",
+ "unit-type": "task",
+ "content-type": "component",
+ };
+ this.descriptor.units.splice(position,0,codeditor);
+ }
+}
+
+module.exports = { Experiment };
+
+// need to handle optional menu items
+
+/*
+
+TODO
+
+Removing this becaiuse it is optional and we have not yet handled
+the case.
+
+ {
+ "target": "posttest.html",
+ "source": "posttest.js",
+ "label": "Posttest",
+ "unit-type": "task",
+ "content-type": "assesment"
+ },
+
+*/
diff --git a/exp_build/learning_unit.js b/exp_build/learning_unit.js
new file mode 100644
index 00000000..7bb56179
--- /dev/null
+++ b/exp_build/learning_unit.js
@@ -0,0 +1,85 @@
+const path = require("path");
+const fs = require("fs");
+const marked = require("marked");
+const process = require("process");
+const Handlebars = require("handlebars");
+const shell = require("shelljs");
+
+const {Unit} = require("./unit.js");
+const {Task} = require("./task.js");
+const {Aim} = require("./aim.js");
+
+const {UnitTypes, ContentTypes, validType, validContentType} = require("../enums.js");
+const log = require("../logger.js");
+
+class LearningUnit extends Unit {
+ constructor(
+ unit_type,
+ label,
+ exp_path,
+ basedir,
+ units
+ ) {
+ super(unit_type, label, exp_path, basedir);
+ if (units) {
+ this.units = Array.from(units).map(
+ (u) => {
+ switch(u["unit-type"]){
+ case UnitTypes.LU:
+ return LearningUnit.fromRecord(u, exp_path);
+ break;
+ case UnitTypes.TASK:
+ u.basedir = basedir;
+ return Task.fromRecord(u, label, exp_path);
+ break;
+ case UnitTypes.AIM:
+ return (new Aim(this.basedir, label, exp_path));
+ break;
+ }
+ });
+ }
+ else {
+ units = [];
+ }
+ }
+
+ static unit_type = UnitTypes.LU;
+
+ static fromRecord(lu, exp_path) {
+ const u = new LearningUnit(
+ lu["unit-type"],
+ lu["label"],
+ exp_path,
+ lu["basedir"],
+ lu["units"]
+ );
+ return u;
+ }
+
+
+ menuItemInfo(host_path) {
+ return {
+ label: this.label,
+ unit_type: this.unit_type,
+ id: this.label.toLowerCase().replace(/ /g, '-'),
+ units: this.units?this.units.map(t => {
+ let info = t.menuItemInfo(host_path);
+ //console.log(info);
+ return info;
+ }):[]
+ };
+ }
+
+
+ build(exp_info, lab_data, options) {
+ log.debug(`Building LU ${this.label}`);
+ if (this.units.length > 0) {
+ this.units.forEach(u => {
+ u.build(exp_info, lab_data, options);
+ });
+ }
+ log.debug(`Finished building LU ${this.label}`);
+ }
+}
+
+module.exports = {LearningUnit};
diff --git a/exp_build/plugin-config.production.js b/exp_build/plugin-config.production.js
new file mode 100644
index 00000000..58eeb48a
--- /dev/null
+++ b/exp_build/plugin-config.production.js
@@ -0,0 +1,52 @@
+const { PluginScope } = require("../enums.js");
+const issues = require("../assets_plugins/json/bug-report-questions.js");
+
+const config = [
+ {
+ id: "plugin-bug-report",
+ scope: PluginScope.PAGE,
+ js_modules: [
+ "https://cdn.vlabs.ac.in/bug-report/vlabs-bug-report.js"
+ ],
+ attributes: {
+ issues: JSON.stringify(issues),
+ },
+ },
+ {
+ id: "svc-rating",
+ scope: PluginScope.PAGE,
+ repo: "https://github.com/virtual-labs/svc-rating",
+ tag: "v1.1.0.beta",
+ label: "Experiment Rating",
+ js_modules: [
+ "https://virtual-labs.github.io/svc-rating/index.js",
+ "https://virtual-labs.github.io/svc-rating/config.js",
+ "https://apis.google.com/js/api.js",
+ ],
+ css_modules: [
+ "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css",
+ ],
+ attributes: {
+ spreadsheetID: "1x12nhpp0QvnsA6x-O1sV4IA9SAbfVsq_wiexWkutOmU",
+ sheetName: "Exp-Rating-Clean",
+ columnName: "Experiment Short Name",
+ columnValue: "expName",
+ title: "Rate this experiment",
+ imagesDirectory: "https://virtual-labs.github.io/svc-rating/",
+ },
+
+ },
+ {
+ id: "VLABS-code-editor",
+ div_id: "code-editor",
+ js_modules: [
+ "https://virtual-labs.github.io/comp-code-editor/js/codeditor.js"
+ ],
+ css_modules: [
+ "https://virtual-labs.github.io/comp-code-editor/css/codeditor.css"
+ ],
+ label: "Code Assessment",
+ },
+];
+
+module.exports = config;
diff --git a/exp_build/plugin-config.testing.js b/exp_build/plugin-config.testing.js
new file mode 100644
index 00000000..c6576998
--- /dev/null
+++ b/exp_build/plugin-config.testing.js
@@ -0,0 +1,75 @@
+const { PluginScope } = require("../enums.js");
+const issues = require("../assets_plugins/json/bug-report-questions.js");
+
+const config = [
+ {
+ id: "plugin-bug-report",
+ scope: PluginScope.PAGE,
+ js_modules: [
+ "https://cdn.vlabs.ac.in/bug-report/vlabs-bug-report.js"
+ ],
+ attributes: {
+ issues: JSON.stringify(issues),
+ },
+ },
+ {
+ id: "tool-performance",
+ scope: PluginScope.EXPERIMENT,
+ repo: "https://github.com/virtual-labs/tool-performance",
+ template: "handlebars/performance-report.handlebars",
+ target: "performance-report.html",
+ label: "Performance Tool",
+ },
+ {
+ id: "tool-validation",
+ scope: PluginScope.EXPERIMENT,
+ repo: "https://github.com/virtual-labs/tool-validation",
+ tag: "v1.0.1",
+ template: "handlebars/validator-report.handlebars",
+ target: "validator-report.html",
+ label: "Validation Tool",
+ },
+ {
+ id: "tool-validation",
+ scope: PluginScope.POSTBUILD,
+ repo: "https://github.com/virtual-labs/tool-validation",
+ tag: "v1.0.1",
+ command: "npm i && node js/link_validation.js",
+ },
+ {
+ id: "svc-rating",
+ scope: PluginScope.PAGE,
+ repo: "https://github.com/virtual-labs/svc-rating",
+ tag: "v1.1.0.beta",
+ label: "Experiment Rating",
+ js_modules: [
+ "index.js",
+ "config.js",
+ "https://apis.google.com/js/api.js",
+ ],
+ css_modules: [
+ "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css",
+ ],
+ attributes: {
+ spreadsheetID: "1x12nhpp0QvnsA6x-O1sV4IA9SAbfVsq_wiexWkutOmU",
+ sheetName: "Experiment-Database",
+ columnName: "Experiment Short Name",
+ columnValue: "expName",
+ title: "Rate this experiment",
+ imagesDirectory: "/plugins/svc-rating/images/",
+ },
+ },
+ {
+ id: "VLABS-code-editor",
+ div_id: "code-editor",
+ js_modules: [
+ "https://virtual-labs.github.io/comp-code-editor/js/codeditor.js"
+ ],
+ css_modules: [
+ "https://virtual-labs.github.io/comp-code-editor/css/codeditor.css"
+ ],
+ label: "Code Assessment",
+ },
+];
+
+module.exports = config;
diff --git a/exp_build/plugin.js b/exp_build/plugin.js
new file mode 100644
index 00000000..1379325e
--- /dev/null
+++ b/exp_build/plugin.js
@@ -0,0 +1,284 @@
+const path = require("path");
+const fs = require("fs");
+const shell = require("shelljs");
+const { JSDOM } = require("jsdom");
+
+const Config = require("../config.js");
+const { PluginConfig, PluginScope } = require("../enums.js");
+const log = require("../logger.js");
+
+function setCurr(component, targetPath, subTaskFlag = false) {
+ let obj = { ...component },
+ isCurrentItem = false;
+
+ if (obj.unit_type === "aim") {
+ subTaskFlag = true;
+ isCurrentItem = true;
+ }
+
+ if (!subTaskFlag) {
+ obj = component.menuItemInfo(targetPath);
+ }
+
+ if (obj.unit_type === "lu") {
+ obj.units = [
+ ...obj.units.map((subComponent) =>
+ setCurr(subComponent, targetPath, true)
+ ),
+ ];
+ }
+
+ return { ...obj, isCurrentItem: isCurrentItem };
+}
+
+
+// function isURL(source) {
+// try {
+// new URL(source);
+// return true;
+// } catch (e) {
+// log.debug(`${source} is not a valid URL`);
+// return false;
+// }
+// }
+
+function finalPath(target_path ,pluginId, modules) {
+ const pluginPath = path.resolve("plugins", pluginId);
+ let final_paths = [];
+ for (let module of modules) {
+ if(Config.isURL(module)) {
+ log.debug(`${module} is a valid URL`);
+ final_paths.push(module);
+ continue;
+ }
+ const absolute_path = path.resolve(
+ path.join(pluginPath, module)
+ );
+ // check if the file exists
+ if (fs.existsSync(absolute_path)) {
+ log.debug(`${absolute_path} is found successfully`);
+ final_paths.push(
+ path.posix.join("/", "plugins", pluginId, module)
+ );
+ }
+ else {
+ log.error(`${absolute_path} does not exist`);
+ }
+ }
+ return final_paths;
+}
+
+function prepareRepo(repoInfo) {
+ log.debug(`Preparing repo ${repoInfo.id}`);
+ if (!fs.existsSync(repoInfo.id)) {
+ if (repoInfo.tag) {
+ shell.exec(`git clone --depth=1 ${repoInfo.repo} --branch ${repoInfo.tag}`, { silent: true });
+ }
+ else {
+ shell.exec(`git clone --depth=1 ${repoInfo.repo}`, { silent: true });
+ }
+ } else {
+ shell.cd(`${repoInfo.id}`);
+ shell.exec(`git pull`, { silent: true });
+ shell.cd("..");
+ }
+}
+
+class Plugin {
+ static getConfigFileName(options_env) {
+ const env = options_env || BuildEnvs.TESTING;
+ const pluginConfigFile = `./plugin-config.${env}.js`;
+ return pluginConfigFile;
+ }
+
+ static loadAllPlugins(options) {
+ const pluginConfigFile = Plugin.getConfigFileName(options.env);
+ const pluginConfig = require(pluginConfigFile);
+
+ if (!fs.existsSync("plugins")) {
+ shell.exec("mkdir plugins");
+ }
+ pluginConfig.map((plugin) => {
+ if(plugin.label == 'Code Assessment') {
+ return;
+ }
+ shell.cd("plugins");
+ prepareRepo(plugin);
+ shell.cd("..");
+ });
+ }
+
+ static loadCodeAssessment(options) {
+ const pluginConfigFile = Plugin.getConfigFileName(options.env);
+ const code_assessment = require(pluginConfigFile).find(plugin => plugin.label == "Code Assessment")
+
+ return [code_assessment.id, code_assessment.div_id, code_assessment.js_modules, code_assessment.css_modules]
+ }
+
+ static processExpScopePlugins(exp_info, hb, lab_data, options) {
+ log.debug("Processing experiment scope plugins");
+ let pluginConfig = require(Plugin.getConfigFileName(options.env));
+
+ if (!options.isValidate) {
+ pluginConfig = pluginConfig.filter((p) => p.id !== "tool-validation");
+ }
+
+ const expScopePlugins = pluginConfig.filter(
+ (p) => p.scope === PluginScope.EXPERIMENT
+ );
+
+ let plugins = [];
+ expScopePlugins.forEach((plugin) => {
+ try {
+ const pluginPath = path.resolve("plugins", plugin.id);
+ const page_template = fs.readFileSync(
+ path.resolve(pluginPath, plugin.template)
+ );
+
+ let assets_path = path.relative(
+ path.dirname(
+ path.join(Config.build_path(exp_info.src), plugin.target)
+ ),
+ Config.build_path(exp_info.src)
+ );
+ assets_path = assets_path ? assets_path : ".";
+ const cssModule = plugin.cssModule || PluginConfig.Default.CSS_MODULE;
+ const jsModule = plugin.jsModule || PluginConfig.Default.JS_MODULE;
+
+ const page_data = {
+ experiment_name: exp_info.name,
+ assets_path: assets_path,
+ units: exp_info.menu.map((component) =>
+ setCurr(
+ component,
+ path.join(Config.build_path(exp_info.src), plugin.target)
+ )
+ ),
+ cssModule: path.join("plugins", plugin.id, cssModule),
+ jsModule: path.join("plugins", plugin.id, jsModule),
+ };
+
+ fs.writeFileSync(
+ path.join(Config.build_path(exp_info.src), plugin.target),
+ hb.compile(page_template.toString())(page_data)
+ );
+
+ plugins.push({ target: plugin.target, label: plugin.label });
+ log.debug(`Plugin ${plugin.id} processed`);
+ } catch (e) {
+ log.error(`Error while processing plugin ${plugin.id}`);
+ log.error(e);
+ }
+ });
+
+ // shell.exec(
+ // `rsync -av "${path.resolve("./plugins")}" "${Config.build_path(
+ // exp_info.src
+ // )}" --exclude=.git`
+ // );
+ // remove .git folder from all folders in plugins
+ shell.rm("-rf", path.resolve("./plugins", "**", ".git/"));
+ shell.cp("-r", path.resolve("./plugins"), Config.build_path(exp_info.src));
+ return plugins;
+ }
+
+ static preProcessPageScopePlugins(options) {
+ const pluginConfigFile = Plugin.getConfigFileName(options.env);
+ const pluginConfig = require(pluginConfigFile);
+ const pageScopePlugins = {};
+ pluginConfig
+ .filter((p) => p.scope === PluginScope.PAGE)
+ .forEach((element) => {
+ pageScopePlugins[element.id] = element;
+ });
+ // console.log(plugins["plugin-bug-report"].attributes.bug_options);
+ return pageScopePlugins;
+ }
+
+
+ static processPageScopePlugins(page, options) {
+ log.debug(`Processing page scope plugins`);
+ const pluginConfigFile = Plugin.getConfigFileName(options.env);
+ const pluginConfig = require(pluginConfigFile);
+
+ const pageScopePlugins = pluginConfig.filter(
+ (p) => p.scope === PluginScope.PAGE
+ );
+
+ const html = fs.readFileSync(path.resolve(page.targetPath()));
+ const dom = new JSDOM(html);
+ const { document } = dom.window;
+
+ pageScopePlugins.forEach((plugin) => {
+ // Render the Plugin UI component inside the parent
+ const pluginParent = document.getElementById(plugin.id);
+ log.debug(`Processing plugin ${plugin.id}`);
+ if (pluginParent) {
+ // Write code to process a template file and add to the parent
+ }
+
+ // add the css-modules in the head
+ if (plugin.css_modules) {
+ const css_modules = finalPath(page.targetPath(), plugin.id, plugin.css_modules);
+ css_modules.forEach((css) => {
+ const cssNode = document.createElement("link");
+ cssNode.rel = "stylesheet";
+ cssNode.href = css;
+
+ document.head.appendChild(cssNode);
+ });
+ }
+ // add the js-modules at the bottom of the body
+ if (plugin.js_modules) {
+ const js_modules = finalPath(page.targetPath(), plugin.id, plugin.js_modules);
+ js_modules.forEach((mjs) => {
+ const scriptNode = document.createElement("script");
+ scriptNode.type = "module";
+ scriptNode.src = mjs;
+
+ document.body.appendChild(scriptNode);
+ });
+ }
+ log.debug(`Plugin ${plugin.id} processed`);
+ });
+ fs.writeFileSync(page.targetPath(), dom.serialize());
+ }
+
+ static processPostBuildPlugins(exp_info, options) {
+ log.debug("Processing post build plugins");
+ let pluginConfig = require(Plugin.getConfigFileName(options.env));
+
+ if (!options.isValidate) {
+ pluginConfig = pluginConfig.filter((p) => p.id !== "tool-validation");
+ }
+
+ const postBuildScopePlugins = pluginConfig.filter(
+ (p) => p.scope === PluginScope.POSTBUILD
+ );
+ if (!fs.existsSync("plugins")) {
+ shell.exec("mkdir plugins");
+ }
+
+ postBuildScopePlugins.forEach((plugin) => {
+ try {
+ log.debug(`Processing plugin ${plugin.id}`);
+ shell.cd("plugins");
+ shell.cd(`${plugin.id}`);
+ try {
+ shell.exec(`${plugin.command} ${exp_info.bp}`, { silent: true });
+ } catch (e) {
+ log.error(`Error while executing command ${plugin.command}`);
+ log.error(e);
+ }
+ shell.cd("..");
+ shell.cd("..");
+ log.debug(`Plugin ${plugin.id} processed`);
+ } catch (e) {
+ log.error(`Error while processing plugin ${plugin.id}`);
+ log.error(e);
+ }
+ });
+ }
+}
+
+module.exports = { Plugin };
diff --git a/exp_build/renderer.js b/exp_build/renderer.js
new file mode 100644
index 00000000..3cdc40ec
--- /dev/null
+++ b/exp_build/renderer.js
@@ -0,0 +1,284 @@
+const marked = require("marked");
+const katex = require("katex");
+const Config = require("../config.js");
+const path = require("path");
+const shell = require("shelljs");
+const args = require("minimist")(process.argv.slice(2));
+const log = require("../logger.js");
+
+
+let src = ".";
+if (args.src) {
+ src = args.src;
+ src = path.resolve(Config.PROJECT_ROOT, src);
+}
+
+let LaTeXinMD = true;
+let expressions = [];
+let descriptorPath = path.resolve(
+ src,
+ `${Config.Experiment.descriptor_name}`
+);
+
+if (!shell.test("-f", descriptorPath)) {
+ LaTeXinMD = false;
+} else {
+ descriptor = require(descriptorPath);
+ LaTeXinMD = descriptor.LaTeXinMD || false;
+}
+
+const tokenizeDoubleDollar = (md) => {
+ // Find all $$ by for loop
+ const matches = [];
+ for (let i = 0; i < md.length - 1; i++) {
+ if (md[i] === "$" && md[i + 1] === "$") {
+ let info = {
+ start: i,
+ end: i + 1,
+ type: "doubleDollar",
+ };
+ matches.push(info);
+ i++;
+ }
+ }
+ return matches;
+};
+
+const tokenizeSingleDollar = (md) => {
+ // Find all $ by for loop
+ const matches = [];
+ for (let i = 0; i < md.length; i++) {
+ if (md[i] === "$") {
+ if (i > 0 && md[i - 1] === "$") {
+ continue;
+ }
+ if (i < md.length - 1 && md[i + 1] === "$") {
+ continue;
+ }
+
+ let info = {
+ start: i,
+ end: i,
+ type: "singleDollar",
+ };
+
+ matches.push(info);
+ }
+ }
+ return matches;
+};
+
+const handleDoubleDollar = (matches, md) => {
+ let num_matches = matches.length;
+ num_matches = num_matches % 2 === 0 ? num_matches : num_matches - 1;
+
+ // replace everything between $$...$$ with {{LATEX-EXPRESSSION}}
+ let offset = 0;
+ for (let i = 0; i < num_matches - 1; i++) {
+ let j = i + 1;
+ let match1 = matches[i];
+ let match2 = matches[j];
+
+ let start1 = match1.start + offset;
+ let end1 = match1.end + offset;
+ let start2 = match2.start + offset;
+ let end2 = match2.end + offset;
+
+ let latex = md.substring(start1, end2 + 1);
+ let expression = `{{LATEX-EXPRESSSION-${expressions.length}}}`;
+ expressions.push(katexRender(latex, true));
+ md = md.substring(0, start1) + expression + md.substring(end2 + 1);
+
+ offset += expression.length - latex.length;
+ i++;
+ }
+ return md;
+};
+
+const checkForNewline = (md, pos1, pos2) => {
+ for (let i = pos1; i < pos2; i++) {
+ if (md[i] === "\n") {
+ return true;
+ }
+ }
+ return false;
+};
+
+const handleSingleDollar = (matches, md) => {
+ let num_matches = matches.length;
+ // num_matches = num_matches % 2 === 0 ? num_matches : num_matches - 1;
+
+ let offset = 0;
+ for (let i = 0; i < num_matches - 1; i++) {
+ let j = i + 1;
+ // console.log(i,j);
+ if (
+ checkForNewline(md, matches[i].start + offset, matches[j].start + offset)
+ ) {
+ continue;
+ }
+
+ let match1 = matches[i];
+ let match2 = matches[j];
+
+ let start1 = match1.start + offset;
+ let end1 = match1.end + offset;
+ let start2 = match2.start + offset;
+ let end2 = match2.end + offset;
+
+ let latex = md.substring(start1, end2 + 1);
+ let expression = `{{LATEX-EXPRESSSION-${expressions.length}}}`;
+ expressions.push(katexRender(latex, false));
+ md = md.substring(0, start1) + expression + md.substring(end2 + 1);
+
+ offset += expression.length - latex.length;
+ i++;
+ }
+ return md;
+};
+
+
+// function to detect maths expression and replace it with {{LATEX-EXPRESSSION}} keyword
+function preProcessData(md) {
+ const matchesDD = tokenizeDoubleDollar(md);
+ const renderedDD = handleDoubleDollar(matchesDD, md);
+ const matchesSD = tokenizeSingleDollar(renderedDD);
+ const renderedSD = handleSingleDollar(matchesSD, renderedDD);
+ return renderedSD;
+}
+
+function katexRender(texString, displayMode) {
+ let html = "";
+
+ // remove all $ from texString start and end
+ while (texString[0] === "$") {
+ texString = texString.substring(1);
+ }
+ while (texString[texString.length - 1] === "$") {
+ texString = texString.substring(0, texString.length - 1);
+ }
+
+ try {
+ html = katex.renderToString(texString, { displayMode: displayMode });
+ } catch (e) {
+ if (e instanceof katex.ParseError) {
+ // KaTeX can't parse the expression
+ html = ("Error in LaTeX '" + texString + "': " + e.message)
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+ // wrap in to make it more visible
+ html = "" + html + "";
+ } else {
+ throw e; // other error
+ }
+ }
+ return html;
+}
+
+function mathsExpression(expr) {
+ // trim expr till both sides start with $
+ while (expr[0] !== "$" && expr.length > 0) {
+ expr = expr.substring(1);
+ }
+ while (expr[expr.length - 1] !== "$" && expr.length > 0) {
+ expr = expr.substring(0, expr.length - 1);
+ }
+
+ if (expr.match(/^\$\$[\s\S]*\$\$$/)) {
+ expr = expr.substr(2, expr.length - 4);
+ return katexRender(expr, true);
+ } else if (expr.match(/^\$[\s\S]*\$$/)) {
+ expr = expr.substr(1, expr.length - 2);
+ return katexRender(expr, false);
+ }
+}
+
+function replaceCodeBlocks(html) {
+
+ if (typeof html === "string") {
+ html = html.replace(/{{LATEX-EXPRESSSION-\d+}}/g, function(match) {
+ const index = parseInt(match.match(/\d+/)[0]);
+ return expressions[index];
+ });
+ }
+ return html;
+}
+
+function renderMarkdown(md) {
+ log.debug("Rendering Markdown" + (LaTeXinMD ? " with LaTeX" : ""));
+ if (LaTeXinMD) {
+ expressions = [];
+ const preProcessedMd = preProcessData(md);
+ let html = marked(preProcessedMd);
+ html = replaceCodeBlocks(html);
+ return html;
+ } else {
+ return marked(md);
+ }
+}
+
+function renderJSON(json) {
+ log.debug("Rendering JSON");
+ json = JSON.parse(json);
+
+ // check for versioning
+ if (json.version) {
+ let questions = json.questions;
+ questions.forEach((question) => {
+ // render question
+ let processedQuestion = preProcessData(question.question);
+ let questionHTML = replaceCodeBlocks(processedQuestion);
+ question.question = questionHTML;
+
+ // render answers
+ let answers = question.answers;
+ for (option in answers) {
+ let answer = answers[option];
+ let processedAnswer = preProcessData(answer);
+ let answerHTML = replaceCodeBlocks(processedAnswer);
+ question.answers[option] = answerHTML;
+ }
+
+ // render explanations
+ let explanations = question.explanations;
+ for (option in explanations) {
+ let explanation = explanations[option];
+ let processedExplanation = preProcessData(explanation);
+ let explanationHTML = replaceCodeBlocks(processedExplanation);
+ question.explanations[option] = explanationHTML;
+ }
+ });
+ }
+ else {
+ // older version, list of objects
+ json.forEach((element) => {
+
+ let question = element.question;
+ let answers = element.answers;
+ let corectAnswer = element.correctAnswer;
+
+ // render question
+ let processedQuestion = preProcessData(question);
+ let questionHTML = replaceCodeBlocks(processedQuestion);
+ element.question = questionHTML;
+
+ // render answers
+ for (option in answers) {
+ let answer = answers[option];
+ let processedAnswer = preProcessData(answer);
+ let answerHTML = replaceCodeBlocks(processedAnswer);
+ element.answers[option] = answerHTML;
+ }
+
+ // render correct answer
+ let processedCorrectAnswer = preProcessData(corectAnswer);
+ let correctAnswerHTML = replaceCodeBlocks(processedCorrectAnswer);
+ element.correctAnswer = correctAnswerHTML;
+ });
+ }
+
+ return JSON.stringify(json);
+}
+
+module.exports = { renderMarkdown, renderJSON };
diff --git a/page-components/feedback-ctnt.html b/exp_build/static_content/feedback.md
similarity index 66%
rename from page-components/feedback-ctnt.html
rename to exp_build/static_content/feedback.md
index 609563b0..33fd1349 100644
--- a/page-components/feedback-ctnt.html
+++ b/exp_build/static_content/feedback.md
@@ -4,7 +4,7 @@
Feedback
Thanks for using Virtual Labs. Your opinion is valuable to us. To help us improve, we'd like to ask you a few questions about your experience. It will only take 3 minutes and your answers will help us make Virtual Labs better for you and other users.