diff options
Diffstat (limited to 'appl/ebook/ebook.b')
| -rw-r--r-- | appl/ebook/ebook.b | 1893 |
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; +} |
