MODULE TextSetters;
(**
	project	= "BlackBox"
	organization	= "www.oberon.ch"
	contributors	= "Oberon microsystems"
	version	= "System/Rsrc/About"
	copyright	= "System/Rsrc/About"
	license	= "Docu/BB-License"
	changes	= "
	- 20070820, bh, Reader.sString eliminated
	- 20160203, center #96, fixed string detection in GatherString for nonASCII texts
	- 20170620, center #163, adding decimal tabs to TextRulers
	"
	issues	= "
	- ...
	"

**)

	(* correct NextPage postcond in docu *)
	(* make s.r, s.rd reducible? *)
	(* paraShutoff needs to be controlled by an approx flag to certain ops (later ...?) *)

	IMPORT
		Ports, Stores, Strings, Views, Properties, TextModels, TextRulers;

	CONST
		(** Pref.opts, options of setter-aware views; 0 overrides 1 **)
		lineBreak* = 0; wordJoin* = 1; wordPart* = 2; flexWidth* = 3;

		tab = TextModels.tab; line = TextModels.line; para = TextModels.para;
		zwspace = TextModels.zwspace; nbspace = TextModels.nbspace;
		hyphen = TextModels.hyphen; nbhyphen = TextModels.nbhyphen;
		digitspace = TextModels.digitspace;
		softhyphen = TextModels.softhyphen;

		mm = Ports.mm;
		minTabWidth = 2 * Ports.point; stdTabWidth = 4 * mm;
		leftLineGap = 2 * Ports.point; rightLineGap = 3 * Ports.point;
		adjustMask = {TextRulers.leftAdjust, TextRulers.rightAdjust};
		centered = {}; leftFlush = {TextRulers.leftAdjust}; rightFlush = {TextRulers.rightAdjust};
		blocked = adjustMask;

		boxCacheLen = 64;
		seqCacheLen = 16;

		paraShutoff = MAX(INTEGER);	(* longest stretch read backwards to find start of paragraph *)
															(* unsafe: disabled *)
		cachedRulers = FALSE;	(* caching ruler objects trades speed against GC effectiveness *)
		periodInWords = FALSE;
		colonInWords = FALSE;

		minVersion = 0; maxVersion = 0; maxStdVersion = 0;


	TYPE
		Pref* = RECORD (Properties.Preference)
			opts*: SET;
			endW*: INTEGER;	(** preset (to width of view) **)
			dsc*: INTEGER	(** preset (to dominating line descender) **)
		END;


		Reader* = POINTER TO ABSTRACT RECORD
			r-: TextModels.Reader;	(** look-ahead state **)
			(** unit **)
			string*: ARRAY 64 OF CHAR;	(** single chars in string[0] **)
			view*: Views.View;
			(** unit props **)
			textOpts*: SET;
			mask*: CHAR;
			setterOpts*: SET;
			w*, endW*, h*, dsc*: INTEGER;
			attr*: TextModels.Attributes;
			(** reading state **)
			eot*: BOOLEAN;
			pos*: INTEGER;
			x*: INTEGER;	(** to be advanced by client! **)
			adjStart*: INTEGER;
			spaces*: INTEGER;
			tabIndex*: INTEGER;	(** tabs being processed; initially -1 **)
			tabType*: SET;	(** type of tab being processed; initially {} **)
			(** line props **)
			vw*: INTEGER;
			hideMarks*: BOOLEAN;
			ruler*: TextRulers.Ruler;
			rpos*: INTEGER
		END;

		Setter* = POINTER TO ABSTRACT RECORD (Stores.Store)
			text-: TextModels.Model;	(** connected iff text # NIL **)
			defRuler-: TextRulers.Ruler;
			vw-: INTEGER;
			hideMarks-: BOOLEAN
		END;


		LineBox* = RECORD
			len*: INTEGER;
			ruler*: TextRulers.Ruler;
			rpos*: INTEGER;
			left*, right*, asc*, dsc*: INTEGER;
			rbox*, bop*, adj*, eot*: BOOLEAN;	(** adj => adjW > 0; adj & blocked => spaces > 0 **)
			views*: BOOLEAN;
			skipOff*: INTEGER;	(** chars in [skipOff, len) take endW **)
			adjOff*: INTEGER;	(** offset of last block in box - adjust only this block **)
			spaces*: INTEGER;	(** valid, > 0 if adj & blocked **)
			adjW*: INTEGER;	(** valid if adj - to be distributed over spaces **)
			tabW*: ARRAY TextRulers.maxTabs OF INTEGER	(** delta width of tabs (<= 0) **)
		END;


		Directory* = POINTER TO ABSTRACT RECORD END;


		Worder = RECORD
			box: LineBox; next: INTEGER;
			i: INTEGER
		END;

		StdReader = POINTER TO RECORD (Reader)
			radixChar: CHAR; (* the current decimal tab's radix character or 0X if not processing a decimal tab *)
		END;

		StdSetter = POINTER TO RECORD (Setter)
			rd: Reader;	(* subject to reduction? *)
			r: TextModels.Reader;	(* subject to reduction? *)
			ruler: TextRulers.Ruler;
			rpos: INTEGER;
			key: INTEGER
		END;

		StdDirectory = POINTER TO RECORD (Directory) END;


	VAR
		dir-, stdDir-: Directory;

		nextKey: INTEGER;
		boxIndex, seqIndex: INTEGER;
		boxCache: ARRAY boxCacheLen OF RECORD
			key: INTEGER;	(* valid iff key > 0 *)
			start: INTEGER;
			line: LineBox	(* inv ruler = NIL *)
		END;
		seqCache: ARRAY seqCacheLen OF RECORD
			key: INTEGER;	(* valid iff key > 0 *)
			start, pos: INTEGER	(* sequence [start, end), end >= pos *)
		END;


	(** Reader **)

	PROCEDURE (rd: Reader) Set* (
		old: TextModels.Reader;
		text: TextModels.Model; x, pos: INTEGER;
		ruler: TextRulers.Ruler; rpos: INTEGER; vw: INTEGER; hideMarks: BOOLEAN
	), NEW, EXTENSIBLE;
	BEGIN
		ASSERT(text # NIL, 20);
		ASSERT(ruler # NIL, 22);
		rd.r := text.NewReader(old); rd.r.SetPos(pos); rd.r.Read;
		rd.string[0] := 0X; rd.view := NIL;
		rd.textOpts := {};
		rd.setterOpts := {}; rd.w := 0; rd.endW := 0; rd.h := 0; rd.dsc := 0;
		rd.attr := NIL;
		rd.eot := FALSE; rd.pos := pos; rd.x := x;
		rd.tabIndex := -1; rd.tabType := {};
		rd.adjStart := pos; rd.spaces := 0;
		rd.ruler := ruler; rd.rpos := rpos; rd.vw := vw; rd.hideMarks := hideMarks
	END Set;

	PROCEDURE (rd: Reader) Read*, NEW, EXTENSIBLE;
	(** pre: rd set **)
	(** post: rd.pos = rd.pos' + Length(rd.string) **)
	BEGIN
		rd.string[0] := rd.r.char; rd.string[1] := 0X;
		rd.view := rd.r.view;
		rd.textOpts := {};
		rd.setterOpts := {};
		rd.w := rd.r.w; rd.endW := rd.w; rd.h := rd.r.h; rd.dsc := 0;
		rd.attr := rd.r.attr;
		rd.eot := rd.r.eot;
		INC(rd.pos);
		rd.r.Read
	END Read;

	PROCEDURE (rd: Reader) AdjustWidth* (start, pos: INTEGER; IN box: LineBox;
		VAR w: INTEGER
	), NEW, ABSTRACT;

	PROCEDURE (rd: Reader) SplitWidth* (w: INTEGER): INTEGER, NEW, ABSTRACT;


	(** Setter **)

	PROCEDURE (s: Setter) CopyFrom- (source: Stores.Store), EXTENSIBLE;
	BEGIN
		WITH source: Setter DO
			s.text := source.text; s.defRuler := source.defRuler;
			s.vw := source.vw;  s.hideMarks := source.hideMarks
		END
	END CopyFrom;

	PROCEDURE (s: Setter) Internalize- (VAR rd: Stores.Reader), EXTENSIBLE;
		VAR thisVersion: INTEGER;
	BEGIN
		s.Internalize^(rd);
		IF rd.cancelled THEN RETURN END;
		rd.ReadVersion(minVersion, maxVersion, thisVersion)
	END Internalize;

	PROCEDURE (s: Setter) Externalize- (VAR wr: Stores.Writer), EXTENSIBLE;
	BEGIN
		s.Externalize^(wr);
		wr.WriteVersion(maxVersion)
	END Externalize;

	PROCEDURE (s: Setter) ConnectTo* (text: TextModels.Model;
		defRuler: TextRulers.Ruler; vw: INTEGER; hideMarks: BOOLEAN
	), NEW, EXTENSIBLE;
	BEGIN
		IF text # NIL THEN
			s.text := text; s.defRuler := defRuler; s.vw := vw; s.hideMarks := hideMarks
		ELSE
			s.text := NIL; s.defRuler := NIL
		END
	END ConnectTo;


	PROCEDURE (s: Setter) ThisPage* (pageH: INTEGER; pageNo: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, 0 <= pageNo **)
	(** post: (result = -1) & (pageNo >= maxPageNo) OR (result = pageStart(pageNo)) **)

	PROCEDURE (s: Setter) NextPage* (pageH: INTEGER; start: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, ThisPage(pageH, pageNo) = start [with pageNo = NumberOfPageAt(start)] **)
	(** post: (result = start) & last-page(start) OR result = next-pageStart(start) **)


	PROCEDURE (s: Setter) ThisSequence* (pos: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, 0 <= pos <= s.text.Length() **)
	(** post: (result = 0) OR (char(result - 1) IN {line, para}) **)

	PROCEDURE (s: Setter) NextSequence* (start: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, ThisSequence(start) = start **)
	(** post: (result = start) & last-line(start) OR (ThisSequence(t, result - 1) = start) **)

	PROCEDURE (s: Setter) PreviousSequence* (start: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, ThisSequence(t, start) = start **)
	(** post: (result = 0) & (start = 0) OR (result = ThisSequence(t, start - 1)) **)


	PROCEDURE (s: Setter) ThisLine* (pos: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, 0 <= pos <= s.text.Length() **)
	(** post: result <= pos, (pos < NextLine(result)) OR last-line(result) **)

	PROCEDURE (s: Setter) NextLine* (start: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, ThisLine(start) = start **)
	(** post: (result = 0) & (start = 0) OR
				(result = start) & last-line(start) OR
				(ThisLine(result - 1) = start) **)

	PROCEDURE (s: Setter) PreviousLine* (start: INTEGER): INTEGER, NEW, ABSTRACT;
	(** pre: connected, ThisLine(start) = start **)
	(** post: (result = 0) & (start = 0) OR (result = ThisLine(start - 1)) **)


	PROCEDURE (s: Setter) GetWord* (pos: INTEGER; OUT beg, end: INTEGER), NEW, ABSTRACT;
	(** pre: connected, 0 <= pos <= s.text.Length() **)
	(** post: c set, beg <= pos <= end **)

	PROCEDURE (s: Setter) GetLine* (start: INTEGER; OUT box: LineBox), NEW, ABSTRACT;
	(** pre: connected, ThisLine(start) = start, 0 <= start <= s.text.Length() **)
	(** post: (c, box) set (=> box.ruler # NIL), (box.len > 0) OR box.eot,
		 0 <= box.left <= box.right <= ruler.right **)

	PROCEDURE (s: Setter) GetBox* (start, end, maxW, maxH: INTEGER;
		OUT w, h: INTEGER
	), NEW, ABSTRACT;
	(** pre: connected, ThisLine(start) = start, 0 <= start <= end <= s.text.Length() **)
	(** post: c set, maxW > undefined => w <= maxW, maxH > undefined => h <= maxH **)


	PROCEDURE (s: Setter) NewReader* (old: Reader): Reader, NEW, ABSTRACT;
	(** pre: connected **)


	PROCEDURE (s: Setter) GridOffset* (dsc: INTEGER; IN box: LineBox): INTEGER, NEW, ABSTRACT;
	(** pre: connected, dsc >= 0: dsc is descender of previous line; dsc = -1 for first line **)
	(** post: dsc + GridOffset(dsc, box) + box.asc = k*ruler.grid (k >= 0) >= ruler.asc + ruler.grid **)


	(** Directory **)

	PROCEDURE (d: Directory) New* (): Setter, NEW, ABSTRACT;


	(* line box cache *)

	PROCEDURE InitCache;
		VAR i: INTEGER;
	BEGIN
		nextKey := 1; boxIndex := 0; seqIndex := 0;
		i := 0; WHILE i < boxCacheLen DO boxCache[i].key := -1; INC(i) END;
		i := 0; WHILE i < seqCacheLen DO seqCache[i].key := -1; INC(i) END
	END InitCache;

	PROCEDURE ClearCache (key: INTEGER);
		VAR i, j: INTEGER;
	BEGIN
		i := 0; j := boxIndex;
		WHILE i < boxCacheLen DO
			IF boxCache[i].key = key THEN boxCache[i].key := -1; j := i END;
			INC(i)
		END;
		boxIndex := j;
		i := 0; j := seqIndex;
		WHILE i < seqCacheLen DO
			IF seqCache[i].key = key THEN seqCache[i].key := -1; j := i END;
			INC(i)
		END;
		seqIndex := j
	END ClearCache;


	PROCEDURE CacheIndex (key, start: INTEGER): INTEGER;
		VAR i: INTEGER;
	BEGIN
RETURN -1;
		i := 0;
		WHILE (i < boxCacheLen) & ~((boxCache[i].key = key) & (boxCache[i].start = start)) DO
			INC(i)
		END;
		IF i = boxCacheLen THEN i := -1 END;
		RETURN i
	END CacheIndex;

	PROCEDURE GetFromCache (s: StdSetter; i: INTEGER; VAR l: LineBox);
	BEGIN
		l := boxCache[i].line;
		IF ~cachedRulers THEN
			IF l.rpos >= 0 THEN
				s.r := s.text.NewReader(s.r); s.r.SetPos(l.rpos); s.r.Read;
				l.ruler := s.r.view(TextRulers.Ruler)
			ELSE l.ruler := s.defRuler
			END
		END
	END GetFromCache;

	PROCEDURE AddToCache (key, start: INTEGER; VAR l: LineBox);
		VAR i: INTEGER;
	BEGIN
		i := boxIndex; boxIndex := (i + 1) MOD boxCacheLen;
		boxCache[i].key := key; boxCache[i].start := start; boxCache[i].line := l;
		IF ~cachedRulers THEN
			boxCache[i].line.ruler := NIL
		END
	END AddToCache;


	PROCEDURE CachedSeqStart (key, pos: INTEGER): INTEGER;
		VAR start: INTEGER; i: INTEGER;
	BEGIN
		i := 0;
		WHILE (i < seqCacheLen)
		& ~((seqCache[i].key = key) & (seqCache[i].start <= pos) & (pos <= seqCache[i].pos)) DO
			INC(i)
		END;
		IF i < seqCacheLen THEN start := seqCache[i].start ELSE start := -1 END;
		RETURN start
	END CachedSeqStart;

	PROCEDURE AddSeqStartToCache (key, pos, start: INTEGER);
		VAR i: INTEGER;
	BEGIN
		i := 0;
		WHILE (i < seqCacheLen) & ~((seqCache[i].key = key) & (seqCache[i].start = start)) DO
			INC(i)
		END;
		IF i < seqCacheLen THEN
			IF seqCache[i].pos < pos THEN seqCache[i].pos := pos END
		ELSE
			i := seqIndex; seqIndex := (i + 1) MOD seqCacheLen;
			seqCache[i].key := key; seqCache[i].pos := pos; seqCache[i].start := start
		END
	END AddSeqStartToCache;


	(* StdReader *)

(*
	PROCEDURE WordPart (ch, ch1: CHAR): BOOLEAN;
	(* needs more work ... put elsewhere? *)
	BEGIN
		CASE ORD(ch) OF
		  ORD("0") .. ORD("9"), ORD("A") .. ORD("Z"), ORD("a") .. ORD("z"),
		  ORD(digitspace), ORD(nbspace), ORD(nbhyphen), ORD("_"),
		  0C0H .. 0C6H, 0E0H .. 0E6H,	(* ~ A *)
		  0C7H, 0E7H,	(* ~ C *)
		  0C8H .. 0CBH, 0E8H .. 0EBH,	(* ~ E *)
		  0CCH .. 0CFH, 0ECH .. 0EFH,	(* ~ I *)
		  0D1H, 0F1H,	(* ~ N *)
		  0D2H .. 0D6H, 0D8H, 0F2H .. 0F6H, 0F8H,	(* ~ O *)
		  0D9H .. 0DCH, 0F9H .. 0FCH,	(* ~ U *)
		  0DDH, 0FDH, 0FFH,	(* ~ Y *)
		  0DFH:	(* ~ ss *)
			RETURN TRUE
		| ORD("."), ORD(":"):
			IF (ch = ".") & periodInWords OR (ch = ":") & colonInWords THEN
				CASE ch1 OF
				  0X, TextModels.viewcode, tab, line, para, " ":
					RETURN FALSE
				ELSE RETURN TRUE
				END
			ELSE RETURN FALSE
			END
		ELSE RETURN FALSE
		END
	END WordPart;
*)

	PROCEDURE WordPart (ch, ch1: CHAR): BOOLEAN;
		(* Same as .net function System.Char.IsLetterOrDigit(ch)
				+ digit space, nonbreaking space, nonbreaking hyphen, & underscore
			ch1 unused *)
		VAR low: INTEGER;
	BEGIN
		low := ORD(ch) MOD 256;
		CASE ORD(ch) DIV 256 OF
		| 001H, 015H, 034H..04CH, 04EH..09EH, 0A0H..0A3H, 0ACH..0D6H, 0F9H, 0FCH: RETURN TRUE
		| 000H: CASE low OF
			| 030H..039H, 041H..05AH, 061H..07AH, 0AAH, 0B5H, 0BAH, 0C0H..0D6H, 0D8H..0F6H, 0F8H..0FFH,
				ORD(digitspace), ORD(nbspace), ORD(nbhyphen), ORD("_"): RETURN TRUE
			ELSE
			END
		| 002H: CASE low OF
			| 000H..041H, 050H..0C1H, 0C6H..0D1H, 0E0H..0E4H, 0EEH: RETURN TRUE
			ELSE
			END
		| 003H: CASE low OF
			| 07AH, 086H, 088H..08AH, 08CH, 08EH..0A1H, 0A3H..0CEH, 0D0H..0F5H, 0F7H..0FFH: RETURN TRUE
			ELSE
			END
		| 004H: CASE low OF
			| 000H..081H, 08AH..0CEH, 0D0H..0F9H: RETURN TRUE
			ELSE
			END
		| 005H: CASE low OF
			| 000H..00FH, 031H..056H, 059H, 061H..087H, 0D0H..0EAH, 0F0H..0F2H: RETURN TRUE
			ELSE
			END
		| 006H: CASE low OF
			| 021H..03AH, 040H..04AH, 060H..069H, 06EH..06FH, 071H..0D3H, 0D5H, 0E5H..0E6H, 0EEH..0FCH, 0FFH: RETURN TRUE
			ELSE
			END
		| 007H: CASE low OF
			| 010H, 012H..02FH, 04DH..06DH, 080H..0A5H, 0B1H: RETURN TRUE
			ELSE
			END
		| 009H: CASE low OF
			| 004H..039H, 03DH, 050H, 058H..061H, 066H..06FH, 07DH, 085H..08CH, 08FH..090H, 093H..0A8H, 0AAH..0B0H, 0B2H, 0B6H..0B9H, 0BDH, 0CEH, 0DCH..0DDH, 0DFH..0E1H, 0E6H..0F1H: RETURN TRUE
			ELSE
			END
		| 00AH: CASE low OF
			| 005H..00AH, 00FH..010H, 013H..028H, 02AH..030H, 032H..033H, 035H..036H, 038H..039H, 059H..05CH, 05EH, 066H..06FH, 072H..074H, 085H..08DH, 08FH..091H, 093H..0A8H, 0AAH..0B0H, 0B2H..0B3H, 0B5H..0B9H, 0BDH, 0D0H, 0E0H..0E1H, 0E6H..0EFH: RETURN TRUE
			ELSE
			END
		| 00BH: CASE low OF
			| 005H..00CH, 00FH..010H, 013H..028H, 02AH..030H, 032H..033H, 035H..039H, 03DH, 05CH..05DH, 05FH..061H, 066H..06FH, 071H, 083H, 085H..08AH, 08EH..090H, 092H..095H, 099H..09AH, 09CH, 09EH..09FH, 0A3H..0A4H, 0A8H..0AAH, 0AEH..0B9H, 0E6H..0EFH: RETURN TRUE
			ELSE
			END
		| 00CH: CASE low OF
			| 005H..00CH, 00EH..010H, 012H..028H, 02AH..033H, 035H..039H, 060H..061H, 066H..06FH, 085H..08CH, 08EH..090H, 092H..0A8H, 0AAH..0B3H, 0B5H..0B9H, 0BDH, 0DEH, 0E0H..0E1H, 0E6H..0EFH: RETURN TRUE
			ELSE
			END
		| 00DH: CASE low OF
			| 005H..00CH, 00EH..010H, 012H..028H, 02AH..039H, 060H..061H, 066H..06FH, 085H..096H, 09AH..0B1H, 0B3H..0BBH, 0BDH, 0C0H..0C6H: RETURN TRUE
			ELSE
			END
		| 00EH: CASE low OF
			| 001H..030H, 032H..033H, 040H..046H, 050H..059H, 081H..082H, 084H, 087H..088H, 08AH, 08DH, 094H..097H, 099H..09FH, 0A1H..0A3H, 0A5H, 0A7H, 0AAH..0ABH, 0ADH..0B0H, 0B2H..0B3H, 0BDH, 0C0H..0C4H, 0C6H, 0D0H..0D9H, 0DCH..0DDH: RETURN TRUE
			ELSE
			END
		| 00FH: CASE low OF
			| 000H, 020H..029H, 040H..047H, 049H..06AH, 088H..08BH: RETURN TRUE
			ELSE
			END
		| 010H: CASE low OF
			| 000H..021H, 023H..027H, 029H..02AH, 040H..049H, 050H..055H, 0A0H..0C5H, 0D0H..0FAH, 0FCH: RETURN TRUE
			ELSE
			END
		| 011H: CASE low OF
			| 000H..059H, 05FH..0A2H, 0A8H..0F9H: RETURN TRUE
			ELSE
			END
		| 012H: CASE low OF
			| 000H..048H, 04AH..04DH, 050H..056H, 058H, 05AH..05DH, 060H..088H, 08AH..08DH, 090H..0B0H, 0B2H..0B5H, 0B8H..0BEH, 0C0H, 0C2H..0C5H, 0C8H..0D6H, 0D8H..0FFH: RETURN TRUE
			ELSE
			END
		| 013H: CASE low OF
			| 000H..010H, 012H..015H, 018H..05AH, 080H..08FH, 0A0H..0F4H: RETURN TRUE
			ELSE
			END
		| 014H: IF low >= 001H THEN RETURN TRUE END
		| 016H: CASE low OF
			| 000H..06CH, 06FH..076H, 081H..09AH, 0A0H..0EAH: RETURN TRUE
			ELSE
			END
		| 017H: CASE low OF
			| 000H..00CH, 00EH..011H, 020H..031H, 040H..051H, 060H..06CH, 06EH..070H, 080H..0B3H, 0D7H, 0DCH, 0E0H..0E9H: RETURN TRUE
			ELSE
			END
		| 018H: CASE low OF
			| 010H..019H, 020H..077H, 080H..0A8H: RETURN TRUE
			ELSE
			END
		| 019H: CASE low OF
			| 000H..01CH, 046H..06DH, 070H..074H, 080H..0A9H, 0C1H..0C7H, 0D0H..0D9H: RETURN TRUE
			ELSE
			END
		| 01AH: IF low < 017H THEN RETURN TRUE END
		| 01DH: IF low < 0C0H THEN RETURN TRUE END
		| 01EH: CASE low OF
			| 000H..09BH, 0A0H..0F9H: RETURN TRUE
			ELSE
			END
		| 01FH: CASE low OF
			| 000H..015H, 018H..01DH, 020H..045H, 048H..04DH, 050H..057H, 059H, 05BH, 05DH, 05FH..07DH, 080H..0B4H, 0B6H..0BCH, 0BEH, 0C2H..0C4H, 0C6H..0CCH, 0D0H..0D3H, 0D6H..0DBH, 0E0H..0ECH, 0F2H..0F4H, 0F6H..0FCH: RETURN TRUE
			ELSE
			END
		| 020H: CASE low OF
			| 071H, 07FH, 090H..094H: RETURN TRUE
			ELSE
			END
		| 021H: CASE low OF
			| 002H, 007H, 00AH..013H, 015H, 019H..01DH, 024H, 026H, 028H, 02AH..02DH, 02FH..031H, 033H..039H, 03CH..03FH, 045H..049H: RETURN TRUE
			ELSE
			END
		| 02CH: CASE low OF
			| 000H..02EH, 030H..05EH, 080H..0E4H: RETURN TRUE
			ELSE
			END
		| 02DH: CASE low OF
			| 000H..025H, 030H..065H, 06FH, 080H..096H, 0A0H..0A6H, 0A8H..0AEH, 0B0H..0B6H, 0B8H..0BEH, 0C0H..0C6H, 0C8H..0CEH, 0D0H..0D6H, 0D8H..0DEH: RETURN TRUE
			ELSE
			END
		| 030H: CASE low OF
			| 005H..006H, 031H..035H, 03BH..03CH, 041H..096H, 09DH..09FH, 0A1H..0FAH, 0FCH..0FFH: RETURN TRUE
			ELSE
			END
		| 031H: CASE low OF
			| 005H..02CH, 031H..08EH, 0A0H..0B7H, 0F0H..0FFH: RETURN TRUE
			ELSE
			END
		| 04DH: IF low < 0B6H THEN RETURN TRUE END
		| 09FH: IF low < 0BCH THEN RETURN TRUE END
		| 0A4H: IF low < 08DH THEN RETURN TRUE END
		| 0A8H: CASE low OF
			| 000H..001H, 003H..005H, 007H..00AH, 00CH..022H: RETURN TRUE
			ELSE
			END
		| 0D7H: IF low < 0A4H THEN RETURN TRUE END
		| 0FAH: CASE low OF
			| 000H..02DH, 030H..06AH, 070H..0D9H: RETURN TRUE
			ELSE
			END
		| 0FBH: CASE low OF
			| 000H..006H, 013H..017H, 01DH, 01FH..028H, 02AH..036H, 038H..03CH, 03EH, 040H..041H, 043H..044H, 046H..0B1H, 0D3H..0FFH: RETURN TRUE
			ELSE
			END
		| 0FDH: CASE low OF
			| 000H..03DH, 050H..08FH, 092H..0C7H, 0F0H..0FBH: RETURN TRUE
			ELSE
			END
		| 0FEH: CASE low OF
			| 070H..074H, 076H..0FCH: RETURN TRUE
			ELSE
			END
		| 0FFH: CASE low OF
			| 010H..019H, 021H..03AH, 041H..05AH, 066H..0BEH, 0C2H..0C7H, 0CAH..0CFH, 0D2H..0D7H, 0DAH..0DCH: RETURN TRUE
			ELSE
			END
		ELSE
		END;
		RETURN FALSE		
	END WordPart;

(*
	PROCEDURE ExtendToEOL (x, right: INTEGER): INTEGER;
	BEGIN
		IF right - x > 5 * mm THEN RETURN right - x ELSE RETURN 5 * mm END
	END ExtendToEOL;
*)

	PROCEDURE Right (ra: TextRulers.Attributes; vw: INTEGER): INTEGER;
	BEGIN
		IF TextRulers.rightFixed IN ra.opts THEN
			RETURN ra.right
		ELSE
			RETURN vw
		END
	END Right;

	PROCEDURE GetViewPref (rd: StdReader);
		CONST maxH = 1600 * Ports.point;
		VAR ra: TextRulers.Attributes; tp: TextModels.Pref; sp: Pref;
	BEGIN
		ra := rd.ruler.style.attr;
		tp.opts := {}; Views.HandlePropMsg(rd.view, tp);
		rd.textOpts := tp.opts; rd.mask := tp.mask;
		sp.opts := {}; sp.dsc := ra.dsc; sp.endW := rd.w; Views.HandlePropMsg(rd.view, sp);
		rd.setterOpts := sp.opts; rd.dsc := sp.dsc; rd.endW := sp.endW;
		IF rd.w >= 10000 * mm THEN rd.w := 10000 * mm END;
		IF (TextModels.hideable IN tp.opts) & rd.hideMarks THEN
			rd.h := 0; sp.dsc := 0;
(*
rd.w := 0;
*)
			IF ~( (rd.view IS TextRulers.Ruler)
					OR (TextModels.maskChar IN rd.textOpts) & (rd.mask = para) ) THEN
				rd.w := 0
			END
(**)
		ELSIF rd.h > maxH THEN rd.h := maxH
		END;
		IF TextModels.maskChar IN rd.textOpts THEN
			rd.string[0] := rd.mask; rd.string[1] := 0X
		ELSE rd.string[0] := TextModels.viewcode
		END
	END GetViewPref;

	PROCEDURE GatherString (rd: StdReader);
		VAR i, len: INTEGER; ch, radixChar: CHAR;
	BEGIN
		i := 1; len := LEN(rd.string) - 1; ch := rd.r.char;
		radixChar := rd.radixChar;
		IF rd.string[0] # radixChar THEN
			WHILE (i < len)
				& (rd.r.view = NIL)
				& ((rd.r.attr = rd.attr) OR ((rd.attr # NIL) & (rd.r.attr # NIL) & rd.attr.Equals(rd.r.attr)))
				& (	 (" " < ch) & (ch <= "~") & (ch # "-")
					OR  (ch = digitspace)
					OR  (ch >= nbspace) & (ch < 100X) & (ch # softhyphen)
					OR Strings.IsAlpha(ch)
					)
				& (ch # radixChar)
			DO	(* rd.r.char > " " => ~rd.eot *)
				rd.string[i] := ch; INC(i);
				rd.eot := rd.r.eot;
				rd.r.Read; ch := rd.r.char; INC(rd.pos)
			END
		END;
		rd.string[i] := 0X; rd.setterOpts := {wordJoin};
		IF i = 1 THEN
			IF WordPart(rd.string[0], 0X) THEN INCL(rd.setterOpts, wordPart) END
		END;
		rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := rd.w
	END GatherString;

	PROCEDURE SpecialChar (rd: StdReader);
		VAR ra: TextRulers.Attributes; i, tabs, spaceW, dW: INTEGER; type: SET;
	BEGIN
		ra := rd.ruler.style.attr;
		CASE ORD(rd.string[0]) OF
		  ORD(tab):
			rd.textOpts := {TextModels.hideable};
			rd.endW := minTabWidth;
			rd.adjStart := rd.pos; rd.spaces := 0;
			(*
			i := 0; WHILE (i < ra.tabs.len) & (ra.tabs.tab[i].stop < rd.x + minTabWidth) DO INC(i) END;
			*)
			i := rd.tabIndex + 1;
			rd.radixChar := 0X;
			IF i < ra.tabs.len THEN
				type := ra.tabs.tab[i].type;
				rd.w := MAX(minTabWidth, ra.tabs.tab[i].stop - rd.x);
				IF TextRulers.barTab IN type THEN
					IF (TextRulers.rightTab IN type) & ~(TextRulers.centerTab IN type) THEN
						rd.w := MAX(minTabWidth, rd.w - leftLineGap)
					ELSIF ~(TextRulers.centerTab IN type) THEN
						INC(rd.w, rightLineGap)
					END
				END;
				IF (TextRulers.rightTab IN type) & (TextRulers.centerTab IN type) THEN
					rd.radixChar := TextRulers.GetRadixChar(ra, i)
				END;
				rd.tabIndex := i; rd.tabType := type
			ELSE	(* for "reasonable" fonts: round to closest multiple of spaces of this font *)
				spaceW := rd.attr.font.SStringWidth(" ");
				IF (1 <= spaceW) & (spaceW <= stdTabWidth) THEN
					rd.w := (stdTabWidth + spaceW DIV 2) DIV spaceW * spaceW
				ELSE
					rd.w := stdTabWidth
				END;
				rd.tabIndex := TextRulers.maxTabs; rd.tabType := {}
			END
		| ORD(line):
			rd.setterOpts := {lineBreak}; rd.w := 0; rd.endW := 0
		| ORD(para):
(*
			IF rd.hideMarks THEN
				rd.w := 0; rd.h := 0; rd.dsc := 0
			ELSE
				rd.w := ExtendToEOL(rd.x, Right(ra, rd.vw)) + 1
			END;
			INC(rd.h, ra.lead);
			rd.textOpts := {TextModels.hideable};
			rd.endW := rd.w
*)
(*
			rd.setterOpts := {lineBreak};
*)
			IF rd.hideMarks THEN rd.h := 0; rd.dsc := 0 END;
			INC(rd.h, ra.lead); rd.textOpts := {TextModels.hideable};
			IF (rd.view = NIL) OR ~(rd.view IS TextRulers.Ruler) THEN
				rd.w := 10000 * Ports.mm (* ExtendToEOL(rd.x, Right(ra, rd.vw)) + 1 *)
			END;
			rd.endW := rd.w
(**)
		| ORD(" "):
			rd.setterOpts := {flexWidth};
			rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := 0; INC(rd.spaces)
		| ORD(zwspace):
			rd.w := 0; rd.endW := 0
		| ORD(digitspace):
			rd.setterOpts := {wordPart};
			rd.w := rd.attr.font.StringWidth("0"); rd.endW := rd.w
		| ORD("-"):
			rd.setterOpts := {};
			rd.w := rd.attr.font.StringWidth("-"); rd.endW := rd.w
		| ORD(hyphen):
			rd.setterOpts := {};
			rd.string[0] := "-" (*softhyphen*);
			rd.w := rd.attr.font.StringWidth("-" (*softhyphen*)); rd.endW := rd.w
		| ORD(nbhyphen):
			rd.setterOpts := {wordJoin, wordPart};
			rd.string[0] := "-" (*softhyphen*);
			rd.w := rd.attr.font.StringWidth("-" (*softhyphen*)); rd.endW := rd.w
		| ORD(softhyphen):
			rd.setterOpts := {wordPart}; rd.textOpts := {TextModels.hideable};
			rd.string[0] := "-";
			rd.endW := rd.attr.font.StringWidth("-" (*softhyphen*));
			IF rd.hideMarks THEN rd.w := 0 ELSE rd.w := rd.endW END
		ELSE
			rd.setterOpts := {wordJoin};
			IF WordPart(rd.string[0], rd.r.char) THEN INCL(rd.setterOpts, wordPart) END;
			rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := rd.w
		END
	END SpecialChar;
(*
	PROCEDURE LongChar (rd: StdReader);
		VAR ra: TextRulers.Attributes;
	BEGIN
		ra := rd.ruler.style.attr;
		rd.setterOpts := {wordJoin, wordPart};
		rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := rd.w
	END LongChar;
*)

	PROCEDURE (rd: StdReader) Read;
	(* pre: connected *)
		VAR ra: TextRulers.Attributes; asc, dsc, w: INTEGER; ch: CHAR;
	BEGIN
		rd.Read^;
		IF ~rd.eot THEN
			IF rd.view = NIL THEN
				rd.attr.font.GetBounds(asc, dsc, w);
				rd.h := asc + dsc; rd.dsc := dsc
			ELSE
				GetViewPref(rd)
			END;
			IF (rd.view = NIL) OR (TextModels.maskChar IN rd.textOpts) THEN
				ch := rd.string[0];
				IF (rd.view = NIL)
				 & (	(" " < ch) & (ch < "~") & (ch # "-")
				 	OR (ch = digitspace)
				 	OR (ch >= nbspace) & (ch # softhyphen)
				  	)
				THEN
					GatherString(rd)
				ELSE
					SpecialChar(rd)
				END
			END
		ELSE
			ra := rd.ruler.style.attr;
			rd.w := 0; rd.endW := 0; rd.h := ra.asc + ra.dsc; rd.dsc := ra.dsc
		END
	END Read;

	PROCEDURE (rd: StdReader) AdjustWidth (start, pos: INTEGER; IN box: LineBox; VAR w: INTEGER);
		VAR i: INTEGER; form: SET;
	BEGIN
		IF box.adj & (pos >= start + box.adjOff) THEN
			form := box.ruler.style.attr.opts * adjustMask;
			IF (form = blocked) & (rd.string[0] = " ") THEN
				INC(w, box.adjW DIV box.spaces)
			ELSIF (form # blocked) & (rd.string[0] = tab) THEN
				INC(w, box.adjW)	(* is this correct ??? *)
			END
		END;
		i := rd.tabIndex;	(* rd.string[0] = tab  =>  i >= 0 *)
		IF (rd.string[0] = tab) & (i < box.ruler.style.attr.tabs.len) THEN
			w := box.tabW[i]
		END
	END AdjustWidth;

	PROCEDURE (rd: StdReader) SplitWidth (w: INTEGER): INTEGER;
	BEGIN
		IF (rd.string[1] = 0X) & (rd.view = NIL) THEN
			RETURN (w + 1) DIV 2
		ELSE RETURN w
		END
	END SplitWidth;


	(* Worder *)

	PROCEDURE SetWorder (VAR w: Worder; s: StdSetter; pos: INTEGER; OUT start: INTEGER);
		CONST wordCutoff = LEN(s.rd.string);
	BEGIN
		start := s.ThisSequence(pos);
		IF pos - start >= wordCutoff THEN
			start := pos; WHILE pos - start < wordCutoff DO start := s.PreviousLine(start) END
		END;
		s.GetLine(start, w.box); w.next := start + w.box.len;
		s.rd.Set(s.r, s.text, w.box.left, start, w.box.ruler, w.box.rpos, s.vw, s.hideMarks);
		w.i := 0; s.rd.string[0] := 0X
	END SetWorder;

	PROCEDURE StepWorder (VAR w: Worder; s: StdSetter; VAR part: BOOLEAN);
		VAR rd: Reader;
	BEGIN
		rd := s.rd;
		IF rd.string[w.i] = 0X THEN
			IF rd.pos < w.next THEN
				rd.Read; w.i := 0
			ELSE
				IF ~w.box.eot THEN
					s.GetLine(w.next, w.box);
					s.rd.Set(s.r, s.text, w.box.left, w.next, w.box.ruler, w.box.rpos, s.vw, s.hideMarks);
					rd.Read; w.i := 0;
					INC(w.next, w.box.len)
				ELSE
					rd.string[0] := 0X
				END
			END
		END;
		IF rd.string[0] = 0X THEN	(* end of text *)
			part := TRUE
		ELSIF rd.string[1] = 0X THEN	(* special character *)
			part := wordPart IN rd.setterOpts; INC(w.i)
		ELSE	(* gathered sString *)
			part := WordPart(rd.string[w.i], rd.string[w.i + 1]); INC(w.i)
		END
	END StepWorder;


	(* StdSetter *)

	PROCEDURE (s: StdSetter) CopyFrom (source: Stores.Store);
	BEGIN
		s.CopyFrom^(source);
		WITH source: StdSetter DO
			s.ruler := source.ruler; s.rpos := source.rpos; s.key := source.key;
			s.rd := NIL; s.r := NIL
		END
	END CopyFrom;

	PROCEDURE (s: StdSetter) Externalize (VAR wr: Stores.Writer);
	BEGIN
		s.Externalize^(wr);
		wr.WriteVersion(maxStdVersion)
	END Externalize;

	PROCEDURE (s: StdSetter) Internalize (VAR rd: Stores.Reader);
		VAR thisVersion: INTEGER;
	BEGIN
		s.Internalize^(rd);
		IF rd.cancelled THEN RETURN END;
		rd.ReadVersion(minVersion, maxStdVersion, thisVersion);
		IF rd.cancelled THEN RETURN END;
		s.text := NIL; s.defRuler := NIL; s.ruler := NIL; s.rd := NIL; s.r := NIL
	END Internalize;


	PROCEDURE (s: StdSetter) ConnectTo (text: TextModels.Model;
		defRuler: TextRulers.Ruler; vw: INTEGER; hideMarks: BOOLEAN
	);
	BEGIN
		s.ConnectTo^(text, defRuler, vw, hideMarks);
		ClearCache(s.key);
		IF text # NIL THEN
			s.ruler := defRuler; s.rpos := -1; s.key := nextKey; INC(nextKey)
		ELSE
			s.ruler := NIL
		END
	END ConnectTo;


	PROCEDURE (s: StdSetter) ThisPage (pageH: INTEGER; pageNo: INTEGER): INTEGER;
	(* pre: connected, 0 <= pageNo *)
	(* post: (result = -1) & (pageNo >= maxPageNo) OR (result = pageStart(pageNo)) *)
		VAR start, prev: INTEGER;
	BEGIN
		ASSERT(s.text # NIL, 20); ASSERT(pageNo >= 0, 21);
		start := 0;
		WHILE pageNo > 0 DO
			prev := start; DEC(pageNo); start := s.NextPage(pageH, start);
			IF start = prev THEN start := -1; pageNo := 0 END
		END;
		RETURN start
	END ThisPage;

	PROCEDURE (s: StdSetter) NextPage (pageH: INTEGER; start: INTEGER): INTEGER;
	(* pre: connected, ThisPage(pageH, x) = start *)
	(* post: (result = s.text.Length()) OR result = next-pageStart(start) *)
		CONST
			noBreakInside = TextRulers.noBreakInside;
			pageBreak = TextRulers.pageBreak;
			parJoin = TextRulers.parJoin;
			regular = 0; protectInside = 1; joinFirst = 2; joinNext = 3; confirmSpace = 4;	(* state *)
		VAR
			box: LineBox; ra: TextRulers.Attributes;
			h, asc, dsc, backup, pos, state: INTEGER; isRuler: BOOLEAN;

		PROCEDURE FetchNextLine;
		BEGIN
			s.GetLine(pos, box);
			IF box.len > 0 THEN
				ra := box.ruler.style.attr; isRuler := box.rpos = pos;
				asc := box.asc + s.GridOffset(dsc, box); dsc := box.dsc; h := asc + dsc
			END
		END FetchNextLine;

		PROCEDURE HandleRuler;
			CONST norm = 0; nbi = 1; pj = 2;
			VAR strength: INTEGER;
		BEGIN
			IF isRuler & (pos > start) & ~(pageBreak IN ra.opts) THEN
				IF parJoin IN ra.opts THEN strength := pj
				ELSIF noBreakInside IN ra.opts THEN strength := nbi
				ELSE strength := norm
				END;
				CASE state OF
				| regular:
					CASE strength OF
					| norm:
					| nbi: state := protectInside; backup := pos
					| pj: state := joinFirst; backup := pos
					END
				| protectInside:
					CASE strength OF
					| norm: state := regular
					| nbi: backup := pos
					| pj: state := joinFirst; backup := pos
					END
				| joinFirst:
					CASE strength OF
					| norm: state := confirmSpace
					| nbi: state := protectInside
					| pj: state := joinNext
					END
				| joinNext:
					CASE strength OF
					| norm: state := confirmSpace
					| nbi: state := protectInside
					| pj:
					END
				| confirmSpace:
					CASE strength OF
					| norm: state := regular
					| nbi: state := protectInside; backup := pos
					| pj: state := joinFirst; backup := pos
					END
				END
			END
		END HandleRuler;

		PROCEDURE IsEmptyLine (): BOOLEAN;
		BEGIN
			RETURN (box.right = box.left)  OR  s.hideMarks & isRuler & ~(pageBreak IN ra.opts)
		END IsEmptyLine;

	BEGIN
		ASSERT(s.text # NIL, 20);
		ASSERT(0 <= start, 21); ASSERT(start <= s.text.Length(), 22);
		pos := start; dsc := -1;
		FetchNextLine;
		IF box.len > 0 THEN
			state := regular;
			REPEAT	(* at least one line per page *)
				HandleRuler; DEC(pageH, h); INC(pos, box.len);
				IF (state = confirmSpace) & ~IsEmptyLine() THEN state := regular END;
				FetchNextLine
			UNTIL (box.len = 0)  OR  (pageH - h < 0)  OR  isRuler & (pageBreak IN ra.opts);
			IF ~isRuler OR ~(pageBreak IN ra.opts) THEN
				WHILE (box.len > 0) & IsEmptyLine() DO	(* skip empty lines at top of page *)
					HandleRuler; INC(pos, box.len); FetchNextLine
				END
			END;
			HandleRuler;
			IF (state # regular) & ~(isRuler & (pageBreak IN ra.opts) OR (box.len = 0)) THEN pos := backup END
		END;
		RETURN pos
	END NextPage;


	PROCEDURE (s: StdSetter) NextSequence (start: INTEGER): INTEGER;
	(* pre: connected, ThisSequence(start) = start *)
	(* post: (result = start) & last-line(start) OR (ThisSequence(t, result - 1) = start) *)
		VAR rd: TextModels.Reader; ch: CHAR;
	BEGIN
		ASSERT(s.text # NIL, 20);
		s.r := s.text.NewReader(s.r); rd := s.r; rd.SetPos(start);
		REPEAT rd.ReadChar(ch) UNTIL rd.eot OR (ch = line) OR (ch = para);
		IF rd.eot THEN RETURN start ELSE RETURN rd.Pos() END
	END NextSequence;

	PROCEDURE (s: StdSetter) ThisSequence (pos: INTEGER): INTEGER;
	(* pre: connected, 0 <= pos <= t.Length() *)
	(* post: (result = 0) OR (char(result - 1) IN {line, para}) *)
		VAR rd: TextModels.Reader; start, limit: INTEGER; ch: CHAR;
	BEGIN
		ASSERT(s.text # NIL, 20); ASSERT(0 <= pos, 21); ASSERT(pos <= s.text.Length(), 22);
		IF pos = 0 THEN
			RETURN 0
		ELSE
			start := CachedSeqStart(s.key, pos);
			IF start < 0 THEN
				s.r := s.text.NewReader(s.r); rd := s.r; rd.SetPos(pos);
				limit := paraShutoff;
				REPEAT rd.ReadPrevChar(ch); DEC(limit)
				UNTIL rd.eot OR (ch = line) OR (ch = para) OR (limit = 0);
				IF rd.eot THEN start := 0 ELSE start := rd.Pos() + 1 END;
				AddSeqStartToCache(s.key, pos, start)
			END;
			RETURN start
		END
	END ThisSequence;

	PROCEDURE (s: StdSetter) PreviousSequence (start: INTEGER): INTEGER;
	(* pre: connected, ThisSequence(t, start) = start *) 
	(* post: (result = 0) & (start = 0) OR (result = ThisSequence(t, start - 1)) *)
	BEGIN
		IF start <= 1 THEN RETURN 0 ELSE RETURN s.ThisSequence(start - 1) END
	END PreviousSequence;


	PROCEDURE (s: StdSetter) ThisLine (pos: INTEGER): INTEGER;
	(* pre: connected *)
		VAR start, next: INTEGER;
	BEGIN
		next := s.ThisSequence(pos);
		REPEAT start := next; next := s.NextLine(start) UNTIL (next > pos) OR (next = start);
		RETURN start
	END ThisLine;

	PROCEDURE (s: StdSetter) NextLine (start: INTEGER): INTEGER;
	(* pre: connected, ThisLine(start) = start *)
	(* post: (result = 0) & (start = 0) OR
				(result = start) & last-line(start) OR
				(ThisLine(result - 1) = start) *)
		VAR box: LineBox; len: INTEGER; i: INTEGER; eot: BOOLEAN;
	BEGIN
		i := CacheIndex(s.key, start);
		IF i >= 0 THEN
			len := boxCache[i].line.len; eot := boxCache[i].line.eot
		ELSE
			s.GetLine(start, box); len := box.len; eot := box.eot
		END;
		IF ~eot THEN RETURN start + len ELSE RETURN start END
	END NextLine;

	PROCEDURE (s: StdSetter) PreviousLine (start: INTEGER): INTEGER;
	(* pre: connected, ThisLine(start) = start *)
	(* post: (result = 0) & (start = 0) OR (result = ThisLine(start - 1)) *)
	BEGIN
		IF start <= 1 THEN start := 0 ELSE start := s.ThisLine(start - 1) END;
		RETURN start
	END PreviousLine;


	PROCEDURE (s: StdSetter) GetWord (pos: INTEGER; OUT beg, end: INTEGER);
	(* pre: connected, 0 <= pos <= s.text.Length() *)
	(* post: beg <= pos <= end *)
		CONST wordCutoff = LEN(s.rd.string);
		VAR w: Worder; part: BOOLEAN;
	BEGIN
		ASSERT(s.text # NIL, 20); ASSERT(0 <= pos, 21); ASSERT(pos <= s.text.Length(), 22);
		SetWorder(w, s, pos, beg); end := beg;
		REPEAT
			StepWorder(w, s, part); INC(end);
			IF ~part THEN beg := end END
		UNTIL end >= pos;
		DEC(end);
		REPEAT
			StepWorder(w, s, part); INC(end)
		UNTIL ~part OR (s.rd.string[0] = 0X) OR (end - beg > wordCutoff)
	END GetWord;

	PROCEDURE (s: StdSetter) GetLine (start: INTEGER; OUT box: LineBox);
		TYPE RadixData = RECORD found: BOOLEAN; right: INTEGER END;
		VAR rd: StdReader; ra: TextRulers.Attributes; brk: LineBox;
			d, off, right, w: INTEGER; i, tabsN: INTEGER; form: SET; adj: BOOLEAN; ch: CHAR;
			boxRadix, brkRadix: RadixData;

		PROCEDURE TrueW (VAR b: LineBox; w: INTEGER): INTEGER;
			VAR i: INTEGER; type: SET;
		BEGIN
			i := rd.tabIndex;
			IF (0 <= i ) & (i < TextRulers.maxTabs) & (rd.string[0] # tab) THEN
				type := rd.tabType * {TextRulers.centerTab, TextRulers.rightTab};
				IF type = {TextRulers.centerTab} THEN
					DEC(w, b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w DIV 2))
				ELSIF type = {TextRulers.rightTab} THEN
					DEC(w, b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w))
				ELSIF type = {TextRulers.centerTab, TextRulers.rightTab} THEN
					IF rd.string[0] = rd.radixChar THEN
						IF boxRadix.found THEN w := 0 END
					ELSIF ~boxRadix.found THEN
						DEC(w, b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w))
					END
				END
			END;
			RETURN w
		END TrueW;

		PROCEDURE Enclose (VAR b: LineBox; w: INTEGER; VAR radix: RadixData);
			VAR off, i, d: INTEGER; type: SET;
		BEGIN
			b.len := rd.pos - start; INC(b.right, w);
			off := rd.attr.offset; i := rd.tabIndex;
			IF rd.h - rd.dsc + off > b.asc THEN b.asc := rd.h - rd.dsc + off END;
			IF rd.dsc - off > b.dsc THEN b.dsc := rd.dsc - off END;
			IF rd.view # NIL THEN b.views := TRUE END;
			IF (0 <= i ) & (i < TextRulers.maxTabs) THEN
				type := rd.tabType * {TextRulers.centerTab, TextRulers.rightTab};
				IF rd.string[0] = tab THEN
					b.tabW[i] := w;
					IF type = {TextRulers.centerTab, TextRulers.rightTab} THEN radix.found := FALSE END
				ELSE
					IF type = {TextRulers.centerTab} THEN
						d := b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w DIV 2);
						DEC(b.tabW[i], d); DEC(b.right, d)
					ELSIF type = {TextRulers.rightTab} THEN
						d := b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w);
						DEC(b.tabW[i], d); DEC(b.right, d)
					ELSIF type = {TextRulers.centerTab, TextRulers.rightTab} THEN
						IF rd.string[0] = rd.radixChar THEN
							IF radix.found THEN w := b.right - radix.right;
								d := b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w);
								DEC(b.tabW[i], d); DEC(b.right, d); radix.right := b.right
							ELSE radix.found := TRUE; radix.right := b.right
							END
						ELSIF ~radix.found THEN
							d := b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w);
							DEC(b.tabW[i], d); DEC(b.right, d)
						END
					END
				END
			END
		END Enclose;

	BEGIN
		ASSERT(s.text # NIL, 20); ASSERT(0 <= start, 21); ASSERT(start <= s.text.Length(), 22);
		i := CacheIndex(s.key, start);
		IF i >= 0 THEN
			GetFromCache(s, i, box)
		ELSE
			TextRulers.GetValidRuler(s.text, start, s.rpos, s.ruler, s.rpos);
			IF s.rpos > start THEN s.ruler := s.defRuler; s.rpos := -1 END;
			box.ruler := s.ruler; box.rpos := s.rpos;
			ra := s.ruler.style.attr; tabsN := ra.tabs.len; right := Right(ra, s.vw);
			s.r := s.text.NewReader(s.r);
			IF start = 0 THEN s.r.SetPos(start); ch := para
			ELSE s.r.SetPos(start - 1); s.r.ReadChar(ch)
			END;
			s.r.Read;

(*
			IF s.r.char = para THEN box.rbox := ~s.hideMarks; box.bop := s.hideMarks; box.left := 0
			ELSIF ch = para THEN box.rbox := FALSE; box.bop := TRUE; box.left := ra.first
			ELSE box.rbox := FALSE; box.bop := FALSE; box.left := ra.left
			END;
*)
			IF s.r.char = para THEN box.rbox := TRUE; box.bop := FALSE; box.left := 0
			ELSIF ch = para THEN box.rbox := FALSE; box.bop := TRUE; box.left := ra.first
			ELSE box.rbox := FALSE; box.bop := FALSE; box.left := ra.left
			END;
(**)
			box.views := FALSE;
			box.asc := 0; box.dsc := 0; box.right := box.left;
			box.len := 0; box.adjOff := 0; box.spaces := 0;
			boxRadix.found := FALSE;
			brk.right := 0;
	
			s.rd := s.NewReader(s.rd); rd := s.rd(StdReader);
			rd.Set(s.r, s.text, box.left, start, box.ruler, box.rpos, s.vw, s.hideMarks);
			rd.Read;
			WHILE ~rd.eot & (box.right + (*rd.w*) TrueW(box, rd.w) <= right)
			& ~(lineBreak IN rd.setterOpts) DO
				IF ~(wordJoin IN rd.setterOpts) & (box.right + rd.endW <= right) THEN
					(*brk := box;*)
					brk.len := box.len; brk.ruler := box.ruler; brk.rpos := box.rpos;
					brk.left := box.left; brk.right := box.right; brk.asc := box.asc; brk.dsc := box.dsc;
					brk.rbox := box.rbox; brk.bop := box.bop; brk.adj := box.adj; brk.eot := box.eot;
					brk.views := box.views; brk.skipOff := box.skipOff; brk.adjOff := box.adjOff;
					brk.spaces := box.spaces; brk.adjW := box.adjW;
					i := 0; WHILE i < tabsN DO brk.tabW[i] := box.tabW[i]; INC(i) END;
					brkRadix := boxRadix;
					(*---*)
					Enclose(brk, rd.endW, brkRadix);
					brk.eot := rd.r.eot	(* rd.r.eot one ahead of rd.eot *)
				END;
				box.adjOff := rd.adjStart - start; box.spaces := rd.spaces;
				Enclose(box, rd.w, boxRadix);
				rd.x := box.right; rd.Read
			END;
			IF (lineBreak IN rd.setterOpts) (* & ~box.rbox *) THEN Enclose(box, 0, boxRadix) END;
			box.eot := rd.eot; adj := FALSE; box.skipOff := box.len;
			IF box.right + rd.w > right THEN	(* rd.w > 0 => ~rd.eot & ~(lineBreak IN setterOpts) *)
				IF ~(wordJoin IN rd.setterOpts) & (box.right + rd.endW <= right) THEN
					IF rd.string[0] = " " THEN DEC(box.spaces) END;
					Enclose(box, rd.endW, boxRadix);
					adj := TRUE
				ELSIF brk.right > 0 THEN
					(*box := brk;*)
					box.len := brk.len; box.ruler := brk.ruler; box.rpos := brk.rpos;
					box.left := brk.left; box.right := brk.right; box.asc := brk.asc; box.dsc := brk.dsc;
					box.rbox := brk.rbox; box.bop := brk.bop; box.adj := brk.adj; box.eot := brk.eot;
					box.views := brk.views; box.skipOff := brk.skipOff; box.adjOff := brk.adjOff;
					box.spaces := brk.spaces; box.adjW := brk.adjW;
					i := 0; WHILE i < tabsN DO box.tabW[i] := brk.tabW[i]; INC(i) END;
					(*---*)
					box.skipOff := box.len - 1; adj := TRUE
				ELSIF box.right = box.left THEN
					Enclose(box, rd.w, boxRadix)	(* force at least one per line *)
				END
			ELSIF (box.right = box.left) & box.eot THEN
				box.asc := ra.asc; box.dsc := ra.dsc	(* force empty line to ruler's default height *)
			END;
	
			box.adj := FALSE;
			d := right - box.right;
			IF d > 0 THEN
				form := ra.opts * adjustMask;
				IF form = blocked THEN
					IF adj & (box.spaces > 0) THEN
						box.right := right; box.adj := TRUE; box.adjW := d
					END
				ELSIF form = rightFlush THEN
					IF box.adjOff > 0 THEN
						box.adjW := d; box.adj := TRUE
					ELSE
						INC(box.left, d)
					END;
					box.right := right
				ELSIF form = centered THEN
					IF box.adjOff > 0 THEN
						box.adjW := d DIV 2; box.adj := TRUE
					ELSE
						INC(box.left, d DIV 2)
					END;
					INC(box.right, d DIV 2)
				END
			END;

			AddToCache(s.key, start, box)
		END;

		ASSERT(box.eot OR (box.len > 0), 100)
	END GetLine;


	PROCEDURE (s: StdSetter) GetBox (start, end, maxW, maxH: INTEGER; OUT w, h: INTEGER);
		VAR box: LineBox; asc, dsc: INTEGER;
	BEGIN
		ASSERT(s.text # NIL, 20);
		ASSERT(0 <= start, 21);
		ASSERT(start <= end, 22);
		ASSERT(end <= s.text.Length(), 23);
		w := 0; h := 0; dsc := -1;
		IF maxW <= Views.undefined THEN maxW := MAX(INTEGER) END;
		IF maxH <= Views.undefined THEN maxH := MAX(INTEGER) END;
		WHILE (start < end) & (h < maxH) DO
			s.GetLine(start, box);
			IF box.rbox THEN w := MAX(w, Right(box.ruler.style.attr, s.vw))
			ELSE w := MAX(w, box.right)
			END;
			asc := box.asc + s.GridOffset(dsc, box); dsc := box.dsc;
			INC(start, box.len); INC(h, asc + dsc)
		END;
		w := MIN(w, maxW); h := MIN(h, maxH)
	END GetBox;


	PROCEDURE (s: StdSetter) NewReader (old: Reader): Reader;
	(* pre: connected *)
		VAR rd: StdReader;
	BEGIN
		ASSERT(s.text # NIL, 20);
		IF (old # NIL) & (old IS StdReader) THEN RETURN old
		ELSE NEW(rd); RETURN rd
		END
	END NewReader;


	PROCEDURE (s: StdSetter) GridOffset (dsc: INTEGER; IN box: LineBox): INTEGER;
		VAR ra: TextRulers.Attributes; h, h0: INTEGER;
		(* minimal possible line spacing h0, minimal legal line spacing h *)
	BEGIN
		IF ~box.rbox THEN
			ra := box.ruler.style.attr;
			IF dsc < 0 THEN
RETURN 0	(* no longer try to correct first line's grid position -- should be done when printing... *)
(*
				h0 := box.asc; h := ra.asc;
				IF h < h0 THEN	(* override legal spacing if to small *)
					h := h - (h - h0) DIV ra.grid * ra.grid	(* adjust to next larger grid line *)
				END;
				RETURN h - h0
*)
			ELSE
				h0 := box.asc + dsc; h := ra.asc + ra.dsc;
				IF h < h0 THEN h := h0 END;	(* override legal spacing if to small *)
				RETURN - (-h) DIV ra.grid * ra.grid - h0	(* adjust to next larger grid line *)
			END
		ELSE
			RETURN 0
		END
	END GridOffset;


	(* StdDirectory *)

	PROCEDURE (d: StdDirectory) New (): Setter;
		VAR s: StdSetter;
	BEGIN
		NEW(s); s.text := NIL; RETURN s
	END New;


	(** miscellaneous **)

	PROCEDURE Init;
		VAR d: StdDirectory;
	BEGIN
		InitCache;
		NEW(d); dir := d; stdDir := d
	END Init;

	PROCEDURE SetDir* (d: Directory);
	BEGIN
		ASSERT(d # NIL, 20); dir := d
	END SetDir;

BEGIN
	Init
END TextSetters.
