// page.js - module to generate pages
//
// Copyright (C) 2018-2024 Jean-Francois Moine
//
// This file is part of abc2svg.
//
// abc2svg is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// abc2svg 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with abc2svg. If not, see <http://www.gnu.org/licenses/>.
//
// This module is loaded when "%%pageheight" appears in a ABC source.
//
// Parameters
// %%pageheight <unit>
if (typeof abc2svg == "undefined")
var abc2svg = {}
abc2svg.page = {
// function called at end of generation
abc_end: function(of) {
if (this.cfmt().pageheight) {
var page = this.page
if (page && page.in_page)
abc2svg.page.close_page(page)
if (abc2svg.page.user_out) {
user.img_out = abc2svg.page.user_out
abc2svg.page.user_out = null
}
}
of()
}, // abc_end()
// output the SVG tag
svg_tag: function(w, h, ty) {
w = Math.ceil(w)
h = Math.ceil(h)
abc2svg.page.user_out(
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1"\n\
xmlns:xlink="http://www.w3.org/1999/xlink"\n\
class="'
+ ty + '" '
+ (user.imagesize != undefined
? (user.imagesize)
: ('width="' + w + 'px" height="' + h + 'px"')
)
+ ' viewBox="0 0 ' + w + ' ' + h + '">'
)
}, // svg_tag()
// generate a header or a footer in page.hf and return its height
gen_hf: function(page, ty) {
var a, i, j, k, x, y, y0, s, str,
font = page.abc.get_font(ty.substr(0, 6)),
cfmt = page.abc.cfmt(),
fh = font.size * 1.1,
pos = [ '">',
'" text-anchor="middle">',
'" text-anchor="end">' ]
// replace <>& by XML character references
function clean_txt(txt) {
return txt.replace(/<|>|&.*?;|&/g, function(c) {
switch (c) {
case '<': return "<"
case '>': return ">"
case '&': return "&"
}
return c
})
} // clean_txt()
// clear a field if $x said so
function clr(str) {
return str.indexOf('\u00ff') >= 0 ? '' : str
} //clr()
// create the text of a header or a footer
function header_footer(o_font, str) {
var c, d, i, k, t, n_font, s,
c_font = o_font,
nl = 1,
j = 0,
r = ["", "", ""]
if (str[0] == '"')
str = str.slice(1, -1)
while (1) {
i = str.indexOf('$', j)
if (i < 0)
break
c = str[++i]
s = '$' + c // string to replace
switch (c) {
case 'd':
if (!abc2svg.get_mtime)
break // cannot know the change time of the file
d = abc2svg.get_mtime(abc.parse.fname)
// fall thru
case 'D':
if (c == 'D')
d = new Date()
if (cfmt.dateformat[0] == '"')
cfmt.dateformat = cfmt.dateformat.slice(1, -1)
d = strftime(cfmt.dateformat, d)
break
case 'F':
d = typeof document != "undefined"
? window.location.href
: page.abc.parse.fname
break
case 'I':
c = str[++i]
s += c
// fall thru
case 'T':
t = page.abc.info()[c]
d = t ? t.split('\n', 1)[0] : ''
break
case 'P': // current page number
case 'Q': // absolute page number
t = c == 'P' ? page.pn : page.pna
switch (str[i + 1]) {
case '0':
s += '0'
d = (t & 1) ? '\u00ff' : t
break
case '1':
s += '1'
d = (t & 1) ? t : '\u00ff'
break
default:
d = t
break
}
break
case 'V':
d = "abc2svg-" + abc2svg.version
break
default:
d = ''
if (c == '0')
n_font = o_font
else if (c >= '1' && c < '9')
n_font = page.abc.get_font("u" + c)
else
break
// handle the font changes
if (n_font == c_font)
break
if (c_font != o_font)
d += "</tspan>"
c_font = n_font
if (c_font == o_font)
break
d += '<tspan class="' +
font_class(n_font) + '">'
break
}
str = str.replace(s, d)
j = i
}
if (c_font != o_font)
str += "</tspan>";
str = str.split('\n')
r[4] = str.length // number of lines
for (j = 0; j < str.length; j++) {
if (j != 0)
for (i = 0; i < 3; i++)
r[i] += '\n'
t = str[j].split('\t')
if (t.length == 1) {
r[1] += clr(t[0])
} else {
for (i = 0; i < 3; i++) {
if (t[i])
r[i] += clr(t[i])
}
}
}
return r
} // header_footer()
function font_class(font) {
if (font.class)
return 'f' + font.fid + cfmt.fullsvg + ' ' + font.class
return 'f' + font.fid + cfmt.fullsvg
}
// gen_hf
if (!(page.pn & 1))
str = page[ty + '2'] || page[ty]
else
str = page[ty]
if (str[0] == '-') { // not on 1st page
if (page.pn == 1)
return 0
str = str.slice(1)
}
a = header_footer(font, clean_txt(str))
y0 = font.size * .8
for (i = 0; i < 3; i++) {
str = a[i]
if (!str)
continue
if (i == 0)
x = cfmt.leftmargin
else if (i == 1)
x = cfmt.pagewidth / 2
else
x = cfmt.pagewidth - cfmt.rightmargin
y = y0
k = 0
while (1) {
j = str.indexOf('\n', k)
if (j >= 0)
s = str.slice(k, j)
else
s = str.slice(k)
if (s)
page.hf += '<text class="' +
font_class(font) +
'" x="' + x.toFixed(1) +
'" y="' + y.toFixed(1) +
pos[i] +
s + '</text>\n'
if (j < 0)
break
k = j + 1
y += fh
}
}
return fh * a[4]
}, // gen_hf()
// start a new page
open_page: function(page,
ht) { // spacing under the header
var h,
abc = page.abc,
cfmt = abc.cfmt(),
sty = '<div style="line-height:0'
page.pn++
page.pna++
// start a new page
if (page.first)
page.first = false
else
sty += ";page-break-before:always"
if (page.gutter)
sty += ";margin-left:" +
((page.pn & 1) ? page.gutter : -page.gutter).toFixed(1) + "px"
abc2svg.page.user_out(sty + '">')
page.in_page = true
ht += page.topmargin
page.hmax = cfmt.pageheight - page.botmargin - ht
// define the header/footer
page.hf = ''
if (page.header) {
abc.clr_sty()
if (!cfmt.headerfont)
abc.param_set_font("headerfont", "text,serif 16")
h = abc2svg.page.gen_hf(page, "header")
if (!h && page.pn == 1 && page.header1)
h = abc2svg.page.gen_hf(page, "header1")
sty = abc.get_font_style() // new style(s)
if (cfmt.fullsvg || sty != page.hsty) {
page.hsty = sty
sty = '<style>' + sty + '\n</style>\n'
} else {
sty = ''
}
abc2svg.page.svg_tag(cfmt.pagewidth, ht + h, "header")
abc2svg.page.user_out(sty +
'<g transform="translate(0,' +
page.topmargin.toFixed(1) + ')">\n' +
page.hf + '</g>\n</svg>')
page.hmax -= h;
page.hf = ''
} else {
abc2svg.page.svg_tag(cfmt.pagewidth, ht, "header")
abc2svg.page.user_out('\n</svg>')
}
if (page.footer) {
abc.clr_sty()
if (!cfmt.footerfont)
abc.param_set_font("footerfont", "text,serif 16")
page.fh = abc2svg.page.gen_hf(page, "footer")
sty = abc.get_font_style() // new style(s)
if (cfmt.fullsvg || sty != page.fsty) {
page.fsty = sty
page.ffsty = '<style>' + sty + '\n</style>\n'
} else {
page.ffsty = ''
}
page.hmax -= page.fh
}
page.h = 0
}, // open_page()
close_page: function(page) {
var h,
cfmt = page.abc.cfmt()
page.in_page = false
if (page.footer) {
h = page.hmax + page.fh - page.h
abc2svg.page.svg_tag(cfmt.pagewidth, h, "footer")
abc2svg.page.user_out(page.ffsty +
'<g transform="translate(0,' +
(h - page.fh).toFixed(1) + ')">\n' +
page.hf + '</g>\n</svg>')
}
abc2svg.page.user_out('</div>')
page.h = 0
}, // close_page()
// handle the output flow of the abc2svg generator
img_in: function(p) {
var h, ht, nh,
page = this.page
// copy a block
function blkcpy(page) {
while (page.blk.length)
abc2svg.page.user_out(page.blk.shift())
page.blk = null // direct output
} // blkcpy()
// img_in()
switch (p.slice(0, 4)) {
case "<div": // block of new tune
if (p.indexOf('newpage') > 0
|| (page.oneperpage && this.info().X)
|| !page.h) { // empty page
if (page.in_page)
abc2svg.page.close_page(page)
abc2svg.page.open_page(page, 0)
}
page.blk = [] // in block
page.hb = page.h // keep the offset of the start of tune
break
case "<svg": // SVG image
h = Number(p.match(/viewBox="0 0 \d+ (\d+)"/)[1])
while (h + page.h >= page.hmax) { // if (still) page overflow
ht = page.blk ? 0 :
this.cfmt().topspace // tune continuation
if (page.blk) {
if (!page.hb) { // overflow on the first page
blkcpy(page)
nh = 0
} else {
nh = page.h - page.hb
page.h = page.hb
}
}
abc2svg.page.close_page(page)
abc2svg.page.open_page(page, ht)
if (page.blk) { // if inside a block
blkcpy(page) // output the beginning of the tune
page.h = nh
}
if (h > page.hmax)
break // error
}
// if no overflow yet, keep the block
if (page.blk)
page.blk.push(p)
else
abc2svg.page.user_out(p)
page.h += h
break
case "</di": // end of block
if (page.blk)
blkcpy(page)
break
// default:
////fixme: %%beginml cannot be treated (no information about its height)
// break
}
}, // img_in()
// handle the page related parameters
set_fmt: function(of, cmd, parm) {
var v,
cfmt = this.cfmt(),
page = this.page
if (cmd == "pageheight") {
v = this.get_unit(parm)
if (isNaN(v)) {
this.syntax(1, this.errs.bad_val, '%%' + cmd)
return
}
if (!user.img_out || !abc2svg.abc_end)
v = 0
cfmt.pageheight = v
if (!v) {
if (abc2svg.page.user_out) {
user.img_out = abc2svg.page.user_out
abc2svg.page.user_out = null
}
delete this.page
return
}
// if first definition, install the hook
if (!page || !abc2svg.page.user_out) {
this.page = page = {
abc: this,
topmargin: 38, // 1cm
botmargin: 38, // 1cm
// gutter: 0,
h: 0, // current page height
pn: 0, // page number
pna: 0, // absolute page number
ffsty: '', // style of the footer
first: true // no skip to next page
}
// don't let the backend handle the header/footer
if (cfmt.header) {
page.header = cfmt.header;
cfmt.header = null
}
if (cfmt.footer) {
page.footer = cfmt.footer;
cfmt.footer = null
}
if (cfmt.header1) {
page.header1 = cfmt.header1
cfmt.header1 = null
}
if (cfmt.header2) {
page.header2 = cfmt.header2
cfmt.header2 = null
}
if (cfmt.footer2) {
page.footer2 = cfmt.footer2
cfmt.footer2 = null
}
// get the previously defined page parameters
if (cfmt.botmargin != undefined) {
v = this.get_unit(cfmt.botmargin)
if (!isNaN(v))
page.botmargin = v
}
if (cfmt.topmargin != undefined) {
v = this.get_unit(cfmt.topmargin)
if (!isNaN(v))
page.topmargin = v
}
if (cfmt.gutter != undefined) {
v = this.get_unit(cfmt.gutter)
if (!isNaN(v))
page.gutter = v
}
if (cfmt.oneperpage)
page.oneperpage = this.get_bool(cfmt.oneperpage)
if (!cfmt.dateformat)
cfmt.dateformat = "%b %e, %Y %H:%M"
// set the hooks
if (!abc2svg.page.user_out)
abc2svg.page.user_out = user.img_out
user.img_out = abc2svg.page.img_in.bind(this);
abc2svg.abc_end = abc2svg.page.abc_end.bind(this,
abc2svg.abc_end)
}
return
}
if (page) {
switch (cmd) {
case "header":
case "footer":
case "header1":
case "header2":
case "footer2":
page[cmd] = parm
return
case "newpage":
if (!parm)
break
v = Number(parm)
if (isNaN(v)) {
this.syntax(1, this.errs.bad_val, '%%' + cmd)
return
}
page.pn = v - 1
return
case "gutter":
case "botmargin":
case "topmargin":
v = this.get_unit(parm)
if (isNaN(v)) {
this.syntax(1, this.errs.bad_val, '%%' + cmd)
return
}
page[cmd] = v
return
case "oneperpage":
page[cmd] = this.get_bool(parm)
return
}
}
of(cmd, parm)
}, // set_fmt()
set_hooks: function(abc) {
abc.set_format = abc2svg.page.set_fmt.bind(abc, abc.set_format)
user.page_format = true // do page formatting
abc.set_pagef()
}
} // page
if (!abc2svg.mhooks)
abc2svg.mhooks = {}
abc2svg.mhooks.page = abc2svg.page.set_hooks
/* Port of strftime() by T. H. Doan (https://thdoan.github.io/strftime/)
*
* Day of year (%j) code based on Joe Orost's answer:
* http://stackoverflow.com/questions/8619879/javascript-calculate-the-day-of-the-year-1-366
*
* Week number (%V) code based on Taco van den Broek's prototype:
* http://techblog.procurios.nl/k/news/view/33796/14863/calculate-iso-8601-week-and-year-in-javascript.html
*/
// Copyright (c) 2016 Tom Doan - License MIT
function strftime(sFormat, date) {
if (!(date instanceof Date)) date = new Date();
var nDay = date.getDay(),
nDate = date.getDate(),
nMonth = date.getMonth(),
nYear = date.getFullYear(),
nHour = date.getHours(),
aDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
aMonths = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
aDayCount = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],
isLeapYear = function() {
return (nYear%4===0 && nYear%100!==0) || nYear%400===0;
},
getThursday = function() {
var target = new Date(date);
target.setDate(nDate - ((nDay+6)%7) + 3);
return target;
},
zeroPad = function(nNum, nPad) {
return ((Math.pow(10, nPad) + nNum) + '').slice(1);
};
return sFormat.replace(/%[a-z]/gi, function(sMatch) {
return (({
'%a': aDays[nDay].slice(0,3),
'%A': aDays[nDay],
'%b': aMonths[nMonth].slice(0,3),
'%B': aMonths[nMonth],
'%c': date.toUTCString(),
'%C': Math.floor(nYear/100),
'%d': zeroPad(nDate, 2),
'%e': nDate,
'%F': date.toISOString().slice(0,10),
'%G': getThursday().getFullYear(),
'%g': (getThursday().getFullYear() + '').slice(2),
'%H': zeroPad(nHour, 2),
'%I': zeroPad((nHour+11)%12 + 1, 2),
'%j': zeroPad(aDayCount[nMonth] + nDate + ((nMonth>1 && isLeapYear()) ? 1 : 0), 3),
'%k': nHour,
'%l': (nHour+11)%12 + 1,
'%m': zeroPad(nMonth + 1, 2),
'%n': nMonth + 1,
'%M': zeroPad(date.getMinutes(), 2),
'%p': (nHour<12) ? 'AM' : 'PM',
'%P': (nHour<12) ? 'am' : 'pm',
'%s': Math.round(date.getTime()/1000),
'%S': zeroPad(date.getSeconds(), 2),
'%u': nDay || 7,
'%V': (function() {
var target = getThursday(),
n1stThu = target.valueOf();
target.setMonth(0, 1);
var nJan1 = target.getDay();
if (nJan1!==4) target.setMonth(0, 1 + ((4-nJan1)+7)%7);
return zeroPad(1 + Math.ceil((n1stThu-target)/604800000), 2);
})(),
'%w': nDay,
'%x': date.toLocaleDateString(),
'%X': date.toLocaleTimeString(),
'%y': (nYear + '').slice(2),
'%Y': nYear,
'%z': date.toTimeString().replace(/.+GMT([+-]\d+).+/, '$1'),
'%Z': date.toTimeString().replace(/.+\((.+?)\)$/, '$1')
}[sMatch] || '') + '') || sMatch;
});
}