Last active
December 30, 2015 21:28
-
-
Save wakaba/7887273 to your computer and use it in GitHub Desktop.
HTML outline bookmarklet
http://www.whatwg.org/specs/web-apps/current-work/#headings-and-sections
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
javascript: | |
/* You are granted a license to use, reproduce and create derivative works of this document. */ | |
function walk (root, enter, exit) { | |
var node = root; | |
start: while (node) { | |
enter(node); | |
if (node.firstChild) { | |
node = node.firstChild; | |
continue start; | |
} | |
while (node) { | |
exit(node); | |
if (node == root) { | |
node = null; | |
} else if (node.nextSibling) { | |
node = node.nextSibling; | |
continue start; | |
} else { | |
node = node.parentNode; | |
} | |
} | |
} | |
} | |
var HTML_NS = 'http://www.w3.org/1999/xhtml'; | |
function Outline (el) { | |
this.element = el; | |
this.sections = []; | |
} | |
Outline.prototype.getLastSection = function () { | |
return this.lastSection; | |
}; | |
Outline.prototype.appendSection = function (sect) { | |
this.sections.push (sect); | |
this.lastSection = sect; | |
}; | |
function Section (el) { | |
this.element = el; // or null | |
this.heading = null; | |
this.sections = []; | |
} | |
Section.prototype.appendSection = function (sect) { | |
this.sections.push (sect); | |
sect.parentSection = this; | |
this.lastSection = sect; | |
}; | |
Section.prototype.getLastSection = function () { | |
return this.lastSection; | |
}; | |
Section.prototype.appendOutline = function (o) { | |
this.sections.push (o); | |
/* | |
for (var i = 0; i < o.length; i++) { | |
o[i].parentSection = this; | |
} | |
*/ | |
}; | |
function getRank (el) { | |
if (!el || !el.localName) return 0; | |
if (el.namespaceURI === HTML_NS && /^h[1-6]$/.test (el.localName)) { | |
return 7 - parseInt (el.localName.substring (1, 2)); | |
} else { | |
if (el.getElementsByTagName ('h1')[0]) return 6; | |
if (el.getElementsByTagName ('h2')[0]) return 5; | |
if (el.getElementsByTagName ('h3')[0]) return 4; | |
if (el.getElementsByTagName ('h4')[0]) return 3; | |
if (el.getElementsByTagName ('h5')[0]) return 2; | |
if (el.getElementsByTagName ('h6')[0]) return 1; | |
return 6; | |
} | |
} // getRank | |
function isSectioningContent (el) { | |
return (el.namespaceURI === HTML_NS && /^(?:article|aside|nav|section)$/.test(el.localName)); | |
} | |
function isHeadingContent (el) { | |
return (el.namespaceURI === HTML_NS && /^(?:h[1-6]|hgroup)$/.test(el.localName)); | |
} | |
function isSectioningRoot (el) { | |
return (el.namespaceURI === HTML_NS && /^(?:blockquote|body|details|dialog|fieldset|figure|td)$/.test(el.localName)); | |
} | |
function createOutline (root) { | |
var currentOutlineTarget = null; | |
var currentSection = null; | |
var stack = []; | |
var outlines = []; | |
var getOutlineByElement = function (el) { | |
for (var i = 0; i < outlines.length; i++) { | |
if (outlines[i].element === el) return outlines[i]; | |
} | |
return null; | |
}; | |
walk (root, function (node) { | |
var stackTop = stack[stack.length - 1]; | |
if (stackTop && (isHeadingContent(stackTop) || (stackTop.namespaceURI === HTML_NS && stackTop.hasAttribute('hidden')))) { | |
// | |
} else if (node.namespaceURI === HTML_NS && node.hasAttribute ('hidden')) { | |
stack.push (node); | |
} else if (isSectioningContent (node) || isSectioningRoot (node)) { | |
if (currentOutlineTarget && !currentSection.heading) { | |
currentSection.heading = {}; | |
} | |
if (currentOutlineTarget) { | |
stack.push (currentOutlineTarget); | |
} | |
currentOutlineTarget = node; | |
currentSection = new Section (currentOutlineTarget); | |
var outline = new Outline (currentOutlineTarget); | |
outline.appendSection (currentSection); | |
outlines.push (outline); | |
} else if (isHeadingContent (node)) { | |
if (!currentSection.heading) { | |
currentSection.heading = node; | |
} else if (getRank (node) >= getRank (getOutlineByElement (currentOutlineTarget).getLastSection ().heading)) { | |
var newSection = new Section; | |
getOutlineByElement (currentOutlineTarget).appendSection (newSection); | |
currentSection = newSection; | |
currentSection.heading = node; | |
} else { | |
var candidateSection = currentSection; | |
HEADING_LOOP: while (true) { | |
if (getRank (node) < getRank (candidateSection.heading)) { | |
var newSection = new Section; | |
candidateSection.appendSection (newSection); | |
currentSection = newSection; | |
newSection.heading = node; | |
break HEADING_LOOP; | |
} | |
candidateSection = candidateSection.parentSection; | |
} // HEADING_LOOP | |
} | |
stack.push (node); | |
} else { | |
// | |
} | |
}, function (node) { | |
var stackTop = stack[stack.length - 1]; | |
if (stackTop === node) { | |
stack.pop (); | |
} else if (stackTop && (isHeadingContent (stackTop) || (stackTop.namespaceURI === HTML_NS && stackTop.hasAttribute('hidden')))) { | |
// | |
} else if (isSectioningContent (node) && stack.length) { | |
if (!currentSection.heading) { | |
currentSection.heading = {}; | |
} | |
currentOutlineTarget = stack.pop (); | |
currentSection = getOutlineByElement (currentOutlineTarget).getLastSection (); | |
currentSection.appendOutline (getOutlineByElement (node)); | |
} else if (isSectioningRoot (node) && stack.length) { | |
if (!currentSection.heading) { | |
currentSection.heading = {}; | |
} | |
currentOutlineTarget = stack.pop (); | |
currentSection = getOutlineByElement (currentOutlineTarget).getLastSection (); | |
FINDING_DEEPEST_CHILD: while (true) { | |
var nextLastSection = currentSection.getLastSection (); | |
if (!nextLastSection) break FINDING_DEEPEST_CHILD; | |
currentSection = nextLastSection; | |
} | |
} else if (isSectioningContent (node) || isSectioningRoot (node)) { | |
if (!currentSection.heading) { | |
currentSection.heading = {}; | |
} | |
} else { | |
// | |
} | |
}); | |
return outlines.filter (function (o) { return isSectioningRoot (o.element) }); | |
} // createOutline | |
var outlines = createOutline (document.body); | |
console.log (outlines); | |
showOutline (outlines[0]); | |
function showOutline (outline) { | |
var container = document.createElement ('aside'); | |
container.innerHTML = '<style scoped>.outline-list { background: white; color: black; display: block; position: relative; float: none; border: 2px blue solid; padding: 0.5em; font-size: 1rem; font-style: normal; font-weight: normal; text-align: left; z-index: 21000000; } .outline-list ul { margin-left: 2em }</style><ul class=outline-list></ul>'; | |
dumpSections (outline.sections, container.lastChild); | |
document.body.appendChild (container); | |
container.scrollIntoView (); | |
} // showOutline | |
function dumpSections (sections, parent) { | |
for (var i = 0; i < sections.length; i++) { | |
var section = sections[i]; | |
if (section instanceof Outline) { | |
dumpSections (section.sections, parent); | |
continue; | |
} | |
var sectionEl = document.createElement ('li'); | |
sectionEl.innerHTML = '<span></span>'; | |
sectionEl.firstChild.textContent = section.heading ? section.heading.textContent || '(Section)' : '(Section)'; | |
sectionEl.firstChild.sectionTargetElement = section.element || section.heading; | |
sectionEl.firstChild.title = 'Section: ' + (section.element ? section.element.nodeName : 'Implied') + "\n" | |
+ 'Heading: ' + (section.heading.nodeName || 'Implied'); | |
sectionEl.firstChild.onclick = function (ev) { ev.target.sectionTargetElement.scrollIntoView () }; | |
if (section.sections.length) { | |
var ul = document.createElement ('ul'); | |
dumpSections (section.sections, ul); | |
sectionEl.appendChild (ul); | |
} | |
parent.appendChild (sectionEl); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment