chiark / gitweb /
doxygen: initial client-side search implementation.
authorVladimír Vondruš <mosra@centrum.cz>
Tue, 23 Jan 2018 09:35:16 +0000 (10:35 +0100)
committerVladimír Vondruš <mosra@centrum.cz>
Sat, 3 Feb 2018 09:51:55 +0000 (10:51 +0100)
71 files changed:
artwork/README.rst
artwork/magnifier.svg [new file with mode: 0644]
css/m-dark+doxygen.compiled.css
css/m-dark.doxygen.compiled.css
css/m-doxygen.css
css/m-light+doxygen.compiled.css
css/m-light.doxygen.compiled.css
doc/doxygen.rst
doxygen/.gitignore
doxygen/dox2html5.py
doxygen/search.js [new file with mode: 0644]
doxygen/templates/base.html
doxygen/test/compound_detailed/Doxyfile
doxygen/test/compound_ignored/Doxyfile
doxygen/test/compound_listing/Class_8h.html
doxygen/test/compound_listing/Doxyfile
doxygen/test/compound_listing/File_8h.html
doxygen/test/compound_listing/annotated.html
doxygen/test/compound_listing/classRoot_1_1Directory_1_1Sub_1_1Class.html
doxygen/test/compound_listing/dir_4b0d5f8864bf89936129251a2d32609b.html
doxygen/test/compound_listing/dir_bbe5918fe090eee9db2d9952314b6754.html
doxygen/test/compound_listing/files.html
doxygen/test/compound_listing/namespaceAnother.html
doxygen/test/compound_listing/namespaceRoot_1_1Directory.html
doxygen/test/compound_listing/namespaces.html
doxygen/test/compound_listing/page-no-toc.html
doxygen/test/compound_listing/pages.html
doxygen/test/compound_modules/Doxyfile
doxygen/test/compound_modules/group__group.html
doxygen/test/compound_modules/group__group2.html
doxygen/test/compound_modules/group__subgroup.html
doxygen/test/compound_modules/modules.html
doxygen/test/compound_warnings/Doxyfile
doxygen/test/contents_blocks/Doxyfile
doxygen/test/contents_code/Doxyfile
doxygen/test/contents_code_language/Doxyfile
doxygen/test/contents_custom/Doxyfile
doxygen/test/contents_image/Doxyfile
doxygen/test/contents_math/Doxyfile
doxygen/test/contents_tagfile/Doxyfile
doxygen/test/contents_typography/Doxyfile
doxygen/test/cpp_derived/Doxyfile
doxygen/test/cpp_enum_class/Doxyfile
doxygen/test/cpp_template_alias/Doxyfile
doxygen/test/example/Doxyfile
doxygen/test/layout/Doxyfile
doxygen/test/layout/pages.html
doxygen/test/layout_minimal/index.html
doxygen/test/layout_navbar_single_column/Doxyfile
doxygen/test/layout_navbar_single_column/index.html
doxygen/test/layout_search_binary/Doxyfile [new file with mode: 0644]
doxygen/test/layout_search_binary/index.html [new file with mode: 0644]
doxygen/test/layout_search_binary/indexpage.xml [new file with mode: 0644]
doxygen/test/page_brief/Doxyfile
doxygen/test/page_empty_index/Doxyfile
doxygen/test/page_footernavigation/Doxyfile
doxygen/test/page_in_navbar/Doxyfile
doxygen/test/page_in_navbar/page-b.html
doxygen/test/page_in_navbar/page-in-navbar.html
doxygen/test/page_order/Doxyfile
doxygen/test/page_toc/Doxyfile
doxygen/test/populate-js-test-data.py [new file with mode: 0755]
doxygen/test/search/Dir/File.h [new file with mode: 0644]
doxygen/test/search/Doxyfile [new file with mode: 0644]
doxygen/test/search/UndocumentedDir/UndocumentedFile.h [new file with mode: 0644]
doxygen/test/search/example.cpp [new file with mode: 0644]
doxygen/test/search/input.dox [new file with mode: 0644]
doxygen/test/test-search.js [new file with mode: 0644]
doxygen/test/test_doxyfile.py
doxygen/test/test_layout.py
doxygen/test/test_search.py

index 9a32477978978d3e115540ec2ba76f01c65a7325..566c0db999ec70517e763f106802bf1199d7d122 100644 (file)
@@ -9,3 +9,9 @@ Export as 16x16 PNG and convert to an ``*.ico``:
 .. code:: sh
 
     convert favicon.png favicon.ico
+
+magnifier.svg
+=============
+
+Doxygen search icon. Export as "Optimized SVG" and copy into the ``base.html``
+template.
diff --git a/artwork/magnifier.svg b/artwork/magnifier.svg
new file mode 100644 (file)
index 0000000..77b2463
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 16.00091 16.000004"
+   enable-background="new 0 0 1000 1000"
+   xml:space="preserve"
+   id="svg10"
+   sodipodi:docname="magnifier.svg"
+   inkscape:version="0.92.2 5c3e80d, 2017-08-06"
+   width="16.00091"
+   height="16.000004"><defs
+     id="defs14" /><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1024"
+     id="namedview12"
+     showgrid="false"
+     inkscape:zoom="32"
+     inkscape:cx="-4.6745911"
+     inkscape:cy="6.2637304"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer2"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" /><metadata
+     id="metadata2"> Svg Vector Icons : http://www.onlinewebfonts.com/icon <rdf:RDF><cc:Work
+     rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+       rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="bg"><path
+       style="stroke-width:1.00000012"
+       d="M 6 0 C 2.68562 0 0 2.68563 0 6 C 0 9.31439 2.68562 12 6 12 C 7.48575 12 8.8463413 11.459173 9.8945312 10.564453 C 9.8781312 10.902423 10.041871 11.322994 10.394531 11.714844 L 13.617188 15.503906 C 14.169037 16.117806 15.068921 16.169351 15.619141 15.619141 C 16.169361 15.068921 16.117806 14.169047 15.503906 13.617188 L 11.714844 10.392578 C 11.323004 10.039918 10.902423 9.8778881 10.564453 9.8925781 C 11.459173 8.8443881 12 7.4837969 12 5.9980469 C 12 2.6852969 9.31438 1.1842379e-15 6 0 z M 6 1.5625 A 4.4375 4.4375 0 0 1 10.4375 6 A 4.4375 4.4375 0 0 1 6 10.4375 A 4.4375 4.4375 0 0 1 1.5625 6 A 4.4375 4.4375 0 0 1 6 1.5625 z "
+       id="path4" /></g></svg>
\ No newline at end of file
index 82c1a4f6b921b14637af7f7f8353aae8479473b9..4213a0743f2efa8f645b99eb77fdcfbe75791e51 100644 (file)
@@ -1989,3 +1989,99 @@ article section.m-dox-details:target > div {
   border-left-width: 0.25rem;
   border-left-color: #a5c9ea;
 }
+a.m-dox-search-icon {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+a.m-dox-search-icon svg {
+  height: 0.9rem;
+  fill: #ffffff;
+}
+body > header > nav #m-navbar-collapse a.m-dox-search-icon svg {
+  vertical-align: -5%;
+}
+a.m-dox-search-icon:focus svg, a.m-dox-search-icon:hover svg, a.m-dox-search-icon:active svg {
+  fill: #a5c9ea;
+}
+.m-dox-search {
+  display: none;
+  z-index: 10;
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background-color: rgba(34, 39, 46, 0.75);
+}
+.m-dox-search:target {
+  display: block;
+}
+.m-dox-search > a {
+  display: block;
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+.m-dox-search-header {
+  margin-top: 2.5rem;
+  padding: 0.5rem 1rem;
+  height: 2rem;
+}
+.m-dox-search-header > div:first-child {
+  float: right;
+}
+.m-dox-search-content {
+  background-color: #22272e;
+  border-radius: 0.2rem;
+  padding: 1rem;
+}
+.m-dox-search input {
+  width: 100%;
+  height: 3rem;
+  font-size: 1.2rem;
+  border-width: 0;
+  color: #dcdcdc;
+  background-color: #34424d;
+  border-radius: 0.2rem;
+  margin-bottom: 1rem;
+  padding: 0 1rem;
+}
+.m-dox-search #search-notfound {
+  display: none;
+}
+.m-dox-search ul#search-results {
+  list-style-type: none;
+  padding-left: 0;
+  max-height: calc(100vh - 12.5rem);
+  overflow-y: auto;
+  display: none;
+}
+.m-dox-search ul#search-results li a {
+  display: block;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  text-decoration: none;
+  width: 100%;
+  line-height: 1.5rem;
+  color: #dcdcdc;
+}
+.m-dox-search ul#search-results li a > div {
+  white-space: nowrap;
+  overflow: hidden;
+  direction: rtl;
+}
+.m-dox-search ul#search-results li#search-current a {
+  background-color: #34424d;
+}
+.m-dox-search-typed {
+  color: #5b9dd9;
+}
+.m-dox-search input[type="search"] { -webkit-appearance: textfield; }
+.m-dox-search input[type="search"]::-webkit-search-decoration,
+.m-dox-search input[type="search"]::-webkit-search-cancel-button,
+.m-dox-search input[type="search"]::-webkit-search-results-button,
+.m-dox-search input[type="search"]::-webkit-search-results-decoration {
+  display: none;
+}
index b52000f0be2667f29b26a84b5841e518a1529ca1..5183f313ea23d6658b3b7cfcd13bb7fe3172d068 100644 (file)
@@ -175,3 +175,99 @@ article section.m-dox-details:target > div {
   border-left-width: 0.25rem;
   border-left-color: #a5c9ea;
 }
+a.m-dox-search-icon {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+a.m-dox-search-icon svg {
+  height: 0.9rem;
+  fill: #ffffff;
+}
+body > header > nav #m-navbar-collapse a.m-dox-search-icon svg {
+  vertical-align: -5%;
+}
+a.m-dox-search-icon:focus svg, a.m-dox-search-icon:hover svg, a.m-dox-search-icon:active svg {
+  fill: #a5c9ea;
+}
+.m-dox-search {
+  display: none;
+  z-index: 10;
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background-color: rgba(34, 39, 46, 0.75);
+}
+.m-dox-search:target {
+  display: block;
+}
+.m-dox-search > a {
+  display: block;
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+.m-dox-search-header {
+  margin-top: 2.5rem;
+  padding: 0.5rem 1rem;
+  height: 2rem;
+}
+.m-dox-search-header > div:first-child {
+  float: right;
+}
+.m-dox-search-content {
+  background-color: #22272e;
+  border-radius: 0.2rem;
+  padding: 1rem;
+}
+.m-dox-search input {
+  width: 100%;
+  height: 3rem;
+  font-size: 1.2rem;
+  border-width: 0;
+  color: #dcdcdc;
+  background-color: #34424d;
+  border-radius: 0.2rem;
+  margin-bottom: 1rem;
+  padding: 0 1rem;
+}
+.m-dox-search #search-notfound {
+  display: none;
+}
+.m-dox-search ul#search-results {
+  list-style-type: none;
+  padding-left: 0;
+  max-height: calc(100vh - 12.5rem);
+  overflow-y: auto;
+  display: none;
+}
+.m-dox-search ul#search-results li a {
+  display: block;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  text-decoration: none;
+  width: 100%;
+  line-height: 1.5rem;
+  color: #dcdcdc;
+}
+.m-dox-search ul#search-results li a > div {
+  white-space: nowrap;
+  overflow: hidden;
+  direction: rtl;
+}
+.m-dox-search ul#search-results li#search-current a {
+  background-color: #34424d;
+}
+.m-dox-search-typed {
+  color: #5b9dd9;
+}
+.m-dox-search input[type="search"] { -webkit-appearance: textfield; }
+.m-dox-search input[type="search"]::-webkit-search-decoration,
+.m-dox-search input[type="search"]::-webkit-search-cancel-button,
+.m-dox-search input[type="search"]::-webkit-search-results-button,
+.m-dox-search input[type="search"]::-webkit-search-results-decoration {
+  display: none;
+}
index 05a6bc284ff4e9ddbfd561fe9882530cad2d8e45..d83db15fed45ad8383d5df969e35bd982d18596a 100644 (file)
@@ -179,4 +179,117 @@ article section.m-dox-details:target > div {
   border-left-color: var(--article-heading-color);
 }
 
