ref: 3efb5bbb4061056e523858b134c555949591efe2
dir: /appl/ebook/ebook.b/
implement Ebook;
include "sys.m";
sys: Sys;
include "draw.m";
draw: Draw;
Point, Rect: import draw;
include "tk.m";
tk: Tk;
include "tkclient.m";
tkclient: Tkclient;
include "bufio.m";
bufio: Bufio;
Iobuf: import bufio;
include "string.m";
str: String;
include "keyboard.m";
include "url.m";
url: Url;
ParsedUrl: import url;
include "xml.m";
include "stylesheet.m";
include "cssparser.m";
include "oebpackage.m";
oebpackage: OEBpackage;
Package: import oebpackage;
include "reader.m";
reader: Reader;
Datasource, Mark, Block: import reader;
include "profile.m";
profile: Profile;
include "arg.m";
Doprofile: con 0;
# TO DO
# - error notices.
# + indexes based on display size and font information.
# - navigation by spine contents
# - navigation by guide, tour contents
# - searching?
Ebook: module {
init: fn(ctxt: ref Draw->Context, argv: list of string);
};
Font: con "/fonts/charon/plain.small.font";
LASTPAGE: con 16r7fffffff;
Book: adt {
win: ref Tk->Toplevel;
evch: string;
size: Point;
w: string;
showannot: int;
d: ref Document;
pkg: ref OEBpackage->Package;
fallbacks: list of (string, string);
item: ref OEBpackage->Item;
page: int;
indexprogress: chan of int;
sequence: list of ref OEBpackage->Item; # currently selected sequence
new: fn(f: string, win: ref Tk->Toplevel, w: string, evch: string, size: Point,
indexprogress: chan of int): (ref Book, string);
gotolink: fn(book: self ref Book, where: string): string;
gotopage: fn(book: self ref Book, page: int);
goto: fn(book: self ref Book, m: ref Bookmark);
mark: fn(book: self ref Book): ref Bookmark;
forward: fn(book: self ref Book);
back: fn(book: self ref Book);
showannotations: fn(book: self ref Book, showannot: int);
show: fn(book: self ref Book, item: ref OEBpackage->Item);
title: fn(book: self ref Book): string;
};
Bookmark: adt {
item: ref OEBpackage->Item;
page: int; # XXX should be fileoffset
};
Document: adt {
w: string;
p: ref Page; # current page
firstmark: ref Mark; # start of first element on current page
endfirstmark: ref Mark; # end of first element on current page
lastmark: ref Mark; # start of last element on current page
endlastmark: ref Mark; # end of last element on current page (nil if we're there)
nextoffset: int; # y offset of first element on next page
datasrc: ref Datasource;
indexed: int;
pagenum: int;
size: Point;
index: ref Index;
annotations: array of ref Annotation;
showannot: int;
item: ref OEBpackage->Item;
fallbacks: list of (string, string);
indexprogress: chan of int;
new: fn(i: ref OEBpackage->Item, fallbacks: list of (string, string),
win: ref Tk->Toplevel, w: string, size: Point, evch: string,
indexprogress: chan of int): (ref Document, string);
fileoffset: fn(d: self ref Document): int;
title: fn(d: self ref Document): string;
goto: fn(d: self ref Document, n: int): int;
gotooffset: fn(d: self ref Document, o: int);
gotolink: fn(d: self ref Document, name: string): int;
addannotation: fn(d: self ref Document, a: ref Annotation);
delannotation: fn(d: self ref Document, a: ref Annotation);
getannotation: fn(d: self ref Document, fileoffset: int): ref Annotation;
updateannotation: fn(d: self ref Document, a: ref Annotation);
showannotations: fn(d: self ref Document, show: int);
writeannotations: fn(d: self ref Document): string;
};
Index: adt {
rq: chan of (int, chan of (int, (ref Mark, int)));
linkrq: chan of (string, chan of int);
indexed: chan of (array of (ref Mark, int), ref Links);
d: ref Datasource;
size: Point;
length: int; # length of index file
f: string; # name of index file
new: fn(i: ref OEBpackage->Item, d: ref Datasource, size: Point, force: int,
indexprogress: chan of int): ref Index;
get: fn(i: self ref Index, n: int): (int, (ref Mark, int));
getlink: fn(i: self ref Index, name: string): int;
abort: fn(i: self ref Index);
stop: fn(i: self ref Index);
};
Page: adt {
win: ref Tk->Toplevel;
w: string;
min, max: int;
height: int;
yorigin: int;
bmargin: int;
new: fn(win: ref Tk->Toplevel, w: string): ref Page;
del: fn(p: self ref Page);
append: fn(p: self ref Page, b: Block);
remove: fn(p: self ref Page, atend: int): Block;
scrollto: fn(p: self ref Page, y: int);
count: fn(p: self ref Page): int;
bbox: fn(p: self ref Page, n: int): Rect;
bboxw: fn(p: self ref Page, w: string): Rect;
canvasr: fn(p: self ref Page, r: Rect): Rect;
window: fn(p: self ref Page, n: int): string;
maxy: fn(p: self ref Page): int;
conceal: fn(p: self ref Page, y: int);
visible: fn(p: self ref Page): int;
getblock: fn(p: self ref Page, n: int): Block;
};
Annotationwidth: con "20w";
Spikeradius: con 3;
Annotation: adt {
fileoffset: int;
text: string;
};
stderr: ref Sys->FD;
warningch: chan of (Xml->Locator, string);
debug := 0;
usage()
{
sys->fprint(stderr, "usage: ebook [-m] bookfile\n");
raise "fail:usage";
}
Flatopts: con "-bg white -relief flat -activebackground white -activeforeground black";
Menubutopts: con "-bg white -relief ridge -activebackground white -activeforeground black";
gctxt: ref Draw->Context;
init(ctxt: ref Draw->Context, argv: list of string)
{
gctxt = ctxt;
loadmods();
size := Point(400, 600);
arg := load Arg Arg->PATH;
if(arg == nil)
badmodule(Arg->PATH);
arg->init(argv);
while((opt := arg->opt()) != 0)
case opt {
'm' =>
size = Point(240, 320);
'd' =>
debug = 1;
* =>
usage();
}
argv = arg->argv();
arg = nil;
if (len argv != 1)
usage();
sys->pctl(Sys->NEWPGRP, nil);
reader->init(ctxt.display);
(win, ctlchan) := tkclient->toplevel(ctxt, nil, hd argv, Tkclient->Hide);
cch := chan of string;
tk->namechan(win, cch, "c");
evch := chan of string;
tk->namechan(win, evch, "evch");
cmd(win, "frame .f -bg white");
cmd(win, "button .f.up -text {↑} -command {send evch up}" + Flatopts);
cmd(win, "button .f.down -text {↓} -command {send evch down}" + Flatopts);
cmd(win, "button .f.next -text {→} -command {send evch forward}" + Flatopts);
cmd(win, "button .f.prev -text {←} -command {send evch back}" + Flatopts);
cmd(win, "label .f.pagenum -text 0 -bg white -relief flat -bd 0 -width 8w -anchor e");
cmd(win, "menubutton .f.annot -menu .f.annot.m " + Menubutopts + " -text {Opts}");
cmd(win, "menu .f.annot.m");
cmd(win, ".f.annot.m add checkbutton -text {Annotations} -command {send evch annot} -variable annot");
cmd(win, ".f.annot.m invoke 0");
cmd(win, "pack .f.annot -side left");
cmd(win, "pack .f.pagenum .f.down .f.up .f.next .f.prev -side right");
cmd(win, "focus .");
cmd(win, "bind .Wm_t <Button-1> +{focus .}");
cmd(win, "bind .Wm_t.title <Button-1> +{focus .}");
cmd(win, sys->sprint("bind . <Key-%c> {send evch up}", Keyboard->Up));
cmd(win, sys->sprint("bind . <Key-%c> {send evch down}", Keyboard->Down));
cmd(win, sys->sprint("bind . <Key-%c> {send evch forward}", Keyboard->Right));
cmd(win, sys->sprint("bind . <Key-%c> {send evch back}", Keyboard->Left));
cmd(win, "pack .f -side top -fill x");
# pack a temporary frame to see what size we're actually allocated.
cmd(win, "frame .tmp");
cmd(win, "pack .tmp -side top -fill both -expand 1");
cmd(win, "pack propagate . 0");
cmd(win, ". configure -width " + string size.x + " -height " + string size.y);
# fittoscreen(win);
size.x = int cmd(win, ".tmp cget -actwidth");
size.y = int cmd(win, ".tmp cget -actheight");
cmd(win, "destroy .tmp");
spawn showpageproc(win, ".f.pagenum", indexprogress := chan of int, pageprogress := chan of string);
(book, e) := Book.new(hd argv, win, ".d", "evch", size, indexprogress);
if (book == nil) {
pageprogress <-= nil;
sys->fprint(sys->fildes(2), "ebook: cannot open book: %s\n", e);
raise "fail:error";
}
if (book.pkg.guide != nil) {
makemenu(win, ".f.guide", "Guide", book.pkg.guide);
cmd(win, "pack .f.guide -before .f.pagenum -side left");
}
cmd(win, "pack .d -side top -fill both -expand 1");
tkclient->onscreen(win, nil);
tkclient->startinput(win, "kbd"::"ptr"::nil);
warningch = chan of (Xml->Locator, string);
spawn warningproc(warningch);
spawn handlerproc(book, evch, exitedch := chan of int, pageprogress);
for (;;) alt {
s := <-win.ctxt.kbd =>
tk->keyboard(win, s);
s := <-win.ctxt.ptr =>
tk->pointer(win, *s);
s := <-win.ctxt.ctl or
s = <-win.wreq or
s = <-ctlchan =>
if (s == "exit") {
evch <-= "exit";
<-exitedch;
}
tkclient->wmctl(win, s);
}
}
makemenu(win: ref Tk->Toplevel, w: string, title: string, items: list of ref OEBpackage->Reference)
{
cmd(win, "menubutton " + w + " -menu " + w + ".m " + Menubutopts + " -text '" + title);
m := w + ".m";
cmd(win, "menu " + m);
for (; items != nil; items = tl items) {
item := hd items;
# assumes URLs can't have '{}' in them.
cmd(win, m + " add command -text " + tk->quote(item.title) +
" -command {send evch goto " + item.href + "}");
}
}
loadmods()
{
sys = load Sys Sys->PATH;
stderr = sys->fildes(2);
draw = load Draw Draw->PATH;
tk = load Tk Tk->PATH;
bufio = load Bufio Bufio->PATH;
str = load String String->PATH;
if (str == nil)
badmodule(String->PATH);
url = load Url Url->PATH;
if (url == nil)
badmodule(Url->PATH);
url->init();
tkclient = load Tkclient Tkclient->PATH;
if (tkclient == nil)
badmodule(Tkclient->PATH);
tkclient->init();
reader = load Reader Reader->PATH;
if (reader == nil)
badmodule(Reader->PATH);
xml := load Xml Xml->PATH;
if (xml == nil)
badmodule(Xml->PATH);
xml->init();
oebpackage = load OEBpackage OEBpackage->PATH;
if (oebpackage == nil)
badmodule(OEBpackage->PATH);
oebpackage->init(xml);
if (Doprofile) {
profile = load Profile Profile->PATH;
if (profile == nil)
badmodule(Profile->PATH);
profile->init();
profile->sample(10);
}
}
showpageproc(win: ref Tk->Toplevel, w: string, indexprogress: chan of int, pageprogress: chan of string)
{
page := "0";
indexed: int;
for (;;) {
alt {
page = <-pageprogress =>;
indexed = <-indexprogress =>;
}
if (page == nil)
exit;
cmd(win, w + " configure -text {" + page + "/" + string indexed + "}");
cmd(win, "update");
}
}
handlerproc(book: ref Book, evch: chan of string, exitedch: chan of int, pageprogress: chan of string)
{
win := book.win;
newplace(book, pageprogress);
hist, fhist: list of ref Bookmark;
cmd(win, "update");
for (;;) {
(w, c) := splitword(<-evch);
if (Doprofile)
profile->start();
#sys->print("event '%s' '%s'\n", w, c);
(olditem, oldpage) := (book.item, book.page);
case w {
"exit" =>
book.show(nil); # force annotations to be written out.
exitedch <-= 1;
exit;
"forward" =>
book.forward();
"back" =>
book.back();
"up" =>
if (hist != nil) {
bm := book.mark();
book.goto(hd hist);
(hist, fhist) = (tl hist, bm :: fhist);
}
"down" =>
if (fhist != nil) {
bm := book.mark();
book.goto(hd fhist);
(hist, fhist) = (bm :: hist, tl fhist);
}
"goto" =>
(hist, fhist) = (book.mark() :: hist, nil);
e := book.gotolink(c);
if (e != nil)
notice("error getting link: " + e);
"ds" => # an event from a datasource-created widget
if (book.d == nil) {
oops("stray event 'ds " + c + "'");
break;
}
event := book.d.datasrc.event(c);
if (event == nil) {
oops(sys->sprint("nil event on 'ds %s'", c));
break;
}
pick ev := event {
Link =>
if (ev.url != nil) {
(hist, fhist) = (book.mark() :: hist, nil);
e := book.gotolink(ev.url);
if (e != nil)
notice("error getting link: " + e);
}
Texthit =>
a := ref Annotation(ev.fileoffset, nil);
spawn excessevents(evch);
editannotation(win, a);
evch <-= nil;
book.d.addannotation(a);
}
"annotclick" =>
a := book.d.getannotation(int c);
if (a == nil) {
notice("cannot find annotation at " + c);
break;
}
editannotation(win, a);
book.d.updateannotation(a);
"annot" =>
book.showannotations(int cmd(win, "variable annot"));
* =>
oops(sys->sprint("unknown event '%s' '%s'", w, c));
}
if (olditem != book.item || oldpage != book.page)
newplace(book, pageprogress);
cmd(win, "update");
cmd(win, "focus .");
if (Doprofile)
profile->stop();
}
}
excessevents(evch: chan of string)
{
while ((s := <-evch) != nil)
oops("excess: " + s);
}
newplace(book: ref Book, pageprogress: chan of string)
{
pageprogress <-= book.item.id + "." + string (book.page + 1);
tkclient->settitle(book.win, book.title());
}
editannotation(pwin: ref Tk->Toplevel, annot: ref Annotation)
{
(win, ctlchan) := tkclient->toplevel(gctxt,
"-x " + cmd(pwin, ". cget -actx") +
" -y " + cmd(pwin, ". cget -acty"), "Annotation", Tkclient->Appl);
cmd(win, "scrollbar .s -orient vertical -command {.t yview}");
cmd(win, "text .t -yscrollcommand {.s set}");
cmd(win, "pack .s -side left -fill y");
cmd(win, "pack .t -side top -fill both -expand 1");
cmd(win, "pack propagate . 0");
cmd(win, ". configure -width " + cmd(pwin, ". cget -width"));
cmd(win, ".t insert end '" + annot.text);
cmd(win, "update");
# XXX tk bug forces us to do this here rather than earlier
cmd(win, "focus .t");
cmd(win, "update");
tkclient->onscreen(win, nil);
tkclient->startinput(win, "kbd"::"ptr"::nil);
for (;;) alt {
c := <-win.ctxt.kbd =>
tk->keyboard(win, c);
c := <-win.ctxt.ptr =>
tk->pointer(win, *c);
c := <-win.ctxt.ctl or
c = <-win.wreq or
c = <-ctlchan =>
case c {
"task" =>
cmd(pwin, ". unmap");
tkclient->wmctl(win, c);
cmd(pwin, ". map");
cmd(win, "raise .");
cmd(win, "update");
"exit" =>
annot.text = trim(cmd(win, ".t get 1.0 end"));
return;
* =>
tkclient->wmctl(win, c);
}
}
}
warningproc(c: chan of (Xml->Locator, string))
{
for (;;) {
(loc, msg) := <-c;
if (msg == nil)
break;
warning(sys->sprint("%s:%d: %s", loc.systemid, loc.line, msg));
}
}
openpackage(f: string): (ref OEBpackage->Package, string)
{
(pkg, e) := oebpackage->open(f, warningch);
if (pkg == nil)
return (nil, e);
nmissing := pkg.locate();
if (nmissing > 0)
warning(string nmissing + " items missing from manifest");
for (i := pkg.manifest; i != nil; i = tl i)
(hd i).file = cleanname((hd i).file);
return (pkg, nil);
}
blankbook: Book;
Book.new(f: string, win: ref Tk->Toplevel, w: string, evch: string, size: Point,
indexprogress: chan of int): (ref Book, string)
{
(pkg, e) := openpackage(f);
if (pkg == nil)
return (nil, e);
# give section numbers to all the items in the manifest.
# items in the spine are named sequentially;
# other items are given letters corresponding to their order in the manifest.
for (items := pkg.manifest; items != nil; items = tl items)
(hd items).id = nil;
i := 1;
for (items = pkg.spine; items != nil; items = tl items)
(hd items).id = string i++;
i = 0;
for (items = pkg.manifest; items != nil; items = tl items) {
if ((hd items).id == nil) {
c := 'A';
if (i >= 26)
c = 'α';
(hd items).id = sys->sprint("%c", c + i);
i++;
}
}
fallbacks: list of (string, string);
for (items = pkg.manifest; items != nil; items = tl items) {
item := hd items;
if (item.fallback != nil)
fallbacks = (item.file, item.fallback.file) :: fallbacks;
}
book := ref blankbook;
book.win = win;
book.evch = evch;
book.size = size;
book.w = w;
book.pkg = pkg;
book.sequence = pkg.spine;
book.fallbacks = fallbacks;
book.indexprogress = indexprogress;
cmd(win, "frame " + w + " -bg white");
if (book.sequence != nil) {
book.show(hd book.sequence);
if (book.d != nil)
book.page = book.d.goto(0);
}
return (book, nil);
}
Book.title(book: self ref Book): string
{
if (book.d != nil)
return book.d.title();
return nil;
}
Book.mark(book: self ref Book): ref Bookmark
{
if (book.d != nil)
return ref Bookmark(book.item, book.page);
return nil;
}
Book.goto(book: self ref Book, m: ref Bookmark)
{
if (m != nil) {
book.show(m.item);
book.gotopage(m.page);
}
}
Book.gotolink(book: self ref Book, href: string): string
{
fromfile: string;
if (book.item != nil)
fromfile = book.item.file;
(u, err) := makerelativeurl(fromfile, href);
if (u == nil)
return err;
if (book.d == nil || book.item.file != u.path) {
for (i := book.pkg.manifest; i != nil; i = tl i)
if ((hd i).file == u.path)
break;
if (i == nil)
return "item '" + u.path + "' not found in manifest";
book.show(hd i);
}
if (book.d != nil) {
if (u.frag != nil) {
if (book.d.gotolink(u.frag) == -1) {
warning(sys->sprint("link '%s' not found in '%s'", u.frag, book.item.file));
book.d.goto(0);
} else
book.page = book.d.pagenum;
} else
book.d.goto(0);
book.page = book.d.pagenum;
}
return nil;
}
makerelativeurl(fromfile: string, href: string): (ref ParsedUrl, string)
{
dir := "";
for(n := len fromfile; --n >= 0;) {
if(fromfile[n] == '/') {
dir = fromfile[0:n+1];
break;
}
}
u := url->makeurl(href);
if(u.scheme != Url->FILE && u.scheme != Url->NOSCHEME)
return (nil, sys->sprint("URL scheme %s not yet supported", url->schemes[u.scheme]));
if(u.host != "localhost" && u.host != nil)
return (nil, "non-local URLs not supported");
path := u.path;
if (path == nil)
u.path = fromfile;
else {
if(u.pstart != "/")
path = dir+path; # TO DO: security
(ok, d) := sys->stat(path);
if(ok < 0)
return (nil, sys->sprint("'%s': %r", path));
u.path = path;
}
return (u, nil);
}
Book.gotopage(book: self ref Book, page: int)
{
if (book.d != nil)
book.page = book.d.goto(page);
}
#if (goto(next page)) doesn't move on) {
# if (currentdocument is in sequence and it's not the last) {
# close(document);
# open(next in sequence)
# goto(page 0)
# }
#}
Book.forward(book: self ref Book)
{
if (book.item == nil)
return;
if (book.d != nil) {
n := book.d.goto(book.page + 1);
if (n > book.page) {
book.page = n;
return;
}
}
# can't move further on, so try for next in sequence.
for (seq := book.sequence; seq != nil; seq = tl seq)
if (hd seq == book.item)
break;
# not found in current sequence, or nothing following it: nowhere to go.
if (seq == nil || tl seq == nil)
return;
book.show(hd tl seq);
if (book.d != nil)
book.page = book.d.goto(0);
}
Book.back(book: self ref Book)
{
if (book.item == nil)
return;
if (book.d != nil) {
n := book.d.goto(book.page - 1);
if (n < book.page) {
book.page = n;
return;
}
}
# can't move back, so try for previous in sequence
prev: ref OEBpackage->Item;
for (seq := book.sequence; seq != nil; (prev, seq) = (hd seq, tl seq))
if (hd seq == book.item)
break;
# not found in current sequence, or no previous: nowhere to go
if (seq == nil || prev == nil)
return;
book.show(prev);
if (book.d != nil)
book.page = book.d.goto(LASTPAGE);
}
Book.show(book: self ref Book, item: ref OEBpackage->Item)
{
if (book.item == item)
return;
if (book.d != nil) {
book.d.writeannotations();
book.d.index.stop();
cmd(book.win, "destroy " + book.d.w);
book.d = nil;
}
if (item == nil)
return;
(d, e) := Document.new(item, book.fallbacks, book.win, book.w + ".d", book.size, book.evch, book.indexprogress);
if (d == nil) {
notice(sys->sprint("cannot load item %s: %s", item.href, e));
return;
}
d.showannotations(book.showannot);
cmd(book.win, "pack " + book.w + ".d -fill both");
book.page = -1;
book.d = d;
book.item = item;
}
Book.showannotations(book: self ref Book, showannot: int)
{
book.showannot = showannot;
if (book.d != nil)
book.d.showannotations(showannot);
}
#actions:
# goto link
# if (link is to current document) {
# goto(link)
# } else {
# close(document)
# open(linked-to document)
# goto(link);
# }
#
# next page
# if (goto(next page)) doesn't move on) {
# if (currentdocument is in sequence and it's not the last) {
# close(document);
# open(next in sequence)
# goto(page 0)
# }
# }
#
# previous page
# if (page > 0) {
# goto(page - 1);
# } else {
# if (currentdocument is in sequence and it's not the first) {
# close(document)
# open(previous in sequence)
# goto(last page)
# }
displayannotation(d: ref Document, r: Rect, annot: ref Annotation)
{
tag := "o" + string annot.fileoffset;
(win, w) := (d.p.win, d.p.w);
a := cmd(win, w + " create text 0 0 -anchor nw -tags {annot " + tag + "}" +
" -width " + Annotationwidth +
" -text '" + annot.text);
er := s2r(cmd(win, w + " bbox " + a));
delta := er.min;
# desired rectangle for text entry box
er = Rect((r.min.x - Spikeradius, r.max.y), (r.min.x - Spikeradius + er.dx(), r.max.y + er.dy()));
# make sure it's on screen
if (er.max.x > d.size.x)
er = er.subpt((er.max.x - d.size.x, 0));
cmd(win, w + " create polygon" +
" " + p2s(er.min) +
" " + p2s((r.min.x - Spikeradius, er.min.y)) +
" " + p2s(r.min) +
" " + p2s((r.min.x + Spikeradius, er.min.y)) +
" " + p2s((er.max.x, er.min.y)) +
" " + p2s(er.max) +
" " + p2s((er.min.x, er.max.y)) +
" -fill yellow -tags {annot " + tag + "}");
cmd(win, w + " coords " + a + " " + p2s(er.min.sub(delta)));
cmd(win, w + " bind " + tag + " <Button-1> {" + w + " raise " + tag + "}");
cmd(win, w + " bind " + tag + " <Double-Button-1> {send evch annotclick " + string annot.fileoffset + "}");
cmd(win, w + " raise " + a);
}
badmodule(s: string)
{
sys->fprint(stderr, "ebook: can't load %s: %r\n", s);
raise "fail:load";
}
blankdoc: Document;
Document.new(i: ref OEBpackage->Item, fallbacks: list of (string, string),
win: ref Tk->Toplevel, w: string, size: Point, evch: string,
indexprogress: chan of int): (ref Document, string)
{
if (i.mediatype != "text/x-oeb1-document")
return (nil, "invalid mediatype: " + i.mediatype);
if (i.file == nil)
return (nil, "not found: " + i.missing);
(datasrc, e) := Datasource.new(i.file, fallbacks, win, size.x, evch, warningch);
if (datasrc == nil)
return (nil, e);
d := ref blankdoc;
d.item = i;
d.w = w;
d.p = Page.new(win, w + ".p");
d.datasrc = datasrc;
d.pagenum = -1;
d.size = size;
d.indexprogress = indexprogress;
d.index = Index.new(i, datasrc, size, 0, indexprogress);
cmd(win, "frame " + w + " -width " + string size.x + " -height " + string size.y);
cmd(win, "pack propagate " + w + " 0");
cmd(win, "pack " + w + ".p -side top -fill both");
d.annotations = readannotations(i.file + ".annot");
d.showannot = 0;
return (d, nil);
}
Document.fileoffset(nil: self ref Document): int
{
# get nearest file offset corresponding to top of current page.
# XXX
return 0;
}
Document.gotooffset(nil: self ref Document, nil: int)
{
# d.goto(d.index.pageforfileoffset(offset));
# XXX
}
Document.title(d: self ref Document): string
{
return d.datasrc.title;
}
Document.gotolink(d: self ref Document, name: string): int
{
n := d.index.getlink(name);
if (n != -1)
return d.goto(n);
return -1;
}
# this is much too involved for its own good.
Document.goto(d: self ref Document, n: int): int
{
win := d.datasrc.win;
pw := d.w + ".p";
if (n == d.pagenum)
return n;
m: ref Mark;
offset := -999;
# before committing ourselves, make sure that the page exists.
(n, (m, offset)) = d.index.get(n);
if (m == nil || n == d.pagenum)
return d.pagenum;
b: Block;
# remove appropriate element, in case we want to use it in the new page.
if (n > d.pagenum)
b = d.p.remove(1);
else
b = d.p.remove(0);
# destroy the old page and make a new one.
d.p.del();
d.p = Page.new(win, pw);
cmd(win, "pack " + pw + " -side top -fill both -expand 1");
if (n == d.pagenum + 1 && d.lastmark != nil) {
if(debug)sys->print("page 1 forward\n");
# sanity check:
# if d.nextoffset or d.lastmark doesn't match the offset and mark we've obtained
# fpr this page from the index, then the index is invalid, so reindex and recurse
if (d.nextoffset != offset || !d.lastmark.eq(m)) {
notice(sys->sprint("invalid index, reindexing; (index offset: %d, actually %d; mark: %d, actually: %d)\n",
offset, d.nextoffset, d.lastmark.fileoffset(), m.fileoffset()));
d.index.abort();
d.index = Index.new(d.item, d.datasrc, d.size, 1, d.indexprogress);
d.pagenum = -1;
d.firstmark = d.endfirstmark = d.lastmark = d.endlastmark = nil;
d.nextoffset = 0;
return d.goto(n);
}
# if moving to the next page, we don't need to look up in the index;
# just continue on from where we currently are, transferring the
# last item on the current page to the first on the next.
d.p.append(b);
b.w = nil;
d.p.scrollto(d.nextoffset);
d.firstmark = d.lastmark;
if (d.endlastmark != nil) {
d.endfirstmark = d.endlastmark;
d.datasrc.goto(d.endfirstmark);
} else
d.endfirstmark = d.datasrc.mark();
(d.lastmark, nil) = fillpage(d.p, d.size, d.datasrc, d.firstmark, nil, nil);
d.endlastmark = nil;
offset = d.nextoffset;
} else {
d.p.scrollto(offset);
if (n == d.pagenum - 1) {
if(debug)sys->print("page 1 back\n");
# moving to the previous page: re-use the first item on
# the current page as the last on the previous.
newendfirst: ref Mark;
if (!m.eq(d.firstmark)) {
d.datasrc.goto(m);
newendfirst = fillpageupto(d.p, d.datasrc, d.firstmark);
} else
newendfirst = d.endfirstmark;
d.p.append(b);
b.w = nil;
(d.endfirstmark, d.lastmark, d.endlastmark) =
(newendfirst, d.firstmark, d.endfirstmark);
} else if (n > d.pagenum && m.eq(d.lastmark)) {
if(debug)sys->print("page forward, same start element\n");
# moving forward: if new page starts with same element
# that this page ends with, then reuse it.
d.p.append(b);
b.w = nil;
if (d.endlastmark != nil) {
d.datasrc.goto(d.endlastmark);
d.endfirstmark = d.endlastmark;
} else
d.endfirstmark = d.datasrc.mark();
(d.lastmark, nil) = fillpage(d.p, d.size, d.datasrc, m, nil, nil);
d.endlastmark = nil;
} else {
if(debug)sys->print("page goto arbitrary\n");
# XXX could optimise when moving several pages back,
# by limiting fillpage so that it stopped if it got to d.firstmark,
# upon which we could re-use the first widget from the current page.
d.datasrc.goto(m);
(d.lastmark, d.endfirstmark) = fillpage(d.p, d.size, d.datasrc, m, nil, nil);
if (d.endfirstmark == nil)
d.endfirstmark = d.datasrc.mark();
d.endlastmark = nil;
}
d.firstmark = m;
}
d.nextoffset = coverpartialline(d.p, d.datasrc, d.size);
if (b.w != nil)
cmd(win, "destroy " + b.w);
d.pagenum = n;
if (d.showannot)
makeannotations(d, currentannotations(d));
if (debug)sys->print("page %d; firstmark is %d; yoffset: %d, nextoffset: %d; %d items\n", n, d.firstmark.fileoffset(), d.p.yorigin, d.nextoffset, d.p.count());
if(debug)sys->print("now at page %d, offset: %d, nextoffset: %d\n", n, d.p.yorigin, d.nextoffset);
return n;
}
# fill up a page of size _size_ from d;
# m1 marks the start of the first item (already on the page).
# m2 marks the end of the item marked by m1.
# return (lastmark¸ endfirstmark)
# endfirstmark marks the end of the first item placed on the page;
# lastmark marks the start of the last item that overlaps
# the end of the page (or nil at eof).
fillpage(p: ref Page, size: Point, d: ref Datasource,
m1, m2: ref Mark, linkch: chan of (string, string, string)): (ref Mark, ref Mark)
{
endfirst: ref Mark;
err: string;
b: Block;
while (p.maxy() < size.y) {
m1 = d.mark();
# if we've been round once and only once,
# then m1 marks the end of the first element
if (b.w != nil && endfirst == nil)
endfirst = m1;
(b, err) = d.next(linkch);
if (err != nil) {
notice(err);
return (nil, endfirst);
}
if (b.w == nil)
return (nil, endfirst);
p.append(b);
}
if (endfirst == nil)
endfirst = m2;
return (m1, endfirst);
}
# fill a page up until a mark is reached (which is known to be on the page).
# return endfirstmark.
fillpageupto(p: ref Page, d: ref Datasource, upto: ref Mark): ref Mark
{
endfirstmark: ref Mark;
while (!d.atmark(upto)) {
(b, err) := d.next(nil);
if (b.w == nil) {
notice("unexpected EOF");
return nil;
}
p.append(b);
if (endfirstmark == nil)
endfirstmark = d.mark();
}
return endfirstmark;
}
# cover the last partial line on the page; return the y offset
# of the start of that line in the item containing it. (including top margin)
coverpartialline(p: ref Page, d: ref Datasource, size: Point): int
{
# conceal any trailing partially concealed line.
lastn := p.count() - 1;
b := p.getblock(lastn);
r := p.bbox(lastn);
if (r.max.y >= size.y) {
if (r.min.y < size.y) {
offset := d.linestart(p.window(lastn), size.y - r.min.y);
# guard against items larger than the whole page.
if (r.min.y + offset <= 0)
return size.y - r.min.y;
p.conceal(r.min.y + offset);
# if before first line, ensure that we get whole of top margin on next page.
if (offset == 0) {
p.conceal(size.y);
return 0;
}
return offset + b.tmargin;
} else {
p.conceal(size.y);
return 0; # ensure that we get whole of top margin on next page.
}
}
p.conceal(size.y);
return r.dy() + b.tmargin;
}
Document.getannotation(d: self ref Document, fileoffset: int): ref Annotation
{
annotations := d.annotations;
for (i := 0; i < len annotations; i++)
if (annotations[i].fileoffset == fileoffset)
return annotations[i];
return nil;
}
Document.showannotations(d: self ref Document, show: int)
{
if (!show == !d.showannot)
return;
d.showannot = show;
if (show) {
makeannotations(d, currentannotations(d));
} else {
cmd(d.datasrc.win, d.p.w + " delete annot");
}
}
Document.updateannotation(d: self ref Document, annot: ref Annotation)
{
if (annot.text == nil)
d.delannotation(annot);
if (d.showannot) {
# XXX this loses the z-order of the annotation
cmd(d.datasrc.win, d.p.w + " delete o" + string annot.fileoffset);
if (annot.text != nil)
makeannotations(d, array[] of {annot});
}
}
Document.delannotation(d: self ref Document, annot: ref Annotation)
{
for (i := 0; i < len d.annotations; i++)
if (d.annotations[i].fileoffset == annot.fileoffset)
break;
if (i == len d.annotations) {
oops("trying to delete non-existent annotation");
return;
}
d.annotations[i:] = d.annotations[i+1:];
d.annotations[len d.annotations - 1] = nil;
d.annotations = d.annotations[0:len d.annotations - 1];
}
Document.writeannotations(d: self ref Document): string
{
if ((iob := bufio->create(d.item.file + ".annot", Sys->OWRITE, 8r666)) == nil)
return sys->sprint("cannot create %s.annot: %r\n", d.item.file);
a: list of string;
for (i := 0; i < len d.annotations; i++)
a = string d.annotations[i].fileoffset :: d.annotations[i].text :: a;
iob.puts(str->quoted(a));
iob.close();
return nil;
}
Document.addannotation(d: self ref Document, a: ref Annotation)
{
if (a.text == nil)
return;
annotations := d.annotations;
for (i := 0; i < len annotations; i++)
if (annotations[i].fileoffset >= a.fileoffset)
break;
if (i < len annotations && annotations[i].fileoffset == a.fileoffset) {
oops("there's already an annotation there");
return;
}
newa := array[len annotations + 1] of ref Annotation;
newa[0:] = annotations[0:i];
newa[i] = a;
newa[i + 1:] = annotations[i:];
d.annotations = newa;
d.updateannotation(a);
}
makeannotations(d: ref Document, annots: array of ref Annotation)
{
n := d.p.count();
endy := d.p.visible();
for (i := j := 0; i < n && j < len annots; ) {
do {
(ok, r) := d.datasrc.rectforfileoffset(d.p.window(i), annots[j].fileoffset);
# XXX this assumes that y-origins at increasing offsets are monotonically increasing;
# this ain't necessarily the case (think tables)
if (!ok)
break;
r = r.addpt((0, d.p.bbox(i).min.y));
if (r.min.y >= 0 && r.max.y <= endy)
displayannotation(d, d.p.canvasr(r), annots[j]);
j++;
} while (j < len annots);
i++;
}
}
# get all annotations on current page, arranged in fileoffset order.
currentannotations(d: ref Document): array of ref Annotation
{
if (d.firstmark == nil)
return nil;
o1 := d.firstmark.fileoffset();
o2: int;
if (d.endlastmark != nil)
o2 = d.endlastmark.fileoffset();
else
o2 = d.datasrc.fileoffset();
annotations := d.annotations;
for (i := 0; i < len annotations; i++)
if (annotations[i].fileoffset >= o1)
break;
a1 := i;
for (; i < len annotations; i++)
if (annotations[i].fileoffset > o2)
break;
return annotations[a1:i];
}
readannotations(f: string): array of ref Annotation
{
s: string;
if ((iob := bufio->open(f, Sys->OREAD)) == nil)
return nil;
while ((c := iob.getc()) >= 0)
s[len s] = c;
a := str->unquoted(s);
n := len a / 2;
annotations := array[n] of ref Annotation;
for (i := n - 1; i >= 0; i--) {
annotations[i] = ref Annotation(int hd a, hd tl a);
a = tl tl a;
}
return annotations;
}
Index.new(item: ref OEBpackage->Item, d: ref Datasource, size: Point,
force: int, indexprogress: chan of int): ref Index
{
i := ref Index;
i.rq = chan of (int, chan of (int, (ref Mark, int)));
i.linkrq = chan of (string, chan of int);
f := item.file + ".i";
i.length = 0;
(ok, sinfo) := sys->stat(item.file);
if (ok != -1)
i.length = int sinfo.length;
if (!force) {
indexf := bufio->open(f, Sys->OREAD);
if (indexf != nil) {
(pages, links, err) := readindex(indexf, i.length, size, d);
indexprogress <-= len pages;
if (err != nil)
warning(sys->sprint("cannot read index file %s: %s", f, err));
else {
spawn preindexeddealerproc(i.rq, i.linkrq, pages, links);
return i;
}
}
}
#sys->print("reindexing %s\n", f);
i.d = d.copy();
i.size = size;
i.f = f;
i.indexed = chan of (array of (ref Mark, int), ref Links);
spawn indexproc(i.d, size,
c := chan of (ref Mark, int),
linkch := chan of string);
spawn indexdealerproc(i.f, c, i.rq, i.linkrq, chan of (int, chan of int), linkch, i.indexed, indexprogress);
# i.get(LASTPAGE);
return i;
}
Index.abort(i: self ref Index)
{
i.rq <-= (0, nil);
# XXX kill off old indexing proc too.
}
Index.stop(i: self ref Index)
{
if (i.indexed != nil) {
# wait for indexing to complete, so that we can write it out without interruption.
(pages, links) := <-i.indexed;
writeindex(i.d, i.length, i.size, i.f, pages, links);
}
i.rq <-= (0, nil);
}
preindexeddealerproc(rq: chan of (int, chan of (int, (ref Mark, int))), linkrq: chan of (string, chan of int),
pages: array of (ref Mark, int), links: ref Links)
{
for (;;) alt {
(n, reply) := <-rq =>
if (reply == nil)
exit;
if (n < 0)
n = 0;
else if (n >= len pages)
n = len pages - 1;
# XXX are we justified in assuming there's at least one page?
reply <-= (n, pages[n]);
(name, reply) := <-linkrq =>
reply <-= links.get(name);
}
}
readindex(indexf: ref Iobuf, length: int, size: Point, d: ref Datasource): (array of (ref Mark, int), ref Links, string)
{
# n pages
s := indexf.gets('\n');
(n, toks) := sys->tokenize(s, " ");
if (n != 2 || hd tl toks != "pages\n" || int hd toks < 1)
return (nil, nil, "invalid index file");
npages := int hd toks;
# size x y
s = indexf.gets('\n');
(n, toks) = sys->tokenize(s, " ");
if (n != 3 || hd toks != "size")
return (nil, nil, "invalid index file");
if (int hd tl toks != size.x || int hd tl tl toks != size.y)
return (nil, nil, "index for different sized window");
# length n
s = indexf.gets('\n');
(n, toks) = sys->tokenize(s, " ");
if (n != 2 || hd toks != "length")
return (nil, nil, "invalid index file");
if (int hd tl toks != length)
return (nil, nil, "index for file of different length");
pages := array[npages] of (ref Mark, int);
for (i := 0; i < npages; i++) {
ms := indexf.gets('\n');
os := indexf.gets('\n');
if (ms == nil || os == nil)
return (nil, nil, "premature EOF on index");
(m, o) := (d.str2mark(ms), int os);
if (m == nil)
return (nil, nil, "invalid mark");
pages[i] = (m, o);
}
(links, err) := Links.read(indexf);
if (links == nil)
return (nil, nil, "readindex: " + err);
return (pages, links, nil);
}
# index format:
# %d pages
# size %d %d
# length %d
# page0mark
# page0yoffset
# page1mark
# ....
# linkname pagenum
# ...
writeindex(d: ref Datasource, length: int, size: Point, f: string, pages: array of (ref Mark, int), links: ref Links)
{
indexf := bufio->create(f, Sys->OWRITE, 8r666);
if (indexf == nil) {
notice(sys->sprint("cannot create index '%s': %r", f));
return;
}
indexf.puts(string len pages + " pages\n");
indexf.puts(sys->sprint("size %d %d\n", size.x, size.y));
indexf.puts(sys->sprint("length %d\n", length));
for (i := 0; i < len pages; i++) {
(m, o) := pages[i];
indexf.puts(d.mark2str(m));
indexf.putc('\n');
indexf.puts(string o);
indexf.putc('\n');
}
links.write(indexf);
indexf.close();
}
Index.get(i: self ref Index, n: int): (int, (ref Mark, int))
{
c := chan of (int, (ref Mark, int));
i.rq <-= (n, c);
return <-c;
}
Index.getlink(i: self ref Index, name: string): int
{
c := chan of int;
i.linkrq <-= (name, c);
return <-c;
}
# deal out indexes as and when they become available.
indexdealerproc(nil: string,
c: chan of (ref Mark, int),
rq: chan of (int, chan of (int, (ref Mark, int))),
linkrq: chan of (string, chan of int),
offsetrq: chan of (int, chan of int),
linkch: chan of string,
indexed: chan of (array of (ref Mark, int), ref Links),
indexprogress: chan of int)
{
pages := array[4] of (ref Mark, int);
links := Links.new();
rqs: list of (int, chan of (int, (ref Mark, int)));
linkrqs: list of (string, chan of int);
indexedch := chan of (array of (ref Mark, int), ref Links);
npages := 0;
finished := 0;
for (;;) alt {
(m, offset) := <-c =>
if (m == nil) {
if(debug)sys->print("finished indexing; %d pages\n", npages);
indexedch = indexed;
pages = pages[0:npages];
finished = 1;
for (; linkrqs != nil; linkrqs = tl linkrqs)
(hd linkrqs).t1 <-= -1;
} else {
if (npages == len pages)
pages = (array[npages * 2] of (ref Mark, int))[0:] = pages;
pages[npages++] = (m, offset);
indexprogress <-= npages;
}
r := rqs;
for (rqs = nil; r != nil; r = tl r) {
(n, reply) := hd r;
if (n < npages)
reply <-= (n, pages[n]);
else if (finished)
reply <-= (npages - 1, pages[npages - 1]);
else
rqs = hd r :: rqs;
}
(name, reply) := <-linkrq =>
n := links.get(name);
if (n != -1)
reply <-= n;
else if (finished)
reply <-= -1;
else
linkrqs = (name, reply) :: linkrqs;
(offset, reply) := <-offsetrq =>
reply <-= -1; # XXX fix it.
# if (finished && (npages == 0 || offset >= pages[npages - 1].fileoffset
# if (i := 0; i < npages; i++)
(n, reply) := <-rq =>
if (reply == nil)
exit;
if (n < 0)
n = 0;
if (n < npages)
reply <-= (n, pages[n]);
else if (finished)
reply <-= (npages - 1, pages[npages - 1]);
else
rqs = (n, reply) :: rqs;
name := <-linkch =>
links.put(name, npages - 1);
r := linkrqs;
for (linkrqs = nil; r != nil; r = tl r) {
(rqname, reply) := hd r;
if (rqname == name)
reply <-= npages - 1;
else
linkrqs = hd r :: linkrqs;
}
indexedch <-= (pages, links) =>
;
}
}
# accumulate links temporarily while filling a page.
linkproc(linkch: chan of (string, string, string),
terminate: chan of int,
reply: chan of list of (string, string, string))
{
links: list of (string, string, string);
for (;;) {
alt {
<-terminate =>
exit;
(name, w, where) := <-linkch =>
if (name != nil) {
links = (name, w, where) :: links;
} else {
reply <-= links;
links = nil;
}
}
}
}
# generate index values for each page and send them on
# to indexdealerproc to be served up on demand.
indexproc(d: ref Datasource, size: Point, c: chan of (ref Mark, int),
linkpagech: chan of string)
{
spawn linkproc(linkch := chan of (string, string, string),
terminate := chan of int,
reply := chan of list of (string, string, string));
win := d.win;
p := Page.new(win, ".ip");
mark := d.mark();
c <-= (mark, 0);
links: list of (string, string, string); # (linkname, widgetname, tag)
for (;;) {
startoffset := mark.fileoffset();
(mark, nil) = fillpage(p, size, d, mark, nil, linkch);
offset := coverpartialline(p, d, size);
if (debug)sys->print("page index %d items starting at %d, nextyoffset: %d\n", p.count(), startoffset, offset);
linkch <-= (nil, nil, nil);
for (l := <-reply; l != nil; l = tl l)
links = hd l :: links;
links = sendlinks(p, size, d, links, linkpagech);
if (mark == nil)
break;
c <-= (mark, offset);
b := p.remove(1);
p.del();
p = Page.new(win, ".ip");
p.append(b);
p.scrollto(offset);
}
p.del();
terminate <-= 1;
c <-= (nil, 0);
}
# send down ch the name of all the links that reside on the current page.
# return any links that were not on the current page.
sendlinks(p: ref Page, nil: Point, d: ref Datasource,
links: list of (string, string, string), ch: chan of string): list of (string, string, string)
{
nlinks: list of (string, string, string);
vy := p.visible();
for (; links != nil; links = tl links) {
(name, w, where) := hd links;
r := p.bboxw(w);
y := r.min.y + d.linkoffset(w, where);
if (y < vy)
ch <-= name;
else
nlinks = hd links :: nlinks;
}
return nlinks;
}
Links: adt {
a: array of list of (string, int);
new: fn(): ref Links;
read: fn(iob: ref Iobuf): (ref Links, string);
get: fn(l: self ref Links, name: string): int;
put: fn(l: self ref Links, name: string, pagenum: int);
write: fn(l: self ref Links, iob: ref Iobuf);
};
Links.new(): ref Links
{
return ref Links(array[31] of list of (string, int));
}
Links.write(l: self ref Links, iob: ref Iobuf)
{
for (i := 0; i < len l.a; i++) {
for (ll := l.a[i]; ll != nil; ll = tl ll) {
(name, page) := hd ll;
iob.puts(sys->sprint("%s %d\n", name, page));
}
}
}
Links.read(iob: ref Iobuf): (ref Links, string)
{
l := Links.new();
while ((s := iob.gets('\n')) != nil) {
(n, toks) := sys->tokenize(s, " ");
if (n != 2)
return (nil, "expected 2 words, got " + string n);
l.put(hd toks, int hd tl toks);
}
return (l, nil);
}
Links.get(l: self ref Links, name: string): int
{
for (ll := l.a[hashfn(name, len l.a)]; ll != nil; ll = tl ll)
if ((hd ll).t0 == name)
return (hd ll).t1;
return -1;
}
Links.put(l: self ref Links, name: string, pageno: int)
{
v := hashfn(name, len l.a);
l.a[v] = (name, pageno) :: l.a[v];
}
blankpage: Page;
Page.new(win: ref Tk->Toplevel, w: string): ref Page
{
cmd(win, "canvas " + w + " -bg white");
col := cmd(win, w + " cget -bg");
cmd(win, w + " create rectangle -1 -1 -1 -1 -fill " + col + " -outline " + col + " -tags conceal");
p := ref blankpage;
p.win = win;
p.w = w;
setscrollregion(p);
return p;
}
Page.del(p: self ref Page)
{
n := p.count();
for (i := 0; i < n; i++)
cmd(p.win, "destroy " + p.window(i));
cmd(p.win, "destroy " + p.w);
}
# convert a rectangle as returned by Page.window()
# to a rectangle in canvas coordinates
Page.canvasr(p: self ref Page, r: Rect): Rect
{
return r.addpt((0, p.yorigin));
}
Pagewidth: con 5000; # max page width
# create an area on the page, from y downwards.
Page.conceal(p: self ref Page, y: int)
{
cmd(p.win, p.w + " coords conceal 0 " + string (y + p.yorigin) +
" " + string Pagewidth +
" " + string p.height);
cmd(p.win, p.w + " raise conceal");
}
# return vertical space in the page that's not concealed.
Page.visible(p: self ref Page): int
{
r := s2r(cmd(p.win, p.w + " coords conceal"));
return r.min.y - p.yorigin;
}
Page.window(p: self ref Page, n: int): string
{
return cmd(p.win, p.w + " itemcget n" + string (n + p.min) + " -window");
}
Page.append(p: self ref Page, b: Block)
{
h := int cmd(p.win, b.w + " cget -height") + 2 * int cmd(p.win, b.w + " cget -bd");
n := p.max++;
y := p.height;
gap := p.bmargin;
if (b.tmargin > gap)
gap = b.tmargin;
cmd(p.win, p.w + " create window 0 " + string (y + gap) + " -window " + b.w +
" -tags {elem" +
" n" + string n +
" t" + string b.tmargin +
" b" + string b.bmargin +
"} -anchor nw");
p.height += h + gap;
p.bmargin = b.bmargin;
setscrollregion(p);
}
Page.remove(p: self ref Page, atend: int): Block
{
if (p.min == p.max)
return Block(nil, 0, 0);
n: int;
if (atend)
n = --p.max;
else
n = p.min++;
b := getblock(p, n);
h := int cmd(p.win, b.w + " cget -height") + 2 * int cmd(p.win, b.w + " cget -bd");
if (p.min == p.max) {
p.bmargin = 0;
h += b.tmargin;
} else if (atend) {
c := getblock(p, p.max - 1);
if (c.bmargin > b.tmargin)
h += c.bmargin;
else
h += b.tmargin;
p.bmargin = c.bmargin;
} else {
c := getblock(p, p.min);
if (c.tmargin > b.bmargin)
h += c.tmargin;
else
h += b.bmargin;
h += b.tmargin;
}
p.height -= h;
cmd(p.win, p.w + " delete n" + string n);
if (!atend)
cmd(p.win, p.w + " move elem 0 -" + string h);
setscrollregion(p);
return b;
}
getblock(p: ref Page, n: int): Block
{
tag := "n" + string n;
b := Block(cmd(p.win, p.w + " itemcget " + tag + " -window"), 0, 0);
(nil, toks) := sys->tokenize(cmd(p.win, p.w + " gettags " + tag), " ");
for (; toks != nil; toks = tl toks) {
c := (hd toks)[0];
if (c == 't')
b.tmargin = int (hd toks)[1:];
else if (c == 'b')
b.bmargin = int (hd toks)[1:];
}
return b;
}
# scroll the page so y is at the top left visible in the canvas widget.
Page.scrollto(p: self ref Page, y: int)
{
p.yorigin = y;
setscrollregion(p);
cmd(p.win, p.w + " yview moveto 0");
}
# return max y coord of bottom of last item, where y=0
# is at top visible part of canvas.
Page.maxy(p: self ref Page): int
{
return p.height - p.yorigin;
}
Page.count(p: self ref Page): int
{
return p.max - p.min;
}
# XXX what should bbox do about margins? ignoring seems ok for the moment.
Page.bbox(p: self ref Page, n: int): Rect
{
if (p.count() == 0)
return ((0, 0), (0, 0));
tag := "n" + string (n + p.min);
return s2r(cmd(p.win, p.w + " bbox " + tag)).subpt((0, p.yorigin));
}
Page.bboxw(p: self ref Page, w: string): Rect
{
# XXX inefficient algorithm. do better later.
n := p.count();
for (i := 0; i < n; i++)
if (p.window(i) == w)
return p.bbox(i);
sys->fprint(sys->fildes(2), "ebook: bboxw requested for invalid window %s\n", w);
return ((0, 0), (0, 0));
}
Page.getblock(p: self ref Page, n: int): Block
{
return getblock(p, n + p.min);
}
printpage(p: ref Page)
{
n := p.count();
for (i := 0; i < n; i++) {
r := p.bbox(i);
dx := r.max.sub(r.min);
sys->print(" %d: %s %d %d +%d +%d\n", i, p.window(i),
r.min.x, r.min.y, dx.x, dx.y);
}
sys->print(" conceal: %s\n", cmd(p.win, p.w + " bbox conceal"));
}
setscrollregion(p: ref Page)
{
cmd(p.win, p.w + " configure -scrollregion {0 " + string p.yorigin + " " + string Pagewidth + " " + string p.height + "}");
}
notice(s: string)
{
sys->print("notice: %s\n", s);
}
warning(s: string)
{
notice("warning: " + s);
}
oops(s: string)
{
sys->print("oops: %s\n", s);
}
cmd(win: ref Tk->Toplevel, s: string): string
{
# sys->print("%ux %s\n", win, s);
r := tk->cmd(win, s);
# sys->print(" -> %s\n", r);
if (len r > 0 && r[0] == '!') {
sys->fprint(stderr, "ebook: error executing '%s': %s\n", s, r);
raise "tk error";
}
return r;
}
s2r(s: string): Rect
{
(n, toks) := sys->tokenize(s, " ");
if (n != 4) {
sys->print("'%s' is not a rectangle!\n", s);
raise "bad conversion";
}
r: Rect;
(r.min.x, toks) = (int hd toks, tl toks);
(r.min.y, toks) = (int hd toks, tl toks);
(r.max.x, toks) = (int hd toks, tl toks);
(r.max.y, toks) = (int hd toks, tl toks);
return r;
}
p2s(p: Point): string
{
return string p.x + " " + string p.y;
}
r2s(r: Rect): string
{
return string r.min.x + " " + string r.min.y + " " +
string r.max.x + " " + string r.max.y;
}
trim(s: string): string
{
for (i := len s - 1; i >= 0; i--)
if (s[i] != ' ' && s[i] != '\t' && s[i] != '\n')
break;
return s[0:i+1];
}
splitword(s: string): (string, string)
{
for (i := 0; i < len s; i++)
if (s[i] == ' ')
return (s[0:i], s[i + 1:]);
return (s, nil);
}
# compress ../ references and do other cleanups
cleanname(name: string): string
{
# compress multiple slashes
n := len name;
for(i:=0; i<n-1; i++)
if(name[i]=='/' && name[i+1]=='/'){
name = name[0:i]+name[i+1:];
--i;
n--;
}
# eliminate ./
for(i=0; i<n-1; i++)
if(name[i]=='.' && name[i+1]=='/' && (i==0 || name[i-1]=='/')){
name = name[0:i]+name[i+2:];
--i;
n -= 2;
}
found: int;
do{
# compress xx/..
found = 0;
for(i=1; i<=n-3; i++)
if(name[i:i+3] == "/.."){
if(i==n-3 || name[i+3]=='/'){
found = 1;
break;
}
}
if(found)
for(j:=i-1; j>=0; --j)
if(j==0 || name[j-1]=='/'){
i += 3; # character beyond ..
if(i<n && name[i]=='/')
++i;
name = name[0:j]+name[i:];
n -= (i-j);
break;
}
} while(found);
# eliminate trailing .
if(n>=2 && name[n-2]=='/' && name[n-1]=='.')
--n;
if(n == 0)
return ".";
if(n != len name)
name = name[0:n];
return name;
}
hashfn(s: string, n: int): int
{
h := 0;
m := len s;
for(i:=0; i<m; i++){
h = 65599*h+s[i];
}
return (h & 16r7fffffff) % n;
}