`), then we need to put the content in a block.
+
+==== Creating Tabbed Sections
+Hopefully a little bit of background on roles is helpful to understanding the rest of what we'll do to create a tabbed section in a page.
+
+See the Bootstrap docs on https://getbootstrap.com/docs/3.3/components/#nav-tabs[tabs] for details on how to use tabs and pills with Bootstrap. As a quick overview, tabs in Bootstrap are defined like this:
+
+[source,html]
+----
+
+----
+<1> This section creates an unordered list with a line item for each tab. The `data-toggle` and `class` parameters are what tell Bootstrap how to render the content.
+<2> Note the class defined here: `
`. This defines that what follows is the content that will make up the panes of our tabs. We will need to define these in our document.
+<3> In our document, we need to delineate the separate sections of content that will make up each pane.
+
+We have created some custom JavaScript that will do part of the above for us if we assign the proper roles to the blocks of content that we want to appear in the tab panes. To do this, we can use Asciidoctor's block delimiters to define the tabbed content, and the content between the tab.
+
+. Define an "open block" (an unformatted content block), and give it the role `.dynamic-tabs`. An open block is defined by two hyphens on a line before the content that goes in the block, and two hyphens on a line after the content to end the block. We give a block a role by adding a period before the role name, like this:
++
+[source,text]
+----
+[.dynamic-tabs]
+--
+The stuff we'll put in the tabs will go here.
+--
+----
+
+. Next we need to define the content for the tabs between the open block delimiters.
+.. We enclose each tab pane in another type of block, and "example" block. This allows us to include any kind of content in the block and be sure all of the various types of elements (heading, text, examples, etc.) are included in the pane.
+.. We give the example block another role, `tab-pane`, and we must make sure that each pane has a unique ID. We assign IDs with a hash mark (\#) followed by the ID value (`#sect1`).
+.. We also need to define a label for each tab. We do this by adding another role, `tab-label` to the content we want to appear as the name of the tab.
+.. In the end one pane will look like this:
++
+[source,text]
+----
+[example.tab-pane#sect1] <--1-->
+==== <--2-->
+[.tab-label]*Section 1* <--3-->
+My content...
+====
+----
+<1> When we define the example block with `[example]`, it's followed by `.tab-pane#sect1` as the class (each class separated by a period `.`) and the ID defined in the tab definition earlier. Those will become the classes (`class="tab-pane active"`) and ID (`id="sect1"`) in the resulting HTML.
+<2> Example blocks are delimited by 4 equal signs (`====`) before and after the enclosed content.
+<3> The words "Section 1" will appear in the HTML page as the label for this tab.
+
+.. Create `[example.tab-pane#id]` sections for each tab, until you finally end up with something that looks like this:
++
+[source,text]
+----
+[.dynamic-tabs]
+--
+[example.tab-pane#sect1]
+====
+[.tab-label]*Section 1*
+My content...
+====
+
+[example.tab-pane#sect2]
+====
+[.tab-label]*Section 2*
+My content...
+====
+--
+----
+
+. Because these tabbed sections are created when the HTML and related JavaScript is loaded, when the PDF is generated it will ignore all the class and ID information in the page because it is meaningless to it (asciidoctor-pdf does not recognize roles/custom classes). In the PDF, this content will be displayed in Example Blocks, which have some formatting rules, but will not be shown as tabs and shouldn't show any other oddities.
+
== Building the HTML Site
An Ant target `build-site` will build the full HTML site. This target builds the navigation for the left-hand menu, and converts all `.adoc` files to `.html`, including navigation and inter-document links.
+
+Building the HTML has several dependencies that will need to be installed on your local machine. Review the `README.txt` file in the `solr/solr-ref-guide` directory for specific details.
+
+See <
> for more information about building.
+
+=== Build Validation
+
+When you run `ant build-site` to build the HTML, several additional validations occur during that process. See `solr-ref-guide/tools/CheckLinksAndAnchors.java` for details of what that tool does to validate content.
diff --git a/solr/solr-ref-guide/tools/CheckLinksAndAnchors.java b/solr/solr-ref-guide/tools/CheckLinksAndAnchors.java
index 0dc88d70f17..1d9bb2fb7b8 100644
--- a/solr/solr-ref-guide/tools/CheckLinksAndAnchors.java
+++ b/solr/solr-ref-guide/tools/CheckLinksAndAnchors.java
@@ -44,29 +44,46 @@ import org.jsoup.parser.Tag;
import org.jsoup.select.Elements;
import org.jsoup.select.NodeVisitor;
-/**
- * Check various things regarding anchors & links in the generated HTML site.
+/**
+ * Check various things regarding anchors, links & general doc structure in the generated HTML site.
+ *
*
+ * Problems this tool checks for...
+ *
+ *
+ *
+ * -
* Asciidoctor doesn't do a good job of rectifying situations where multiple documents are included in one
- * massive (PDF) document may have identical anchors (either explicitly defined, or implicitly defined because of
- * section headings). Asciidoctor also doesn't support linking directly to another (included) document by name,
+ * massive (PDF) document may have identical anchors (either explicitly defined, or implicitly defined because of
+ * section headings). Asciidoctor also doesn't support linking directly to another (included) document by name,
* unless there is an explicit '#fragement' used in the link.
+ *
+ * -
+ * Any "relative" link should point to a file that actually exists.
+ *
+ * -
+ * Our use of "Bootstrap" features leverage some custom javascript
+ * for manipulating the DOM to keep the markup needed in the source
*.adoc
files simple, but it's
+ * still possible users may create asciidctor "blocks" that break conventions (either in Bootstrap or in our
+ * custom javascript)
+ *
+ *
+ *
+ *
+ * This tool parses the generated HTML site, looking for these situations in order to fail the build, since
+ * (depending on the type of check) these situations will result in inconsistent/broken HTML, or equivilent
+ * problems in the generated PDF.
*
*
- * This tool parses the generated HTML site, looking for these situations in order to fail the build -- since the
- * equivilent PDF will be broken. It also does some general check of the relative URLs to ensure the destination
- * files/anchors actaully exist.
- *
- *
- * This tool supports 2 modes, depending on wether you want to run it against the HTML generated by Jekyll, or
+ * This tool supports 2 modes, depending on wether you want to run it against the HTML generated by Jekyll, or
* the "bare bones" HTML generated directly by asciidoctor...
*
*
* - Jekyll Mode:
*
* CheckLinksAndAnchors html-dir-name/ [false]
- * - Requires all html pages have a "main-content" div; ignores all links & anchors that
- * are not decendents of this div (to exclude redundent template based header, footer, & sidebar links)
+ *
- Requires all html pages have a "main-content" div; ignores all DOM Nodes that are
+ * not decendents of this div (to exclude redundent template based header, footer, & sidebar links)
*
* - Expects that the
<body/>
tag will have an id
matching the page shortname.
*
@@ -79,31 +96,31 @@ import org.jsoup.select.NodeVisitor;
*
*
*
- *
- *
+ *
+ *
* TODO: build a list of all known external links so that some other tool could (optionally) ping them all for 200 status?
*
* @see https://github.com/asciidoctor/asciidoctor/issues/1865
* @see https://github.com/asciidoctor/asciidoctor/issues/1866
*/
-public class CheckLinksAndAnchors {
+public class CheckLinksAndAnchors { // TODO: rename this class now that it does more then just links & anchors
public static final class HtmlFileFilter implements FileFilter {
public boolean accept(File pathname) {
return pathname.getName().toLowerCase().endsWith("html");
}
}
-
+
public static void main(String[] args) throws Exception {
int problems = 0;
-
+
if (args.length < 1 || 2 < args.length ) {
System.err.println("usage: CheckLinksAndAnchors []");
System.exit(-1);
}
final File htmlDir = new File(args[0]);
final boolean bareBones = (2 == args.length) ? Boolean.parseBoolean(args[1]) : false;
-
+
final File[] pages = htmlDir.listFiles(new HtmlFileFilter());
if (0 == pages.length) {
System.err.println("No HTML Files found, wrong htmlDir? forgot to built the site?");
@@ -116,17 +133,17 @@ public class CheckLinksAndAnchors {
int totalLinks = 0;
int totalRelativeLinks = 0;
-
+
for (File file : pages) {
//System.out.println("input File URI: " + file.toURI().toString());
assert ! filesToRelativeLinks.containsKey(file);
final List linksInThisFile = new ArrayList(17);
filesToRelativeLinks.put(file, linksInThisFile);
-
+
final String fileContents = readFile(file.getPath());
final Document doc = Jsoup.parse(fileContents);
-
+
// For Jekyll, we only care about class='main-content' -- we don't want to worry
// about ids/links duplicated in the header/footer of every page,
final String mainContentSelector = bareBones ? "body" : ".main-content";
@@ -145,10 +162,10 @@ public class CheckLinksAndAnchors {
nodesWithIds.add(new Element(Tag.valueOf("body"), "").attr("id", file.getName().replaceAll("\\.html$","")));
} else {
// We have to add Jekyll's to the nodesWithIds so we check the main section anchor as well
- // since we've already
+ // since we've already
nodesWithIds.addAll(doc.select("body[id]"));
}
-
+
boolean foundPreamble = false;
for (Element node : nodesWithIds) {
final String id = node.id();
@@ -168,7 +185,7 @@ public class CheckLinksAndAnchors {
continue;
}
}
-
+
if (idsToFiles.containsKey(id)) {
idsInMultiFiles.add(id);
} else {
@@ -217,6 +234,8 @@ public class CheckLinksAndAnchors {
}
}
}
+
+ problems += validateHtmlStructure(file, mainContent);
}
// if there are problematic ids, report them
@@ -242,7 +261,7 @@ public class CheckLinksAndAnchors {
System.err.println(" ... source: " + source.toURI().toString());
} else if ( ( ! idsToFiles.containsKey(frag) ) || // no file contains this id, or...
// id exists, but not in linked file
- ( ! idsToFiles.get(frag).get(0).getName().equals(path) )) {
+ ( ! idsToFiles.get(frag).get(0).getName().equals(path) )) {
problems++;
System.err.println("Relative link points at id that doesn't exist in dest: " + link);
System.err.println(" ... source: " + source.toURI().toString());
@@ -276,6 +295,102 @@ public class CheckLinksAndAnchors {
br.close();
}
}
-
-}
+ /**
+ * returns the number of problems found with this file
+ */
+ private static int validateHtmlStructure(final File f, final Element mainContent) {
+ final String file = f.toURI().toString();
+ int problems = 0;
+
+ for (Element tab : mainContent.select(".dynamic-tabs")) {
+ // must be at least two tab-pane decendents of each dynamic-tabs
+ final Elements panes = tab.select(".tab-pane");
+ final int numPanes = panes.size();
+ if (numPanes < 2) {
+ System.err.println(file + " contains a 'dynamic-tabs' with "+ numPanes+" 'tab-pane' decendents -- must be at least 2");
+ problems++;
+ }
+
+ // must not have any decendents of a dynamic-tabs that are not part of tab-pane
+ //
+ // this is kind of tricky, because asciidoctor creates wrapper divs around the tab-panes
+ // so we can't make assumptions about direct children
+ //
+ final Elements elementsToIgnore = panes.parents();
+ for (Element pane : panes) {
+ elementsToIgnore.addAll(pane.select("*"));
+ }
+ final Elements nonPaneDecendents = tab.select("*");
+ nonPaneDecendents.removeAll(elementsToIgnore);
+ if (0 != nonPaneDecendents.size()) {
+ System.err.println(file + " contains a 'dynamic-tabs' with content outside of a 'tab-pane': " +
+ shortStr(nonPaneDecendents.text()));
+ problems++;
+ }
+ }
+
+ // Now fetch all tab-panes, even if they aren't in a dynamic-tabs instance
+ // (that's a type of error we want to check for)
+ final Elements validPanes = mainContent.select(".dynamic-tabs .tab-pane");
+ final Elements allPanes = mainContent.select(".tab-pane");
+
+ for (Element pane : allPanes) {
+ // every tab-pane must have an id
+ if (pane.id().trim().isEmpty()) {
+ System.err.println(file + " contains a 'tab-pane' that does not have a (unique) '#id'");
+ problems++;
+ }
+ final String debug = "'tab-pane" + (pane.id().isEmpty() ? "" : "#" + pane.id()) + "'";
+
+ // no 'active' class on any tab-pane
+ if (pane.classNames​().contains("active")) {
+ System.err.println(file + " contains " + debug + " with 'active' defined -- this must be removed");
+ problems++;
+ }
+
+ // every tab-pane must be a decendent of a dynamic-tabs
+ if (! validPanes.contains(pane)) {
+ System.err.println(file + " contains " + debug + " that is not a decendent of a 'dynamic-tabs'");
+ problems++;
+ }
+
+ // every tab-pane must have exactly 1 tab-label which is
+ Elements labels = pane.select(".tab-label");
+ if (1 != labels.size()) {
+ System.err.println(file + " contains " + debug + " with " + labels.size() + " 'tab-label' decendents -- must be exactly 1");
+ problems++;
+ } else {
+ Element label = labels.first();
+ if (! label.tagName().equals("strong")) {
+ System.err.println(file + " contains " + debug + " with a 'tab-label' using <"
+ + labels.first().tagName() + "> -- each 'tab-label' must be (example: '[.tab-label]*Text*')");
+ problems++;
+ }
+ final String labelText = label.text().trim();
+ // if the tab-label is the empty string, asciidoctor should optimize it away -- but let's check for it anyway
+ if (labelText.isEmpty()) {
+ System.err.println(file + " contains " + debug + " with a blank 'tab-label'");
+ problems++;
+ }
+ // validate label must be first paragraph? first text content?
+ if (! pane.text().trim().startsWith(labelText)) {
+ System.err.println(file + " contains " + debug + " with text before the 'tab-label' ('" + labelText + "')");
+ problems++;
+ }
+
+ }
+
+ }
+
+ return problems;
+ }
+
+ public static final String shortStr(String s) {
+ if (s.length() < 20) {
+ return s;
+ }
+ return s.substring(0, 17) + "...";
+ }
+
+}