+a.m-dox-search-icon {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+a.m-dox-search-icon svg {
+  height: 0.9rem;
+  fill: var(--header-link-color);
+}
+body > header > nav #m-navbar-collapse a.m-dox-search-icon svg {
+  vertical-align: -5%;
+}
+a.m-dox-search-icon:focus svg, a.m-dox-search-icon:hover svg, a.m-dox-search-icon:active svg {
+  fill: var(--header-link-active-color);
+}
+.m-dox-search {
+  display: none;
+  z-index: 10;
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background-color: var(--header-background-color-landing);
+}
+.m-dox-search:target {
+  display: block;
+}
+.m-dox-search > a {
+  display: block;
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+.m-dox-search-header {
+  margin-top: 2.5rem;
+  padding: 0.5rem 1rem;
+  height: 2rem;
+}
+.m-dox-search-header > div:first-child {
+  float: right;
+}
+.m-dox-search-content {
+  background-color: var(--header-background-color);
+  border-radius: var(--border-radius);
+  padding: 1rem;
+}
+.m-dox-search input {
+  width: 100%;
+  height: 3rem;
+  font-size: 1.2rem;
+  border-width: 0;
+  color: var(--color);
+  background-color: var(--default-filled-background-color);
+  border-radius: var(--border-radius);
+  margin-bottom: 1rem;
+  padding: 0 1rem; /* putting it on all sides cuts text off in FF */
+}
+.m-dox-search #search-notfound {
+  display: none;
+}
+.m-dox-search ul#search-results {
+  list-style-type: none;
+  padding-left: 0;
+  /* Size breakdown:
+      2.5   margin of .m-dox-search-header from top
+      2     height of .m-dox-search-header
+      1     padding around .m-dox-search-header (twice 0.5rem)
+      1     padding of .m-dox-search-content from top
+      3     height of the input field
+      1     margin under input
+      1     padding of .m-dox-search-content from bottom
+      1     margin under .m-dox-search-content
+     ------
+     12.5   total */
+  max-height: calc(100vh - 12.5rem);
+  overflow-y: auto;
+  display: none;
+}
+.m-dox-search ul#search-results li a {
+  display: block;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  text-decoration: none;
+  width: 100%;
+  line-height: 1.5rem;
+  color: var(--color);
+}
+.m-dox-search ul#search-results li a > div {
+  white-space: nowrap;
+  overflow: hidden;
+  /* This is here in order to cut the text off at the left side. Besides this
+     there's special patching needed for punctuation characters, see search.js
+     for details. */
+  direction: rtl;
+}
+.m-dox-search ul#search-results li#search-current a {
+  background-color: var(--default-filled-background-color);
+}
+.m-dox-search-typed {
+  color: var(--link-color);
+}
+
+/* WELL THANK YOU WEBKIT! FOR SURE I WANTED ALL THAT SHIT HERE! */
+.m-dox-search input[type="search"] { -webkit-appearance: textfield; }
+.m-dox-search input[type="search"]::-webkit-search-decoration,
+.m-dox-search input[type="search"]::-webkit-search-cancel-button,
+.m-dox-search input[type="search"]::-webkit-search-results-button,
+.m-dox-search input[type="search"]::-webkit-search-results-decoration {
+  display: none;
+}
+
 /* kate: indent-width 2; */
index 91174f5db8204720efaa5a08a1983201db778f06..198d0e2010e3fe254bd10f0c3997e48b6958b6d3 100644 (file)
@@ -1925,3 +1925,99 @@ article section.m-dox-details:target > div {
   border-left-width: 0.25rem;
   border-left-color: #cb4b16;
 }
+a.m-dox-search-icon {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+a.m-dox-search-icon svg {
+  height: 0.9rem;
+  fill: #000000;
+}
+body > header > nav #m-navbar-collapse a.m-dox-search-icon svg {
+  vertical-align: -5%;
+}
+a.m-dox-search-icon:focus svg, a.m-dox-search-icon:hover svg, a.m-dox-search-icon:active svg {
+  fill: #cb4b16;
+}
+.m-dox-search {
+  display: none;
+  z-index: 10;
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background-color: rgba(255, 255, 255, 0.75);
+}
+.m-dox-search:target {
+  display: block;
+}
+.m-dox-search > a {
+  display: block;
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+.m-dox-search-header {
+  margin-top: 2.5rem;
+  padding: 0.5rem 1rem;
+  height: 2rem;
+}
+.m-dox-search-header > div:first-child {
+  float: right;
+}
+.m-dox-search-content {
+  background-color: #ffffff;
+  border-radius: 0.2rem;
+  padding: 1rem;
+}
+.m-dox-search input {
+  width: 100%;
+  height: 3rem;
+  font-size: 1.2rem;
+  border-width: 0;
+  color: #000000;
+  background-color: #fbf0ec;
+  border-radius: 0.2rem;
+  margin-bottom: 1rem;
+  padding: 0 1rem;
+}
+.m-dox-search #search-notfound {
+  display: none;
+}
+.m-dox-search ul#search-results {
+  list-style-type: none;
+  padding-left: 0;
+  max-height: calc(100vh - 12.5rem);
+  overflow-y: auto;
+  display: none;
+}
+.m-dox-search ul#search-results li a {
+  display: block;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  text-decoration: none;
+  width: 100%;
+  line-height: 1.5rem;
+  color: #000000;
+}
+.m-dox-search ul#search-results li a > div {
+  white-space: nowrap;
+  overflow: hidden;
+  direction: rtl;
+}
+.m-dox-search ul#search-results li#search-current a {
+  background-color: #fbf0ec;
+}
+.m-dox-search-typed {
+  color: #ea7944;
+}
+.m-dox-search input[type="search"] { -webkit-appearance: textfield; }
+.m-dox-search input[type="search"]::-webkit-search-decoration,
+.m-dox-search input[type="search"]::-webkit-search-cancel-button,
+.m-dox-search input[type="search"]::-webkit-search-results-button,
+.m-dox-search input[type="search"]::-webkit-search-results-decoration {
+  display: none;
+}
index ed0050e5c059e667bfab2aae4d6578da33a5b95f..986762242dba5e6f6b60debdc2eadc30f469a2b0 100644 (file)
@@ -175,3 +175,99 @@ article section.m-dox-details:target > div {
   border-left-width: 0.25rem;
   border-left-color: #cb4b16;
 }
+a.m-dox-search-icon {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+a.m-dox-search-icon svg {
+  height: 0.9rem;
+  fill: #000000;
+}
+body > header > nav #m-navbar-collapse a.m-dox-search-icon svg {
+  vertical-align: -5%;
+}
+a.m-dox-search-icon:focus svg, a.m-dox-search-icon:hover svg, a.m-dox-search-icon:active svg {
+  fill: #cb4b16;
+}
+.m-dox-search {
+  display: none;
+  z-index: 10;
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background-color: rgba(255, 255, 255, 0.75);
+}
+.m-dox-search:target {
+  display: block;
+}
+.m-dox-search > a {
+  display: block;
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+.m-dox-search-header {
+  margin-top: 2.5rem;
+  padding: 0.5rem 1rem;
+  height: 2rem;
+}
+.m-dox-search-header > div:first-child {
+  float: right;
+}
+.m-dox-search-content {
+  background-color: #ffffff;
+  border-radius: 0.2rem;
+  padding: 1rem;
+}
+.m-dox-search input {
+  width: 100%;
+  height: 3rem;
+  font-size: 1.2rem;
+  border-width: 0;
+  color: #000000;
+  background-color: #fbf0ec;
+  border-radius: 0.2rem;
+  margin-bottom: 1rem;
+  padding: 0 1rem;
+}
+.m-dox-search #search-notfound {
+  display: none;
+}
+.m-dox-search ul#search-results {
+  list-style-type: none;
+  padding-left: 0;
+  max-height: calc(100vh - 12.5rem);
+  overflow-y: auto;
+  display: none;
+}
+.m-dox-search ul#search-results li a {
+  display: block;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  text-decoration: none;
+  width: 100%;
+  line-height: 1.5rem;
+  color: #000000;
+}
+.m-dox-search ul#search-results li a > div {
+  white-space: nowrap;
+  overflow: hidden;
+  direction: rtl;
+}
+.m-dox-search ul#search-results li#search-current a {
+  background-color: #fbf0ec;
+}
+.m-dox-search-typed {
+  color: #ea7944;
+}
+.m-dox-search input[type="search"] { -webkit-appearance: textfield; }
+.m-dox-search input[type="search"]::-webkit-search-decoration,
+.m-dox-search input[type="search"]::-webkit-search-cancel-button,
+.m-dox-search input[type="search"]::-webkit-search-results-button,
+.m-dox-search input[type="search"]::-webkit-search-results-decoration {
+  display: none;
+}
index b41e52189b17758ebbfd1fa585dd7e4c7d6ce9bf..7f90b90ab2207527c7ae6cd718146fcf97bbaeb3 100644 (file)
@@ -38,6 +38,8 @@ Doxygen theme
     :language: ini
 .. role:: jinja(code)
     :language: jinja
+.. role:: js(code)
+    :language: js
 .. role:: py(code)
     :language: py
 .. role:: sh(code)
@@ -201,8 +203,6 @@ amount of generated content for no added value.
 `Not yet implemented features`_
 -------------------------------
 
--   Code search. I want to provide something that's actually usable to replace
-    the terribly slow stock client-side search, but I'm not there yet.
 -   Clickable symbols in code snippets. Doxygen has quite a lot of false
     positives while a lot of symbols stay unmatched. I need to find a way
     around that.
@@ -280,6 +280,25 @@ Variable                            Description
 :ini:`M_EXPAND_INNER_TYPES`         Whether to expand inner types (e.g. a class
                                     inside a class) in the symbol tree. If not
                                     set, ``NO`` is used.
+:ini:`M_SEARCH_DISABLED`            Disable search functionality. If this
+                                    option is set, no search data is compiled
+                                    and the rendered HTML does not contain any
+                                    search-related UI or support. If not set,
+                                    ``NO`` is used.
+:ini:`M_SEARCH_DOWNLOAD_BINARY`     Download search data as a binary to save
+                                    bandwidth and initial processing time. If
+                                    not set, ``NO`` is used. See `Search`_ for
+                                    more information.
+:ini:`M_SEARCH_HELP`                HTML code to display as help text on empty
+                                    search popup. If not set, a default message
+                                    is used. Has effect only if
+                                    :ini:`M_SEARCH_DISABLED` is not ``YES``.
+:ini:`M_SEARCH_EXTERNAL_URL`        URL for external search. The ``{query}``
+                                    placeholder is replaced with urlencoded
+                                    search string. If not set, no external
+                                    search is offered. See `Search`_ for more
+                                    information. Has effect only if
+                                    :ini:`M_SEARCH_DISABLED` is not ``YES``.
 =================================== =======================================
 
 Note that namespace, directory and page lists are always fully expanded as
