mirror of https://github.com/apache/lucene.git
SOLR-11584: add JS and CSS to support tabbed content; add "column" style TOC support
This commit is contained in:
parent
3923e9fba3
commit
df70e27d7d
|
@ -446,7 +446,6 @@ div#toc ul li ul li {
|
|||
|
||||
.tab-content {
|
||||
padding: 15px;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
span.tagTitle {font-weight: 500;}
|
||||
|
|
|
@ -3071,10 +3071,13 @@ textarea.input-group-sm > .input-group-btn > .btn {
|
|||
clear: both;
|
||||
}
|
||||
.tab-content > .tab-pane,
|
||||
.pill-content > .pill-pane {
|
||||
.tab-content > .content > .tab-pane,
|
||||
.pill-content > .pill-pane,
|
||||
.tab-content > .content > .tab-pane {
|
||||
display: none;
|
||||
}
|
||||
.tab-content > .active,
|
||||
.tab-content > .content > .active,
|
||||
.pill-content > .active {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -1109,6 +1109,16 @@ body.toc2 #header > h1:nth-last-child(2)
|
|||
border: none;
|
||||
max-width: 300px;
|
||||
}
|
||||
#toc.toc-column
|
||||
{
|
||||
-moz-column-count: 3;
|
||||
-moz-column-gap: 20px;
|
||||
-webkit-column-count: 3;
|
||||
-webkit-column-gap: 20px;
|
||||
column-count: 3;
|
||||
column-gap: 20px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
.toc > ul
|
||||
{
|
||||
|
@ -1149,7 +1159,7 @@ body.toc2 #header > h1:nth-last-child(2)
|
|||
|
||||
div.toc ul li {
|
||||
margin: 8px 0 8px 22px;
|
||||
list-style: square;
|
||||
list-style: disc;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
|
@ -2646,7 +2656,7 @@ p.tableblock
|
|||
.exampleblock > .content
|
||||
{
|
||||
border-color: #e0e0dc;
|
||||
background-color: #fffef7;
|
||||
background-color: transparent;
|
||||
-webkit-box-shadow: 0 1px 4px #e0e0dc;
|
||||
box-shadow: 0 1px 4px #e0e0dc;
|
||||
}
|
||||
|
|
|
@ -4,11 +4,6 @@
|
|||
font-size:16px;
|
||||
}
|
||||
|
||||
.nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus {
|
||||
background-color: #FF833D;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav > li.active > a {
|
||||
background-color: #262130;
|
||||
}
|
||||
|
@ -17,6 +12,17 @@
|
|||
background-color: #FF833D;
|
||||
}
|
||||
|
||||
.nav-tabs > li.active > a,
|
||||
.nav-tabs > li.active > a:hover,
|
||||
.nav-tabs > li.active > a:focus,
|
||||
.nav-pills > li.active > a,
|
||||
.nav-pills > li.active > a:hover,
|
||||
.nav-pills > li.active > a:focus
|
||||
{
|
||||
background-color: #FF833D;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.navbar-collapse .dropdown-menu > li > a:hover {
|
||||
background-color: #D9411E;
|
||||
}
|
||||
|
@ -52,6 +58,15 @@ a[data-toggle="tooltip"] {
|
|||
border-color: #248ec2 !important;
|
||||
}
|
||||
|
||||
.btn-info,
|
||||
a[data-toggle].btn-info {
|
||||
background-color: #FF833D;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Used for nav buttons */
|
||||
.btn-primary {
|
||||
color: #ffffff;
|
||||
|
|
|
@ -22,6 +22,53 @@ $( document ).ready(function() {
|
|||
*/
|
||||
anchors.add('h2,h3,h4,h5');
|
||||
|
||||
// "Bootstrap Bootstrap"
|
||||
// NOTE: by default, we use "dynamic-tabs" in our wrapper instead of "tab-content"
|
||||
// so that if javascript is disabled, bootstrap's CSS doesn't hide all non-active "tab-pane" divs
|
||||
$(".dynamic-tabs").each(function(ignored) {
|
||||
$(this).addClass("tab-content");
|
||||
var nav_ul = $("<ul>", { "class": "nav nav-pills" });
|
||||
$(".tab-pane", this).each(function(tab_index) {
|
||||
var pill_li = $("<li>");
|
||||
// force the first tab to always be the active tab
|
||||
if (0 == tab_index) {
|
||||
$(this).addClass("active");
|
||||
pill_li.addClass("active");
|
||||
} else {
|
||||
// our validator should have prevented this, but remove them just in case
|
||||
$(this).removeClass("active");
|
||||
}
|
||||
|
||||
var pill_a = $("<a>", { "data-toggle" : "pill" } );
|
||||
if ($(this)[0].hasAttribute("id")) {
|
||||
pill_a.attr("href", "#" + $(this).attr("id"));
|
||||
} else {
|
||||
// our validator will complain if a tab-pane has no id, but if the user
|
||||
// views the resultin HTML, draw attention to it...
|
||||
pill_a.append( " BAD TAB-PANE HAS NO ID ");
|
||||
}
|
||||
|
||||
var label = $(".tab-label", this);
|
||||
if (0==label.length) {
|
||||
// our validator will complain if a tab-pane has no tab-label, but if the user
|
||||
// views the resultin HTML, draw attention to it...
|
||||
pill_a.append( " BAD TAB-PANE HAS NO TAB-LABEL ");
|
||||
} else {
|
||||
// NOTE: using the "inner" HTML of the label...
|
||||
// so by default we can use "bold" (or whatever) in our asciidoc and have that
|
||||
// be what people see in the PDF or when javascript is disabled,
|
||||
// but when the pills+tabs get active, the pills won't all be bold (or whatever)
|
||||
pill_a.append( label.html() );
|
||||
// NOTE: Removing the label isn't strictly neccessary, but makes the pills/tabs less redundent
|
||||
label.remove();
|
||||
}
|
||||
|
||||
pill_li.append(pill_a);
|
||||
nav_ul.append(pill_li);
|
||||
});
|
||||
$(this).before(nav_ul);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// needed for nav tabs on pages. See Formatting > Nav tabs for more details.
|
||||
|
|
|
@ -24,59 +24,65 @@ Jekyll is a static site generator, meaning that it takes some set of documents a
|
|||
|
||||
Jekyll is an open source project written in Ruby, online at https://jekyllrb.com/.
|
||||
|
||||
== Jekyll-Asciidoctor Plugin
|
||||
== How We Use Jekyll
|
||||
|
||||
The following sections describe the main features of Jekyll that you will encounter while working with the Solr Ref Guide.
|
||||
|
||||
=== Jekyll-Asciidoctor Plugin
|
||||
We use a plugin for Jekyll from the Asciidoctor project to integrate Jekyll with Asciidoc formatted content. The source for the plugin is available at https://github.com/asciidoctor/jekyll-asciidoc.
|
||||
|
||||
This plugin allows us to use Asciidoctor-style variables with Jekyll, instead of having to maintain two sets of the same variables (one for HTML version and another for PDF version).
|
||||
|
||||
== Jekyll Basics
|
||||
|
||||
The following sections describe the main features of Jekyll that you will encounter while working with the Solr Ref Guide.
|
||||
|
||||
=== _config.yml
|
||||
|
||||
The `_config.yml` is a global configuration file that drives many of the options used when building the site (particularly in our use of Jekyll).
|
||||
|
||||
We have templatized `_config.yml` so in our use of Jekyll you will find it as `solr-ref-guide/_config.yml.template`. This allows us to define some variables during the build, and use common Lucene/Solr build parameters (such as versions, etc.).
|
||||
We have template-ized `_config.yml`, so in our use of Jekyll you will find it in `solr/solr-ref-guide/src` as `_config.yml.template`. This allows us to define some variables during the build and use common Lucene/Solr build parameters (such as project versions).
|
||||
|
||||
=== Front Matter
|
||||
|
||||
Front matter for Jekyll is like a header that defines the title of the page, and any other variables that may be helpful or even required when rendering the page.
|
||||
Front matter for Jekyll is similar to a header that defines the title of the page, and other variables that may be helpful (or even required) when rendering the page.
|
||||
|
||||
Every document that will be converted to HTML *must* include at least the page title at the top of the page.
|
||||
Every document that will be converted to HTML *must* include at least the page title at the top of the page. Page titles are defined with a single equal sign (`=`) followed by the title that will appear at the top of the page (such as `= Topic of the Page`).
|
||||
|
||||
Many guides to Jekyll also say that defining the layout in the front matter is required. However, since we only have one layout for all pages, we have defined this as a default.
|
||||
Many guides to Jekyll also say that defining the layout in the front matter is required. However, since we only use one layout for all pages, we have defined this as a default.
|
||||
|
||||
The Solr Ref Guide uses the front matter to define some custom attributes on a per page basis:
|
||||
|
||||
* `page-children` - ordered list of child pages, this is used to build the site navigation menu that appears to the left of each page's content (and to order the pages in the PDF)
|
||||
|
||||
There are also some optional custom attributes that can be defined in pages to affect the Table of Contents presentation in jekyll:
|
||||
|
||||
* `page-toclevels` - changes how "deep" the TOC will be in terms of nested section/sub-section titles (default = 2)
|
||||
* `page-tocclass` - changes the CSS class applied to the TOC. The default is `normal`, resulting in the class name `toc-normal` being used in the HTML. The other option is `right`, to put the TOC on the right side of the page.
|
||||
* `page-toc` - if this is false, then no TOCs will be generated for the page at all.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The special macro `\{section-toc}` can be used anywhere in a page to create an "In this Section" TOC covering only the sub-headings in the same section.
|
||||
|
||||
Setting `:page-toc: false` will prevent this macro from working, so if you want no "top level" TOC, but you do want section TOCs, use `:page-toclevels: 0`.
|
||||
====
|
||||
Other page-level elements can also be defined, such as an Asciidoctor attribute that should apply only to that page, but are not needed on a regular basis.
|
||||
|
||||
The format for adding any parameter to the front matter is to use colons on both sides of the parameter, followed by the value for the parameter (such as `:page-toc: false`).
|
||||
|
||||
==== Table of Contents
|
||||
There are some optional custom attributes that can be defined in pages to affect the Table of Contents presentation in Jekyll:
|
||||
|
||||
`page-toclevels`::
|
||||
Changes how "deep" the TOC will be in terms of nested section/sub-section titles (default = `2`).
|
||||
`page-tocclass`::
|
||||
Changes the CSS class applied to the TOC. The options are:
|
||||
* `normal`, the default, resulting in the class name `toc-normal` being used in the HTML.
|
||||
* `right`, to put the TOC on the right side of the page.
|
||||
* `column`, to make a very long TOC span 3 columns at the top of the page.
|
||||
`page-toc`::
|
||||
If this is `false`, then no TOCs will be generated for the page at all. The default is `true`, so can usually be left undefined.
|
||||
`\{section-toc}`::
|
||||
A special macro that can be used anywhere in a page to create an "In this Section" TOC covering only the sub-headings in the same section.
|
||||
+
|
||||
NOTE: Setting `:page-toc: false` will prevent the `section-toc` macro from working, so if you want no "top level" TOC, but you do want section TOCs, use `:page-toclevels: 0`.
|
||||
|
||||
=== Layouts
|
||||
|
||||
Layouts define the "look and feel" of each page.
|
||||
Layouts define the "look and feel" of each page. Jekyll uses https://shopify.github.io/liquid/[Liquid] for page templates.
|
||||
|
||||
Jekyll uses Liquid for page templates.
|
||||
For our implementation of Jekyll, layouts are found in `solr-ref-guide/src/_layouts`.
|
||||
|
||||
For our implementation of Jekyll, layouts are found in `solr-ref-guide/src/_layouts`
|
||||
We currently use only the `_layouts/default.html` layout for overall page structure, and `_layouts/page.html` for the page-level content. The `page.html` layout is inserted into the `default.html` layout.
|
||||
|
||||
=== Includes
|
||||
|
||||
Include files are usually small files that are pulled into a layout when a page is being built. They are Liquid templates that define an area of the page. This allows flexibility across layouts - all pages can have the same header without duplicating code, but different pages could have different menu options.
|
||||
Include files are (usually) small files that are pulled into a layout when a page is being built. They are Liquid templates that define an area of the page. This allows flexibility across layouts - all pages can have the same header without duplicating code, but different pages could have different menu options.
|
||||
|
||||
Include files that we use define the top navigation, the page header, the page footer, and tables of contents.
|
||||
|
||||
|
@ -84,10 +90,119 @@ For our implementation of Jekyll, include files are found in `solr-ref-guide/src
|
|||
|
||||
=== Data Files
|
||||
|
||||
Data files include data such as lists, that should be included in each page. The left-hand navigation is an example of a data file.
|
||||
Data files include data such as lists that should be included in each page. The left-hand navigation menu is an example of a data file. However, in our build, the navigation is built from the `page-children` hierarchies.
|
||||
|
||||
For our implementation of Jekyll, data files are found in `solr-ref-guide/src/_data`.
|
||||
|
||||
=== Using Bootstrap Components
|
||||
|
||||
The HTML files include https://getbootstrap.com/docs/3.3/[Bootstrap] (v3.3.4 as of October 2017, see `_includes/head.html` to confirm the Bootstrap version currently being used), so all of the components of Bootstrap are available.
|
||||
|
||||
However, before trying to use these components, it's important to understand how Jekyll and Asciidoctor relate to one another.
|
||||
|
||||
==== How Jekyll Consumes .adoc Files
|
||||
|
||||
Jekyll creates all of the page elements we do not define in an `.adoc` file: the header, footer, navigation elements, comments, and other parts of the page that we don't worry about as we write the content of a page. It produces a template that our content is inserted into.
|
||||
|
||||
Asciidoctor converts our content to HTML - defining its own classes to define default styling - and inserts it into the template Jekyll has provided. While we have a lot of control over the CSS for styling, without creating custom plugins for Asciidoctor, we don't control the HTML elements produced by the plugin.
|
||||
|
||||
This is important because in order to use Bootstrap components, we need to insert `div` classes into the HTML produced by Asciidoctor.
|
||||
|
||||
==== Asciidoctor Roles
|
||||
|
||||
Asciidoctor helpfully provides a way to define custom `<div>` classes in `.adoc` files, as long as we understand how to use it.
|
||||
|
||||
Asciidoctor does not call these "divs" or "classes", but instead "_roles_". We can give any content a role - to images, content blocks (such as `[source]` or `[NOTE]`, etc.), even a word in the middle of a sentence.
|
||||
|
||||
Because roles are so flexible, they only apply to the thing - the word, content block, image, etc. - they are directly applied to. This means that if we want an entire section of content to be given a specific role in the HTML (i.e., enclosed in a `<div>`), 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]
|
||||
----
|
||||
<ul class="nav nav-pills"> <--1-->
|
||||
<li class="active"><a data-toggle="pill" href="#sec1">Section 1</a></li>
|
||||
<li><a data-toggle="pill" href="#sect2">Section 2</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content"> <--2-->
|
||||
<div id="sect1" class="tab-pane active"> <--3-->
|
||||
<h3>Section 1</h3>
|
||||
<p>Some content.</p>
|
||||
</div>
|
||||
<div id="sect2" class="tab-pane">
|
||||
<h3>Section 2</h3>
|
||||
<p>Some other content.</p>
|
||||
</div>
|
||||
</div>
|
||||
----
|
||||
<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: `<div class="tab-content">`. 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 <<how-to-contribute.adoc#publishing-html-version,Publishing the HTML Version>> 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.
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* Problems this tool checks for...
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>
|
||||
* 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.
|
||||
* </li>
|
||||
* <li>
|
||||
* Any "relative" link should point to a file that actually exists.
|
||||
* </li>
|
||||
* <li>
|
||||
* Our use of "<a href="https://getbootstrap.com/">Bootstrap</a>" features leverage some custom javascript
|
||||
* for manipulating the DOM to keep the markup needed in the source <code>*.adoc</code> files simple, but it's
|
||||
* still possible users may create asciidctor "blocks" that break conventions (either in Bootstrap or in our
|
||||
* custom javascript)
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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...
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Jekyll Mode:
|
||||
* <ul>
|
||||
* <li><code>CheckLinksAndAnchors html-dir-name/ [false]</li>
|
||||
* <li>Requires all html pages have a "main-content" div; ignores all links & anchors that
|
||||
* are <em>not</em> decendents of this div (to exclude redundent template based header, footer, & sidebar links)
|
||||
* <li>Requires all html pages have a "main-content" div; ignores all DOM Nodes that are
|
||||
* <em>not</em> decendents of this div (to exclude redundent template based header, footer, & sidebar links)
|
||||
* </li>
|
||||
* <li>Expects that the <code><body/></code> tag will have an <code>id</code> matching the page shortname.</li>
|
||||
* </ul>
|
||||
|
@ -79,31 +96,31 @@ import org.jsoup.select.NodeVisitor;
|
|||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* 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 <htmldir> [<bare-bones-boolean>]");
|
||||
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<URI> linksInThisFile = new ArrayList<URI>(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 <body> 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 <strong>
|
||||
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 <strong> (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) + "...";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue