589 lines
15 KiB
Java
589 lines
15 KiB
Java
/*
|
|
Nile2Iter.java
|
|
*
|
|
* Copyright (c) 2000, Ted Nelson and Tuomas Lukka
|
|
*
|
|
* You may use and distribute under the terms of either the GNU Lesser
|
|
* General Public License, either version 2 of the license or,
|
|
* at your choice, any later version. Alternatively, you may use and
|
|
* distribute under the terms of the XPL.
|
|
*
|
|
* See the LICENSE.lgpl and LICENSE.xpl files for the specific terms of
|
|
* the licenses.
|
|
*
|
|
* This software is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the README
|
|
* file for more details.
|
|
*
|
|
*/
|
|
/*
|
|
* Written by Tuomas Lukka
|
|
*/
|
|
|
|
package org.gzigzag.module;
|
|
import org.gzigzag.*;
|
|
|
|
/** A character-level iterator object that knows about
|
|
* paragraphs for a Nile stream.
|
|
* This object explicitly assumes no modifications to the
|
|
* stream while it is working.
|
|
*/
|
|
|
|
public final class Nile2Iter implements Cloneable {
|
|
public static final String rcsid = "$Id: Nile2Iter.java,v 1.8 2000/12/20 22:26:35 bfallenstein Exp $";
|
|
public static boolean dbg = false;
|
|
static final void p(String s) { if(dbg) System.out.println(s); }
|
|
static final void pa(String s) { System.out.println(s); }
|
|
|
|
private ZZCell c;
|
|
private int offs;
|
|
|
|
// Cache these for speed.
|
|
private String ctext;
|
|
private int clen;
|
|
|
|
private void load(ZZCell c) {
|
|
this.c = c;
|
|
ctext = c.getText();
|
|
clen = ctext.length();
|
|
}
|
|
|
|
private void load(ZZCell c, int offs) {
|
|
load(c);
|
|
if(offs == -1) this.offs = clen;
|
|
else this.offs = offs;
|
|
move(0);
|
|
}
|
|
|
|
private void reload() {
|
|
load(c);
|
|
move(0);
|
|
}
|
|
|
|
static private boolean isStruct(ZZCell c) {
|
|
if(c == null)
|
|
throw new ZZError("Null isStruct?");
|
|
return c.s("d.nile-struct") != null;
|
|
}
|
|
|
|
Nile2Iter() { }
|
|
public Nile2Iter(ZZCursor curs) { set(curs); }
|
|
|
|
public void set(ZZCursor curs) {
|
|
load(curs.get());
|
|
this.offs = curs.getOffs();
|
|
// Ensure positive bias.
|
|
move(0);
|
|
}
|
|
|
|
public void set(Nile2Iter iter) {
|
|
load(iter.c);
|
|
offs = iter.offs;
|
|
}
|
|
|
|
/** Move n characters forwards or backwards within the current
|
|
* paragraph.
|
|
* If the offset given is out of bounds, moves as far as possible
|
|
* and returns false, otherwise returns true.
|
|
*/
|
|
public boolean move(int n) {
|
|
n += offs;
|
|
offs = 0;
|
|
|
|
// Internally, we like to maintain positive bias always.
|
|
// Thus, the greater-OR-EQUALS sign.
|
|
while(n >= clen) {
|
|
n -= clen;
|
|
ZZCell nc = c.s("d.nile");
|
|
if(nc == null || isStruct(nc)) {
|
|
// Only situation for negative bias: end of paragraph.
|
|
offs = clen;
|
|
if(n > 0) return false;
|
|
return true;
|
|
}
|
|
load(nc);
|
|
}
|
|
while(n < 0) {
|
|
ZZCell nc = c.s("d.nile", -1);
|
|
if(nc == null || isStruct(nc)) {
|
|
offs = 0;
|
|
return false;
|
|
}
|
|
load(nc);
|
|
n += clen;
|
|
}
|
|
|
|
offs = n;
|
|
return true;
|
|
}
|
|
|
|
/** Get the character at current offset.
|
|
* Returns -1 if out of bounds.
|
|
*/
|
|
public int cur() {
|
|
if(offs >= clen || offs < 0) return -1;
|
|
return ctext.charAt(offs);
|
|
}
|
|
public int get() { return cur(); }
|
|
|
|
public int next() {
|
|
if(!move(1)) return -1;
|
|
return cur();
|
|
}
|
|
|
|
public int prev() {
|
|
if(!move(-1)) return -1;
|
|
return cur();
|
|
}
|
|
|
|
public int get(int at) {
|
|
ZZCell cache = c;
|
|
int offscache = offs;
|
|
int ret = -1;
|
|
if(move(at)) {
|
|
ret = cur();
|
|
}
|
|
if(c != cache)
|
|
load(cache);
|
|
offs = offscache;
|
|
return ret;
|
|
}
|
|
|
|
/** Move n paragraphs, moving as little as possible.
|
|
* This means that if we move backwards, we will be at the end
|
|
* of the paragraph and vice versa.
|
|
*/
|
|
public boolean shiftPara(int n) {
|
|
// XXX What about empty paragraphs? A LOT of trouble...
|
|
if(n == 0) return true;
|
|
int dir = (n > 0 ? 1 : -1);
|
|
ZZCell nc = c;
|
|
while(n != 0) {
|
|
nc = nc.s("d.nile", dir);
|
|
if(nc == null) return false;
|
|
if(isStruct(nc)) n -= dir;
|
|
}
|
|
// Now we are at the paragraph cell.
|
|
// Need to take one step.
|
|
nc = nc.s("d.nile", dir);
|
|
if(nc == null) {
|
|
return false; // XXX Should set position!
|
|
// throw new ZZError("AUGH! Para shifting null next");
|
|
}
|
|
load(nc);
|
|
offs = (dir < 0 ? clen : 0);
|
|
return true;
|
|
}
|
|
|
|
/** Shift to paragraph beginning or end.
|
|
*/
|
|
public boolean paraEnd(int dir) {
|
|
ZZCell nc = c;
|
|
ZZCell prev = nc;
|
|
while(nc != null && !isStruct(nc)) {
|
|
prev = nc;
|
|
nc = nc.s("d.nile", dir);
|
|
}
|
|
if(nc == null) {
|
|
nc = prev;
|
|
} else {
|
|
nc = nc.s("d.nile", -dir);
|
|
}
|
|
load(nc);
|
|
offs = (dir < 0 ? 0 : clen);
|
|
return true;
|
|
}
|
|
|
|
public void streamEnd(int dir) {
|
|
// XXX When going to beginning, may set pos to struct!
|
|
ZZCell h = c.h("d.nile", dir);
|
|
paraEnd(dir);
|
|
}
|
|
|
|
/** Get the current position with given bias.
|
|
* @param bias Whether we are looking into positive or negative
|
|
* direction. If the cursor is at an edge,
|
|
* this affects which cell is returned.
|
|
*/
|
|
public ZZCursor getCursor(int bias) {
|
|
// XXX Check para?
|
|
if(bias < 0 && offs == 0) {
|
|
ZZCell nc = c.s("d.nile", -1);
|
|
return new ZZCursorVirtual(nc, nc.getText().length());
|
|
}
|
|
return new ZZCursorVirtual(c, offs);
|
|
}
|
|
public ZZCursor getCursor() { return getCursor(1); }
|
|
|
|
|
|
/** Cut the stream at this location.
|
|
* This pointer remains pointing to the prior space,
|
|
* and it returns a iter at the beginning of the new stream.
|
|
*/
|
|
public Nile2Iter cut() {
|
|
move(0);
|
|
Nile2Iter ni = new Nile2Iter();
|
|
if(offs == 0) {
|
|
ZZCell prev = c.getOrNewCell("d.nile", -1);
|
|
prev.disconnect("d.nile", 1);
|
|
ni.load(c, 0);
|
|
load(prev);
|
|
offs = clen;
|
|
return ni;
|
|
}
|
|
if(offs == clen) { // must be very end.
|
|
ZZCell next = c.getOrNewCell("d.nile", 1);
|
|
ni.load(next, 0);
|
|
c.disconnect("d.nile", 1);
|
|
return ni;
|
|
}
|
|
ZZCell spl = split(c, offs, true);
|
|
c.disconnect("d.nile", 1);
|
|
reload();
|
|
ni.load(spl, 0);
|
|
return ni;
|
|
}
|
|
|
|
// XXX Cursors easily cut out!
|
|
|
|
public void join(Nile2Iter to) {
|
|
c.h("d.nile", 1).connect("d.nile", to.c.h("d.nile", -1));
|
|
reload();
|
|
}
|
|
|
|
public void adjustSpaceStart(int nstart) {
|
|
if(dbg) dumpStream();
|
|
Nile2Iter ctr = (Nile2Iter)clone();
|
|
int ncur = 0;
|
|
while(Nile2Unit.ws(ctr.get())) {
|
|
ncur ++; ctr.move(1);
|
|
}
|
|
if(nstart < ncur) {
|
|
move(ncur-nstart);
|
|
// XXX Cursors could go wild.
|
|
set(cut());
|
|
} else if(nstart > ncur) {
|
|
StringBuffer buf = new StringBuffer();
|
|
while(nstart-- > ncur) buf.append(' ');
|
|
ZZCell n = c.N("d.nile", -1);
|
|
n.setText(buf.toString());
|
|
load(n, 0);
|
|
}
|
|
}
|
|
|
|
public void adjustSpaceEnd(int nend) {
|
|
|
|
Nile2Iter ctr = (Nile2Iter)clone();
|
|
ctr.streamEnd(1);
|
|
int ncur = 0;
|
|
while(Nile2Unit.ws(ctr.get(-1))) {
|
|
ncur ++; ctr.move(-1);
|
|
}
|
|
if(nend < ncur) {
|
|
ctr.move(nend); // Move back forwards.
|
|
// XXX Cursors could go wild.
|
|
ctr.cut();
|
|
} else if(nend > ncur) {
|
|
StringBuffer buf = new StringBuffer();
|
|
while(nend-- > ncur) buf.append(' ');
|
|
ZZCell n = ctr.c.h("d.nile", 1).N("d.nile", 1);
|
|
n.setText(buf.toString());
|
|
}
|
|
if(dbg) dumpStream();
|
|
}
|
|
|
|
public void adjustSpaces(int nstart, int nend) {
|
|
p("Adjustspaces: "+nstart+" "+nend);
|
|
adjustSpaceStart(nstart);
|
|
adjustSpaceEnd(nend);
|
|
|
|
}
|
|
|
|
public Object clone() {
|
|
try {
|
|
return super.clone();
|
|
} catch(CloneNotSupportedException e) {
|
|
ZZLogger.exc(e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void dumpStream() {
|
|
pa("Dumping Nile2Iter stream: at offs: "+offs);
|
|
dump(c);
|
|
}
|
|
|
|
/** Returns true if this <= it.
|
|
* Throws an error if not in same stream.
|
|
*/
|
|
public boolean isOrdered(Nile2Iter it) {
|
|
if(c.equals(it.c)) {
|
|
return offs <= it.offs;
|
|
} else {
|
|
if(c.findCell("d.nile", 1, it.c)) return true;
|
|
if(!c.findCell("d.nile", -1, it.c))
|
|
throw new ZZError("IsOrdered: not on same stream!");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void insertInCell(String s) {
|
|
ZZCell spl = split(c, offs, true);
|
|
spl = spl.N("d.nile", -1);
|
|
spl.setText(s);
|
|
load(spl, -1);
|
|
}
|
|
|
|
public void insert(String str) {
|
|
ZZCell n = c;
|
|
if(isStruct(c)) {
|
|
load(c.s("d.nile"), -1);
|
|
ZZCell d = n.N("d.nile");
|
|
d.setText(str);
|
|
ZZLogger.log("AUGH! Insert at struct cell!");
|
|
// throw new ZZError("Inserting");
|
|
} else if((offs == 0 || offs == clen) && c.getSpan() == null) {
|
|
if(offs == clen) {
|
|
c.setText(c.getText() + str);
|
|
load(c, -1);
|
|
} else { // In this case, this iter doesn't move ;)
|
|
ZZCell prev = c.s("d.nile", -1);
|
|
if(isStruct(prev) || prev == null)
|
|
c.N("d.nile", -1).setText(str);
|
|
else
|
|
prev.setText(prev.getText() + str);
|
|
}
|
|
} else {
|
|
if(offs > 0 && offs < clen) split(c, offs, false);
|
|
ZZCell d = c.N("d.nile", offs > 0 ? 1 : -1);
|
|
d.setText(str);
|
|
load(d, -1);
|
|
}
|
|
}
|
|
|
|
/** Make a cell's content into a span. */
|
|
static public Span spanify(ZZCell c0) {
|
|
Span sp = c0.getSpan();
|
|
if(sp != null) return sp;
|
|
|
|
StringScroll sc = c0.getSpace().getStringScroll();
|
|
String s = c0.getText();
|
|
long soffs = sc.append(s);
|
|
sp = Span.create(
|
|
Address.scrollOffs(sc, soffs),
|
|
Address.scrollOffs(sc, soffs + s.length() - 1)
|
|
);
|
|
c0.setSpan(sp);
|
|
return sp;
|
|
}
|
|
|
|
/** Split the given cell into two at the offset offset.
|
|
* The original cell will remain the first.
|
|
* Returns the cell that is after the split.
|
|
* If offset is zero or greater than or equal to the number of characters
|
|
* in c, nothing is done and either the current cell or the next cell is
|
|
* returned.
|
|
* <p>
|
|
* The most special case involves doing this at the very end of the stream,
|
|
* in which case an empty new cell is created and returned.
|
|
* @param c The cursor to split at
|
|
* @param always Whether to split even if the offset is at the beginning
|
|
* or end of a cell. Used e.g. for inserting a paragraph.
|
|
* @return Never null.
|
|
*/
|
|
static public ZZCell split(ZZCell c0, int offs, boolean always) {
|
|
// XXX Adjust cursors!!!
|
|
if(always) {
|
|
ZZCell c1 = c0.N("d.nile");
|
|
if(c0.s("d.nile-transcluded") != null) {
|
|
c0.insert("d.nile-transcluded", 1, c1);
|
|
c0.insert("d.nile-transcluded-split", 1, c1);
|
|
}
|
|
Span sp = c0.getSpan();
|
|
if(sp == null) {
|
|
String s = c0.getText();
|
|
c0.setText(s.substring(0, offs));
|
|
c1.setText(s.substring(offs));
|
|
} else {
|
|
c0.setSpan(sp.subSpan(0, offs));
|
|
c1.setSpan(sp.subSpan(offs));
|
|
}
|
|
return c1;
|
|
} else {
|
|
if(offs == 0) return c0;
|
|
if(offs == c0.getText().length())
|
|
return c0.getOrNewCell("d.nile");
|
|
return split(c0, offs, true);
|
|
}
|
|
}
|
|
|
|
/** Internal convenience: make a connection and a ring if it isn't yet
|
|
* This is part of a cheat to make jumping between transclusions work:
|
|
* we connect all transclusions of a cell on d.nile-transcluded, even
|
|
* if the transcluded spans do not overlap, and jump between them.
|
|
* Works because currently, a cell can contain only one span.
|
|
* XXX what to do when splitting ???
|
|
* XXX if we cheat anyway, is it more appropriate to use zz clones?
|
|
*/
|
|
static private void transcluded(ZZCell from, ZZCell to) {
|
|
from.insert("d.nile-transcluded", 1, to);
|
|
if(from.s("d.nile-transcluded", -1) == null)
|
|
from.connect("d.nile-transcluded", -1, to);
|
|
}
|
|
|
|
|
|
/** Transcopy the part of the stream between two iters.
|
|
* Sets the iters to point to the beginning and the end of the transcopied
|
|
* stream.
|
|
*/
|
|
static public void copy(Nile2Iter start, Nile2Iter to) {
|
|
if(!start.isOrdered(to)) {
|
|
Nile2Iter tmp = to;
|
|
to = start; start = tmp;
|
|
}
|
|
|
|
// First, spanify all cells.
|
|
ZZCell d = start.c;
|
|
while(true) {
|
|
if(!isStruct(d)) spanify(d);
|
|
p("Spanified "+d+" is span "+d.getSpan());
|
|
if(d == to.c) break;
|
|
d = d.s("d.nile");
|
|
}
|
|
|
|
// Next, create the new stream.
|
|
ZZCell first, last;
|
|
if(start.c.equals(to.c)) {
|
|
first = start.c.N();
|
|
last = first;
|
|
first.setSpan(start.c.getSpan().subSpan(start.offs, to.offs));
|
|
transcluded(start.c, first);
|
|
} else {
|
|
first = start.c.N();
|
|
first.setSpan(start.c.getSpan().subSpan(start.offs));
|
|
transcluded(start.c, first);
|
|
|
|
ZZCell prev = first;
|
|
ZZCell here = start.c.s("d.nile");
|
|
while(here != to.c) {
|
|
prev = prev.N("d.nile");
|
|
prev.setSpan(here.getSpan());
|
|
transcluded(prev, here);
|
|
here = here.s("d.nile");
|
|
}
|
|
|
|
last = prev.N("d.nile");
|
|
last.setSpan(to.c.getSpan().subSpan(0, to.offs));
|
|
transcluded(to.c, last);
|
|
}
|
|
|
|
// Finally, set the iters.
|
|
start.set(new ZZCursorVirtual(first, 0));
|
|
to.set(new ZZCursorVirtual(last, last.getText().length() - 1));
|
|
}
|
|
|
|
static public void dump(ZZCell start) {
|
|
if(start == null) {
|
|
pa("Nile stream Null start!");
|
|
return;
|
|
}
|
|
ZZCell init = start.h("d.nile", -1);
|
|
pa("Nile stream! "+start);
|
|
boolean first = true;
|
|
for(ZZCell c = init; c != null; c=c.s("d.nile")) {
|
|
pa((c.s("d.nile-struct") != null ? "***S*** " : " ") +
|
|
" Cell "+c+" "+
|
|
(c.equals(start) ? " <----------------START" : ""));
|
|
if(!first && c.equals(init)) {
|
|
pa(" WAS A LOOOOOOOP!!!!!!!!!!");
|
|
throw new ZZFatalError("LOOPING NILE STREAM! BAD BAD TROUBLE!");
|
|
}
|
|
first = false;
|
|
}
|
|
}
|
|
|
|
static String[] levels = new String[] {
|
|
"P", "H3", "H2", "H1"
|
|
};
|
|
|
|
static int getMaxLevel() { return levels.length - 1; }
|
|
|
|
// XXX Should use clones.
|
|
/** Set the level of a structural cell.
|
|
*/
|
|
static void setStrLevel(ZZCell str, int level) {
|
|
if(level < 0 || level > levels.length)
|
|
throw new ZZError("Request to set invalid level! "+level);
|
|
str.setText(levels[level]);
|
|
}
|
|
|
|
/** The level of structure the given structure cell is.
|
|
* Note that the levels are a bit interesting: it returns
|
|
* zero for paragraphs, 1 for H6, 2 for H5 and so on.
|
|
* -1 is returned for cells that are not at a structural border.
|
|
*/
|
|
static int getStrLevel(ZZCell str) {
|
|
String txt = str.getText();
|
|
for(int i=0; i<levels.length; i++)
|
|
if(txt.equals(levels[i])) return i;
|
|
throw new ZZError("Invalid nile-struct "+txt);
|
|
}
|
|
|
|
|
|
/** Find an angle in the rank, i.e.the next cell on a rank with
|
|
* a connection into a certain direction.
|
|
*/
|
|
static ZZCell findAngle(ZZCell c, String dim, int dir,
|
|
String adim, int adir, boolean includeThis) {
|
|
LoopDetector ld = new LoopDetector();
|
|
if(includeThis && c.s(adim, adir) != null) return c;
|
|
c = c.s(dim, dir);
|
|
while(c != null) {
|
|
if(c.s(adim, adir) != null) return c;
|
|
ld.detect(c);
|
|
c = c.s(dim, dir);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Find the next or previous cell that is a structural level element.
|
|
*/
|
|
static ZZCell findStruct(ZZCell c, int dir, boolean includeThis) {
|
|
ZZCell ret = findAngle(c, "d.nile", dir, "d.nile-struct", 1, includeThis);
|
|
if(ret == null && dir == -1) {
|
|
if(!includeThis && c.s("d.nile", -1) == null)
|
|
return null;
|
|
return c.h("d.nile", -1);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
/** Break a paragraph at the given position.
|
|
* Sets this position to the first position in the possibly
|
|
* empty new paragraph.
|
|
*/
|
|
public void breakParagraph() {
|
|
ZZCell n = split(c, offs, true);
|
|
load(n, 0);
|
|
// INsert the real structural cell between.
|
|
n = n.N("d.nile", -1);
|
|
ZZCell str = n.N("d.nile-struct");
|
|
setStrLevel(str, 0); // XXX Lists...
|
|
}
|
|
|
|
|
|
/** Get the level of a paragraph.
|
|
* @param c Any cell in the paragraph - does not need to be the first.
|
|
*/
|
|
static public int getLevel(ZZCell c) {
|
|
ZZCell para = findStruct(c, -1, true);
|
|
ZZCell str = para.s("d.nile-struct");
|
|
if(str == null) return 6;
|
|
return getStrLevel(str);
|
|
}
|
|
|
|
|
|
|
|
} |