@@ -376,6 +395,34 @@ This will put links to namespaces Foo, Bar and Utils as a sub-items of a
 top-level *Namespaces* item and links to two subdirectories as sub-items of the
 *Files* item.
 
+`Search`_
+---------
+
+Symbol search is implemented using JavaScript Typed Arrays and does not need
+any server-side functionality to perform well --- the client automatically
+downloads a tightly packed binary containing search data and performs search
+directly on it.
+
+However, due to `restrictions of Chromium-based browsers <https://bugs.chromium.org/p/chromium/issues/detail?id=40787&q=ajax%20local&colspec=ID%20Stars%20Pri%20Area%20Feature%20Type%20Status%20Summary%20Modified%20Owner%20Mstone%20OS>`_,
+it's not possible to download data using :js:`XMLHttpRequest` when served from
+local file-system. Because of that, the search defaults to producing a
+Base85-encoded representation of the search binary and loading that
+asynchronously as a plain JavaScript file. This results in the search data
+being 25% larger, but since this is for serving from a local filesystem, it's
+not considered a problem. If your docs are accessed through a server (or you
+don't need Chrome support), enable the :ini:`M_SEARCH_DOWNLOAD_BINARY` option.
+
+If :ini:`M_SEARCH_EXTERNAL_URL` is specified, full-text search using an
+external search engine is offered if nothing is found for given string or if
+the user has JavaScript disabled. It's recommended to restrict the search to
+a particular domain or add additional keywords to the search query to filter
+out irrelevant results. Example, using Google search engine and restricting
+the search to a subdomain:
+
+.. code:: ini
+
+    M_SEARCH_EXTERNAL_URL = "https://google.com/search?q=site:doc.magnum.graphics+{query}"
+
 `Command-line options`_
 =======================
 
@@ -856,6 +903,8 @@ Property                                Description
                                         template file from above
 :py:`compound.id`                       Unique compound identifier, usually
                                         corresponding to output file name
+:py:`compound.url`                      Compound URL (or where this file will
+                                        be saved)
 :py:`compound.name`                     Compound name
 :py:`compound.templates`                Template specification. Set only for
                                         classes. See `Template properties`_ for
@@ -964,8 +1013,6 @@ Property                                Description
                                         structs and unions; on templated
                                         classes contains also the list of
                                         template parameter names.
-:py:`compound.save_as`                  Filename including extension where the
-                                        result will be saved
 ======================================= =======================================
 
 `Navigation properties`_
index 68f1973952c28001c923793892dafc3b9e7147c5..ccb3caeba6cf1c63f8827ae4d06bcb8f942ca71a 100644 (file)
@@ -1,2 +1,4 @@
 test/*/html/
 test/*/xml/
+test/js-test-data/
+coverage/
index 9294184b961a3b8bcf06e99d209dcb47d2337eb0..20bfb11d3a1f2010c6da7f8d8e202ad05503faa7 100755 (executable)
@@ -26,6 +26,7 @@
 
 import xml.etree.ElementTree as ET
 import argparse
+import base64
 import sys
 import re
 import html
@@ -177,10 +178,13 @@ class State:
     def __init__(self):
         self.basedir = ''
         self.compounds: Dict[str, Any] = {}
+        self.search: List[Any] = []
         self.examples: List[Any] = []
         self.doxyfile: Dict[str, str] = {}
         self.images: List[str] = []
         self.current = ''
+        self.current_prefix = []
+        self.current_url = ''
 
 def slugify(text: str) -> str:
     # Maybe some Unicode normalization would be nice here?
@@ -1085,11 +1089,18 @@ def parse_enum(state: State, element: ET.Element):
         if ''.join(enumvalue.find('briefdescription').itertext()).strip():
             logging.warning("{}: ignoring brief description of enum value {}::{}".format(state.current, enum.name, value.name))
         value.description = parse_desc(state, enumvalue.find('detaileddescription'))
-        if value.description: enum.has_value_details = True
+        if value.description:
+            enum.has_value_details = True
+            if not state.doxyfile['M_SEARCH_DISABLED']:
+                state.search += [(state.current_url + '#' + value.id, state.current_prefix + [enum.name], value.name)]
         enum.values += [value]
 
     enum.has_details = enum.description or enum.has_value_details
-    return enum if enum.brief or enum.has_details or enum.has_value_details else None
+    if enum.brief or enum.has_details or enum.has_value_details:
+        if not state.doxyfile['M_SEARCH_DISABLED']:
+            state.search += [(state.current_url + '#' + enum.id, state.current_prefix, enum.name)]
+        return enum
+    return None
 
 def parse_template_params(state: State, element: ET.Element, description):
     if element is None: return False, None
@@ -1143,7 +1154,10 @@ def parse_typedef(state: State, element: ET.Element):
     typedef.has_template_details, typedef.templates = parse_template_params(state, element.find('templateparamlist'), templates)
 
     typedef.has_details = typedef.description or typedef.has_template_details
-    return typedef if typedef.brief or typedef.has_details else None
+    if typedef.brief or typedef.has_details:
+        state.search += [(state.current_url + '#' + typedef.id, state.current_prefix, typedef.name)]
+        return typedef
+    return None
 
 def parse_func(state: State, element: ET.Element):
     assert element.tag == 'memberdef' and element.attrib['kind'] == 'function'
@@ -1234,7 +1248,11 @@ def parse_func(state: State, element: ET.Element):
     if params: logging.warning("{}: function parameter description doesn't match parameter names: {}".format(state.current, repr(params)))
 
     func.has_details = func.description or func.has_template_details or func.has_param_details or func.return_value
-    return func if func.brief or func.has_details else None
+    if func.brief or func.has_details:
+        if not state.doxyfile['M_SEARCH_DISABLED']:
+            state.search += [(state.current_url + '#' + func.id, state.current_prefix, func.name + '()')]
+        return func
+    return None
 
 def parse_var(state: State, element: ET.Element):
     assert element.tag == 'memberdef' and element.attrib['kind'] == 'variable'
@@ -1255,7 +1273,11 @@ def parse_var(state: State, element: ET.Element):
     var.description = parse_var_desc(state, element)
 
     var.has_details = not not var.description
-    return var if var.brief or var.has_details else None
+    if var.brief or var.has_details:
+        if not state.doxyfile['M_SEARCH_DISABLED']:
+            state.search += [(state.current_url + '#' + var.id, state.current_prefix, var.name)]
+        return var
+    return None
 
 def parse_define(state: State, element: ET.Element):
     assert element.tag == 'memberdef' and element.attrib['kind'] == 'define'
@@ -1284,7 +1306,11 @@ def parse_define(state: State, element: ET.Element):
     if params: logging.warning("{}: define parameter description doesn't match parameter names: {}".format(state.current, repr(params)))
 
     define.has_details = define.description or define.return_value
-    return define if define.brief or define.has_details else None
+    if define.brief or define.has_details:
+        if not state.doxyfile['M_SEARCH_DISABLED']:
+            state.search += [(state.current_url + '#' + define.id, [], define.name + ('' if define.params is None else '()'))]
+        return define
+    return None
 
 def extract_metadata(state: State, xml):
     logging.debug("Extracting metadata from {}".format(os.path.basename(xml)))
@@ -1423,6 +1449,61 @@ def postprocess_state(state: State):
     if state.doxyfile['M_FAVICON']:
         state.doxyfile['M_FAVICON'] = (state.doxyfile['M_FAVICON'], mimetypes.guess_type(state.doxyfile['M_FAVICON'])[0])
 
+def _build_search_data(state: State, prefix, id: str, trie: Trie, map: ResultMap):
+    compound = state.compounds[id]
+    if not compound.brief and not compound.has_details: return 0
+
+    # Add current item name to prefix list
+    prefixed_name = prefix + [compound.leaf_name]
+
+    # Calculate fully-qualified name
+    if compound.kind in ['namespace', 'struct', 'class', 'union']:
+        joiner = '::'
+    elif compound.kind in ['file', 'dir']:
+        joiner = '/'
+    else:
+        joiner = ''
+
+    # If just a leaf name, add it once
+    if not joiner:
+        # TODO: escape elsewhere so i don't have to unescape here
+        name = html.unescape(compound.leaf_name)
+        trie.insert(name.lower(), map.add(name, compound.url))
+
+    # Otherwise add it multiple times with all possible prefixes
+    else:
+        # TODO: escape elsewhere so i don't have to unescape here
+        index = map.add(html.unescape(joiner.join(prefixed_name)), compound.url)
+        for i in range(len(prefixed_name)):
+            trie.insert(html.unescape(joiner.join(prefixed_name[i:])).lower(), index)
+
+    for i in compound.children:
+        if i in state.compounds:
+            _build_search_data(state, prefixed_name, i, trie, map)
+
+def build_search_data(state: State) -> bytearray:
+    trie = Trie()
+    map = ResultMap()
+
+    for id, compound in state.compounds.items():
+        if compound.parent: continue # start from the root
+        _build_search_data(state, [], id, trie, map)
+
+    for url, prefix, name in state.search:
+        # Add current item name to prefix list
+        prefixed_name = prefix + [name]
+
+        # TODO: escape elsewhere so i don't have to unescape here
+        index = map.add(html.unescape('::'.join(prefixed_name)), url)
+        for i in range(len(prefixed_name)):
+            trie.insert(html.unescape('::'.join(prefixed_name[i:])).lower(), index)
+
+    return serialize_search_data(trie, map)
+
+def base85encode_search_data(data: bytearray) -> bytearray:
+    return (b"/* Generated by http://mcss.mosra.cz/doxygen/. Do not edit. */\n" +
+            b"Search.load('" + base64.b85encode(data, True) + b"');\n")
+
 def parse_xml(state: State, xml: str):
     # Reset counter for unique math formulas
     m.math.counter = 0
@@ -1461,6 +1542,8 @@ def parse_xml(state: State, xml: str):
     # Compound name is page filename, so we have to use title there. The same
     # is for groups.
     compound.name = compounddef.find('title').text if compound.kind in ['page', 'group'] else compounddef.find('compoundname').text
+    # Compound URL is ID, except for index page
+    compound.url = (compounddef.find('compoundname').text if compound.kind == 'page' else compound.id) + '.html'
     compound.has_template_details = False
     compound.templates = None
     compound.brief = parse_desc(state, compounddef.find('briefdescription'))
@@ -1512,6 +1595,14 @@ def parse_xml(state: State, xml: str):
         for i in reversed(path_reverse):
             compound.breadcrumb += [(state.compounds[i].leaf_name, state.compounds[i].url)]
 
+        # Save current prefix for search
+        state.current_prefix = [name for name, _ in compound.breadcrumb]
+    else:
+        state.current_prefix = []
+
+    # Save current compound URL for search data building
+    state.current_url = compound.url
+
     if compound.kind == 'page':
         # Drop TOC for pages, if not requested
         if compounddef.find('tableofcontents') is None:
@@ -1969,11 +2060,6 @@ def parse_xml(state: State, xml: str):
 
     parsed = Empty()
     parsed.version = root.attrib['version']
-
-    # Decide about save as filename. Pages mess this up, because index page has
-    # "indexpage" as a name so we have to use the compound name instead
-    parsed.save_as = (compounddef.find('compoundname').text if compound.kind == 'page' else compound.id) + '.html'
-
     parsed.compound = compound
     return parsed
 
@@ -2117,7 +2203,11 @@ def parse_doxyfile(state: State, doxyfile, config = None):
         'M_FAVICON': [],
         'M_LINKS_NAVBAR1': ['pages', 'namespaces'],
         'M_LINKS_NAVBAR2': ['annotated', 'files'],
-        'M_PAGE_FINE_PRINT': ['[default]']
+        'M_PAGE_FINE_PRINT': ['[default]'],
+        'M_SEARCH_DISABLED': ['NO'],
+        'M_SEARCH_DOWNLOAD_BINARY': ['NO'],
+        'M_SEARCH_HELP': ['Search for symbols, headers, pages or example source files. You can omit any prefix from the symbol or file path.'],
+        'M_SEARCH_EXTERNAL_URL': ['']
     }
 
     def parse_value(var):
