metanohi-misc-subsites/projects/totxt/totxt.js

726 lines
23 KiB
JavaScript
Executable File

/*
totxt: A program that transforms HTML elements to plain text
Copyright (C) 2009 Niels Serup
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
Version: 0.9
You can contact Niels at <ns@metanohi.name>.
Latest version is always available at <http://metanohi.org/projects/totxt/>,
along with documentation.
*/
/* To avoid overuse of global variables, all functions and actions have been
stuffed into the 'load_totxt_generator' function. */
function load_totxt_generator(action, var_a) {
/* Normally, this function will load a bunch of functions and then generate
PRE elements. This isn't the case if a special action is defined. */
function count(haystack, needle) {
// Counts needles in haystack
if (needle == undefined)
var needle = '\n'
var c, offset
offset = -1
c = 0
while (offset) {
offset = haystack.indexOf(needle, offset) + 1
c++
}
return c
}
if (action == 'show_or_hide') {
/* Used by dynamic boxes that utilize sliding functionality. To open an
article box, it is sufficient to click on it, but to close one, one
must doubleclick on it. This is partly why there are some
differences in what type of state the box is in. */
var obj = document.totxt_preElements[var_a]
if (obj.state) {
// If open, COLLAPSE
obj.state = 0
if (obj.quickdyn) {
// Don't slide
obj.innerHTML = obj.header
obj.className = obj.className.replace(obj.dynclass + '1', obj.dynclass + '0')
obj.onclick = function(){load_totxt_generator('show_or_hide', this.number)}
obj.ondblclick = undefined
}
else {
// Do slide
obj.onclick = undefined
obj.ondblclick = undefined
obj.slide_expand = 0
obj.line = undefined
load_totxt_generator('slide', var_a)
}
}
else {
// If closed, EXPAND
obj.state = 1
obj.className = obj.className.replace(obj.dynclass + '0', obj.dynclass + '1')
if (obj.quickdyn) {
// Don't slide
obj.innerHTML = obj.fulltext
obj.onclick = undefined
obj.ondblclick = function(){load_totxt_generator('show_or_hide', this.number)}
}
else {
// Do slide
obj.onclick = undefined
obj.ondblclick = undefined
obj.slide_expand = 1
obj.line = undefined
load_totxt_generator('slide', var_a)
}
}
return true
}
else if (action == 'slide') {
/* Using the setTimeout function, this action is used by 'show_or_hide'
actions to either expand or collapse a box slidingly. */
var obj, hh, th, obdl
obj = document.totxt_preElements[var_a]
hh = obj.headerheight
th = obj.fulltextheight
if (obj.line == undefined) {
if (obj.slide_expand)
obj.line = hh
else
obj.line = th
}
obj.innerHTML = obj.fulltext.split('\n', obj.line).join('\n')
// Add lines when expanding, remove lines when collapsing
if (obj.slide_expand) {
obdl = obj.line
if (obdl == th)
obdl++
else {
obdl = obj.line + obj.dynlines
if (obdl > th)
obdl = th
}
obj.line = obdl
}
else {
obdl = obj.line
if (obdl == hh)
obdl--
else {
obdl = obj.line - obj.dynlines
if (obdl < hh)
obdl = hh
}
obj.line = obdl
}
if (obj.line >= hh && obj.line <= th)
setTimeout('load_totxt_generator(\'slide\', ' + var_a + ')', obj.dynspeed)
else {
// End 'loop'
if (obj.slide_expand) {
obj.onclick = undefined
obj.ondblclick = function(){load_totxt_generator('show_or_hide', this.number)}
}
else {
obj.onclick = function(){load_totxt_generator('show_or_hide', this.number)}
obj.ondblclick = undefined
obj.className = obj.className.replace(obj.dynclass + '1', obj.dynclass + '0')
}
// Not necessary. Has no noticable effect.
delete document.slide_object
delete obj.slide_expand
delete obj.line
}
return true
}
/*
No more special actions exist. If none of the two above actions were
specified, code execution will begin here.
*/
// Various functions
function isin(ref, str, end, defau) {
// Checks for string str in array ref. If end is true, look for match in
// beginning of str and return the end of str
var x, c
for (x in ref) {
c = ref[x]
if (end) {
if (c.indexOf(str) == 0)
return c.substr(str.length)
}
else if (c == str)
return true
}
if (defau)
return defau
else
return false
}
function ctimes(cha, times) {
// <character> * times (a, 3 = aaa)
var t = ''
for (var i = 0; i < times; i++) {
t += cha
}
return t
}
function transform_text(str, w, align, del_xws) {
/* This function transforms text seen in ordinary HTML tags into text
that fits simple monospace character boxes. */
// Prepare text
str = str.replace(new RegExp('^\\s+'), '').replace(new RegExp('\\s+$'), '')
str = str.replace(/<br \/>/gi, '<br>').replace(/<br>\n/gi, '<br>')
//str = str.replace(/&nbsp;/g, ' ')
str = str.replace(/\n/g, ' ')
if (del_xws) {
// Delete excessive whitespace
str = str.replace(new RegExp('\\s+', 'g'), ' ')
}
str = str.replace(/<br>/gi, '\n')
if (del_xws) {
// Delete excessive whitespace caused by line break transformation
str = str.replace(/\n /g, '\n')
}
var tags, tlen, intag, start, i, c, x, sstr, ntxt, ctxt, txt, cw, clen
var entities, inentity, filter, tminus, l, spaces
// Store and remove HTML tags in text (will be readded in text later)
// Also look for HTML entities, such as &lt; (<)
tagent = [] // To store HTML tags and HTML entities
tlen = str.length
start = 0
intag = false
inentity = false
filter = ' <>!"\'\\/*-+,.:;'
ntxt = ''
for (i = 0; i < tlen; i++) {
c = str.substr(i, 1)
if (c == '<' && !intag) {
intag = true
start = i
}
else if (c == '>' && intag) {
intag = false
tagent[tagent.length] = ['tag', start, str.substr(start, i - start + 1)]
}
else if (!intag) {
if (c == '&' && !inentity) {
if (filter.indexOf(str.substr(i+1, 1)) == -1) {
inentity = true
start = i
}
ntxt += c
}
else if (c == ';' && inentity) {
inentity = false
tagent[tagent.length] = ['ent', start, str.substr(start, i - start + 1)]
}
else if (!inentity)
ntxt += c
}
}
str = ntxt
// Change line width of text. When inserting a hyphen and a newline, check
// if positions of HTML tags and HTML entities need to be changed.
sstr = str.split('\n')
ntxt = ''
for (ctxt in sstr) {
txt = sstr[ctxt]
while (txt) {
if (txt.length <= w) {
ntxt += txt
break
}
if (txt.substr(0, w).indexOf(' ') == -1) {
ntxt += txt.substr(0, w - 1) + '-\n'
txt = txt.substr(w - 1)
clen = ntxt.length
tminus = 0
for (x in tagent) {
if (tagent[x][1] - tminus > clen)
tagent[x][1] += 2
l = tagent[x][2].length
if (tagent[x][0] == 'ent')
l--
tminus += l
}
continue
}
cw = w
while (txt.substr(cw, 1) != ' ')
cw -= 1
ntxt += txt.substr(0, cw) + '\n'
txt = txt.substr(cw + 1)
// No need to readjust positions of HTML stuff here, as the
// new newline replaces a space character
}
ntxt += '\n'
}
while (ntxt.substr(ntxt.length - 1) == '\n')
ntxt = ntxt.substr(0, ntxt.length - 1)
str = ntxt
// Readd removed HTML tags and HTML entities
for (x in tagent) {
c = tagent[x]
if (c[0] == 'tag')
str = str.substr(0, c[1]) + c[2] + str.substr(c[1])
else if (c[0] == 'ent')
str = str.substr(0, c[1]) + c[2] + str.substr(c[1]+1)
}
// Align (0 = left, 1 = center, 2 = right)
if (align != 0 && align != 1 && align != 2)
align = 0
sstr = str.split('\n')
ntxt = ''
for (ctxt in sstr) {
txt = sstr[ctxt]
tlen = txt.length
clen = 0
intag = false
inentity = false
for (i = 0; i < tlen; i++) {
c = txt.substr(i, 1)
if (c == '<' && !intag)
intag = true
else if (c == '>' && intag)
intag = false
else if (c == '&' && !inentity) {
if (filter.indexOf(txt.substr(i+1, 1)) == -1)
inentity = true
clen++
}
else if (c == ';' && inentity)
inentity = false
else if (!intag && !inentity)
clen++
}
spaces = w - clen
ntxt += '\n'
if (align == 0) // Left
ntxt += txt + ctimes(' ', spaces)
else if (align == 2) // Right
ntxt += ctimes(' ', spaces) + txt
else if (align == 1) // Center
ntxt += ctimes(' ', Math.floor(spaces / 2)) + txt + ctimes(' ', Math.ceil(spaces / 2))
}
ntxt = ntxt.substr(1)
if (ntxt)
str = ntxt
return str
}
function add_borders(txt, border, padding) {
// Adds borders to boxes
if (border == undefined)
border = '|'
if (padding == undefined)
padding = ' '
var s, x, c, n
s = txt.split('\n')
n = ''
for (x in s) {
c = s[x]
n += '\n' + border + padding + c + padding + border
}
return n.substr(1)
}
function transform_text_in_list(arr, w, del_xws, borders, padding, align) {
// Transform, then join
var x, c, f, t
f = ''
for (x in arr) {
c = arr[x]
if (align == undefined)
align = 0 // Left
t = transform_text(c, w, align, del_xws)
if (borders)
t = add_borders(t, borders, padding)
f += '\n' + t
}
return f.substr(1)
}
// No more functions
// Begin load + search + generate
// Local variables
var h1s, x, z, y, ok, cur, classes, par, chs, cur_found, ccur,
header, subheaders, texts, elems, subheader, content,
stdclass, dynclass, widclass, xwsclass, quiclass, speclass, linclass,
stdwidth, width, stddynamic, dynamic, isdynamic, delxws,
dynspeed, c_speed, dynlines, c_lines, quickdyn, c_quidyn, alwdelxws,
stdhalign, stdshalign, stdtalign, halclass, shalclass, talclass,
c_halign, c_shalign, c_talign
var h_elmnm, sh_elmnm, txt_elmnm
var tmps, text, preElements, elmminus, prelms, plen, celm
var /*poss_classes, n_classes,*/ pid, tmp
// Default classnames for recognition purposes.
// Can be changed using global variables.
stdclass = 'totxt'
try {
stdclass = totxt_CLASSNAME ? totxt_CLASSNAME : stdclass
}
catch(e) {}
dynclass = 'dynamic'
try {
dynclass = totxt_DYNAMIC_CLASSNAME ? totxt_DYNAMIC_CLASSNAME : dynclass
}
catch(e) {}
widclass = 'width'
try {
widclass = totxt_WIDTH_CLASSNAME ? totxt_WIDTH_CLASSNAME : widclass
}
catch(e) {}
xwsclass = 'del_excess_whitespace'
try {
xwsclass = totxt_DELXWS_CLASSNAME ? totxt_DELXWS_CLASSNAME : xwsclass
}
catch(e) {}
speclass = 'speed'
try {
speclass = totxt_DYNSPEED_CLASSNAME ? totxt_DYNSPEED_CLASSNAME : speclass
}
catch(e) {}
linclass = 'lines'
try {
linclass = totxt_DYNLINES_CLASSNAME ? totxt_DYNLINES_CLASSNAME : linclass
}
catch(e) {}
quiclass = 'quick_dynamic'
try {
quiclass = totxt_QUICK_CLASSNAME ? totxt_QUICK_CLASSNAME : quiclass
}
catch(e) {}
halclass = 'halign'
try {
halclass = totxt_HALIGN_CLASSNAME ? totxt_HALIGN_CLASSNAME : halclass
}
catch(e) {}
shalclass = 'shalign'
try {
shalclass = totxt_SHALIGN_CLASSNAME ? totxt_SHALIGN_CLASSNAME : shalclass
}
catch(e) {}
talclass = 'talign'
try {
talclass = totxt_TALIGN_CLASSNAME ? totxt_TALIGN_CLASSNAME : talclass
}
catch(e) {}
h_elmnm = 'H1'
try {
h_elmnm = totxt_HEADER_ELEMENT ? totxt_HEADER_ELEMENT.toUpperCase() : h_elmnm
}
catch(e) {}
sh_elmnm = 'H2'
try {
sh_elmnm = totxt_SUBHEADER_ELEMENT ? totxt_SUBHEADER_ELEMENT.toUpperCase() : sh_elmnm
}
catch(e) {}
txt_elmnm = 'P'
try {
txt_elmnm = totxt_TEXT_ELEMENT ? totxt_TEXT_ELEMENT.toUpperCase() : txt_elmnm
}
catch(e) {}
h1s = document.getElementsByTagName(h_elmnm)
if (!h1s)
return false
// Default values
// Line width
stdwidth = 80
try {
stdwidth = totxt_WIDTH * 1 ? totxt_WIDTH : stdwidth
}
catch(e) {}
if (stdwidth < 10)
stdwidth = 10
// Should articles be allowed to collapse and expand with a mouseclick?
stddynamic = -1
try {
stddynamic = totxt_DYNAMIC
}
catch(e) {}
// Sliding speed
dynspeed = 50
try {
dynspeed = totxt_DYNAMIC_SPEED * 1
}
catch(e) {}
// Line(s) to (dis)appear per function call
dynlines = 1
try {
dynlines = totxt_DYNAMIC_LINES * 1 ? totxt_DYNAMIC_LINES * 1 : dynlines
}
catch(e) {}
// Don't slide?
quickdyn = false
try {
quickdyn = totxt_QUICK_DYNAMIC
}
catch(e) {}
// Delete excessive whitespace?
alwdelxws = false
try {
alwdelxws = totxt_DEL_XWS
}
catch(e) {}
// Header alignment
stdhalign = 1 // Center
try {
stdhalign = totxt_HEADER_ALIGN * 1
}
catch(e) {}
// Subheader alignment
stdshalign = 0 // Left
try {
stdshalign = totxt_SUBHEADER_ALIGN * 1
}
catch(e) {}
// Text alignment
stdtalign = 0 // Left
try {
stdtalign = totxt_TEXT_ALIGN * 1
}
catch(e) {}
// Checking for impossible alignments
if (stdhalign > 2) stdhalign = 2
else if (stdhalign < 0) stdhalign = 0
if (stdshalign > 2) stdshalign = 2
else if (stdshalign < 0) stdshalign = 0
if (stdtalign > 2) stdtalign = 2
else if (stdtalign < 0) stdtalign = 0
elems = [] // Elements that contain info to be used in final PRE elements.
prelms = [] // PRE elements
/*
// Special classes
poss_classes = [stdclass, dynclass, widclass, xwsclass, speclass, linclass, quiclass, halclass, shalclass, talclass]
*/
// document.totxt_preElements is the variable in which to store the PRE elements.
try {
/* document.totxt_preElements likely doesn't exist, which could result in an
error if not called inside a try statement. */
preElements = document.totxt_preElements
elmminus = preElements.length
}
catch(e) {
document.totxt_preElements = []
preElements = []
elmminus = 0
}
// Look up H1 elements
for (x in h1s) {
cur = h1s[x]
if (!cur || !cur.className)
continue
// Filters
classes = cur.className.split(' ')
if (!isin(classes, stdclass))
continue
par = cur.parentNode
if (!par)
continue
// Find H2 and P elements that are "below" the current H1 element
chs = par.childNodes
cur_found = 0
header = cur.innerHTML
subheaders = []
texts = []
elems[elems.length] = cur
for (z in chs) {
ccur = chs[z]
if (ccur == cur) {
cur_found = 1
continue
}
if (!cur_found || !ccur.tagName)
continue
if (ccur.tagName == sh_elmnm)
subheaders[subheaders.length] = ccur.innerHTML
else if (ccur.tagName == txt_elmnm)
texts[texts.length] = ccur.innerHTML
else
break
// If either H2 or P element, add object to elems var
elems[elems.length] = ccur
}
width = isin(classes, widclass, 1, stdwidth) * 1
if (width < 10)
width = stdwidth
dynamic = isin(classes, dynclass, 1, stddynamic) * 1
isdynamic = dynamic == 0 || dynamic == 1
delxws = isin(classes, xwsclass, 0, alwdelxws)
c_halign = isin(classes, halclass, 1, stdhalign) * 1
c_shalign = isin(classes, shalclass, 1, stdshalign) * 1
c_talign = isin(classes, talclass, 1, stdtalign) * 1
tmps = [' ' + ctimes('-', width - 2),
'|' + ctimes('_', width - 2) + '|',
'| ' + ctimes('~', width - 4) + ' |\n']
// Now generate the content
header = add_borders(transform_text(header, width - 4, c_halign, delxws), '|')
subheader = subheaders ? transform_text_in_list(subheaders, width - 4, delxws, '|', ' ', c_shalign) : ''
content = texts ? transform_text_in_list(texts, width - 4, delxws, '|', ' ', c_talign) : ''
header = tmps[0] + '\n' + header + '\n' + tmps[0]
text = header + '\n'
if (subheader)
text += subheader + '\n' + tmps[2]
if (content)
text += content + '\n'
text += tmps[1]
/*
// Check for special classes that have nothing to do with settings
n_classes = []
for (z in classes) {
for (y in poss_classes) {
ok = true
if (!classes[z].indexOf(poss_classes[y])) {
ok = false
break
}
}
if (ok)
n_classes[n_classes.length] = classes[z]
}
n_classes = n_classes.join(' ')
*/
// Find eventual id
pid = cur.id
delete cur.id // To avoid two elements with alike ids
plen = prelms.length
prelms[plen] = document.createElement('pre')
celm = prelms[plen]
celm.number = plen + elmminus // preElements may not be empty
celm.className = cur.className
/*
celm.className = stdclass
if (n_classes)
celm.className += ' ' + n_classes
*/
if (pid)
celm.id = pid
// Find and apply eventual styles
for (z in cur.style) {
c = cur.style[z]
if (c) {
try {
if (c.indexOf(':') != -1)
tmp = c.split(':')
celm.style.setProperty(tmp[0], tmp[1].replace(';', ''), '')
}
catch(e){}
}
}
celm.style.display = 'none'
celm.innerHTML = text
if (isdynamic) {
c_speed = isin(classes, speclass, 1, dynspeed) * 1
c_lines = isin(classes, linclass, 1, dynlines) * 1
if (!c_lines) c_lines = dynlines
c_quidyn = isin(classes, quiclass, 0, quickdyn)
celm.fulltext = text
celm.header = header
celm.headerheight = count(header)
celm.fulltextheight = count(text)
celm.state = dynamic
if (!isin(classes, dynclass))
celm.className += ' ' + dynclass
if (!isin(classes, dynclass + dynamic))
celm.className += ' ' + dynclass + dynamic
/*
if (c_quidyn)
celm.className += ' ' + quiclass
*/
celm.dynclass = dynclass
celm.dynspeed = c_speed
celm.dynlines = c_lines
celm.quickdyn = c_quidyn
if (dynamic == 0)
celm.onclick = function(){load_totxt_generator('show_or_hide', this.number)}
else
celm.ondblclick = function(){load_totxt_generator('show_or_hide', this.number)}
if (!dynamic)
celm.innerHTML = header
}
par.insertBefore(prelms[plen], cur)
}
// Remove used elements
for (x in elems) {
elems[x].parentNode.removeChild(elems[x])
}
// Show new elements
for (x in prelms) {
prelms[x].style.display = 'block'
}
for (x in prelms) {
preElements[preElements.length] = prelms[x]
}
document.totxt_preElements = preElements
return true
}