summaryrefslogtreecommitdiff
path: root/appl/ebook/ebook.b
diff options
context:
space:
mode:
Diffstat (limited to 'appl/ebook/ebook.b')
-rw-r--r--appl/ebook/ebook.b1893
1 files changed, 1893 insertions, 0 deletions
diff --git a/appl/ebook/ebook.b b/appl/ebook/ebook.b
new file mode 100644
index 00000000..01a092a3
--- /dev/null
+++ b/appl/ebook/ebook.b
@@ -0,0 +1,1893 @@
+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;
+}