";
+
+ final InputStream htmlTemplate = getInputStream(Generator.getBaseDirectory() + SEP + "ext" + SEP + "bootstrap" + SEP + "assets" + SEP + "HelpTemplate.html");
+ final String templateHtml = new String(htmlTemplate.readAllBytes(), StandardCharsets.UTF_8);
+ html.append(templateHtml);
+
+ // Create the css and js links and scripts
final String stylesheetLink = "
";
final String javascriptText = "";
-
- final String css = String.format(stylesheetLink, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/css/app.css"));
- final String noScript = String.format(stylesheetLink, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/css/noscript.css"));
- final String cssBootstrap = String.format(stylesheetLink, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/css/bootstrap.css"));
- final String jquery = String.format("", getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.min.js"));
- final String dropotron = String.format(javascriptText, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.dropotron.min.js"));
- final String scrolly = String.format(javascriptText, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.scrolly.min.js"));
- final String scrollex = String.format(javascriptText, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.scrollex.min.js"));
- final String browser = String.format(javascriptText, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/browser.min.js"));
- final String breakpoints = String.format(javascriptText, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/breakpoints.min.js"));
- final String appJS = String.format(javascriptText, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/app.js"));
- final String boostrapjs = String.format(javascriptText, getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/js/bootstrap.js"));
- final String cookiejs = String.format("", getFileURLString(separator, Generator.getBaseDirectory(), "ext/bootstrap/js/js.cookie.min.js"));
-
- final String scriptTag = "";
-
- // Add items to StringBuilder
- html.append(css);
- html.append(NEWLINE);
- html.append(noScript);
- html.append(NEWLINE);
- html.append(cssBootstrap);
- html.append(NEWLINE);
- html.append(jquery);
- html.append(NEWLINE);
- html.append(cookiejs);
- html.append(NEWLINE);
- html.append(dropotron);
- html.append(NEWLINE);
- html.append(scrolly);
- html.append(NEWLINE);
- html.append(scrollex);
- html.append(NEWLINE);
- html.append(browser);
- html.append(NEWLINE);
- html.append(breakpoints);
- html.append(NEWLINE);
- html.append(appJS);
- html.append(NEWLINE);
- html.append(boostrapjs);
- html.append(NEWLINE);
- html.append(startRowDiv);
- html.append(NEWLINE);
- html.append(startColDiv);
- html.append(NEWLINE);
+
+ final String css = String.format(stylesheetLink, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/css/app.css"));
+ final String noScript = String.format(stylesheetLink, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/css/noscript.css"));
+ final String cssBootstrap = String.format(stylesheetLink, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/css/bootstrap.css"));
+ final String searchcss = String.format(stylesheetLink, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/css/search.css"));
+ final String jquery = String.format("", getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.min.js"));
+ final String dropotron = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.dropotron.min.js"));
+ final String scrolly = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.scrolly.min.js"));
+ final String scrollex = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/jquery.scrollex.min.js"));
+ final String browser = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/browser.min.js"));
+ final String breakpoints = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/breakpoints.min.js"));
+ final String appJS = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/assets/js/app.js"));
+ final String bootstrapjs = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/js/bootstrap.js"));
+ final String cookiejs = String.format("", getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/js/js.cookie.min.js"));
+ final String searchjs = String.format(javascriptText, getFileURLString(SEP, Generator.getBaseDirectory(), "ext/bootstrap/js/index.min.js"));
+
+ final StringBuilder scripts = new StringBuilder();
+ scripts.append(css).append(NEWLINE);
+ scripts.append(noScript).append(NEWLINE);
+ scripts.append(cssBootstrap).append(NEWLINE);
+ scripts.append(searchcss).append(NEWLINE);
+ scripts.append(jquery).append(NEWLINE);
+ scripts.append(dropotron).append(NEWLINE);
+ scripts.append(scrolly).append(NEWLINE);
+ scripts.append(scrollex).append(NEWLINE);
+ scripts.append(browser).append(NEWLINE);
+ scripts.append(breakpoints).append(NEWLINE);
+ scripts.append(appJS).append(NEWLINE);
+ scripts.append(bootstrapjs).append(NEWLINE);
+ scripts.append(cookiejs).append(NEWLINE);
+ scripts.append(searchjs).append(NEWLINE);
+
+ // Add in the links and scripts to the top of the file
+ final int scriptIndex = html.indexOf("SCRIPTS");
+ html.replace(scriptIndex, scriptIndex + 7, scripts.toString());
+
+
+ // Add in the TOC
final String tocString = new String(tocInput.readAllBytes(), StandardCharsets.UTF_8);
final Parser parser = Parser.builder().build();
final HtmlRenderer renderer = HtmlRenderer.builder().build();
final Node tocDocument = parser.parse(tocString);
final String tocHtml = renderer.render(tocDocument);
- html.append(tocHtml);
- html.append(NEWLINE);
- html.append(endDiv);
- html.append(NEWLINE);
- html.append(startInnerColDiv);
- html.append(NEWLINE);
+ final int tocIndex = html.indexOf("TABLE_OF_CONTENTS");
+ html.replace(tocIndex, tocIndex + 17, tocHtml);
+
+ // Add in the help page
final String rawInput = new String(pageInput.readAllBytes(), StandardCharsets.UTF_8);
final Node pageDocument = parser.parse(rawInput);
final String pageHtml = renderer.render(pageDocument);
- html.append(pageHtml);
- html.append(NEWLINE);
- html.append(endDiv);
- html.append(NEWLINE);
- html.append(endDiv);
- html.append(NEWLINE);
- html.append(endDiv);
- html.append(NEWLINE);
- html.append(scriptTag);
-
- String htmlString = html.toString();
- final int headTagIndex = htmlString.indexOf(" -1) {
- insertPos = htmlString.substring(headTagIndex).indexOf(">") + headTagIndex + 1;
+ final int pageIndex = html.indexOf("MAIN_PAGE");
+ html.replace(pageIndex, pageIndex + 9, pageHtml);
+
+ // Add in the list of documents to search
+ final List
documents = createSearchDocument(tocHtml);
+ final StringBuilder documentString = new StringBuilder();
+ documentString.append(documents.getFirst());
+ for (int i = 1; i < documents.size(); i++) {
+ documentString.append(",");
+ documentString.append(documents.get(i));
}
- final int metaIndex = htmlString.indexOf("" + htmlString.substring(insertPos);
- } else {
- // check the meta tag
- final int metaTagEnd = htmlString.substring(metaIndex).indexOf(">");
- String metaString = htmlString.substring(metaIndex, metaIndex + metaTagEnd + 1);
- final int contentIndex = metaString.indexOf("content=\"");
- if (contentIndex > -1) {
- final int endContentPos = metaString.substring(contentIndex+9).indexOf("\"");
- String contentString = metaString.substring(contentIndex + 9, contentIndex + 9 + endContentPos);
- final int charsetIndex = contentString.indexOf("charset=");
- if (charsetIndex > -1) {
- // find end of charset info and overwrite it
- int charsetEndIndex = contentString.substring(charsetIndex).indexOf(";");
- // a value of -1 means id was the last entry of the content attribute
- if (charsetEndIndex == -1) {
- charsetEndIndex = contentString.length();
- }
- // replace the charset value with utf-8
- contentString = contentString.substring(0, charsetIndex + 8) + "utf-8" + contentString.substring(charsetEndIndex);
- } else {
- // no charset specified in the content attribute, add it to the content string
- contentString = contentString + "; charset=utf-8";
- }
- // insert new content string into metastring
- metaString = metaString.substring(0, contentIndex + 9) + contentString + metaString.substring(contentIndex + 9 + endContentPos);
- } else {
- // no content attribute
- // check for charset attribute
- final int charsetAttrIndex = metaString.indexOf("charset=\"");
- if (charsetAttrIndex > -1) {
- int charsetEndIndex = metaString.substring(charsetAttrIndex + 9).indexOf("\"");
- if (charsetEndIndex > -1) {
- // put utf-8 in the charset string
- metaString = metaString.substring(0, charsetAttrIndex + 9) + "utf-8" + metaString.substring(charsetAttrIndex + 9 + charsetEndIndex);
- }
- } else {
- // add a content attribute to the metaString
- metaString = metaString.substring(0, metaString.lastIndexOf("\"") + 1) + " content=\"text/html; charset=utf-8\">";
- }
- }
- // insert the new/updated metastring into htmlString
- final String nonMetaHtml = htmlString.substring(0, metaIndex) + htmlString.substring(metaIndex + metaTagEnd + 1);
- htmlString = nonMetaHtml.substring(0, insertPos) + metaString + nonMetaHtml.substring(insertPos);
- }
- return htmlString;
+ final int documentsIndex = html.indexOf("HELP_PAGES");
+ html.replace(documentsIndex, documentsIndex + 10, documentString.toString());
+
+ return html.toString();
}
/**
@@ -289,16 +200,14 @@ protected static String generateHTMLOutput(final String separator, final InputSt
*/
@Override
public boolean display(final HelpCtx helpCtx) {
- final String sep = File.separator;
-
final Preferences prefs = NbPreferences.forModule(HelpPreferenceKeys.class);
final boolean isOnline = prefs.getBoolean(HelpPreferenceKeys.HELP_KEY, HelpPreferenceKeys.ONLINE_HELP);
final String helpId = helpCtx.getHelpID();
LOGGER.log(Level.INFO, "display help for: {0}", helpId);
- final String helpDefaultPath = sep + "ext" + sep + "docs" + sep + "CoreFunctionality" + sep + "src" + sep + "au" + sep + "gov"
- + sep + "asd" + sep + "tac" + sep + "constellation" + sep + "functionality" + sep + "about-constellation.md";
+ final String helpDefaultPath = SEP + "ext" + SEP + "docs" + SEP + "CoreFunctionality" + SEP + "src" + SEP + "au" + SEP + "gov"
+ + SEP + "asd" + SEP + "tac" + SEP + "constellation" + SEP + "functionality" + SEP + "about-constellation.md";
final String helpAddress = HelpMapper.getHelpAddress(helpId);
// use the requested help file, or the About Constellation page if one is not given
@@ -312,7 +221,7 @@ public boolean display(final HelpCtx helpCtx) {
}
url = OFFICIAL_CONSTELLATION_WEBSITE + helpLink.replace(".md", ".html");
} else {
- final File file = new File(Generator.getBaseDirectory() + sep + helpLink);
+ final File file = new File(Generator.getBaseDirectory() + SEP + helpLink);
final URL fileUrl = file.toURI().toURL();
final int currentPort = HelpWebServer.start();
url = String.format("http://localhost:%d/%s", currentPort, fileUrl);
@@ -352,4 +261,74 @@ public static Future> browse(final URI uri) {
}
});
}
+
+ public static List createSearchDocument(final String toc) {
+ // Creates json file of documents to search
+ // Each entry has an id, title, category and link
+ final List documents = new ArrayList<>();
+
+ final String[] elements = SPLIT_REGEX.split(toc);
+ int index = 0;
+ String category = "";
+ String pageName = "";
+ String link = "";
+
+ for (final String element : elements) {
+ if (element.contains("data-target")) {
+ final int dataIndex = element.indexOf("data-target");
+ final String categoryString = element.substring(dataIndex);
+ final int beginningIndex = categoryString.indexOf(">");
+ final int endIndex = categoryString.indexOf("<");
+ category = categoryString.substring(beginningIndex + 1, endIndex);
+ }
+ if (element.contains("a href")) {
+ final int hrefIndex = element.indexOf("a href");
+ final String linkString = element.substring(hrefIndex + 7);
+ final int quoteIndex = linkString.indexOf("\"");
+ final int endIndex = linkString.indexOf(">");
+ link = linkString.substring(quoteIndex + 1, endIndex - 1);
+ // Changing this to a replace instead of replaceAll stops the search from working as the links require double \
+ link = link.replaceAll("\\\\", "\\\\\\\\");
+ pageName = linkString.substring(endIndex + 1);
+
+ final String page = String.format("""
+ {
+ "id": %s,
+ "title": "%s",
+ "category": "%s",
+ "link": "%s"
+ }
+ """, index, pageName, category, link);
+ documents.add(page);
+ index++;
+
+ }
+ }
+
+ // To update the online help search file change the boolean to true
+ // Must also run adaptors when updating online help so those results aren't removed from the search file
+ // Reset back to false after updating the search file
+ final boolean updateOnlineHelp = false;
+
+ if (updateOnlineHelp) {
+ // Create a json file for the online help search
+ final String path = Generator.getOnlineHelpTOCDirectory(Generator.getBaseDirectory()) + "search.json";
+ try (final FileWriter search = new FileWriter(path)) {
+ final StringBuilder documentString = new StringBuilder();
+ documentString.append("[");
+ documentString.append(documents.getFirst());
+ for (int i = 1; i < documents.size(); i++) {
+ documentString.append(",");
+ documentString.append(documents.get(i));
+ }
+ documentString.append("]");
+ search.write(documentString.toString());
+
+ } catch (final IOException ex) {
+ LOGGER.log(Level.SEVERE, ex.getLocalizedMessage());
+ }
+ }
+
+ return documents;
+ }
}
diff --git a/CoreHelp/src/au/gov/asd/tac/constellation/help/utilities/Generator.java b/CoreHelp/src/au/gov/asd/tac/constellation/help/utilities/Generator.java
index 093bffb21a..c9bb6652d9 100644
--- a/CoreHelp/src/au/gov/asd/tac/constellation/help/utilities/Generator.java
+++ b/CoreHelp/src/au/gov/asd/tac/constellation/help/utilities/Generator.java
@@ -77,7 +77,7 @@ public void run() {
final boolean updateOnlineHelp = false;
if (updateOnlineHelp) {
- onlineTocDirectory = getOnlineHelpTOCDirectory(baseDirectory);
+ onlineTocDirectory = getOnlineHelpTOCDirectory(baseDirectory) + TOC_FILE_NAME;
// First: create the TOCFile in the base directory for ONLINE help
// Create the online root node for application-wide table of contents
@@ -106,7 +106,7 @@ public void run() {
}
/**
- * get the directory that the table of contents is saved to
+ * Get the directory that the table of contents is saved to
*
* @return a String path for the file location
*/
@@ -116,7 +116,7 @@ public static String getTOCDirectory() {
}
/**
- * get a list of the xml files using a lookup
+ * Get a list of the xml files using a lookup
*
* @param baseDirectory
* @return
@@ -168,19 +168,14 @@ protected static String getResource() throws IllegalArgumentException {
return newPath != null ? newPath + File.separator + "ext" : "";
}
- protected static String getOnlineHelpTOCDirectory(final String filePath) {
+ public static String getOnlineHelpTOCDirectory(final String filePath) {
// include "modules" in the check, because looking for "constellation" alone can match earlier in the path
// ie. /home/constellation/test/rc1/constellation/modules/ext/
int index = filePath.indexOf("constellation" + File.separator + "modules");
if (index <= 0) {
index = filePath.indexOf("constellation" + File.separator);
}
- if (index <= 0) {
- return filePath + TOC_FILE_NAME;
- } else {
- final String newPath = filePath.substring(0, index + 14);
- return newPath + TOC_FILE_NAME;
- }
+ return index <= 0 ? filePath : filePath.substring(0, index + 14);
}
}
diff --git a/CoreHelp/test/unit/src/au/gov/asd/tac/constellation/help/ConstellationHelpDisplayerNGTest.java b/CoreHelp/test/unit/src/au/gov/asd/tac/constellation/help/ConstellationHelpDisplayerNGTest.java
index 64cfd7a90b..f5b992e832 100644
--- a/CoreHelp/test/unit/src/au/gov/asd/tac/constellation/help/ConstellationHelpDisplayerNGTest.java
+++ b/CoreHelp/test/unit/src/au/gov/asd/tac/constellation/help/ConstellationHelpDisplayerNGTest.java
@@ -127,7 +127,7 @@ public void testCopy() {
try (MockedStatic mockedHelpDisplayerStatic = Mockito.mockStatic(ConstellationHelpDisplayer.class)) {
mockedHelpDisplayerStatic.when(() -> ConstellationHelpDisplayer.copy(Mockito.anyString(), Mockito.any())).thenCallRealMethod();
mockedHelpDisplayerStatic.when(() -> ConstellationHelpDisplayer.getInputStream(Mockito.anyString())).thenCallRealMethod();
- mockedHelpDisplayerStatic.when(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.anyString(), Mockito.any(), Mockito.any())).thenReturn(returnHTML);
+ mockedHelpDisplayerStatic.when(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.any(), Mockito.any())).thenReturn(returnHTML);
ConstellationHelpDisplayer.copy(filePath, out);
out.flush();
@@ -175,7 +175,7 @@ public void testCopyReturnEarly() throws IOException {
try (MockedStatic mockedHelpDisplayerStatic2 = Mockito.mockStatic(ConstellationHelpDisplayer.class)) {
mockedHelpDisplayerStatic2.when(() -> ConstellationHelpDisplayer.copy(Mockito.anyString(), Mockito.any())).thenCallRealMethod();
mockedHelpDisplayerStatic2.when(() -> ConstellationHelpDisplayer.getInputStream(Mockito.anyString())).thenReturn(fis);
- mockedHelpDisplayerStatic2.when(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.anyString(), Mockito.any(), Mockito.any())).thenReturn("");
+ mockedHelpDisplayerStatic2.when(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.any(), Mockito.any())).thenReturn("");
ConstellationHelpDisplayer.copy("anypath.css", os);
mockedHelpDisplayerStatic2.verify(() -> ConstellationHelpDisplayer.getInputStream(Mockito.anyString()), times(2));
@@ -186,11 +186,11 @@ public void testCopyReturnEarly() throws IOException {
try (MockedStatic mockedHelpDisplayerStatic3 = Mockito.mockStatic(ConstellationHelpDisplayer.class)) {
mockedHelpDisplayerStatic3.when(() -> ConstellationHelpDisplayer.copy(Mockito.anyString(), Mockito.any())).thenCallRealMethod();
mockedHelpDisplayerStatic3.when(() -> ConstellationHelpDisplayer.getInputStream(Mockito.anyString())).thenReturn(fis);
- mockedHelpDisplayerStatic3.when(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.anyString(), Mockito.any(), Mockito.any())).thenReturn("");
+ mockedHelpDisplayerStatic3.when(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.any(), Mockito.any())).thenReturn("");
ConstellationHelpDisplayer.copy("anypath.txt", os);
mockedHelpDisplayerStatic3.verify(() -> ConstellationHelpDisplayer.getInputStream(Mockito.anyString()), times(2));
- mockedHelpDisplayerStatic3.verify(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.anyString(), Mockito.eq(fis), Mockito.eq(fis)), times(1));
+ mockedHelpDisplayerStatic3.verify(() -> ConstellationHelpDisplayer.generateHTMLOutput(Mockito.eq(fis), Mockito.eq(fis)), times(1));
Mockito.verify(os, times(1)).write(Mockito.eq(arr));
}
}
diff --git a/CoreWhatsNewView/src/au/gov/asd/tac/constellation/views/whatsnew/whatsnew.txt b/CoreWhatsNewView/src/au/gov/asd/tac/constellation/views/whatsnew/whatsnew.txt
index c0dfd174b1..caf70a7198 100644
--- a/CoreWhatsNewView/src/au/gov/asd/tac/constellation/views/whatsnew/whatsnew.txt
+++ b/CoreWhatsNewView/src/au/gov/asd/tac/constellation/views/whatsnew/whatsnew.txt
@@ -1,6 +1,10 @@
== 3030-12-31 Getting Started
If you're new to Constellation, read the getting started guide.
+== 2025-02-03 Search Added to Online and Offline Help
+A search has been added to the help documentation to make finding help pages easier.
+This search uses the names of each help page and the category they are in for the table of contents to narrow down results.
+
== 2024-12-18 Animation updates
Direction Indicators animation moved to Animations -> Experimental.
New Color Warp animation added to Animations -> Experimental.
diff --git a/bootstrap/assets/css/search.css b/bootstrap/assets/css/search.css
new file mode 100644
index 0000000000..8cba8e12a1
--- /dev/null
+++ b/bootstrap/assets/css/search.css
@@ -0,0 +1,130 @@
+.SearchBox {
+ position: relative;
+}
+
+.Search {
+ position: relative;
+}
+
+.Search button.clear {
+ position: absolute;
+ top: 0;
+ bottom: 0.2em;
+ right: 0.5em;
+ font-size: 1.5em;
+ line-height: 1;
+ z-index: 20;
+ border: none;
+ background: none;
+ outline: none;
+ margin: 0;
+ padding: 0;
+ color: black;
+}
+
+.Search input {
+ position: relative;
+ width: 100%;
+ padding: 0.5em;
+ left: 40px;
+ font-size: 16px;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ outline: none;
+ color: #555;
+ box-shadow: none;
+}
+
+.Search button.icon {
+ position: absolute;
+ top: 0;
+ bottom: 0.2em;
+ left: 0;
+ font-size: 1.5em;
+ line-height: 1;
+ z-index: 20;
+ border: none;
+ background: none;
+ outline: none;
+ margin: 0;
+ padding: 0;
+}
+
+.hasResults .Explanation {
+ display: none;
+}
+
+.ResultList {
+ margin: 1em 0 0 0;
+ padding: 0;
+ list-style: none;
+ flex-grow: 1;
+ position: relative;
+ overflow-y: scroll;
+ -webkit-overflow-scrolling: touch;
+}
+
+.ResultList:before {
+ content: '';
+ display: block;
+ position: sticky;
+ z-index: 10;
+ left: 0;
+ right: 0;
+ top: -1px;
+ width: 100%;
+ height: 0.7em;
+ margin-bottom: -0.7em;
+ background: linear-gradient(white, rgba(255, 255, 255, 0));
+}
+
+.ResultList:after {
+ content: '';
+ display: block;
+ position: sticky;
+ z-index: 10;
+ left: 0;
+ right: 0;
+ bottom: -1px;
+ width: 100%;
+ height: 0.7em;
+ margin-bottom: -0.7em;
+ background: linear-gradient(rgba(255, 255, 255, 0), white);
+}
+
+.SuggestionList {
+ display: none;
+ list-style: none;
+ padding: 0;
+ border: 1px solid #ccc;
+ border-top: 0;
+ margin: 0 0 0.2em 0;
+ border-radius: 3px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ background: rgba(255, 255, 255, 0.93);
+ position: absolute;
+ z-index: 20;
+ left: 0;
+ right: 0;
+}
+
+.hasSuggestions .SuggestionList {
+ display: block;
+}
+
+.Suggestion {
+ padding: 0.5em 1em;
+ border-bottom: 1px solid #eee;
+}
+
+.Suggestion:last-child {
+ border: none;
+}
+
+.Suggestion.selected {
+ background: rgba(240, 240, 240, 0.95);
+}
+
+.Suggestion:hover:not(.selected) {
+ background: rgba(250, 250, 250, 0.95);
+}
\ No newline at end of file
diff --git a/bootstrap/js/index.min.js b/bootstrap/js/index.min.js
new file mode 100644
index 0000000000..bb6421491b
--- /dev/null
+++ b/bootstrap/js/index.min.js
@@ -0,0 +1,8 @@
+/**
+ * Minified by jsDelivr using Terser v5.19.2.
+ * Original file: /npm/minisearch@7.1.1/dist/umd/index.js
+ *
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).MiniSearch=e()}(this,(function(){"use strict";function t(t,e,s,i){return new(s||(s=Promise))((function(n,o){function r(t){try{u(i.next(t))}catch(t){o(t)}}function c(t){try{u(i.throw(t))}catch(t){o(t)}}function u(t){var e;t.done?n(t.value):(e=t.value,e instanceof s?e:new s((function(t){t(e)}))).then(r,c)}u((i=i.apply(t,e||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;const e="KEYS",s="VALUES",i="";class n{constructor(t,e){const s=t._tree,i=Array.from(s.keys());this.set=t,this._type=e,this._path=i.length>0?[{node:s,keys:i}]:[]}next(){const t=this.dive();return this.backtrack(),t}dive(){if(0===this._path.length)return{done:!0,value:void 0};const{node:t,keys:e}=o(this._path);if(o(e)===i)return{done:!1,value:this.result()};const s=t.get(o(e));return this._path.push({node:s,keys:Array.from(s.keys())}),this.dive()}backtrack(){if(0===this._path.length)return;const t=o(this._path).keys;t.pop(),t.length>0||(this._path.pop(),this.backtrack())}key(){return this.set._prefix+this._path.map((({keys:t})=>o(t))).filter((t=>t!==i)).join("")}value(){return o(this._path).node.get(i)}result(){switch(this._type){case s:return this.value();case e:return this.key();default:return[this.key(),this.value()]}}[Symbol.iterator](){return this}}const o=t=>t[t.length-1],r=(t,e,s,n,o,c,u,h)=>{const d=c*u;t:for(const a of t.keys())if(a===i){const e=o[d-1];e<=s&&n.set(h,[t.get(a),e])}else{let i=c;for(let t=0;ts)continue t}r(t.get(a),e,s,n,o,i,u,h+a)}};class c{constructor(t=new Map,e=""){this._size=void 0,this._tree=t,this._prefix=e}atPrefix(t){if(!t.startsWith(this._prefix))throw new Error("Mismatched prefix");const[e,s]=u(this._tree,t.slice(this._prefix.length));if(void 0===e){const[e,n]=m(s);for(const s of e.keys())if(s!==i&&s.startsWith(n)){const i=new Map;return i.set(s.slice(n.length),e.get(s)),new c(i,t)}}return new c(e,t)}clear(){this._size=void 0,this._tree.clear()}delete(t){return this._size=void 0,a(this._tree,t)}entries(){return new n(this,"ENTRIES")}forEach(t){for(const[e,s]of this)t(e,s,this)}fuzzyGet(t,e){return((t,e,s)=>{const i=new Map;if(void 0===e)return i;const n=e.length+1,o=n+s,c=new Uint8Array(o*n).fill(s+1);for(let t=0;t{if(0===e.length||null==t)return[t,s];for(const n of t.keys())if(n!==i&&e.startsWith(n))return s.push([t,n]),u(t.get(n),e.slice(n.length),s);return s.push([t,e]),u(void 0,"",s)},h=(t,e)=>{if(0===e.length||null==t)return t;for(const s of t.keys())if(s!==i&&e.startsWith(s))return h(t.get(s),e.slice(s.length))},d=(t,e)=>{const s=e.length;t:for(let n=0;t&&n{const[s,n]=u(t,e);if(void 0!==s)if(s.delete(i),0===s.size)l(n);else if(1===s.size){const[t,e]=s.entries().next().value;f(n,t,e)}},l=t=>{if(0===t.length)return;const[e,s]=m(t);if(e.delete(s),0===e.size)l(t.slice(0,-1));else if(1===e.size){const[s,n]=e.entries().next().value;s!==i&&f(t.slice(0,-1),s,n)}},f=(t,e,s)=>{if(0===t.length)return;const[i,n]=m(t);i.set(n+e,s),i.delete(n)},m=t=>t[t.length-1],g="or";class _{constructor(t){if(null==(null==t?void 0:t.fields))throw new Error('MiniSearch: option "fields" must be provided');const e=null==t.autoVacuum||!0===t.autoVacuum?O:t.autoVacuum;this._options=Object.assign(Object.assign(Object.assign({},v),t),{autoVacuum:e,searchOptions:Object.assign(Object.assign({},x),t.searchOptions||{}),autoSuggestOptions:Object.assign(Object.assign({},z),t.autoSuggestOptions||{})}),this._index=new c,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldIds={},this._fieldLength=new Map,this._avgFieldLength=[],this._nextId=0,this._storedFields=new Map,this._dirtCount=0,this._currentVacuum=null,this._enqueuedVacuum=null,this._enqueuedVacuumConditions=I,this.addFields(this._options.fields)}add(t){const{extractField:e,tokenize:s,processTerm:i,fields:n,idField:o}=this._options,r=e(t,o);if(null==r)throw new Error(`MiniSearch: document does not have ID field "${o}"`);if(this._idToShortId.has(r))throw new Error(`MiniSearch: duplicate ID ${r}`);const c=this.addDocumentId(r);this.saveStoredFields(c,t);for(const o of n){const n=e(t,o);if(null==n)continue;const r=s(n.toString(),o),u=this._fieldIds[o],h=new Set(r).size;this.addFieldLength(c,u,this._documentCount-1,h);for(const t of r){const e=i(t,o);if(Array.isArray(e))for(const t of e)this.addTerm(u,c,t);else e&&this.addTerm(u,c,e)}}}addAll(t){for(const e of t)this.add(e)}addAllAsync(t,e={}){const{chunkSize:s=10}=e,i={chunk:[],promise:Promise.resolve()},{chunk:n,promise:o}=t.reduce((({chunk:t,promise:e},i,n)=>(t.push(i),(n+1)%s==0?{chunk:[],promise:e.then((()=>new Promise((t=>setTimeout(t,0))))).then((()=>this.addAll(t)))}:{chunk:t,promise:e})),i);return o.then((()=>this.addAll(n)))}remove(t){const{tokenize:e,processTerm:s,extractField:i,fields:n,idField:o}=this._options,r=i(t,o);if(null==r)throw new Error(`MiniSearch: document does not have ID field "${o}"`);const c=this._idToShortId.get(r);if(null==c)throw new Error(`MiniSearch: cannot remove document with ID ${r}: it is not in the index`);for(const o of n){const n=i(t,o);if(null==n)continue;const r=e(n.toString(),o),u=this._fieldIds[o],h=new Set(r).size;this.removeFieldLength(c,u,this._documentCount,h);for(const t of r){const e=s(t,o);if(Array.isArray(e))for(const t of e)this.removeTerm(u,c,t);else e&&this.removeTerm(u,c,e)}}this._storedFields.delete(c),this._documentIds.delete(c),this._idToShortId.delete(r),this._fieldLength.delete(c),this._documentCount-=1}removeAll(t){if(t)for(const e of t)this.remove(e);else{if(arguments.length>0)throw new Error("Expected documents to be present. Omit the argument to remove all documents.");this._index=new c,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldLength=new Map,this._avgFieldLength=[],this._storedFields=new Map,this._nextId=0}}discard(t){const e=this._idToShortId.get(t);if(null==e)throw new Error(`MiniSearch: cannot discard document with ID ${t}: it is not in the index`);this._idToShortId.delete(t),this._documentIds.delete(e),this._storedFields.delete(e),(this._fieldLength.get(e)||[]).forEach(((t,s)=>{this.removeFieldLength(e,s,this._documentCount,t)})),this._fieldLength.delete(e),this._documentCount-=1,this._dirtCount+=1,this.maybeAutoVacuum()}maybeAutoVacuum(){if(!1===this._options.autoVacuum)return;const{minDirtFactor:t,minDirtCount:e,batchSize:s,batchWait:i}=this._options.autoVacuum;this.conditionalVacuum({batchSize:s,batchWait:i},{minDirtCount:e,minDirtFactor:t})}discardAll(t){const e=this._options.autoVacuum;try{this._options.autoVacuum=!1;for(const e of t)this.discard(e)}finally{this._options.autoVacuum=e}this.maybeAutoVacuum()}replace(t){const{idField:e,extractField:s}=this._options,i=s(t,e);this.discard(i),this.add(t)}vacuum(t={}){return this.conditionalVacuum(t)}conditionalVacuum(t,e){return this._currentVacuum?(this._enqueuedVacuumConditions=this._enqueuedVacuumConditions&&e,null!=this._enqueuedVacuum||(this._enqueuedVacuum=this._currentVacuum.then((()=>{const e=this._enqueuedVacuumConditions;return this._enqueuedVacuumConditions=I,this.performVacuuming(t,e)}))),this._enqueuedVacuum):!1===this.vacuumConditionsMet(e)?Promise.resolve():(this._currentVacuum=this.performVacuuming(t),this._currentVacuum)}performVacuuming(e,s){return t(this,void 0,void 0,(function*(){const t=this._dirtCount;if(this.vacuumConditionsMet(s)){const s=e.batchSize||S.batchSize,i=e.batchWait||S.batchWait;let n=1;for(const[t,e]of this._index){for(const[t,s]of e)for(const[i]of s)this._documentIds.has(i)||(s.size<=1?e.delete(t):s.delete(i));0===this._index.get(t).size&&this._index.delete(t),n%s==0&&(yield new Promise((t=>setTimeout(t,i)))),n+=1}this._dirtCount-=t}yield null,this._currentVacuum=this._enqueuedVacuum,this._enqueuedVacuum=null}))}vacuumConditionsMet(t){if(null==t)return!0;let{minDirtCount:e,minDirtFactor:s}=t;return e=e||O.minDirtCount,s=s||O.minDirtFactor,this.dirtCount>=e&&this.dirtFactor>=s}get isVacuuming(){return null!=this._currentVacuum}get dirtCount(){return this._dirtCount}get dirtFactor(){return this._dirtCount/(1+this._documentCount+this._dirtCount)}has(t){return this._idToShortId.has(t)}getStoredFields(t){const e=this._idToShortId.get(t);if(null!=e)return this._storedFields.get(e)}search(t,e={}){const{searchOptions:s}=this._options,i=Object.assign(Object.assign({},s),e),n=this.executeQuery(t,e),o=[];for(const[t,{score:e,terms:s,match:r}]of n){const n=s.length||1,c={id:this._documentIds.get(t),score:e*n,terms:Object.keys(r),queryTerms:s,match:r};Object.assign(c,this._storedFields.get(t)),(null==i.filter||i.filter(c))&&o.push(c)}return t===_.wildcard&&null==i.boostDocument||o.sort(k),o}autoSuggest(t,e={}){e=Object.assign(Object.assign({},this._options.autoSuggestOptions),e);const s=new Map;for(const{score:i,terms:n}of this.search(t,e)){const t=n.join(" "),e=s.get(t);null!=e?(e.score+=i,e.count+=1):s.set(t,{score:i,terms:n,count:1})}const i=[];for(const[t,{score:e,terms:n,count:o}]of s)i.push({suggestion:t,terms:n,score:e/o});return i.sort(k),i}get documentCount(){return this._documentCount}get termCount(){return this._index.size}static loadJSON(t,e){if(null==e)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJS(JSON.parse(t),e)}static loadJSONAsync(e,s){return t(this,void 0,void 0,(function*(){if(null==s)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJSAsync(JSON.parse(e),s)}))}static getDefault(t){if(v.hasOwnProperty(t))return p(v,t);throw new Error(`MiniSearch: unknown option "${t}"`)}static loadJS(t,e){const{index:s,documentIds:i,fieldLength:n,storedFields:o,serializationVersion:r}=t,c=this.instantiateMiniSearch(t,e);c._documentIds=j(i),c._fieldLength=j(n),c._storedFields=j(o);for(const[t,e]of c._documentIds)c._idToShortId.set(e,t);for(const[t,e]of s){const s=new Map;for(const t of Object.keys(e)){let i=e[t];1===r&&(i=i.ds),s.set(parseInt(t,10),j(i))}c._index.set(t,s)}return c}static loadJSAsync(e,s){return t(this,void 0,void 0,(function*(){const{index:t,documentIds:i,fieldLength:n,storedFields:o,serializationVersion:r}=e,c=this.instantiateMiniSearch(e,s);c._documentIds=yield V(i),c._fieldLength=yield V(n),c._storedFields=yield V(o);for(const[t,e]of c._documentIds)c._idToShortId.set(e,t);let u=0;for(const[e,s]of t){const t=new Map;for(const e of Object.keys(s)){let i=s[e];1===r&&(i=i.ds),t.set(parseInt(e,10),yield V(i))}++u%1e3==0&&(yield T(0)),c._index.set(e,t)}return c}))}static instantiateMiniSearch(t,e){const{documentCount:s,nextId:i,fieldIds:n,averageFieldLength:o,dirtCount:r,serializationVersion:u}=t;if(1!==u&&2!==u)throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version");const h=new _(e);return h._documentCount=s,h._nextId=i,h._idToShortId=new Map,h._fieldIds=n,h._avgFieldLength=o,h._dirtCount=r||0,h._index=new c,h}executeQuery(t,e={}){if(t===_.wildcard)return this.executeWildcardQuery(e);if("string"!=typeof t){const s=Object.assign(Object.assign(Object.assign({},e),t),{queries:void 0}),i=t.queries.map((t=>this.executeQuery(t,s)));return this.combineResults(i,s.combineWith)}const{tokenize:s,processTerm:i,searchOptions:n}=this._options,o=Object.assign(Object.assign({tokenize:s,processTerm:i},n),e),{tokenize:r,processTerm:c}=o,u=r(t).flatMap((t=>c(t))).filter((t=>!!t)).map(b(o)).map((t=>this.executeQuerySpec(t,o)));return this.combineResults(u,o.combineWith)}executeQuerySpec(t,e){const s=Object.assign(Object.assign({},this._options.searchOptions),e),i=(s.fields||this._options.fields).reduce(((t,e)=>Object.assign(Object.assign({},t),{[e]:p(s.boost,e)||1})),{}),{boostDocument:n,weights:o,maxFuzzy:r,bm25:c}=s,{fuzzy:u,prefix:h}=Object.assign(Object.assign({},x.weights),o),d=this._index.get(t.term),a=this.termResults(t.term,t.term,1,t.termBoost,d,i,n,c);let l,f;if(t.prefix&&(l=this._index.atPrefix(t.term)),t.fuzzy){const e=!0===t.fuzzy?.2:t.fuzzy,s=e<1?Math.min(r,Math.round(t.term.length*e)):e;s&&(f=this._index.fuzzyGet(t.term,s))}if(l)for(const[e,s]of l){const o=e.length-t.term.length;if(!o)continue;null==f||f.delete(e);const r=h*e.length/(e.length+.3*o);this.termResults(t.term,e,r,t.termBoost,s,i,n,c,a)}if(f)for(const e of f.keys()){const[s,o]=f.get(e);if(!o)continue;const r=u*e.length/(e.length+o);this.termResults(t.term,e,r,t.termBoost,s,i,n,c,a)}return a}executeWildcardQuery(t){const e=new Map,s=Object.assign(Object.assign({},this._options.searchOptions),t);for(const[t,i]of this._documentIds){const n=s.boostDocument?s.boostDocument(i,"",this._storedFields.get(t)):1;e.set(t,{score:n,terms:[],match:{}})}return e}combineResults(t,e=g){if(0===t.length)return new Map;const s=e.toLowerCase(),i=y[s];if(!i)throw new Error(`Invalid combination operator: ${e}`);return t.reduce(i)||new Map}toJSON(){const t=[];for(const[e,s]of this._index){const i={};for(const[t,e]of s)i[t]=Object.fromEntries(e);t.push([e,i])}return{documentCount:this._documentCount,nextId:this._nextId,documentIds:Object.fromEntries(this._documentIds),fieldIds:this._fieldIds,fieldLength:Object.fromEntries(this._fieldLength),averageFieldLength:this._avgFieldLength,storedFields:Object.fromEntries(this._storedFields),dirtCount:this._dirtCount,index:t,serializationVersion:2}}termResults(t,e,s,i,n,o,r,c,u=new Map){if(null==n)return u;for(const h of Object.keys(o)){const d=o[h],a=this._fieldIds[h],l=n.get(a);if(null==l)continue;let f=l.size;const m=this._avgFieldLength[a];for(const n of l.keys()){if(!this._documentIds.has(n)){this.removeTerm(a,n,e),f-=1;continue}const o=r?r(this._documentIds.get(n),e,this._storedFields.get(n)):1;if(!o)continue;const g=l.get(n),_=this._fieldLength.get(n)[a],y=s*i*d*o*w(g,f,this._documentCount,_,m,c),b=u.get(n);if(b){b.score+=y,F(b.terms,t);const s=p(b.match,e);s?s.push(h):b.match[e]=[h]}else u.set(n,{score:y,terms:[t],match:{[e]:[h]}})}}return u}addTerm(t,e,s){const i=this._index.fetch(s,C);let n=i.get(t);if(null==n)n=new Map,n.set(e,1),i.set(t,n);else{const t=n.get(e);n.set(e,(t||0)+1)}}removeTerm(t,e,s){if(!this._index.has(s))return void this.warnDocumentChanged(e,t,s);const i=this._index.fetch(s,C),n=i.get(t);null==n||null==n.get(e)?this.warnDocumentChanged(e,t,s):n.get(e)<=1?n.size<=1?i.delete(t):n.delete(e):n.set(e,n.get(e)-1),0===this._index.get(s).size&&this._index.delete(s)}warnDocumentChanged(t,e,s){for(const i of Object.keys(this._fieldIds))if(this._fieldIds[i]===e)return void this._options.logger("warn",`MiniSearch: document with ID ${this._documentIds.get(t)} has changed before removal: term "${s}" was not present in field "${i}". Removing a document after it has changed can corrupt the index!`,"version_conflict")}addDocumentId(t){const e=this._nextId;return this._idToShortId.set(t,e),this._documentIds.set(e,t),this._documentCount+=1,this._nextId+=1,e}addFields(t){for(let e=0;eObject.prototype.hasOwnProperty.call(t,e)?t[e]:void 0,y={[g]:(t,e)=>{for(const s of e.keys()){const i=t.get(s);if(null==i)t.set(s,e.get(s));else{const{score:t,terms:n,match:o}=e.get(s);i.score=i.score+t,i.match=Object.assign(i.match,o),M(i.terms,n)}}return t},and:(t,e)=>{const s=new Map;for(const i of e.keys()){const n=t.get(i);if(null==n)continue;const{score:o,terms:r,match:c}=e.get(i);M(n.terms,r),s.set(i,{score:n.score+o,terms:n.terms,match:Object.assign(n.match,c)})}return s},and_not:(t,e)=>{for(const s of e.keys())t.delete(s);return t}},w=(t,e,s,i,n,o)=>{const{k:r,b:c,d:u}=o;return Math.log(1+(s-e+.5)/(e+.5))*(u+t*(r+1)/(t+r*(1-c+c*i/n)))},b=t=>(e,s,i)=>({term:e,fuzzy:"function"==typeof t.fuzzy?t.fuzzy(e,s,i):t.fuzzy||!1,prefix:"function"==typeof t.prefix?t.prefix(e,s,i):!0===t.prefix,termBoost:"function"==typeof t.boostTerm?t.boostTerm(e,s,i):1}),v={idField:"id",extractField:(t,e)=>t[e],tokenize:t=>t.split(L),processTerm:t=>t.toLowerCase(),fields:void 0,searchOptions:void 0,storeFields:[],logger:(t,e)=>{"function"==typeof(null===console||void 0===console?void 0:console[t])&&console[t](e)},autoVacuum:!0},x={combineWith:g,prefix:!1,fuzzy:!1,maxFuzzy:6,boost:{},weights:{fuzzy:.45,prefix:.375},bm25:{k:1.2,b:.7,d:.5}},z={combineWith:"and",prefix:(t,e,s)=>e===s.length-1},S={batchSize:1e3,batchWait:10},I={minDirtFactor:.1,minDirtCount:20},O=Object.assign(Object.assign({},S),I),F=(t,e)=>{t.includes(e)||t.push(e)},M=(t,e)=>{for(const s of e)t.includes(s)||t.push(s)},k=({score:t},{score:e})=>e-t,C=()=>new Map,j=t=>{const e=new Map;for(const s of Object.keys(t))e.set(parseInt(s,10),t[s]);return e},V=e=>t(void 0,void 0,void 0,(function*(){const t=new Map;let s=0;for(const i of Object.keys(e))t.set(parseInt(i,10),e[i]),++s%1e3==0&&(yield T(0));return t})),T=t=>new Promise((e=>setTimeout(e,t))),L=/[\n\r\p{Z}\p{P}]+/u;return _}));
+//# sourceMappingURL=/sm/0f05ede3003a11c0848176daa6dae791d4aa6c5c93da9e99ae929f75084ce0d0.map
\ No newline at end of file
diff --git a/build_help.sh b/build_help.sh
deleted file mode 100644
index 44e652a2bd..0000000000
--- a/build_help.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/bash
-
-# remove any existing markdown files
-#find */src/ -path '*/docs/*' -name \*.md |
-# xargs rm
-
-# convert html to markdown
-find */src/ -path '*/docs/*' -name *.html |
- while read x
- do
- pandoc -f html -t commonmark $x > "$x".md
- done
-
-find */src/ -path '*/docs/*' -name *.html.md |
- while read x
- do
- mv "$x" "${x%.html.md}".md
- done
-
-# remove the old html files
-find */src/ -path '*/docs/*' -name *.html | while read x; do rm "$x"; done
-
-# add files to git
-git add *.html
-git add *.png
-git add \*.md
diff --git a/move_help.sh b/move_help.sh
deleted file mode 100644
index 6d79c27ae9..0000000000
--- a/move_help.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/sh
-
-# convert markdown to html & add headers
-#find */src/ -path '*/docs/*' -name "*.md" |
-# while read x
-# do
-# pandoc --template="onlinePandocTemplate.txt" -f commonmark -t html $x > "$x".html
-# done
-
-#echo "renaming all .md.html to .html"
-#find */src -path '*/docs/*' -name "*.md.html" |
-# while read x
-# do
- # mv "$x" "${x%.md.html}".html
- # done
-
-
-# move all html files to the docs folder
-find */src -path '*/docs/*' -name "*md" | xargs cp --parents -t HelpDocumentation
-
-# move all of the png & jpegs to the docs folder structure
-find */src -path '*/docs/*' -name "*.png" | xargs cp --parents -t HelpDocumentation
-find */src -path '*/docs/*' -name "*.jpg" | xargs cp --parents -t HelpDocumentation
diff --git a/onlinePandocTemplate.txt b/onlinePandocTemplate.txt
index 3988c6866b..b9493b72f7 100644
--- a/onlinePandocTemplate.txt
+++ b/onlinePandocTemplate.txt
@@ -6,6 +6,7 @@
+
@@ -20,6 +21,7 @@
+
@@ -51,6 +53,20 @@