Bug 1279717 - inverse the mask to use a white background on pages with bright text color. r=jaws
authorMike de Boer <mdeboer@mozilla.com>
Wed, 22 Jun 2016 19:22:07 +0200
changeset 302339 229e285859a7349b5958157b0f0ae60a7b2a574c
parent 302338 aa3e5b818a71c843464421deaa495d0a399ca513
child 302340 5f29b11f5ae8ce218baf26ad8e19340f6ee35b70
push id19737
push usermdeboer@mozilla.com
push dateThu, 23 Jun 2016 08:56:37 +0000
treeherderfx-team@229e285859a7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1279717
milestone50.0a1
Bug 1279717 - inverse the mask to use a white background on pages with bright text color. r=jaws MozReview-Commit-ID: D2UFsOfMJln
toolkit/modules/FinderHighlighter.jsm
toolkit/modules/RemoteFinder.jsm
toolkit/modules/tests/browser/browser.ini
toolkit/modules/tests/browser/browser_FinderHighlighter.js
toolkit/modules/tests/browser/file_FinderSample.html
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -5,27 +5,31 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["FinderHighlighter"];
 
 const { interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm");
 
 const kHighlightIterationSizeMax = 100;
 const kModalHighlightRepaintFreqMs = 10;
 const kModalHighlightPref = "findbar.modalHighlight";
-const kFontPropsCSS = ["font-family", "font-kerning", "font-size", "font-size-adjust",
-  "font-stretch", "font-variant", "font-weight", "letter-spacing", "text-emphasis",
-  "text-orientation", "text-transform", "word-spacing"];
+const kFontPropsCSS = ["color", "font-family", "font-kerning", "font-size",
+  "font-size-adjust", "font-stretch", "font-variant", "font-weight", "letter-spacing",
+  "text-emphasis", "text-orientation", "text-transform", "word-spacing"];
 const kFontPropsCamelCase = kFontPropsCSS.map(prop => {
   let parts = prop.split("-");
   return parts.shift() + parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("");
 });
+const kRGBRE = /^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*/i
 // This uuid is used to prefix HTML element IDs and classNames in order to make
 // them unique and hard to clash with IDs and classNames content authors come up
 // with, since the stylesheet for modal highlighting is inserted as an agent-sheet
 // in the active HTML document.
 const kModalIdPrefix = "cedee4d0-74c5-4f2d-ab43-4d37c0f9d463";
 const kModalOutlineId = kModalIdPrefix + "-findbar-modalHighlight-outline";
 const kModalStyle = `
 .findbar-modalHighlight-outline {
@@ -65,20 +69,28 @@ const kModalStyle = `
 .findbar-modalHighlight-outlineMask {
   background: #000;
   mix-blend-mode: multiply;
   opacity: .2;
   position: absolute;
   z-index: 1;
 }
 
+.findbar-modalHighlight-outlineMask[brighttext] {
+  background: #fff;
+}
+
 .findbar-modalHighlight-rect {
   background: #fff;
   border: 1px solid #666;
   position: absolute;
+}
+
+.findbar-modalHighlight-outlineMask[brighttext] > .findbar-modalHighlight-rect {
+  background: #000;
 }`;
 const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * FinderHighlighter class that is used by Finder.jsm to take care of the
  * 'Highlight All' feature, which can highlight all find occurrences in a page.
  *
  * @param {Finder} finder Finder.jsm instance
@@ -273,16 +285,17 @@ FinderHighlighter.prototype = {
       return;
 
     if (this._modalHighlightOutline)
       this._modalHighlightOutline.setAttributeForElement(kModalOutlineId, "hidden", "true");
 
     window = window || this.finder._getWindow();
     this._removeHighlightAllMask(window);
     this._removeModalHighlightListeners(window);
+    delete this._brightText;
   },
 
   /**
    * Called by the Finder after a find result comes in; update the position and
    * content of the outline to the newly found occurrence.
    * To make sure that the outline covers the found range completely, all the
    * CSS styles that influence the text are copied and applied to the outline.
    *
@@ -316,16 +329,22 @@ FinderHighlighter.prototype = {
     let textContent = this._getRangeContentArray(foundRange);
     if (!textContent.length) {
       this.hide(window);
       return;
     }
 
     let rect = foundRange.getBoundingClientRect();
     let fontStyle = this._getRangeFontStyle(foundRange);
+    if (typeof this._brightText == "undefined") {
+      this._brightText = this._isColorBright(fontStyle.color);
+    }
+
+    // Text color in the outline is determined by our stylesheet.
+    delete fontStyle.color;
 
     let anonNode = this.show(window);
 
     anonNode.setTextContentForElement(kModalOutlineId + "-text", textContent.join(" "));
     anonNode.setAttributeForElement(kModalOutlineId + "-text", "style",
       this._getHTMLFontStyle(fontStyle));
 
     if (typeof anonNode.getAttributeForElement(kModalOutlineId, "hidden") == "string")
@@ -493,16 +512,30 @@ FinderHighlighter.prototype = {
       if (idx == -1)
         continue
       style.push(`${kFontPropsCSS[idx]}: ${fontStyle[prop]};`);
     }
     return style.join(" ");
   },
 
   /**
+   * Checks whether a CSS RGB color value can be classified as being 'bright'.
+   *
+   * @param  {String} cssColor RGB color value in the default format rgb[a](r,g,b)
+   * @return {Boolean}
+   */
+  _isColorBright(cssColor) {
+    cssColor = cssColor.match(kRGBRE);
+    if (!cssColor || !cssColor.length)
+      return false;
+    cssColor.shift();
+    return new Color(...cssColor).isBright;
+  },
+
+  /**
    * Add a range to the list of ranges to highlight on, or cut out of, the dimmed
    * background.
    *
    * @param {nsIDOMRange}  range  Range object that should be inspected
    * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed
    */
   _modalHighlight(range, controller, window) {
     if (!this._getRangeContentArray(range).length)
@@ -594,16 +627,18 @@ FinderHighlighter.prototype = {
     const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask";
     let maskNode = document.createElement("div");
 
     // Make sure the dimmed mask node takes the full width and height that's available.
     let {width, height} = this._getWindowDimensions(window);
     maskNode.setAttribute("id", kMaskId);
     maskNode.setAttribute("class", kMaskId);
     maskNode.setAttribute("style", `width: ${width}px; height: ${height}px;`);
+    if (this._brightText)
+      maskNode.setAttribute("brighttext", "true");
 
     // Create a DOM node for each rectangle representing the ranges we found.
     let maskContent = [];
     const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect";
     if (this._modalHighlightRectsMap) {
       for (let rects of this._modalHighlightRectsMap.values()) {
         for (let rect of rects) {
           maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
--- a/toolkit/modules/RemoteFinder.jsm
+++ b/toolkit/modules/RemoteFinder.jsm
@@ -305,13 +305,13 @@ RemoteFinderListener.prototype = {
         this._finder.keyPress(data);
         break;
 
       case "Finder:MatchesCount":
         this._finder.requestMatchesCount(data.searchString, data.matchLimit, data.linksOnly);
         break;
 
       case "Finder:ModalHighlightChange":
-        this._finder.ModalHighlightChange(data.useModalHighlight);
+        this._finder.onModalHighlightChange(data.useModalHighlight);
         break;
     }
   }
 };
--- a/toolkit/modules/tests/browser/browser.ini
+++ b/toolkit/modules/tests/browser/browser.ini
@@ -23,16 +23,17 @@ support-files =
 
 [browser_AsyncPrefs.js]
 [browser_Battery.js]
 [browser_Deprecated.js]
 [browser_Finder.js]
 [browser_Finder_hidden_textarea.js]
 [browser_FinderHighlighter.js]
 skip-if = debug
+support-files = file_FinderSample.html
 [browser_Geometry.js]
 [browser_InlineSpellChecker.js]
 [browser_WebNavigation.js]
 [browser_WebRequest.js]
 [browser_WebRequest_cookies.js]
 [browser_WebRequest_filtering.js]
 [browser_PageMetadata.js]
 [browser_PromiseMessage.js]
--- a/toolkit/modules/tests/browser/browser_FinderHighlighter.js
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
@@ -2,16 +2,17 @@
 
 Cu.import("resource://testing-common/BrowserTestUtils.jsm", this);
 Cu.import("resource://testing-common/ContentTask.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const kPrefModalHighlight = "findbar.modalHighlight";
+const kFixtureBaseURL = "https://example.com/browser/toolkit/modules/tests/browser/";
 
 function promiseOpenFindbar(findbar) {
   findbar.onFindCommand()
   return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise;
 }
 
 function promiseFindResult(findbar, str = null) {
   let highlightFinished = false;
@@ -29,85 +30,100 @@ function promiseFindResult(findbar, str 
         }
       },
       onHighlightFinished() {
         highlightFinished = true;
         if (findFinished) {
           findbar.browser.finder.removeResultListener(listener);
           resolve();
         }
-      }
+      },
+      onMatchesCountResult: () => {}
     };
     findbar.browser.finder.addResultListener(listener);
   });
 }
 
 function promiseEnterStringIntoFindField(findbar, str) {
   let promise = promiseFindResult(findbar, str);
   for (let i = 0; i < str.length; i++) {
     let event = document.createEvent("KeyEvents");
     event.initKeyEvent("keypress", true, true, null, false, false,
                        false, false, 0, str.charCodeAt(i));
     findbar._findField.inputField.dispatchEvent(event);
   }
   return promise;
 }
 
-function promiseTestHighlighterOutput(browser, word, expectedResult) {
-  return ContentTask.spawn(browser, { word, expectedResult }, function* ({ word, expectedResult }) {
+function promiseTestHighlighterOutput(browser, word, expectedResult, extraTest = () => {}) {
+  return ContentTask.spawn(browser, { word, expectedResult, extraTest: extraTest.toSource() },
+    function* ({ word, expectedResult, extraTest }) {
     let document = content.document;
 
     return new Promise((resolve, reject) => {
-      let stubbed = [document.insertAnonymousContent,
-        document.removeAnonymousContent];
+      let stubbed = {};
       let callCounts = {
         insertCalls: [],
         removeCalls: []
       };
 
       // Amount of milliseconds to wait after the last time one of our stubs
       // was called.
       const kTimeoutMs = 1000;
       // The initial timeout may wait for a while for results to come in.
-      let timeout = content.setTimeout(finish, kTimeoutMs * 4);
+      let timeout = content.setTimeout(() => finish(false, "Timeout"), kTimeoutMs * 5);
 
-      function finish(ok = true, message) {
+      function finish(ok = true, message = "finished with error") {
         // Restore the functions we stubbed out.
-        document.insertAnonymousContent = stubbed[0];
-        document.removeAnonymousContent = stubbed[1];
+        document.insertAnonymousContent = stubbed.insert;
+        document.removeAnonymousContent = stubbed.remove;
+        stubbed = {};
         content.clearTimeout(timeout);
 
-        Assert.equal(callCounts.insertCalls.length, expectedResult.insertCalls,
-          `Insert calls should match for '${word}'.`);
-        Assert.equal(callCounts.removeCalls.length, expectedResult.removeCalls,
-          `Remove calls should match for '${word}'.`);
+        if (expectedResult.rectCount !== 0)
+          Assert.ok(ok, message);
+
+        Assert.greaterOrEqual(callCounts.insertCalls.length, expectedResult.insertCalls[0],
+          `Min. insert calls should match for '${word}'.`);
+        Assert.lessOrEqual(callCounts.insertCalls.length, expectedResult.insertCalls[1],
+          `Max. insert calls should match for '${word}'.`);
+        Assert.greaterOrEqual(callCounts.removeCalls.length, expectedResult.removeCalls[0],
+          `Min. remove calls should match for '${word}'.`);
+        Assert.lessOrEqual(callCounts.removeCalls.length, expectedResult.removeCalls[1],
+          `Max. remove calls should match for '${word}'.`);
 
         // We reached the amount of calls we expected, so now we can check
         // the amount of rects.
         let lastMaskNode = callCounts.insertCalls.pop();
         if (!lastMaskNode && expectedResult.rectCount !== 0) {
           Assert.ok(false, `No mask node found, but expected ${expectedResult.rectCount} rects.`);
         }
+
         if (lastMaskNode) {
           Assert.equal(lastMaskNode.getElementsByTagName("div").length,
             expectedResult.rectCount, `Amount of inserted rects should match for '${word}'.`);
         }
 
+        // Allow more specific assertions to be tested in `extraTest`.
+        extraTest = eval(extraTest);
+        extraTest(lastMaskNode);
+
         resolve();
       }
 
       // Create a function that will stub the original version and collects
       // the arguments so we can check the results later.
       function stub(which) {
+        stubbed[which] = document[which + "AnonymousContent"];
         let prop = which + "Calls";
         return function(node) {
           callCounts[prop].push(node);
           content.clearTimeout(timeout);
           timeout = content.setTimeout(finish, kTimeoutMs);
-          return node;
+          return stubbed[which].call(document, node);
         };
       }
       document.insertAnonymousContent = stub("insert");
       document.removeAnonymousContent = stub("remove");
     });
   });
 }
 
@@ -116,84 +132,141 @@ add_task(function* setup() {
     ["findbar.highlightAll", true],
     ["findbar.modalHighlight", true]
   ]});
 });
 
 // Test the results of modal highlighting, which is on by default.
 add_task(function* testModalResults() {
   let tests = new Map([
-    ["mo", {
-      rectCount: 4,
-      insertCalls: 2,
-      removeCalls: AppConstants.platform == "linux" ? 1 : 2
+    ["Roland", {
+      rectCount: 2,
+      insertCalls: [2, 4],
+      removeCalls: [1, 2]
     }],
-    ["m", {
-      rectCount: 8,
-      insertCalls: 1,
-      removeCalls: 1
+    ["ro", {
+      rectCount: 41,
+      insertCalls: [1, 2],
+      removeCalls: [1, 2]
     }],
     ["new", {
-      rectCount: 1,
-      insertCalls: 1,
-      removeCalls: 1
+      rectCount: 2,
+      insertCalls: [1, 2],
+      removeCalls: [1, 2]
     }],
     ["o", {
-      rectCount: 1217,
-      insertCalls: 1,
-      removeCalls: 1
+      rectCount: 492,
+      insertCalls: [1, 2],
+      removeCalls: [1, 2]
     }]
   ]);
-  yield BrowserTestUtils.withNewTab("about:mozilla", function* (browser) {
-    // We're inserting 1200 additional o's at the end of the document.
-    yield ContentTask.spawn(browser, null, function* () {
-      let document = content.document;
-      document.getElementsByTagName("section")[0].innerHTML += "<p>" +
-        (new Array(1200).join(" o ")) + "</p>";
-    });
-
+  let url = kFixtureBaseURL + "file_FinderSample.html";
+  yield BrowserTestUtils.withNewTab(url, function* (browser) {
     let findbar = gBrowser.getFindBar();
 
     for (let [word, expectedResult] of tests) {
       yield promiseOpenFindbar(findbar);
       Assert.ok(!findbar.hidden, "Findbar should be open now.");
 
       let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
       yield promiseEnterStringIntoFindField(findbar, word);
       yield promise;
 
-      findbar.close();
+      findbar.close(true);
     }
   });
 });
 
 // Test if runtime switching of highlight modes between modal and non-modal works
 // as expected.
 add_task(function* testModalSwitching() {
-  yield BrowserTestUtils.withNewTab("about:mozilla", function* (browser) {
+  let url = kFixtureBaseURL + "file_FinderSample.html";
+  yield BrowserTestUtils.withNewTab(url, function* (browser) {
     let findbar = gBrowser.getFindBar();
 
     yield promiseOpenFindbar(findbar);
     Assert.ok(!findbar.hidden, "Findbar should be open now.");
 
-    let word = "mo";
+    let word = "Roland";
     let expectedResult = {
-      rectCount: 4,
-      insertCalls: 2,
-      removeCalls: AppConstants.platform == "linux" ? 1 : 2
+      rectCount: 2,
+      insertCalls: [2, 4],
+      removeCalls: [1, 2]
     };
     let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
     yield promiseEnterStringIntoFindField(findbar, word);
     yield promise;
 
     yield SpecialPowers.pushPrefEnv({ "set": [[ kPrefModalHighlight, false ]] });
 
     expectedResult = {
       rectCount: 0,
-      insertCalls: 0,
-      removeCalls: 0
+      insertCalls: [0, 0],
+      removeCalls: [0, 0]
     };
     promise = promiseTestHighlighterOutput(browser, word, expectedResult);
     findbar.clear();
     yield promiseEnterStringIntoFindField(findbar, word);
     yield promise;
+
+    findbar.close(true);
+  });
+
+  yield SpecialPowers.pushPrefEnv({ "set": [[ kPrefModalHighlight, true ]] });
+});
+
+// Test if highlighting a dark page is detected properly.
+add_task(function* testDarkPageDetection() {
+  let url = kFixtureBaseURL + "file_FinderSample.html";
+  yield BrowserTestUtils.withNewTab(url, function* (browser) {
+    let findbar = gBrowser.getFindBar();
+
+    yield promiseOpenFindbar(findbar);
+
+    let word = "Roland";
+    let expectedResult = {
+      rectCount: 2,
+      insertCalls: [2, 4],
+      removeCalls: [1, 2]
+    };
+    let promise = promiseTestHighlighterOutput(browser, word, expectedResult, function(node) {
+      Assert.ok(!node.hasAttribute("brighttext"), "White HTML page shouldn't have 'brighttext' set");
+    });
+    yield promiseEnterStringIntoFindField(findbar, word);
+    yield promise;
+
+    findbar.close(true);
+  });
+
+  yield BrowserTestUtils.withNewTab(url, function* (browser) {
+    let findbar = gBrowser.getFindBar();
+
+    yield promiseOpenFindbar(findbar);
+
+    let word = "Roland";
+    let expectedResult = {
+      rectCount: 2,
+      insertCalls: [2, 4],
+      removeCalls: [1, 2]
+    };
+
+    yield ContentTask.spawn(browser, null, function* () {
+      let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIDOMWindowUtils);
+      let uri = "data:text/css;charset=utf-8," + encodeURIComponent(`
+        body {
+          background: maroon radial-gradient(circle, #a01010 0%, #800000 80%) center center / cover no-repeat;
+          color: white;
+        }`);
+      try {
+        dwu.loadSheetUsingURIString(uri, dwu.USER_SHEET);
+      } catch (e) {}
+    });
+
+    let promise = promiseTestHighlighterOutput(browser, word, expectedResult, node => {
+      Assert.ok(node.hasAttribute("brighttext"), "Dark HTML page should have 'brighttext' set");
+    });
+    yield promiseEnterStringIntoFindField(findbar, word);
+    yield promise;
+
+    findbar.close(true);
   });
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_FinderSample.html
@@ -0,0 +1,824 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Childe Roland</title>
+</head>
+<body>
+<h1>"Childe Roland to the Dark Tower Came"</h1><h5>Robert Browning</h5>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>I.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>My first thought was, he lied in every word,
+<dl>
+<dd>That hoary cripple, with malicious eye</dd>
+<dd>Askance to watch the working of his lie</dd>
+</dl>
+</dd>
+<dd>On mine, and mouth scarce able to afford</dd>
+<dd>Suppression of the glee that pursed and scored
+<dl>
+<dd>Its edge, at one more victim gained thereby.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>II.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>What else should he be set for, with his staff?
+<dl>
+<dd>What, save to waylay with his lies, ensnare</dd>
+<dd>All travellers who might find him posted there,</dd>
+</dl>
+</dd>
+<dd>And ask the road? I guessed what skull-like laugh</dd>
+<dd>Would break, what crutch 'gin write my epitaph
+<dl>
+<dd>For pastime in the dusty thoroughfare,</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>III.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>If at his counsel I should turn aside
+<dl>
+<dd>Into that ominous tract which, all agree,</dd>
+<dd>Hides the Dark Tower. Yet acquiescingly</dd>
+</dl>
+</dd>
+<dd>I did turn as he pointed: neither pride</dd>
+<dd>Nor hope rekindling at the end descried,
+<dl>
+<dd>So much as gladness that some end might be.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>IV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For, what with my whole world-wide wandering,
+<dl>
+<dd>What with my search drawn out thro' years, my hope</dd>
+<dd>Dwindled into a ghost not fit to cope</dd>
+</dl>
+</dd>
+<dd>With that obstreperous joy success would bring,</dd>
+<dd>I hardly tried now to rebuke the spring
+<dl>
+<dd>My heart made, finding failure in its scope.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>V.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>As when a sick man very near to death
+<dl>
+<dd>Seems dead indeed, and feels begin and end</dd>
+<dd>The tears and takes the farewell of each friend,</dd>
+</dl>
+</dd>
+<dd>And hears one bid the other go, draw breath</dd>
+<dd>Freelier outside ("since all is o'er," he saith,
+<dl>
+<dd>"And the blow fallen no grieving can amend;")</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>While some discuss if near the other graves
+<dl>
+<dd>Be room enough for this, and when a day</dd>
+<dd>Suits best for carrying the corpse away,</dd>
+</dl>
+</dd>
+<dd>With care about the banners, scarves and staves:</dd>
+<dd>And still the man hears all, and only craves
+<dl>
+<dd>He may not shame such tender love and stay.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Thus, I had so long suffered in this quest,
+<dl>
+<dd>Heard failure prophesied so oft, been writ</dd>
+<dd>So many times among "The Band" - to wit,</dd>
+</dl>
+</dd>
+<dd>The knights who to the Dark Tower's search addressed</dd>
+<dd>Their steps - that just to fail as they, seemed best,
+<dl>
+<dd>And all the doubt was now—should I be fit?</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>VIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So, quiet as despair, I turned from him,
+<dl>
+<dd>That hateful cripple, out of his highway</dd>
+<dd>Into the path he pointed. All the day</dd>
+</dl>
+</dd>
+<dd>Had been a dreary one at best, and dim</dd>
+<dd>Was settling to its close, yet shot one grim
+<dl>
+<dd>Red leer to see the plain catch its estray.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>IX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For mark! no sooner was I fairly found
+<dl>
+<dd>Pledged to the plain, after a pace or two,</dd>
+<dd>Than, pausing to throw backward a last view</dd>
+</dl>
+</dd>
+<dd>O'er the safe road, 'twas gone; grey plain all round:</dd>
+<dd>Nothing but plain to the horizon's bound.
+<dl>
+<dd>I might go on; nought else remained to do.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>X.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So, on I went. I think I never saw
+<dl>
+<dd>Such starved ignoble nature; nothing throve:</dd>
+<dd>For flowers - as well expect a cedar grove!</dd>
+</dl>
+</dd>
+<dd>But cockle, spurge, according to their law</dd>
+<dd>Might propagate their kind, with none to awe,
+<dl>
+<dd>You'd think; a burr had been a treasure trove.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>No! penury, inertness and grimace,
+<dl>
+<dd>In some strange sort, were the land's portion. "See</dd>
+<dd>Or shut your eyes," said Nature peevishly,</dd>
+</dl>
+</dd>
+<dd>"It nothing skills: I cannot help my case:</dd>
+<dd>'Tis the Last Judgment's fire must cure this place,
+<dl>
+<dd>Calcine its clods and set my prisoners free."</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>If there pushed any ragged thistle-stalk
+<dl>
+<dd>Above its mates, the head was chopped; the bents</dd>
+<dd>Were jealous else. What made those holes and rents</dd>
+</dl>
+</dd>
+<dd>In the dock's harsh swarth leaves, bruised as to baulk</dd>
+<dd>All hope of greenness? 'tis a brute must walk
+<dl>
+<dd>Pashing their life out, with a brute's intents.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>As for the grass, it grew as scant as hair
+<dl>
+<dd>In leprosy; thin dry blades pricked the mud</dd>
+<dd>Which underneath looked kneaded up with blood.</dd>
+</dl>
+</dd>
+<dd>One stiff blind horse, his every bone a-stare,</dd>
+<dd>Stood stupefied, however he came there:
+<dl>
+<dd>Thrust out past service from the devil's stud!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Alive? he might be dead for aught I know,
+<dl>
+<dd>With that red gaunt and colloped neck a-strain,</dd>
+<dd>And shut eyes underneath the rusty mane;</dd>
+</dl>
+</dd>
+<dd>Seldom went such grotesqueness with such woe;</dd>
+<dd>I never saw a brute I hated so;
+<dl>
+<dd>He must be wicked to deserve such pain.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>I shut my eyes and turned them on my heart.
+<dl>
+<dd>As a man calls for wine before he fights,</dd>
+<dd>I asked one draught of earlier, happier sights,</dd>
+</dl>
+</dd>
+<dd>Ere fitly I could hope to play my part.</dd>
+<dd>Think first, fight afterwards - the soldier's art:
+<dl>
+<dd>One taste of the old time sets all to rights.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not it! I fancied Cuthbert's reddening face
+<dl>
+<dd>Beneath its garniture of curly gold,</dd>
+<dd>Dear fellow, till I almost felt him fold</dd>
+</dl>
+</dd>
+<dd>An arm in mine to fix me to the place</dd>
+<dd>That way he used. Alas, one night's disgrace!
+<dl>
+<dd>Out went my heart's new fire and left it cold.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Giles then, the soul of honour - there he stands
+<dl>
+<dd>Frank as ten years ago when knighted first.</dd>
+<dd>What honest men should dare (he said) he durst.</dd>
+</dl>
+</dd>
+<dd>Good - but the scene shifts - faugh! what hangman hands</dd>
+<dd>Pin to his breast a parchment? His own bands
+<dl>
+<dd>Read it. Poor traitor, spit upon and curst!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XVIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Better this present than a past like that;
+<dl>
+<dd>Back therefore to my darkening path again!</dd>
+<dd>No sound, no sight as far as eye could strain.</dd>
+</dl>
+</dd>
+<dd>Will the night send a howlet or a bat?</dd>
+<dd>I asked: when something on the dismal flat
+<dl>
+<dd>Came to arrest my thoughts and change their train.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XIX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>A sudden little river crossed my path
+<dl>
+<dd>As unexpected as a serpent comes.</dd>
+<dd>No sluggish tide congenial to the glooms;</dd>
+</dl>
+</dd>
+<dd>This, as it frothed by, might have been a bath</dd>
+<dd>For the fiend's glowing hoof - to see the wrath
+<dl>
+<dd>Of its black eddy bespate with flakes and spumes.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>So petty yet so spiteful! All along
+<dl>
+<dd>Low scrubby alders kneeled down over it;</dd>
+<dd>Drenched willows flung them headlong in a fit</dd>
+</dl>
+</dd>
+<dd>Of mute despair, a suicidal throng:</dd>
+<dd>The river which had done them all the wrong,
+<dl>
+<dd>Whate'er that was, rolled by, deterred no whit.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Which, while I forded, - good saints, how I feared
+<dl>
+<dd>To set my foot upon a dead man's cheek,</dd>
+<dd>Each step, or feel the spear I thrust to seek</dd>
+</dl>
+</dd>
+<dd>For hollows, tangled in his hair or beard!</dd>
+<dd>—It may have been a water-rat I speared,
+<dl>
+<dd>But, ugh! it sounded like a baby's shriek.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Glad was I when I reached the other bank.
+<dl>
+<dd>Now for a better country. Vain presage!</dd>
+<dd>Who were the strugglers, what war did they wage,</dd>
+</dl>
+</dd>
+<dd>Whose savage trample thus could pad the dank</dd>
+<dd>Soil to a plash? Toads in a poisoned tank,
+<dl>
+<dd>Or wild cats in a red-hot iron cage—</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>The fight must so have seemed in that fell cirque.
+<dl>
+<dd>What penned them there, with all the plain to choose?</dd>
+<dd>No foot-print leading to that horrid mews,</dd>
+</dl>
+</dd>
+<dd>None out of it. Mad brewage set to work</dd>
+<dd>Their brains, no doubt, like galley-slaves the Turk
+<dl>
+<dd>Pits for his pastime, Christians against Jews.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>And more than that - a furlong on - why, there!
+<dl>
+<dd>What bad use was that engine for, that wheel,</dd>
+<dd>Or brake, not wheel - that harrow fit to reel</dd>
+</dl>
+</dd>
+<dd>Men's bodies out like silk? with all the air</dd>
+<dd>Of Tophet's tool, on earth left unaware,
+<dl>
+<dd>Or brought to sharpen its rusty teeth of steel.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Then came a bit of stubbed ground, once a wood,
+<dl>
+<dd>Next a marsh, it would seem, and now mere earth</dd>
+<dd>Desperate and done with; (so a fool finds mirth,</dd>
+</dl>
+</dd>
+<dd>Makes a thing and then mars it, till his mood</dd>
+<dd>Changes and off he goes!) within a rood—
+<dl>
+<dd>Bog, clay and rubble, sand and stark black dearth.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Now blotches rankling, coloured gay and grim,
+<dl>
+<dd>Now patches where some leanness of the soil's</dd>
+<dd>Broke into moss or substances like boils;</dd>
+</dl>
+</dd>
+<dd>Then came some palsied oak, a cleft in him</dd>
+<dd>Like a distorted mouth that splits its rim
+<dl>
+<dd>Gaping at death, and dies while it recoils.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>And just as far as ever from the end!
+<dl>
+<dd>Nought in the distance but the evening, nought</dd>
+<dd>To point my footstep further! At the thought,</dd>
+</dl>
+</dd>
+<dd>A great black bird, Apollyon's bosom-friend,</dd>
+<dd>Sailed past, nor beat his wide wing dragon-penned
+<dl>
+<dd>That brushed my cap—perchance the guide I sought.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXVIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>For, looking up, aware I somehow grew,
+<dl>
+<dd>'Spite of the dusk, the plain had given place</dd>
+<dd>All round to mountains - with such name to grace</dd>
+</dl>
+</dd>
+<dd>Mere ugly heights and heaps now stolen in view.</dd>
+<dd>How thus they had surprised me, - solve it, you!
+<dl>
+<dd>How to get from them was no clearer case.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXIX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Yet half I seemed to recognise some trick
+<dl>
+<dd>Of mischief happened to me, God knows when—</dd>
+<dd>In a bad dream perhaps. Here ended, then,</dd>
+</dl>
+</dd>
+<dd>Progress this way. When, in the very nick</dd>
+<dd>Of giving up, one time more, came a click
+<dl>
+<dd>As when a trap shuts - you're inside the den!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXX.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Burningly it came on me all at once,
+<dl>
+<dd>This was the place! those two hills on the right,</dd>
+<dd>Crouched like two bulls locked horn in horn in fight;</dd>
+</dl>
+</dd>
+<dd>While to the left, a tall scalped mountain... Dunce,</dd>
+<dd>Dotard, a-dozing at the very nonce,
+<dl>
+<dd>After a life spent training for the sight!</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXI.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>What in the midst lay but the Tower itself?
+<dl>
+<dd>The round squat turret, blind as the fool's heart</dd>
+<dd>Built of brown stone, without a counterpart</dd>
+</dl>
+</dd>
+<dd>In the whole world. The tempest's mocking elf</dd>
+<dd>Points to the shipman thus the unseen shelf
+<dl>
+<dd>He strikes on, only when the timbers start.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not see? because of night perhaps? - why, day
+<dl>
+<dd>Came back again for that! before it left,</dd>
+<dd>The dying sunset kindled through a cleft:</dd>
+</dl>
+</dd>
+<dd>The hills, like giants at a hunting, lay</dd>
+<dd>Chin upon hand, to see the game at bay,—
+<dl>
+<dd>"Now stab and end the creature - to the heft!"</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXIII.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>Not hear? when noise was everywhere! it tolled
+<dl>
+<dd>Increasing like a bell. Names in my ears</dd>
+<dd>Of all the lost adventurers my peers,—</dd>
+</dl>
+</dd>
+<dd>How such a one was strong, and such was bold,</dd>
+<dd>And such was fortunate, yet each of old
+<dl>
+<dd>Lost, lost! one moment knelled the woe of years.</dd>
+</dl>
+</dd>
+</dl>
+<p><br /></p>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>XXXIV.</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+<dd>There they stood, ranged along the hillsides, met
+<dl>
+<dd>To view the last of me, a living frame</dd>
+<dd>For one more picture! in a sheet of flame</dd>
+</dl>
+</dd>
+<dd>I saw them and I knew them all. And yet</dd>
+<dd>Dauntless the slug-horn to my lips I set,
+<dl>
+<dd>And blew "<i>Childe Roland to the Dark Tower came.</i>"</dd>
+</dl>
+</dd>
+</dl>
+</body>
+</html>