--- 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,93 @@ 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);
function finish(ok = true, message) {
// 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}'.`);
// 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,45 +125,39 @@ 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,
+ ["Roland", {
+ rectCount: 2,
+ insertCalls: 4,
removeCalls: AppConstants.platform == "linux" ? 1 : 2
}],
- ["m", {
- rectCount: 8,
- insertCalls: 1,
+ ["ro", {
+ rectCount: 41,
+ insertCalls: 2,
removeCalls: 1
}],
["new", {
- rectCount: 1,
- insertCalls: 1,
+ rectCount: 2,
+ insertCalls: 2,
removeCalls: 1
}],
["o", {
- rectCount: 1217,
- insertCalls: 1,
+ rectCount: 492,
+ insertCalls: 2,
removeCalls: 1
}]
]);
- 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);
@@ -163,26 +166,27 @@ add_task(function* testModalResults() {
findbar.close();
}
});
});
// 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,
+ rectCount: 2,
+ insertCalls: 4,
removeCalls: AppConstants.platform == "linux" ? 1 : 2
};
let promise = promiseTestHighlighterOutput(browser, word, expectedResult);
yield promiseEnterStringIntoFindField(findbar, word);
yield promise;
yield SpecialPowers.pushPrefEnv({ "set": [[ kPrefModalHighlight, false ]] });
@@ -190,10 +194,72 @@ add_task(function* testModalSwitching()
rectCount: 0,
insertCalls: 0,
removeCalls: 0
};
promise = promiseTestHighlighterOutput(browser, word, expectedResult);
findbar.clear();
yield promiseEnterStringIntoFindField(findbar, word);
yield promise;
+
+ findbar.close();
+ });
+
+ 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: 4,
+ removeCalls: AppConstants.platform == "linux" ? 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();
+ });
+
+ yield BrowserTestUtils.withNewTab(url, function* (browser) {
+ let findbar = gBrowser.getFindBar();
+
+ yield promiseOpenFindbar(findbar);
+
+ let word = "Roland";
+ let expectedResult = {
+ rectCount: 2,
+ insertCalls: 4,
+ removeCalls: AppConstants.platform == "linux" ? 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();
});
});
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/browser/file_FinderSample.html
@@ -0,0 +1,825 @@
+<!DOCTYPE html>
+<!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>