@@ -2194,7 +2284,9 @@ def parse_doxyfile(state: State, doxyfile, config = None):
               'M_PAGE_HEADER',
               'M_PAGE_FINE_PRINT',
               'M_THEME_COLOR',
-              'M_FAVICON']:
+              'M_FAVICON',
+              'M_SEARCH_HELP',
+              'M_SEARCH_EXTERNAL_URL']:
         if i in config: state.doxyfile[i] = ' '.join(config[i])
 
     # Int values that we want
@@ -2203,7 +2295,9 @@ def parse_doxyfile(state: State, doxyfile, config = None):
         if i in config: state.doxyfile[i] = int(' '.join(config[i]))
 
     # Boolean values that we want
-    for i in ['M_EXPAND_INNER_TYPES']:
+    for i in ['M_EXPAND_INNER_TYPES',
+              'M_SEARCH_DISABLED',
+              'M_SEARCH_DOWNLOAD_BINARY']:
         if i in config: state.doxyfile[i] = ' '.join(config[i]) == 'YES'
 
     # List values that we want. Drop empty lines.
@@ -2277,10 +2371,10 @@ def run(doxyfile, templates=default_templates, wildcard=default_wildcard, index_
             template = env.get_template('{}.html'.format(parsed.compound.kind))
             rendered = template.render(compound=parsed.compound,
                 DOXYGEN_VERSION=parsed.version,
-                FILENAME=parsed.save_as,
+                FILENAME=parsed.compound.url,
                 **state.doxyfile)
 
-            output = os.path.join(html_output, parsed.save_as)
+            output = os.path.join(html_output, parsed.compound.url)
             with open(output, 'w') as f:
                 f.write(rendered)
 
@@ -2302,8 +2396,18 @@ def run(doxyfile, templates=default_templates, wildcard=default_wildcard, index_
         with open(output, 'w') as f:
             f.write(rendered)
 
+    if not state.doxyfile['M_SEARCH_DISABLED']:
+        data = build_search_data(state)
+
+        if state.doxyfile['M_SEARCH_DOWNLOAD_BINARY']:
+            with open(os.path.join(html_output, "searchdata.bin"), 'wb') as f:
+                f.write(data)
+        else:
+            with open(os.path.join(html_output, "searchdata.js"), 'wb') as f:
+                f.write(base85encode_search_data(data))
+
     # Copy all referenced files
-    for i in state.images + state.doxyfile['HTML_EXTRA_STYLESHEET'] + state.doxyfile['HTML_EXTRA_FILES']:
+    for i in state.images + state.doxyfile['HTML_EXTRA_STYLESHEET'] + state.doxyfile['HTML_EXTRA_FILES'] + ([] if state.doxyfile['M_SEARCH_DISABLED'] else ['search.js']):
         # Skip absolute URLs
         if urllib.parse.urlparse(i).netloc: continue
 
diff --git a/doxygen/search.js b/doxygen/search.js
new file mode 100644 (file)
index 0000000..22e8b5c
--- /dev/null
@@ -0,0 +1,405 @@
+/*
+    This file is part of m.css.
+
+    Copyright © 2017, 2018 Vladimír Vondruš <mosra@centrum.cz>
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included
+    in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+*/
+
+"use strict"; /* it summons the Cthulhu in a proper way, they say */
+
+var Search = {
+    trie: null,
+    map: null,
+    symbolCount: 0,
+    maxResults: 0,
+
+    /* Always contains at least the root node offset and then one node offset
+       per entered character */
+    searchString: '',
+    searchStack: [],
+
+    init: function(buffer, maxResults) {
+        let view = new DataView(buffer);
+
+        /* The file is too short to contain at least the headers */
+        if(view.byteLength < 18) {
+            console.error("Search data too short");
+            return false;
+        }
+
+        if(view.getUint8(0) != 'M'.charCodeAt(0) ||
+           view.getUint8(1) != 'C'.charCodeAt(0) ||
+           view.getUint8(2) != 'S'.charCodeAt(0)) {
+            console.error("Invalid search data signature");
+            return false;
+        }
+
+        if(view.getUint8(3) != 0) {
+            console.error("Invalid search data version");
+            return false;
+        }
+
+        /* Separate the data into the trie and the result map */
+        let mapOffset = view.getUint32(4, true);
+        this.trie = new DataView(buffer, 8, mapOffset - 8);
+        this.map = new DataView(buffer, mapOffset);
+
+        /* Set initial properties */
+        this.symbolCount = (this.map.getUint32(0, true) & 0x00ffffff)/4 - 1;
+        this.maxResults = maxResults ? maxResults : 100;
+        this.searchString = '';
+        this.searchStack = [this.trie.getUint32(0, true)];
+
+        /* istanbul ignore if */
+        if(typeof document !== 'undefined') {
+            document.getElementById('search-symbolcount').innerHTML =
+                this.symbolCount + ' symbols (' +
+                Math.round(buffer.byteLength/102.4)/10 + "kB)";
+            document.getElementById('search-input').disabled = false;
+            document.getElementById('search-input').placeholder = "Type something here …";
+            document.getElementById('search-input').focus();
+
+            /* Search for the input value (there might be something already,
+               for example when going back in the browser) */
+            let value = document.getElementById('search-input').value;
+            if(value.length) Search.renderResults(value, Search.search(value));
+        }
+
+        return true;
+    },
+
+    download: /* istanbul ignore next */ function(url) {
+        var req = window.XDomainRequest ? new XDomainRequest() : new XMLHttpRequest();
+        if(!req) return;
+
+        req.open("GET", url, true);
+        req.responseType = 'arraybuffer';
+        req.onreadystatechange = function() {
+            if(req.readyState != 4) return;
+
+            Search.init(req.response);
+        }
+        req.send();
+    },
+
+    base85decode: function(base85string) {
+        function charValue(char) {
+            if(char >=  48 && char <  58) /* 0-9 -> 0-9 */
+                return char - 48 + 0;
+            if(char >=  65 && char <  91) /* A-Z -> 10-35 */
+                return char - 65 + 10;
+            if(char >=  97 && char < 123) /* a-z -> 36-61 */
+                return char - 97 + 36;
+            if(char ==  33)               /*  !  -> 62 */
+                return 62;
+            /* skipping 34 (') */
+            if(char >=  35 && char <  39) /* #-& -> 63-66 */
+                return char - 35 + 63;
+            /* skipping 39 (") */
+            if(char >=  40 && char <  44) /* (-+ -> 67-70 */
+                return char - 40 + 67;
+            /* skipping 44 (,) */
+            if(char ==  45)               /*  -  -> 71 */
+                return 71;
+            if(char >=  59 && char <  65) /* ;-@ -> 72-77 */
+                return char - 59 + 72;
+            if(char >=  94 && char <  97) /* ^-` -> 78-80 */
+                return char - 94 + 78;
+            if(char >= 123 && char < 127) /* {-~ -> 81-84 */
+                return char - 123 + 81;
+
+            return 0; /* Interpret padding values as zeros */
+        }
+
+        /* Pad the string for easier decode later. We don't read past the file
+           end, so it doesn't matter what garbage is there. */
+        if(base85string.length % 5) {
+            console.log("Expected properly padded base85 data");
+            return;
+        }
+
+        let buffer = new ArrayBuffer(base85string.length*4/5);
+        let data8 = new DataView(buffer);
+        for(let i = 0; i < base85string.length; i += 5) {
+            let char1 = charValue(base85string.charCodeAt(i + 0));
+            let char2 = charValue(base85string.charCodeAt(i + 1));
+            let char3 = charValue(base85string.charCodeAt(i + 2));
+            let char4 = charValue(base85string.charCodeAt(i + 3));
+            let char5 = charValue(base85string.charCodeAt(i + 4));
+
+            data8.setUint32(i*4/5, char5 +
+                                   char4*85 +
+                                   char3*85*85 +
+                                   char2*85*85*85 +
+                                   char1*85*85*85*85, false); /* BE, yes */
+        }
+
+        return buffer;
+    },
+
+    load: function(base85string) {
+        return this.init(this.base85decode(base85string));
+    },
+
+    search: function(searchString) {
+        /* Normalize the search string first */
+        searchString = searchString.toLowerCase().trim();
+
+        /* TODO: maybe i could make use of InputEvent.data and others here */
+
+        /* Find longest common prefix of previous and current value so we don't
+           need to needlessly search again */
+        let max = Math.min(searchString.length, this.searchString.length);
+        let commonPrefix = 0;
+        for(; commonPrefix != max; ++commonPrefix)
+            if(searchString[commonPrefix] != this.searchString[commonPrefix]) break;
+
+        /* Drop items off the stack if it has has more than is needed for the
+           common prefix (it needs to have at least one item, though) */
+        if(commonPrefix + 1 < this.searchStack.length)
+            this.searchStack.splice(commonPrefix + 1, this.searchStack.length - commonPrefix - 1);
+
+        /* Add new characters from the search string */
+        let foundPrefix = commonPrefix;
+        for(; foundPrefix != searchString.length; ++foundPrefix) {
+            /* Calculate offset and count of children */
+            let offset = this.searchStack[this.searchStack.length - 1];
+            let nodeSize = this.trie.getUint8(offset)*2;
+            let relChildOffset = 2 + this.trie.getUint8(offset + 1)*2;
+            let childCount = (nodeSize - relChildOffset)/4;
+
+            /* Go through all children and find the next offset */
+            let childOffset = offset + relChildOffset;
+            let found = false;
+            for(let j = 0; j != childCount; ++j) {
+                if(String.fromCharCode(this.trie.getUint8(childOffset + j*4 + 3)) != searchString[foundPrefix])
+                    continue;
+
+                this.searchStack.push(this.trie.getUint32(childOffset + j*4, true) & 0x00ffffff);
+                found = true;
+                break;
+            }
+
+            /* Character not found, exit */
+            if(!found) break;
+        }
+
+        /* Save the whole found prefix for next time */
+        this.searchString = searchString.substr(0, foundPrefix);
+
+        /* If the whole thing was not found, return an empty result and offer
+           external search */
+        if(foundPrefix != searchString.length) {
+            /* istanbul ignore if */
+            if(typeof document !== 'undefined') {
+                let link = document.getElementById('search-external');
+                if(link)
+                    link.href = link.dataset.searchEngine.replace('{query}', encodeURIComponent(searchString));
+            }
+            return [];
+        }
+
+        /* Otherwise recursively gather the results */
+        let results = [];
+        this.gatherResults(this.searchStack[this.searchStack.length - 1], 0, results);
+        return results;
+    },
+
+    gatherResults: function(offset, suffixLength, results) {
+        let valueCount = this.trie.getUint8(offset + 1);
+
+        /* Populate the results with all values associated with this node */
+        for(let i = 0; i != valueCount; ++i) {
+            let index = this.trie.getUint16(offset + (i + 1)*2, true);
+            //let flags = this.map.getUint8(index*4 + 3); /* not used yet */
+            let resultOffset = this.map.getUint32(index*4, true) & 0x00ffffff;
+            let nextResultOffset = this.map.getUint32((index + 1)*4, true) & 0x00ffffff;
+
+            let name = '';
+            let j = resultOffset;
+            for(; j != nextResultOffset; ++j) {
+                let c = this.map.getUint8(j);
+
+                /* End of null-delimited name */
+                if(!c) {
+                    ++j;
+                    break; /* null-delimited */
+                }
+
+                name += String.fromCharCode(c); /* eheh. IS THIS FAST?! */
+            }
+
+            let url = '';
+            for(; j != nextResultOffset; ++j) {
+                url += String.fromCharCode(this.map.getUint8(j));
+            }
+
+            results.push({name: name, url: url, suffixLength: suffixLength});
+
+            /* 'nuff said. */
+            /* TODO: remove once proper barriers are in */
+            if(results.length >= this.maxResults) return true;
+        }
+
+        /* Dig deeper. If the child already has enough, return. */
+        /* TODO: hmmm. this is helluvalot duplicated code. hmm. */
+        let nodeSize = this.trie.getUint8(offset)*2;
+        let relChildOffset = 2 + this.trie.getUint8(offset + 1)*2;
+        let childCount = (nodeSize - relChildOffset)/4;
+        let childOffset = offset + relChildOffset;
+        for(let j = 0; j != childCount; ++j)
+            if(this.gatherResults(this.trie.getUint32(childOffset + j*4, true) & 0x00ffffff, suffixLength + 1, results))
+                return true;
+
+        /* Still hungry. */
+        return false;
+    },
+
+    escapeForRtl: function(name) {
+        /* Besides the obvious escaping of HTML entities we also need
+           to escape punctuation, because due to the RTL hack to cut
+           text off on left side the punctuation characters get
+           reordered (of course). Prepending &lrm; works for most
+           characters, parentheses we need to *soak* in it. But only
+           the right ones. And that for some reason needs to be also for &.
+           Huh. https://en.wikipedia.org/wiki/Right-to-left_mark */
+        return name.replace(/[\"&<>]/g, function (a) {
+            return { '"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;' }[a];
+        }).replace(/[:=]/g, '&lrm;$&').replace(/(\)|&gt;|&amp;|\/)/g, '&lrm;$&&lrm;');
+    },
+
+    renderResults: /* istanbul ignore next */ function(value, results) {
+        /* Normalize the value length so the slicing works properly */
+        value = value.trim();
+
+        if(!value.length) {
+            document.getElementById('search-help').style.display = 'block';
+            document.getElementById('search-results').style.display = 'none';
+            document.getElementById('search-notfound').style.display = 'none';
+            return;
+        }
+
+        document.getElementById('search-help').style.display = 'none';
+
+        if(results.length) {
+            document.getElementById('search-results').style.display = 'block';
+            document.getElementById('search-notfound').style.display = 'none';
+
+            var list = '';
+            for(let i = 0; i != results.length; ++i) {
+                list += '<li' + (i ? '' : ' id="search-current"') + '><a href="' + results[i].url + '" onmouseover="selectResult(event)"><div><span class="m-text m-dim">' + this.escapeForRtl(results[i].name.substr(0, results[i].name.length - value.length - results[i].suffixLength)) + '</span><span class="m-dox-search-typed">' + this.escapeForRtl(results[i].name.substr(results[i].name.length - value.length - results[i].suffixLength, value.length)) + '</span>' + this.escapeForRtl(results[i].name.substr(results[i].name.length - results[i].suffixLength)) + '</div></a></li>';
+            }
+            document.getElementById('search-results').innerHTML = list;
+            document.getElementById('search-current').scrollIntoView(true);
+
+        } else {
+            document.getElementById('search-results').style.display = 'none';
+            document.getElementById('search-notfound').style.display = 'block';
+        }
+    },
+};
+
+/* istanbul ignore next */
+function selectResult(event) {
+    if(event.currentTarget.parentNode.id == 'search-current') return;
+
+    let current = document.getElementById('search-current');
+    current.id = '';
+    event.currentTarget.parentNode.id = 'search-current';
+}
+
+/* istanbul ignore next */
+function showSearch() {
+    window.location.hash = '#search';
+    document.getElementById('search-input').value = '';
+    document.getElementById('search-input').focus();
+    document.getElementById('search-results').style.display = 'none';
+    document.getElementById('search-notfound').style.display = 'none';
+    document.getElementById('search-help').style.display = 'block';
+    return false;
+}
+
+/* istanbul ignore next */
+function hideSearch() {
+    window.location.hash = '#!';
+    window.history.pushState('', '', window.location.pathname);
+    return false;
+}
+
+/* Only in case we're running in a browser. Why a simple if(document) doesn't
+   work is beyond me. */ /* istanbul ignore if */
+if(typeof document !== 'undefined') {
+    document.getElementById('search-input').oninput = function(event) {
+        let value = document.getElementById('search-input').value;
+        Search.renderResults(value, Search.search(value));
+    };
+
+    document.onkeydown = function(event) {
+        /* Search shown */
+        if(window.location.hash == '#search') {
+            /* Close the search */
+            if(event.key == 'Escape') {
+                hideSearch();
+
+            /* Select next item */
+            } else if(event.key == 'ArrowDown' || (event.key == 'Tab' && !event.shiftKey)) {
+                let current = document.getElementById('search-current');
+                if(current) {
+                    let next = current.nextSibling;
+                    if(next) {
+                        current.id = '';
+                        next.id = 'search-current';
+                        next.scrollIntoView(false);
+                    }
+                }
+                return false; /* so the keypress doesn't affect input cursor */
+
+            /* Select prev item */
+            } else if(event.key == 'ArrowUp' || (event.key == 'Tab' && event.shiftKey)) {
+                let current = document.getElementById('search-current');
+                if(current) {
+                    let prev = current.previousSibling;
+                    if(prev) {
+                        current.id = '';
+                        prev.id = 'search-current';
+                        prev.scrollIntoView(false);
+                    }
+                }
+                return false; /* so the keypress doesn't affect input cursor */
+
+            /* Go to result */
+            } else if(event.key == 'Enter') {
+                document.getElementById('search-current').firstElementChild.click();
+                return false; /* so the keypress doesn't affect input cursor */
+            }
+
+        /* Search hidden */
+        } else {
+            /* Open the search on the T or Tab key */
+            if(event.key.toLowerCase() == 't' || event.key == 'Tab') {
+                showSearch();
+                return false; /* so T doesn't get entered into the box */
+            }
+        }
+    };
+}
+
+/* For Node.js testing */ /* istanbul ignore else */
+if(typeof module !== 'undefined') { module.exports = { Search: Search }; }
index 46963d0afbfbd7b05ccad404889722d72c8feb5f..2128cc6fae0bde8549d96c1487738828255660a7 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">{{ PROJECT_NAME }}{% if PROJECT_BRIEF %} <span class="m-thin">{{ PROJECT_BRIEF }}</span>{% endif %}</a>
-      {% if M_LINKS_NAVBAR1 or M_LINKS_NAVBAR2 %}
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      {% if M_LINKS_NAVBAR1 or M_LINKS_NAVBAR2 or not M_SEARCH_DISABLED %}
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        {% if not M_SEARCH_DISABLED %}
+        <a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+          <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+        </svg></a>
+        {% endif %}
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="{% if M_LINKS_NAVBAR2 %}m-col-t-6{% else %}m-col-t-12{% endif %} m-col-m-none">
@@ -42,7 +49,7 @@
             {% endif %}
             {% endfor %}
           </ol>
-          {% if M_LINKS_NAVBAR2 %}
+          {% if M_LINKS_NAVBAR2 or not M_SEARCH_DISABLED %}
           {% set start = M_LINKS_NAVBAR1|length + 1 %}
           <ol class="m-col-t-6 m-col-m-none" start="{{ start }}">
             {% for title, link, id, sub in M_LINKS_NAVBAR2 %}
             </li>
             {% endif %}
             {% endfor %}
+            {% if not M_SEARCH_DISABLED %}
+            <li class="m-show-m"><a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+              <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+            </svg></a></li>
+            {% endif %}
           </ol>
           {% endif %}
         </div>
     </div>
   </div>
 </article></main>
+{% if not M_SEARCH_DISABLED %}
+<div class="m-dox-search" id="search">
+  <a href="#!" onclick="return hideSearch()"></a>
+  <div class="m-container">
+    <div class="m-row">
+      <div class="m-col-m-8 m-push-m-2">
+        <div class="m-dox-search-header m-text m-small">
+          <div><span class="m-label m-default">Tab</span> / <span class="m-label m-default">T</span> to search, <span class="m-label m-default">Esc</span> to close</div>
+          <div id="search-symbolcount">&hellip;</div>
+        </div>
+        <div class="m-dox-search-content">
+          <input type="search" id="search-input" placeholder="Loading &hellip;" disabled="disabled" autofocus="autofocus" />
+          <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript.{% if M_SEARCH_EXTERNAL_URL %} Enable it or <a href="{{ M_SEARCH_EXTERNAL_URL|replace('{query}', '') }}">use an external search engine</a>.{% endif %}</noscript>
+          <div id="search-help" class="m-text m-dim m-text-center">
+            {{ M_SEARCH_HELP }}
+          </div>
+          <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.{% if M_SEARCH_EXTERNAL_URL %}<br />Maybe try a full-text <a href="#" id="search-external" data-search-engine="{{ M_SEARCH_EXTERNAL_URL }}">search with external engine</a>?{% endif %}</div>
+          <ul id="search-results"></ul>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+<script src="search.js"></script>
+{% if M_SEARCH_DOWNLOAD_BINARY %}
+<script>
+  Search.download(window.location.pathname.substr(0, window.location.pathname.lastIndexOf('/') + 1) + "searchdata.bin");
+</script>
+{% else %}
+<script src="searchdata.js" async="async"></script>
+{% endif %}
+{% endif %}
 {% if M_PAGE_FINE_PRINT %}
 <footer><nav>
   <div class="m-container">
index 262c15500b433e3aa16cc363d02a026a4c26a104..44a6d04a2dfa40c8703e22c4fad39757ee5ac38f 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 6165277cb0f6bf3c81b1c168adb0dde142ad6d11..14b91551f2e48d97306c92984aaf5974c9902c86 100644 (file)
@@ -8,4 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
-
+M_SEARCH_DISABLED       = YES
index a392234786f3ca529c51d2d7681e6a76e35a664c..d322b0a6940624eefcb4f63d766a173de54e1e96 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index d1821c2749fe8006993826fe80192a620efc17b0..9e0db80368bb12f557ce65a769418b5d1e2c05b6 100644 (file)
@@ -12,4 +12,5 @@ M_CLASS_TREE_EXPAND_LEVELS = 5
 M_EXPAND_INNER_TYPES    = YES
 M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
+M_SEARCH_DISABLED       = YES
 # Keeping navbar enabled so we can test for highlighted items
index a304ca1956965842af8c869fb30e1563a62eaf18..dc4de03cc5b764d1f2d7a559b28c25a20e78f5d4 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index a06adf1e3539149b72287c52482cc96f74d0dfd4..9962974c720f0dcec9d584f6e6fc3533e68b4cb9 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index 1c3a70e5449280f2733287d064045911e2c1debb..89cdbf8c5b399e4742e6f7a7fcfb5d1593d2ffc7 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index a3cde8ff6910811712196091173449cf6186fa28..876f5236c48fc1b659a8f23492f34f47d6d86318 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index aca3ac2bbc5d22065fd4cd56aa4c81b5836b9f5f..dd5490453527c2f7f25313742f0337c11f4627c9 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index bb672f8aee7ae5be420a9cd9669e4cd11f38b06a..2c66c13dfbf46545972e1e5bfc93b88a78f717e5 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index 2093dfba93d3f2764b53fae3b970f5e71c6cdca7..409958d6970a9d7efd829a56725c11ac2e9f4568 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index f11c2c5f3de1e6671ccc7a4ef79743fa2285ff09..961037565662e7098566c38015faeeca62817bc1 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index f9dced2249fe05e39ba8855a612fa807834ba5df..0befde3606483f908aca1aaffb408795b290af3b 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index 72ca76788ed6121499c599cb1aaf1c26fd0aa3a7..87785d2f5d71257abc730bbd5370e32cbe7917d2 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index 3d34c5d27a9bad42f6bb415635783587264486a9..33ad7ce4d272d3317baaa4e89fecc8a8ed77dcb4 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
@@ -57,4 +59,4 @@
   </div>
 </article></main>
 </body>
-</html>
\ No newline at end of file
+</html>
index 0a336c90f1d7d39d42c91b4f68793fd7a2ee3504..487d5a3080145283674202267cf32bc2c2abfe49 100644 (file)
@@ -8,3 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         = modules
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 62db666a2658cd0cfea6328c1b7077c5e2baac5b..a1ffbcfd9084d2d3792dd78d3d185f3f2b14857b 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-12 m-col-m-none">
index 6b5c124ee575e3a15d25748005a845fc159d15c3..2993a8a92ecc37d50e9d45987889dca987d304ed 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-12 m-col-m-none">
index 0033b88c496a917b57b462ef1c34ae5c916ddc39..05ce3982b2b9073bb40ef6d0c6270d824a36656a 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-12 m-col-m-none">
index 3a6ea8eb18f16c02e929a13de703ef328a369642..5d6773edc9fee6eeb4131a168e40680039940aa2 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-12 m-col-m-none">
index 717f06b938697001979569a0ce325ba2797fc665..75c8e4ee32953f0af45ea0153b3da06b860b361b 100644 (file)
@@ -12,3 +12,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 8ad35e1d4ad1ceb91313200eebe8b03d9de29093..c9d7928a0d7212c7992e49f86a07544b06ce2996 100644 (file)
@@ -8,3 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index e30874b3619d30f3c49ad41feee6f7c1a83a7fcb..a8ae742d03764916b5113bfc20dd008fc55aebea 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index e30874b3619d30f3c49ad41feee6f7c1a83a7fcb..a8ae742d03764916b5113bfc20dd008fc55aebea 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 34b659d0d8875eed12c7eb66e2576854a7016afa..cd29908b998a45d1b2ff8d1a3bb3d9800684cb3a 100644 (file)
@@ -9,6 +9,7 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
 
 ALIASES = \
     "m_div{1}=@xmlonly<mcss:div xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" mcss:class=\"\1\">@endxmlonly" \
index 8e208b5cae06538058715b25bf063834f8bb3a11..46ce2116744d3dc4cff6f7185593e8ee0cf85746 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 645853848d7cb944cff3a2d22162aaf1f747376f..d572a12fc160ca0c42a0c8b8a3bffe3f6272eca3 100644 (file)
@@ -8,3 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index eff13271863331611e5e5d894e5b62ed613d9bd6..33efe17dda554fbdecda22c1278b718fc2de2fb7 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 645853848d7cb944cff3a2d22162aaf1f747376f..d572a12fc160ca0c42a0c8b8a3bffe3f6272eca3 100644 (file)
@@ -8,3 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 03f063f3f6d8f90e15a62e905edcf04df0bf32f6..b1bf4f712d1391929035350eab31b4f514bee026 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 262c15500b433e3aa16cc363d02a026a4c26a104..44a6d04a2dfa40c8703e22c4fad39757ee5ac38f 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 262c15500b433e3aa16cc363d02a026a4c26a104..44a6d04a2dfa40c8703e22c4fad39757ee5ac38f 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 9bb626100945b7832dacfe500f4db1eb66965d93..2291f14d5ff4e896712e0a3be7b52dd89daac1e5 100644 (file)
@@ -12,3 +12,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 8cc3d14932570aa28628cd7afa8ba844949f6a00..8a903cc04e29abb9222ed64a94d7d10f9320c608 100644 (file)
@@ -10,3 +10,5 @@ M_PAGE_FINE_PRINT = "<p><a href=\"http://doxygen.org\">Doxygen</a> version {doxy
 M_LINKS_NAVBAR1 = "files pages namespaces" \
                   "annotated namespaces pages"
 M_LINKS_NAVBAR2 = "pages pages annotated"
+M_SEARCH_EXTERNAL_URL = "https://google.com/search?q=site:mcss.mosra.cz+{}"
+M_SEARCH_HELP = "Some <em>help</em>."
index 3bd44cd3a072383b77fc0a3cea67f6fafd793696..61dd77d059945757e2df37ceeda248228637ad3d 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">A project <span class="m-thin">is cool</span></a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+          <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+        </svg></a>
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
@@ -42,6 +47,9 @@
                 <li><a href="annotated.html">Classes</a></li>
               </ol>
             </li>
+            <li class="m-show-m"><a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+              <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+            </svg></a></li>
           </ol>
         </div>
       </div>
     </div>
   </div>
 </article></main>
+<div class="m-dox-search" id="search">
+  <a href="#!" onclick="return hideSearch()"></a>
+  <div class="m-container">
+    <div class="m-row">
+      <div class="m-col-m-8 m-push-m-2">
+        <div class="m-dox-search-header m-text m-small">
+          <div><span class="m-label m-default">Tab</span> / <span class="m-label m-default">T</span> to search, <span class="m-label m-default">Esc</span> to close</div>
+          <div id="search-symbolcount">&hellip;</div>
+        </div>
+        <div class="m-dox-search-content">
+          <input type="search" id="search-input" placeholder="Loading &hellip;" disabled="disabled" autofocus="autofocus" />
+          <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript. Enable it or <a href="https://google.com/search?q=site:mcss.mosra.cz+{}">use an external search engine</a>.</noscript>
+          <div id="search-help" class="m-text m-dim m-text-center">
+            Some <em>help</em>.
+          </div>
+          <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.<br />Maybe try a full-text <a href="#" id="search-external" data-search-engine="https://google.com/search?q=site:mcss.mosra.cz+{}">search with external engine</a>?</div>
+          <ul id="search-results"></ul>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+<script src="search.js"></script>
+<script src="searchdata.js" async="async"></script>
 <footer><nav>
   <div class="m-container">
     <div class="m-row">
index a7f9046914bc4cdacc77135040b6e0d91ce2387a..e96e7378aa796f24f7a035d2cd1777930a80c57b 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+          <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+        </svg></a>
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
@@ -24,6 +29,9 @@
           <ol class="m-col-t-6 m-col-m-none" start="3">
             <li><a href="annotated.html">Classes</a></li>
             <li><a href="files.html">Files</a></li>
+            <li class="m-show-m"><a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+              <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+            </svg></a></li>
           </ol>
         </div>
       </div>
     </div>
   </div>
 </article></main>
+<div class="m-dox-search" id="search">
+  <a href="#!" onclick="return hideSearch()"></a>
+  <div class="m-container">
+    <div class="m-row">
+      <div class="m-col-m-8 m-push-m-2">
+        <div class="m-dox-search-header m-text m-small">
+          <div><span class="m-label m-default">Tab</span> / <span class="m-label m-default">T</span> to search, <span class="m-label m-default">Esc</span> to close</div>
+          <div id="search-symbolcount">&hellip;</div>
+        </div>
+        <div class="m-dox-search-content">
+          <input type="search" id="search-input" placeholder="Loading &hellip;" disabled="disabled" autofocus="autofocus" />
+          <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript.</noscript>
+          <div id="search-help" class="m-text m-dim m-text-center">
+            Search for symbols, headers, pages or example source files. You can omit any prefix from the symbol or file path.
+          </div>
+          <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.</div>
+          <ul id="search-results"></ul>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+<script src="search.js"></script>
+<script src="searchdata.js" async="async"></script>
 <footer><nav>
   <div class="m-container">
     <div class="m-row">
index 03cfccf51c1e0f5fc9a5d65e3addf7bba171b69e..6f3346464406b9f83a3fb13f94cc14c9805272f7 100644 (file)
@@ -7,3 +7,4 @@ M_LINKS_NAVBAR1 = pages \
 M_LINKS_NAVBAR2 =
 M_PAGE_FINE_PRINT =
 M_THEME_COLOR =
+M_SEARCH_DISABLED       = YES
index bec4abc34d13818eca4f7c902b65ba1e5df85204..041e4faa1ae6f045a1f3a8a47b282be1f85d4e69 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-12 m-col-m-none">
diff --git a/doxygen/test/layout_search_binary/Doxyfile b/doxygen/test/layout_search_binary/Doxyfile
new file mode 100644 (file)
index 0000000..ac4ebd4
--- /dev/null
@@ -0,0 +1,7 @@
+XML_OUTPUT              =
+
+M_PAGE_FINE_PRINT       =
+M_THEME_COLOR           =
+M_LINKS_NAVBAR1         =
+M_LINKS_NAVBAR2         =
+M_SEARCH_DOWNLOAD_BINARY = YES
diff --git a/doxygen/test/layout_search_binary/index.html b/doxygen/test/layout_search_binary/index.html
new file mode 100644 (file)
index 0000000..b418c51
--- /dev/null
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>My Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+doxygen.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+          <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+        </svg></a>
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
+      <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
+        <div class="m-row">
+          <ol class="m-col-t-12 m-col-m-none">
+          </ol>
+          <ol class="m-col-t-6 m-col-m-none" start="1">
+            <li class="m-show-m"><a href="#search" class="m-dox-search-icon" title="Search" onclick="return showSearch()"><svg viewBox="0 0 16 16">
+              <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+            </svg></a></li>
+          </ol>
+        </div>
+      </div>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          My Project
+        </h1>
+      </div>
+    </div>
+  </div>
+</article></main>
+<div class="m-dox-search" id="search">
+  <a href="#!" onclick="return hideSearch()"></a>
+  <div class="m-container">
+    <div class="m-row">
+      <div class="m-col-m-8 m-push-m-2">
+        <div class="m-dox-search-header m-text m-small">
+          <div><span class="m-label m-default">Tab</span> / <span class="m-label m-default">T</span> to search, <span class="m-label m-default">Esc</span> to close</div>
+          <div id="search-symbolcount">&hellip;</div>
+        </div>
+        <div class="m-dox-search-content">
+          <input type="search" id="search-input" placeholder="Loading &hellip;" disabled="disabled" autofocus="autofocus" />
+          <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript.</noscript>
+          <div id="search-help" class="m-text m-dim m-text-center">
+            Search for symbols, headers, pages or example source files. You can omit any prefix from the symbol or file path.
+          </div>
+          <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.</div>
+          <ul id="search-results"></ul>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+<script src="search.js"></script>
+<script>
+  Search.download(window.location.pathname.substr(0, window.location.pathname.lastIndexOf('/') + 1) + "searchdata.bin");
+</script>
+</body>
+</html>
diff --git a/doxygen/test/layout_search_binary/indexpage.xml b/doxygen/test/layout_search_binary/indexpage.xml
new file mode 100644 (file)
index 0000000..9adce17
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<doxygen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="compound.xsd" version="1.8.14">
+  <compounddef id="indexpage" kind="page">
+    <compoundname>index</compoundname>
+    <title>My Project</title>
+    <briefdescription>
+    </briefdescription>
+    <detaileddescription>
+    </detaileddescription>
+  </compounddef>
+</doxygen>
+
index 645853848d7cb944cff3a2d22162aaf1f747376f..d572a12fc160ca0c42a0c8b8a3bffe3f6272eca3 100644 (file)
@@ -8,3 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 645853848d7cb944cff3a2d22162aaf1f747376f..d572a12fc160ca0c42a0c8b8a3bffe3f6272eca3 100644 (file)
@@ -8,3 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 8e684beab700434615853bcf7baaa92b4cbb0a2e..dc73e9127c90152d7125b231104075fd2efa5987 100644 (file)
@@ -9,6 +9,7 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
 
 ALIASES = \
     "m_footernavigation=@xmlonly<mcss:footernavigation xmlns:mcss=\"http://mcss.mosra.cz/doxygen/\" />@endxmlonly"
index 63d3633f8011ab1922adef351d6e0f3e7dfe8bbc..20d9ff62f302df95b9d93cee31b1c947f3dbe294 100644 (file)
@@ -10,3 +10,4 @@ M_LINKS_NAVBAR1         = "page-in-navbar pages page-b" \
                           "page-b"
 M_LINKS_NAVBAR2         = "page-b" \
                           "page-in-navbar page-b files"
+M_SEARCH_DISABLED       = YES
index 42e59b3ad7237fadf60c2f612b39c4654b5576cc..7a0b93f69a23a958d366fd44e9c49ac9d87dc5d0 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
@@ -54,4 +56,4 @@
   </div>
 </article></main>
 </body>
-</html>
\ No newline at end of file
+</html>
index e67b081dc307c75195db0e37624a6b1e72c2a696..d66b4bc43cf53fa58299150662fd85556af6700b 100644 (file)
   <div class="m-container">
     <div class="m-row">
       <a href="index.html" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">My Project</a>
-      <a id="m-navbar-show" href="#navigation" title="Show navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
-      <a id="m-navbar-hide" href="#" title="Hide navigation" class="m-col-t-3 m-hide-m m-text-right"></a>
+      <div class="m-col-t-3 m-hide-m m-text-right m-nopadr">
+        <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+        <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+      </div>
       <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
         <div class="m-row">
           <ol class="m-col-t-6 m-col-m-none">
index 0b25b865878a1827b2d1e4b7084ed8ee4c930ea5..906899b2351e6fe1cd7bc3f6f8fcc46be2ed685b 100644 (file)
@@ -9,3 +9,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
index 645853848d7cb944cff3a2d22162aaf1f747376f..d572a12fc160ca0c42a0c8b8a3bffe3f6272eca3 100644 (file)
@@ -8,3 +8,4 @@ M_PAGE_FINE_PRINT       =
 M_THEME_COLOR           =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
diff --git a/doxygen/test/populate-js-test-data.py b/doxygen/test/populate-js-test-data.py
new file mode 100755 (executable)
index 0000000..a59f57c
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+
+#
+#   This file is part of m.css.
+#
+#   Copyright © 2017, 2018 Vladimír Vondruš <mosra@centrum.cz>
+#
+#   Permission is hereby granted, free of charge, to any person obtaining a
+#   copy of this software and associated documentation files (the "Software"),
+#   to deal in the Software without restriction, including without limitation
+#   the rights to use, copy, modify, merge, publish, distribute, sublicense,
+#   and/or sell copies of the Software, and to permit persons to whom the
+#   Software is furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included
+#   in all copies or substantial portions of the Software.
+#
+#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+#   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+#   DEALINGS IN THE SOFTWARE.
+#
+
+import base64
+import os
+import sys
+import pathlib
+sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
+
+from dox2html5 import Trie, ResultMap, serialize_search_data
+
+basedir = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))/'js-test-data'
+
+with open(basedir/'short.bin', 'wb') as f:
+    f.write(b'')
+with open(basedir/'wrong-magic.bin', 'wb') as f:
+    f.write(b'MOS\0              ')
+with open(basedir/'wrong-version.bin', 'wb') as f:
+    f.write(b'MCS\1              ')
+with open(basedir/'empty.bin', 'wb') as f:
+    f.write(serialize_search_data(Trie(), ResultMap()))
+
+trie = Trie()
+map = ResultMap()
+
+trie.insert("math", map.add("Math", "namespaceMath.html"))
+index = map.add("Math::min()", "namespaceMath.html#min")
+trie.insert("math::min()", index)
+trie.insert("min()", index)
+index = map.add("Math::Vector", "classMath_1_1Vector.html")
+trie.insert("math::vector", index)
+trie.insert("vector", index)
+index = map.add("Math::Vector::min()", "classMath_1_1Vector.html#min")
+trie.insert("math::vector::min()", index)
+trie.insert("vector::min()", index)
+trie.insert("min()", index)
+index = map.add("Math::Range", "classMath_1_1Range.html")
+trie.insert("math::range", index)
+trie.insert("range", index)
+index = map.add("Math::Range::min()", "classMath_1_1Range.html#min")
+trie.insert("math::range::min()", index)
+trie.insert("range::min()", index)
+trie.insert("min()", index)
+
+with open(basedir/'searchdata.bin', 'wb') as f:
+    f.write(serialize_search_data(trie, map))
+with open(basedir/'searchdata.b85', 'wb') as f:
+    f.write(base64.b85encode(serialize_search_data(trie, map), True))
diff --git a/doxygen/test/search/Dir/File.h b/doxygen/test/search/Dir/File.h
new file mode 100644 (file)
index 0000000..967cc66
--- /dev/null
@@ -0,0 +1,62 @@
+/** @dir Dir
+ * @brief A directory
+ */
+
+/** @file
+ * @brief A file
+ */
+
+/** @brief A namespace */
+namespace Namespace {
+
+/** @brief A class */
+class Class {
+    public:
+        /** @brief Function without arguments */
+        void foo();
+
+        void foo() const; /**< @overload */
+
+        void foo() &&; /**< @overload */
+
+        /** @brief Function with arguments */
+        void foo(with, arguments);
+};
+
+/** @brief A variable */
+constexpr int Variable = 5;
+
+/** @brief A typedef */
+typedef int Typedef;
+
+/** @brief An enum */
+enum class Enum {
+    Value = 15  /**< Enum value */
+};
+
+}
+
+/** @brief A macro */
+#define MACRO
+
+/** @brief Macro function */
+#define MACRO_FUNCTION()
+
+/** @brief Macro function with params */
+#define MACRO_FUNCTION_WITH_PARAMS(params)
+
+namespace UndocumentedNamespace {}
+
+class UndocumentedClass {};
+
+void undocumentedFunction();
+
+constexpr int UndocumentedVariable = 42;
+
+typedef int UndocumentedTypedef;
+
+enum class UndocumentedEnum {
+    UndocumentedValue
+};
+
+#define UNDOCUMENTED_MACRO
diff --git a/doxygen/test/search/Doxyfile b/doxygen/test/search/Doxyfile
new file mode 100644 (file)
index 0000000..b501101
--- /dev/null
@@ -0,0 +1,12 @@
+INPUT                   = Dir UndocumentedDir input.dox
+QUIET                   = YES
+GENERATE_HTML           = NO
+GENERATE_LATEX          = NO
+GENERATE_XML            = YES
+EXAMPLE_PATH            = .
+
+M_PAGE_FINE_PRINT       =
+M_THEME_COLOR           =
+M_LINKS_NAVBAR1         =
+M_LINKS_NAVBAR2         =
+M_SEARCH_DOWNLOAD_BINARY = YES
diff --git a/doxygen/test/search/UndocumentedDir/UndocumentedFile.h b/doxygen/test/search/UndocumentedDir/UndocumentedFile.h
new file mode 100644 (file)
index 0000000..04f912e
--- /dev/null
@@ -0,0 +1 @@
+/* Hic sunt leones. */
diff --git a/doxygen/test/search/example.cpp b/doxygen/test/search/example.cpp
new file mode 100644 (file)
index 0000000..9238e52
--- /dev/null
@@ -0,0 +1,3 @@
+int main() {
+    return 42;
+}
diff --git a/doxygen/test/search/input.dox b/doxygen/test/search/input.dox
new file mode 100644 (file)
index 0000000..a652190
--- /dev/null
@@ -0,0 +1,13 @@
+/**
+@page page A page
+
+- @subpage subpage Subpage
+*/
+
+/**
+@page subpage Subpage
+
+Some content.
+*/
+
+/** @example example.cpp */
diff --git a/doxygen/test/test-search.js b/doxygen/test/test-search.js
new file mode 100644 (file)
index 0000000..d9f0353
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+    This file is part of m.css.
+
+    Copyright © 2017, 2018 Vladimír Vondruš <mosra@centrum.cz>
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included
+    in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+*/
+
+"use strict"; /* it summons the Cthulhu in a proper way, they say */
+
+const { Search } = require('../search.js');
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { StringDecoder } = require('string_decoder');
+
+/* HTML escaping with RTL workarounds */
+{
+    assert.equal(Search.escapeForRtl("foo()"), "foo(&lrm;)&lrm;");
+    /* Not sure why / and & has to be escaped from both sides */
+    assert.equal(Search.escapeForRtl("Dir/"), "Dir&lrm;/&lrm;");
+    assert.equal(Search.escapeForRtl("foo() &&"), "foo(&lrm;)&lrm; &lrm;&amp;&lrm;&lrm;&amp;&lrm;");
+    assert.equal(Search.escapeForRtl("operator=()"), "operator&lrm;=(&lrm;)&lrm;");
+    assert.equal(Search.escapeForRtl("NS::Class<int>"), "NS&lrm;:&lrm;:Class&lt;int&lrm;&gt;&lrm;");
+}
+
+/* Simple base85 decoding */
+{
+    let b85 = 'Xk~0{Zy-ZbL0VZLcW-iRWFa9T';
+    let buf = Search.base85decode(b85);
+    assert.equal(Buffer.from(buf).toString(), "hello CRAZY world!!!");
+}
+
+/* Unpadded base85 decoding */
+{
+    let b85 = 'Xk~0{Zy-ZbL0VZLcW-iRWFa9Te';
+    let buf = Search.base85decode(b85);
+    assert.ok(typeof buf === 'undefined');
+}
+
+/* Mess in base85 is interpreted as zeros */
+{
+    let b85 = '\'".: ';
+    let buf = Search.base85decode(b85);
+    assert.deepEqual(Buffer.from(buf), Buffer.from([0, 0, 0, 0]));
+}
+
+/* Verify that base85-decoded file is equivalent to the binary */
+{
+    let binary = fs.readFileSync(path.join(__dirname, "js-test-data/searchdata.bin"));
+    assert.ok(binary.byteLength, 531);
+    let b85 = fs.readFileSync(path.join(__dirname, "js-test-data/searchdata.b85"), {encoding: 'utf-8'});
+    assert.deepEqual(new DataView(binary.buffer.slice(binary.byteOffset, binary.byteOffset + binary.byteLength)), new DataView(Search.base85decode(b85), 0, binary.byteLength));
+}
+
+/* Opening a too short file */
+{
+    let buffer = fs.readFileSync(path.join(__dirname, "js-test-data/short.bin"));
+    assert.ok(!Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
+}
+
+/* Opening file with wrong signature */
+{
+    let buffer = fs.readFileSync(path.join(__dirname, "js-test-data/wrong-magic.bin"));
+    assert.ok(!Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
+}
+
+/* Opening file with  */
+{
+    let buffer = fs.readFileSync(path.join(__dirname, "js-test-data/wrong-version.bin"));
+    assert.ok(!Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
+}
+
+/* Search with empty data */
+{
+    let buffer = fs.readFileSync(path.join(__dirname, "js-test-data/empty.bin"));
+    assert.ok(Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
+    assert.equal(Search.symbolCount, 0);
+    assert.deepEqual(Search.search(''), []);
+}
+
+/* Search */
+{
+    let buffer = fs.readFileSync(path.join(__dirname, "js-test-data/searchdata.bin"));
+    assert.ok(Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
+    assert.equal(Search.symbolCount, 6);
+    assert.equal(Search.maxResults, 100);
+
+    /* Blow up */
+    let resultsForM = [
+        { name: 'Math',
+          url: 'namespaceMath.html',
+          suffixLength: 3 },
+        { name: 'Math::min()',
+          url: 'namespaceMath.html#min',
+          suffixLength: 10 },
+        { name: 'Math::Vector',
+          url: 'classMath_1_1Vector.html',
+          suffixLength: 11 },
+        { name: 'Math::Vector::min()',
+          url: 'classMath_1_1Vector.html#min',
+          suffixLength: 18 },
+        { name: 'Math::Range',
+          url: 'classMath_1_1Range.html',
+          suffixLength: 10 },
+        { name: 'Math::Range::min()',
+          url: 'classMath_1_1Range.html#min',
+          suffixLength: 17 },
+        { name: 'Math::min()',
+          url: 'namespaceMath.html#min',
+          suffixLength: 4 },
+        { name: 'Math::Vector::min()',
+          url: 'classMath_1_1Vector.html#min',
+          suffixLength: 4 },
+        { name: 'Math::Range::min()',
+          url: 'classMath_1_1Range.html#min',
+          suffixLength: 4 } ];
+    assert.deepEqual(Search.search('m'), resultsForM);
+
+    /* Add more characters */
+    assert.deepEqual(Search.search('min'), [
+        { name: 'Math::min()',
+          url: 'namespaceMath.html#min',
+          suffixLength: 2 },
+        { name: 'Math::Vector::min()',
+          url: 'classMath_1_1Vector.html#min',
+          suffixLength: 2 },
+        { name: 'Math::Range::min()',
+          url: 'classMath_1_1Range.html#min',
+          suffixLength: 2 } ]);
+
+    /* Go back, get the same thing */
+    assert.deepEqual(Search.search('m'), resultsForM);
+
+    /* Search for something else */
+    let resultsForVec = [
+        { name: 'Math::Vector',
+          url: 'classMath_1_1Vector.html',
+          suffixLength: 3 },
+        { name: 'Math::Vector::min()',
+          url: 'classMath_1_1Vector.html#min',
+          suffixLength: 10 }];
+    assert.deepEqual(Search.search('vec'), resultsForVec);
+
+    /* Uppercase things and spaces */
+    assert.deepEqual(Search.search(' Vec  '), resultsForVec);
+
+    /* Not found */
+    assert.deepEqual(Search.search('pizza'), []);
+}
+
+/* Search, limiting the results to 3 */
+{
+    let buffer = fs.readFileSync(path.join(__dirname, "js-test-data/searchdata.bin"));
+    assert.ok(Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), 3));
+    assert.equal(Search.symbolCount, 6);
+    assert.equal(Search.maxResults, 3);
+    assert.deepEqual(Search.search('m'), [
+        { name: 'Math',
+          url: 'namespaceMath.html',
+          suffixLength: 3 },
+        { name: 'Math::min()',
+          url: 'namespaceMath.html#min',
+          suffixLength: 10 },
+        { name: 'Math::Vector',
+          url: 'classMath_1_1Vector.html',
+          suffixLength: 11 }]);
+}
+
+/* Search loaded from a base85-encoded file should work properly */
+{
+    let b85 = fs.readFileSync(path.join(__dirname, "js-test-data/searchdata.b85"), {encoding: 'utf-8'});
+    assert.ok(Search.load(b85));
+    assert.equal(Search.symbolCount, 6);
+    assert.equal(Search.maxResults, 100);
+    assert.deepEqual(Search.search('min'), [
+        { name: 'Math::min()',
+          url: 'namespaceMath.html#min',
+          suffixLength: 2 },
+        { name: 'Math::Vector::min()',
+          url: 'classMath_1_1Vector.html#min',
+          suffixLength: 2 },
+        { name: 'Math::Range::min()',
+          url: 'classMath_1_1Range.html#min',
+          suffixLength: 2 } ]);
+}
+
+/* Not testing Search.download() because the xmlhttprequest npm package is *crap* */
index fa8915bd0e5f892da41d26bfd26a57acee6e6a32..df7404bb1d8e4f8081ccee56771e1ae26f0183a4 100644 (file)
@@ -27,6 +27,12 @@ import unittest
 from dox2html5 import parse_doxyfile, State
 
 class Doxyfile(unittest.TestCase):
+    def __init__(self, *args, **kwargs):
+        unittest.TestCase.__init__(self, *args, **kwargs)
+
+        # Display ALL THE DIFFS
+        self.maxDiff = None
+
     def test(self):
         state = State()
         parse_doxyfile(state, 'test/doxyfile/Doxyfile')
@@ -42,6 +48,11 @@ class Doxyfile(unittest.TestCase):
             'M_LINKS_NAVBAR2': ['annotated', 'files'],
             'M_PAGE_FINE_PRINT': 'this is "quotes"',
             'M_PAGE_HEADER': 'this is "quotes" \'apostrophes\'',
+            'M_SEARCH_DISABLED': False,
+            'M_SEARCH_DOWNLOAD_BINARY': False,
+            'M_SEARCH_EXTERNAL_URL': '',
+            'M_SEARCH_HELP': 'Search for symbols, headers, pages or example source files. '
+                             'You can omit any prefix from the symbol or file path.',
             'M_THEME_COLOR': '#22272e',
             'OUTPUT_DIRECTORY': '',
             'PROJECT_BRIEF': 'is cool',
index 4e7c8d262c1bd8895dbcd0a6494e305c412b64f4..de5cecf8617c4de2e7a7295283f1afae498b4f4d 100644 (file)
@@ -22,6 +22,8 @@
 #   DEALINGS IN THE SOFTWARE.
 #
 
+import os
+
 from test import BaseTestCase
 
 class Layout(BaseTestCase):
@@ -31,6 +33,9 @@ class Layout(BaseTestCase):
     def test(self):
         self.run_dox2html5(wildcard='index.xml')
         self.assertEqual(*self.actual_expected_contents('pages.html'))
+        self.assertTrue(os.path.exists(os.path.join(self.path, 'html', 'm-dark+doxygen.compiled.css')))
+        self.assertTrue(os.path.exists(os.path.join(self.path, 'html', 'search.js')))
+        self.assertTrue(os.path.exists(os.path.join(self.path, 'html', 'searchdata.js')))
 
 class LayoutMinimal(BaseTestCase):
     def __init__(self, *args, **kwargs):
@@ -47,3 +52,12 @@ class LayoutNavbarSingleColumn(BaseTestCase):
     def test(self):
         self.run_dox2html5(wildcard='indexpage.xml')
         self.assertEqual(*self.actual_expected_contents('index.html'))
+
+class LayoutSearchBinary(BaseTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, 'search_binary', *args, **kwargs)
+
+    def test(self):
+        self.run_dox2html5(wildcard='indexpage.xml')
+        self.assertEqual(*self.actual_expected_contents('index.html'))
+        self.assertTrue(os.path.exists(os.path.join(self.path, 'html', 'searchdata.bin')))
index cb5c8a932f38a301d3d181ed52da565e2b205dbb..87a9a7cce768c74d8830a4ad4e04a8a00d78fd9b 100755 (executable)
 #
 
 import argparse
-import unittest
+import os
 import sys
+import unittest
 from types import SimpleNamespace as Empty
 
 from dox2html5 import Trie, ResultMap, serialize_search_data, search_data_header_struct
 
+from test import IntegrationTestCase
+
 def _pretty_print_trie(serialized: bytearray, hashtable, stats, base_offset, indent, draw_pipe, show_merged) -> str:
     # Visualize where the trees were merged
     if show_merged and base_offset in hashtable: return ' #'
@@ -289,6 +292,59 @@ range [2]
 """)
         self.assertEqual(len(serialized), 241)
 
+class Search(IntegrationTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, '', *args, **kwargs)
+
+    def test(self):
+        self.run_dox2html5(index_pages=[], wildcard='*.xml')
+
+        with open(os.path.join(self.path, 'html', 'searchdata.bin'), 'rb') as f:
+            search_data_pretty = pretty_print(f.read())[0]
+        #print(search_data_pretty)
+        self.assertEqual(search_data_pretty, """
+namespace [0]
+|        ::class [1]
+|          |    ::foo() [6, 7, 8, 9]
+|          enum [11]
+|          |   ::value [10]
+|          typedef [12]
+|          variable [13]
+class [1]
+|    ::foo() [6, 7, 8, 9]
+a page [2]
+subpage [3]
+dir [4]
+|  /file.h [5]
+file.h [5]
+|oo() [6, 7, 8, 9]
+enum [11]
+|   ::value [10]
+value [10]
+| riable [13]
+typedef [12]
+macro [14]
+|    _function() [15]
+|             _with_params() [16]
+0: Namespace [0] -> namespaceNamespace.html
+1: Namespace::Class [0] -> classNamespace_1_1Class.html
+2: A page [0] -> page.html
+3: Subpage [0] -> subpage.html
+4: Dir [0] -> dir_da5033def2d0db76e9883b31b76b3d0c.html
+5: Dir/File.h [0] -> File_8h.html
+6: Namespace::Class::foo() [0] -> classNamespace_1_1Class.html#aaeba4096356215868370d6ea476bf5d9
+7: Namespace::Class::foo() [0] -> classNamespace_1_1Class.html#ac03c5b93907dda16763eabd26b25500a
+8: Namespace::Class::foo() [0] -> classNamespace_1_1Class.html#ac9e7e80d06281e30cfcc13171d117ade
+9: Namespace::Class::foo() [0] -> classNamespace_1_1Class.html#ac03e8437172963981197eb393e0550d3
+10: Namespace::Enum::Value [0] -> namespaceNamespace.html#add172b93283b1ab7612c3ca6cc5dcfeaa689202409e48743b914713f96d93947c
+11: Namespace::Enum [0] -> namespaceNamespace.html#add172b93283b1ab7612c3ca6cc5dcfea
+12: Namespace::Typedef [0] -> namespaceNamespace.html#abe2a245304bc2234927ef33175646e08
+13: Namespace::Variable [0] -> namespaceNamespace.html#ad3121960d8665ab045ca1bfa1480a86d
+14: MACRO [0] -> File_8h.html#a824c99cb152a3c2e9111a2cb9c34891e
+15: MACRO_FUNCTION() [0] -> File_8h.html#a025158d6007b306645a8eb7c7a9237c1
+16: MACRO_FUNCTION_WITH_PARAMS() [0] -> File_8h.html#a88602bba5a72becb4f2dc544ce12c420
+""".strip())
+
 if __name__ == '__main__': # pragma: no cover
     parser = argparse.ArgumentParser()
     parser.add_argument('file', help="file to pretty-print")