ManImpl.mesa
Copyright Ó 1988, 1990, 1991, 1992 by Xerox Corporation. All rights reserved.
Christian Le Cocq July 29, 1988
Bill Jackson (bj), March 22, 1990 8:30 pm PST
Michael Plass, May 27, 1992 11:20 am PDT
Weiser, November 1, 1991 8:41 am PST
Jim Thornton July 20, 1993 2:15 pm PDT
Derived from CedarChest7.0's ManImpl.mesa of 23-Aug-88 12:45:40 PDT
DIRECTORY
Ascii USING [Digit],
Commander,
CommanderOps,
ImagerFont USING [Find, Font, RopeEscapement],
IO,
NodeProps USING [PutProp],
PFS,
PFSNames,
UserProfile USING [Token, Number],
Real USING [Fix],
RefText USING [ObtainScratch, ReleaseScratch],
Rope,
SymTab USING [Create, Delete, Fetch, Ref, Store, Val],
TextNode USING [Ref],
TiogaFileOps USING [AddLooks, CreateRoot, InsertAsLastChild, Ref, SetContents, SetFormat, SetStyle, Store],
TiogaMenuOps USING [Open, Load],
Vector2 USING [VEC],
ViewerClasses USING [Viewer],
ViewerOps USING [CreateViewer, OpenIcon, SetViewer, PaintViewer],
RuntimeError USING [BoundsFault];
translates (most of the) troff -man source into Tioga document format
ManImpl: CEDAR PROGRAM
IMPORTS Ascii, Commander, CommanderOps, ImagerFont, IO, NodeProps, PFS, PFSNames, UserProfile, Real, RefText, Rope, SymTab, TiogaFileOps, TiogaMenuOps, ViewerOps, RuntimeError ~ {
ROPE: TYPE ~ Rope.ROPE;
File Stuff
FileError: ERROR [huh: ROPE] ~ CODE;
FileInfo: PROC [fileName: ROPE] ~ {
ENABLE PFS.Error => { ERROR FileError[error.explanation] };
path: PFSNames.PATH ~ PFS.PathFromRope[fileName];
fullPath: PFSNames.PATH ¬ PFS.FileLookup[path, NIL];
IF ( fullPath = NIL ) THEN FileError["File not found"];
};
StreamOpen: PROC [fileName: ROPE] RETURNS [s: IO.STREAM] ~ {
ENABLE PFS.Error => { ERROR FileError[error.explanation] };
path: PFSNames.PATH ~ PFS.PathFromRope[fileName];
s ¬ PFS.StreamOpen[path];
};
OpenReadStream: PROC [fileName: ROPE] RETURNS [s: IO.STREAM] ~ {
ENABLE PFS.Error => { ERROR FileError[error.explanation] };
path: PFSNames.PATH ~ PFS.PathFromRope[fileName];
s ¬ PFS.StreamOpen[path];
IF ( shortName.Fetch[] # '/ ) THEN manDir.Cat[slash, shortName];
};
OpenRelativeStream: PROC [shortName: ROPE, openStream: IO.STREAM] RETURNS [s: IO.STREAM] ~ {
Open a STREAM to a file specified with a relative path (eg. man3/textdomain.3). The correct file is found relative to the file that is already open. This procedure is used only for included files.
ENABLE PFS.Error => { ERROR FileError[error.explanation] };
path: PFSNames.PATH ~ PFS.PathFromRope[shortName];
currentFile: PFS.OpenFile ~ PFS.OpenFileFromStream[openStream];
fullPath: PFS.PATH ~ PFS.GetInfo[currentFile].fullFName;
openPath: PFS.PATH ~ PFSNames.Cat[PFSNames.Parent[PFSNames.Parent[fullPath]], path];
s ¬ PFS.StreamOpen[openPath];
};
PATH: TYPE ~ PFSNames.PATH;
shortPattern: PATH ~ PFS.PathFromRope["man*"];
NonStandard: ERROR; -- Indicates a nonstandard manpage file name
PageNameFromFile: PROC [fileName: ROPE] RETURNS [name: ROPE ¬ NIL, section: ROPE ¬ NIL] ~ {
extPos: INT ~ fileName.FindBackward["."];
namePos: INT ~ fileName.FindBackward["/"];
IF extPos = -1 OR namePos = -1 OR namePos >= (extPos - 1) THEN {
ERROR NonStandard;
}
ELSE {
name ¬ fileName.Substr[namePos+1, extPos - namePos - 1];
section ¬ fileName.Substr[extPos+1, fileName.Length[]-extPos];
}
};
ManSubDirectories: PROC [cmd: Commander.Handle, section: ROPE] RETURNS [dirs: LIST OF PFSNames.PATH] ~ {
RecordDirectoryName: PFS.InfoProc ~ {
IF fileType = PFS.tDirectory THEN {
rope: ROPE ¬ IF section # NIL THEN PFS.RopeFromPath[fullFName] ELSE NIL;
IF section = NIL OR section.Fetch[0] = rope.Fetch[rope.Length[]-1] THEN {
dirs ¬ CONS[fullFName, dirs];
}
}
};
manPath: ROPE ¬ "/usr/man";
j: INT ¬ 0;
WITH CommanderOps.GetProp[cmd, $MANPATH] SELECT FROM
rope: ROPE => manPath ¬ rope;
ENDCASE;
FOR i: INT ¬ 0, j+1 UNTIL i > Rope.Size[manPath] DO
j ¬ Rope.SkipTo[manPath, i, ":"];
IF i < j THEN {
dir: PATH ~ PFS.PathFromRope[Rope.Substr[manPath, i, j-i]];
path: PATH ~ PFSNames.Cat[dir, shortPattern];
PFS.EnumerateForInfo[path, RecordDirectoryName !
PFS.Error => CONTINUE];
};
ENDLOOP;
};
FindMatches: PROC [cmd: Commander.Handle, cmdName: ROPE, mode: ATOM, section: ROPE] RETURNS [files: LIST OF ROPE ¬ NIL] ~ {
n: NAT ¬ 0;
failed: BOOL ¬ FALSE;
oldSection: ROPE ¬ NIL;
RecordFileName: PFS.NameProc ~ {
ENABLE
NonStandard => GOTO Punt;
rope: ROPE ~ PFS.RopeFromPath[name];
IF ( mode = $First ) THEN {
IF files = NIL THEN {
files ¬ CONS[rope, files];
[section: oldSection] ¬ PageNameFromFile[rope];
}
ELSE {
section: ROPE;
[section: section] ¬ PageNameFromFile[rope];
IF Rope.Compare[section, oldSection, FALSE] = less THEN {
files.first ¬ rope;
[section: oldSection] ¬ PageNameFromFile[rope];
};
}
}
ELSE {
files ¬ CONS[rope, files];
IF ( (n ¬ n.SUCC) >= UserProfile.Number["Man.MaxMatches", 10] ) THEN { failed ¬ TRUE; continue ¬ FALSE; RETURN }
}
EXITS
Punt => RETURN;
};
dirs: LIST OF PFSNames.PATH ~ ManSubDirectories[cmd, section];
FOR tail: LIST OF PFSNames.PATH ¬ dirs, tail.rest WHILE ( tail # NIL ) DO
pathRope: ROPE ~ IF section = NIL THEN cmdName.Concat[".*"] ELSE cmdName.Concat[Rope.Concat[".", section.Concat["*"]]];
path: PATH ~ PFSNames.Cat[tail.first, PFS.PathFromRope[pathRope]];
PFS.EnumerateForNames[path, RecordFileName ! PFS.Error => CONTINUE];
IF ( failed ) THEN {
SIGNAL TooMany;
EXIT;
};
IF ( n > 0 AND mode = $First ) THEN EXIT;
ENDLOOP;
};
manifest constants
manHost: ROPE ¬ "localhost";
tmpDir: ROPE ~ "/tmp/";
TemporaryFile: PROC [short: ROPE] RETURNS [t: ROPE] ~ {
NoSlash: PROC [old: CHAR] RETURNS [CHAR] = {RETURN [IF old='/ THEN '# ELSE old]};
t ¬ Rope.Concat[tmpDir, Rope.Translate[base: short, translator: NoSlash]];
};
blank: ROPE ~ " ";
slash: ROPE ~ "/";
cedarStyle: ROPE ~ "cedar";
times: ROPE ~ "Xerox/TiogaFonts/TimesRoman";
Text/Tioga Formats
abstract: ROPE ~ "abstract";
block: ROPE ~ "block";
body: ROPE ~ "body";
head: ROPE ~ "head";
item: ROPE ~ "item";
title: ROPE ~ "title";
Types & Globals
verbose: BOOL ¬ FALSE;
emPeru: REAL ¬ 1.0;
emPeri: REAL ¬ 0.02;
Cmd: TYPE ~ { noCmd, as, b, bi, br, ds, fi, ft, hp, i, ib, ip, ir, lp, ne, nf, nx, pp, re, rb, ri, rs, rm, rn, sb, sh, sm, so, ss, ta, th, tp, tx, notImplem, comment, other };
Look: TYPE ~ RECORD [ start, len: INT, look: CHAR ['a..'z] ];
Data: TYPE ~ REF DataRec;
DataRec: TYPE ~ RECORD [
root: TiogaFileOps.Ref ¬ NIL,
nodeStack: LIST OF TiogaFileOps.Ref ¬ NIL,
symbols: SymTab.Ref,
format: ROPE ¬ NIL,
txt: ROPE ¬ NIL,
props: LIST OF Look ¬ NIL,
level: NAT ¬ 1,
prevLook: CHAR ¬ ' ,
escC,
muteComments: BOOL ¬ FALSE,
fill: BOOL ¬ TRUE,
badOps: ROPE ¬ NIL
];
Operations
Parse: PUBLIC PROC [s, err: IO.STREAM, root: TiogaFileOps.Ref] ~ {
line: ROPE;
cmd: Cmd ¬ noCmd;
badOp: ROPE;
data: Data ¬ NEW[DataRec ¬ [root, LIST[TiogaFileOps.InsertAsLastChild[root]], CreateTab[]]];
TiogaFileOps.SetStyle[root, cedarStyle];
UNTIL IO.EndOf[s] DO
[cmd, line, badOp] ¬ ParseRequest[s, err, data];
SELECT cmd FROM
noCmd => AddText[line, data];
as => AddToSymbol[line, data];
b => Bold[line, data];
bi => JoinBI[line, data];
br => JoinBR[line, data];
ds => DefSymbol[line, data];
fi => Fill[data];
ft => ChFont[line, data];
hp => Item[NIL, data];
i => Italics[line, data];
ib => JoinIB[line, data];
ip => Item[line, data];
ir => JoinIR[line, data];
lp => Break[data];
nf => NoFill[data];
nx => s ¬ Next[line, s, err];
pp => Break[data];
re => BreakAndPop[data];
rb => JoinRB[line, data];
ri => JoinRI[line, data];
rm => RemoveSymbol[line, data];
rn => RenameSymbol[line, data];
rs => BreakAndPush[data];
so => Source[line, s, err, root];
sb => SmallBold[line, data];
sh => Head[line, data];
sm => Small[line, data];
ss => SubHead[line, data];
th => Title[line, data];
tp => Item[NIL, data];
tx => AddText[line, data]; -- Don't know how to "resolve" titles
notImplem => BadOp[badOp, data]; --explicitly discard these macro
comment => Comments[line, data];
other => BadOp[badOp, data]; -- by default
ENDCASE => ERROR; --Cmd definition is not compatible with Parse
IF line#NIL AND data.fill THEN CloseLook[' , NIL, data];
ENDLOOP;
[] ¬ Validate[data];
IF (NOT verbose AND data.badOps # NIL) THEN IO.PutF1[err, "Undefined ops: %g\n", IO.rope[data.badOps]];
};
Source: PROC [line: ROPE, current: IO.STREAM, err: IO.STREAM, root: TiogaFileOps.Ref] ~ {
s: IO.STREAM ¬ Next[line, current, err];
IF ( s # NIL ) THEN Parse[s, err, root];
};
Next: PROC [line: ROPE, current: IO.STREAM, err: IO.STREAM] RETURNS [s: IO.STREAM ¬ NIL] ~ {
ris: IO.STREAM ~ IO.RIS[line];
shortName: ROPE ~ MyGetRawToken[ris];
ris.Close[];
IF ( shortName = NIL ) THEN RETURN;
IF (shortName.Fetch[] # '/) THEN {
s ¬ OpenRelativeStream[shortName, current ! FileError => { IO.PutRope[err, huh]; GOTO Failed }]
}
ELSE {
s ¬ OpenReadStream[shortName ! FileError => { IO.PutRope[err, huh]; GOTO Failed }]
};
EXITS Failed => NULL;
};
MyGetRawToken: PROC [s: IO.STREAM] RETURNS [token: ROPE] ~ {
[] ¬ IO.SkipWhitespace[s];
IF IO.PeekChar[s ! IO.EndOfStream => GOTO NoToken]='" THEN {
[] ¬ IO.GetChar[s]; --consumes the leading "
token ← IF IO.EndOf[s] THEN NIL ELSE IO.GetTokenRope[s, LiteralProc].token;
[] ← IO.GetChar[s ! IO.EndOfStream => CONTINUE]; --consumes the trailing ", if any
IF NOT IO.EndOf[s] AND IO.PeekChar[s]='" THEN token ¬ token.Cat["""", MyGetRawToken[s]]; -- double "
}
ELSE token ← IO.GetTokenRope[s, SpaceProc].token;
IF token#NIL AND token.Fetch[token.Length[]-1]='\\ THEN {
token ← token.Substr[0, token.Length[]-1];
token ← token.Cat[blank, MyGetRawToken[s]];
};
EXITS
NoToken => token ← NIL;
};
MyGetToken: PROC [s: IO.STREAM, data: Data] RETURNS [token: ROPE] ~ {
token ← MyGetRawToken[s];
token ← Filter[token, data];
};
GetSixWords: PROC [s: IO.STREAM, data: Data] ~ {
n: NAT ← 0;
WHILE NOT IO.EndOf[s] DO
data.txt ← Rope.Cat[data.txt, MyGetToken[s, data], blank];
n ← n+1;
IF n=6 THEN RETURN;
ENDLOOP;
};
MySkipWhitespace: PROC [s: IO.STREAM] ~ {
WHILE NOT IO.EndOf[s] DO
c: CHARIO.GetChar[s ! IO.EndOfStream => GOTO Eof];
SELECT c FROM
' , '\t => NULL;
ENDCASE => {IO.Backup[s, c]; RETURN};
ENDLOOP;
EXITS
Eof => NULL;
};
MyGetLineRope: PROC [stream: IO.STREAM] RETURNS [line: ROPENIL] = {
bufMax: NAT ~ 256;
buffer: REF TEXT ~ RefText.ObtainScratch[bufMax];
bLen: NAT ← 0;
chars: INT ← 0;
{ ENABLE UNWIND => RefText.ReleaseScratch[buffer];
DO
char: CHAR
IO.GetChar[stream ! IO.EndOfStream => IF chars > 0 THEN EXIT ELSE REJECT];
IF char = '\l THEN {-- instead of \n
IF bLen>0 AND buffer[bLen-1]='\\ THEN {
bLen ← bLen-1;
char ← ' ; --concealed \l
}
ELSE EXIT;
};
chars ← chars + 1;
IF bLen = bufMax THEN {
buffer.length ← bLen;
line ← Rope.Concat[line, Rope.FromRefText[buffer]];
bLen ← 0;
};
buffer[bLen] ← char;
bLen ← bLen+1;
ENDLOOP;
};
buffer.length ← bLen;
IF bLen # 0 THEN line ← Rope.Concat[line, Rope.FromRefText[buffer]];
RefText.ReleaseScratch[buffer];
RETURN [line];
};
GetCmd: PROC [s: IO.STREAM] RETURNS [cmd: ROPENIL] ~ {
buffer: REF TEXT ~ RefText.ObtainScratch[2];
buffer[0] ← IO.GetChar[s];
IF ( buffer[0] IN [IO.NUL..IO.SP] ) THEN {
RefText.ReleaseScratch[buffer];
RETURN;
}; -- no cmd on the line
buffer[1] ← IO.GetChar[s];
buffer.length ← IF ( buffer[1] IN [IO.NUL..IO.SP] ) THEN 1 ELSE 2;
cmd ← Rope.FromRefText[buffer];
RefText.ReleaseScratch[buffer];
};
ParseRequest: PROC [s: IO.STREAM, err: IO.STREAM, data: Data] RETURNS [cmd: Cmd ← noCmd, text: ROPENIL, badOp: ROPE ¬ NIL] ~ {
IF IO.EndOf[s] OR IO.PeekChar[s] # '. THEN { text ← MyGetLineRope[s]; RETURN };
[] ← IO.GetChar[s]; --skip the "."
MySkipWhitespace[s];
{
cmdName: ROPE ← GetCmd[s];
IF ( cmdName = NIL ) THEN { cmd ← comment; RETURN };
MySkipWhitespace[s];
IF ( NOT IO.EndOf[s] ) THEN text ← MyGetLineRope[s];
WITH data.symbols.Fetch[cmdName].val SELECT FROM
refC: REF Cmd => cmd ← refC^;
ENDCASE => {
cmd ← other;
badOp ¬ Rope.Concat[".", cmdName];
IF ( err # NIL AND verbose) THEN {
IO.PutFL[err, "op [.%g] undefined\n", LIST[IO.rope[cmdName]] ];
IO.PutFL[err, " text: %g\n", LIST[IO.rope[text]] ];
};
};
IF ( cmd # comment ) THEN data.muteComments ← TRUE;
keeps only the first comments
}
};
NextTwo: PROC [s: IO.STREAM] RETURNS [pair: ROPE] ~ INLINE {
one: CHAR ~ IO.GetChar[s];
two: CHAR ~ IO.GetChar[s];
pair ← Rope.FromChar[one];
pair ← pair.Concat[Rope.FromChar[two]];
};
Filter: PROC [line: ROPE, data: Data] RETURNS [token: ROPENIL] ~ {
s: IO.STREAMIO.RIS[line];
deltaPos: INT ← 0;
DO
IF NOT IO.EndOf[s] AND IO.PeekChar[s] # '\\ THEN token ← token.Concat[IO.GetTokenRope[s, EscapeProc].token];
IF NOT IO.EndOf[s] AND IO.PeekChar[s] = '\\ THEN {
c: CHARIO.GetChar[s]; --skip the escape
c ← IO.GetChar[s];
SELECT c FROM
'0 => token ← token.Concat["\031"]; -- 031C
'| => token ← token.Concat["\035"]; -- 035C
'^ => NULL;
'& => NULL;
'* => {
c: CHARIO.GetChar[s];
symb: ROPEIF ( c = '( ) THEN NextTwo[s] ELSE Rope.FromChar[c];
WITH data.symbols.Fetch[symb].val SELECT FROM
text: ROPE => {
index: INT ~ IO.GetIndex[s];
line ← text.Concat[line.Substr[index]];
s ← IO.RIS[line];
};
ENDCASE => { NULL };
};
'c => data.escC ← TRUE;
'd => {
deltaPos ← deltaPos+1;
SELECT deltaPos FROM
0 => CloseLook['u, token, data];
ENDCASE => OpenLook['d, token, data];
};
'e => token ← token.Concat["\\"];
'h => {
size: REAL;
MySkipWhitespace[s];
IF IO.GetChar[s]='' THEN {
padding: ROPE;
nChars: INT;
c: CHAR ¬ IO.PeekChar[s];
IF c='\\ OR c = '| THEN size ← Width[s, data]
ELSE size ← IO.GetReal[s ! IO.Error => GOTO noWidth];
c ← IO.GetChar[s];
SELECT c FROM
'' => IO.Backup[s, c]; -- m is default
'm => NULL;
'u => nChars ← Real.Fix[size/emPeru];
'i => nChars ← Real.Fix[size/emPeri];
ENDCASE => NULL; --Well..
c ← IO.GetChar[s]; -- should be '', good spot for a breakpoint
THROUGH [1..nChars] DO
padding ← Rope.Concat[padding, "\035"]; -- 035C
ENDLOOP;
token ← token.Concat[padding];
EXITS noWidth => {}
}
};
'u => {
deltaPos ← deltaPos-1;
SELECT deltaPos FROM
0 => CloseLook['d, token, data];
ENDCASE => OpenLook['u, token, data];
}; -- up 1/2 line
't => token ← token.Concat["\t"];
'\l => token ← token.Concat[blank];
'f => {
font: CHARIO.GetChar[s];
SELECT font FROM
'R, '1 => ChangeLook['r, token, data];
'I, '2 => ChangeLook['i, token, data];
'B, '3 => ChangeLook['b, token, data];
'P => ChangeLook[data.prevLook, token, data];
ENDCASE => NULL; -- IF debug THEN ...
};
's => {
size: CHARIO.GetChar[s];
SELECT size FROM
'0 => {CloseLook['s, token, data]; CloseLook['l, token, data];};
'- => {OpenLook['s, token, data]; [] ← IO.GetChar[s];};
'+ => {OpenLook['l, token, data]; [] ← IO.GetChar[s];};
ENDCASE => NULL; -- IF debug THEN ...
};
'( => {
c ← IO.GetChar[s];
SELECT c FROM
'* => {
cc: CHARIO.GetChar[s];
IF cc='* THEN token ← token.Concat["*"]
ELSE {--greek char
OpenLook['g, token, data];
token ← token.Concat[Rope.FromChar[cc]];
CloseLook['g, token, data];
}; -- they are not mapped quite the same way as ours, oh well...
};
'a => IF IO.GetChar[s]='p THEN token ← token.Concat["~"];
'b => IF IO.GetChar[s]='u THEN token ← token.Concat["%"];
'e => {
SELECT IO.GetChar[s] FROM
'm => token ← token.Concat["%"];
'q => token ← token.Concat["="];
ENDCASE;
};
'g => IF IO.GetChar[s]='a THEN token ← token.Concat["`"];
'l => IF IO.GetChar[s]='q THEN token ← token.Concat["``"];
'm => IF IO.GetChar[s]='i THEN token ← token.Concat["-"];
'p => IF IO.GetChar[s]='l THEN token ← token.Concat["+"];
'r => IF IO.GetChar[s]='q THEN token ← token.Concat["''"];
ENDCASE => { -- display unknown escapes in fixed pitch
OpenLook['f, token, data];
token ← token.Cat[Rope.FromChar[c], Rope.FromChar[IO.GetChar[s]]];
CloseLook['f, token, data];
};
};
ENDCASE => token ← token.Concat[Rope.FromChar[c]];
}
ELSE RETURN;
ENDLOOP;
};
Width: PROC [s: IO.STREAM, data: Data] RETURNS [size: REAL ← 0] ~ {
newData: Data ← NEW[DataRec ← [symbols: data.symbols, txt: data.txt]];
props: LIST OF Look ← data.props;
rope: ROPE;
IF IO.PeekChar[s]='| THEN [] ¬ IO.GetChar[s]; -- dunno what | means
IF IO.GetChar[s]#'\\ THEN RETURN;
IF IO.GetChar[s]#'w THEN RETURN;
IF IO.GetChar[s]#'' THEN RETURN;
rope ← IO.GetTokenRope[s, QuoteProc].token;
IF IO.GetChar[s]#'' THEN RETURN;
UNTIL props=NIL DO
IF props.first.len=0 THEN OpenLook[props.first.look, NIL, newData];
props ← props.rest;
ENDLOOP;
IF newData.props=NIL THEN OpenLook['r, NIL, newData]; -- at least give it the r look
rope ← Filter[rope, newData];
CloseLook[' , NIL, newData];
UNTIL newData.props=NIL DO
fname: ROPE ~ times.Concat[SELECT newData.props.first.look FROM 'i => "10I", 'b => "10B", 's => "8", ENDCASE => "10"];
font: ImagerFont.Font ← ImagerFont.Find[fname];
IF newData.props.first.start IN [0..rope.Size) THEN {
vec: Vector2.VEC ~ ImagerFont.RopeEscapement[font, rope, newData.props.first.start, newData.props.first.len];
size ← size+vec.x;
};
newData.props ← newData.props.rest;
ENDLOOP;
};
GetLine: PROC [s: IO.STREAM] RETURNS [line: ROPE] ~ {
line ← IO.GetTokenRope[s, LineProc ! IO.EndOfStream => GOTO Return].token;
IF line.Fetch[line.Length[]-1]='\\ THEN -- "concealed" newline
line ← line.Cat[blank, GetLine[s]];
EXITS
Return => {};
};
Looks Procs
ChangeLook: PROC [char: CHAR, txt: ROPE, data: Data] ~ {
CloseLook[' , txt, data];
OpenLook[char, txt, data];
};
OpenLook: PROC [char: CHAR, txt: ROPE, data: Data] ~ {
start: INT ← Rope.Length[data.txt];
addLen: INT ← Rope.Length[txt];
IF char IN ['a..'z] THEN data.props ← CONS[[start+addLen, 0, char], data.props];
};
CloseLook: PROC [char: CHAR, txt: ROPE, data: Data] ~ {
start: INT ← Rope.Length[data.txt];
addLen: INT ← Rope.Length[txt];
props: LIST OF Look ← data.props;
UNTIL props=NIL DO
IF char=' THEN {
IF props.first.len=0 THEN
IF props.first.look#'d AND props.first.look#'u THEN {
props.first.len ← addLen+start-props.first.start;
data.prevLook ← props.first.look;
RETURN;
}
}
ELSE IF props.first.look=char THEN {
validClose: BOOL ← addLen+start-props.first.len-props.first.start<0 OR props.first.len=0;
IF validClose THEN {
props.first.len ← addLen+start-props.first.start;
data.prevLook ← props.first.look;
};
};
props ← props.rest;
ENDLOOP;
IF char=' THEN data.prevLook ← 'r;
};
AddLook: PROC [char: CHAR, txt: ROPE, data: Data] ~ {
start: INT ← Rope.Length[data.txt];
len: INT ← Rope.Length[txt];
IF char IN ['a..'z] THEN data.props ← CONS[[start, len, char], data.props];
};
OneLook: PROC [line: ROPE, char: CHAR, data: Data] ~ {
AddLook[char, line, data];
data.txt ← Rope.Concat[data.txt, line];
};
DoAddOneLook: PROC [line: ROPE, char: CHAR, data: Data] ~ {
IF line=NIL THEN OpenLook[char, NIL, data]
ELSE {
ris: IO.STREAMIO.RIS[line];
UNTIL IO.EndOf[ris] DO
token: ROPE ← MyGetToken[ris, data];
IF NOT Rope.IsEmpty[token] THEN OneLook[token, char, data];
IF data.escC THEN data.escC ← FALSE
ELSE data.txt ← Rope.Concat[data.txt, blank];
ENDLOOP;
};
};
DoAddTwoLook: PROC [line: ROPE, char1, char2: CHAR, data: Data] ~ {
IF line=NIL THEN {
OpenLook[char1, NIL, data];
OpenLook[char2, NIL, data];
}
ELSE {
token: ROPE ← Filter[line, data];
AddLook[char1, token, data];
AddLook[char2, token, data];
data.txt ← Rope.Cat[data.txt, token, IF data.escC THEN NIL ELSE blank];
IF data.escC THEN data.escC ← FALSE
};
};
JoinTwoLooks: PROC [line: ROPE, char1, char2: CHAR, data: Data] ~ {
ris: IO.STREAMIO.RIS[line];
parity: BOOLTRUE;
UNTIL IO.EndOf[ris] DO
token: ROPE ← MyGetToken[ris, data];
IF NOT Rope.IsEmpty[token] THEN OneLook[token, IF parity THEN char1 ELSE char2, data];
parity ← NOT parity;
ENDLOOP;
IF data.escC THEN data.escC ← FALSE
ELSE data.txt ← Rope.Concat[data.txt, blank];
};
ChFont: PROC [line: ROPE, data: Data] ~ {
s: IO.STREAMIO.RIS[line];
font: CHARIF IO.EndOf[s] THEN 0C ELSE IO.GetChar[s];
SELECT font FROM
'R, '1 => ChangeLook['r, NIL, data];
'I, '2 => ChangeLook['i, NIL, data];
'B, '3 => ChangeLook['b, NIL, data];
'P => ChangeLook[data.prevLook, NIL, data];
ENDCASE => NULL; -- IF debug THEN ...
};
Symbols
GetSymbol: PROC [line: ROPE] RETURNS [symb, text: ROPENIL] ~ {
len: INT ← Rope.Length[line];
index: INTIF line.Fetch[1] IN [IO.NUL..IO.SP] THEN 1 ELSE 2;
symb ← Rope.Substr[base: line, len: index];
WHILE index<len AND line.Fetch[index] IN [IO.NUL..IO.SP] DO
index ← index+1;
ENDLOOP;
IF index=len THEN RETURN;
IF line.Fetch[index]='" THEN index ¬ index+1;
IF index=len THEN RETURN;
text ¬ Rope.Substr[base: line, start: index];
};
AddToSymbol: PROC [line: ROPE, data: Data] ~ {
s, text: ROPE;
[s, text] ¬ GetSymbol[line];
IF ( data.symbols = NIL ) THEN data.symbols ¬ SymTab.Create[];
WITH data.symbols.Fetch[s].val SELECT FROM
prev: ROPE => { [] ¬ data.symbols.Store[s, prev.Concat[text] ] };
ENDCASE => { [] ¬ data.symbols.Store[s, text] };
};
DefSymbol: PROC [line: ROPE, data: Data] ~ {
s, text: ROPE;
[s, text] ¬ GetSymbol[line];
[] ¬ data.symbols.Store[s, text];
};
RemoveSymbol: PROC [line: ROPE, data: Data] ~ {
s, text: ROPE;
[s, text] ¬ GetSymbol[line];
[] ¬ data.symbols.Delete[s];
};
RenameSymbol: PROC [line: ROPE, data: Data] ~ {
s1, text: ROPE;
s2: ROPE;
[s1, text] ¬ GetSymbol[line];
s2 ¬ text;
IF ( text.Length[] # 1 ) THEN {
first: CHAR ~ line.Fetch[1];
isJunk: BOOL ~ ( first IN [IO.NUL..IO.SP] );
s2 ¬ text.Substr[0, IF ( isJunk ) THEN 1 ELSE 2];
};
{
found: BOOL; val: SymTab.Val;
[found, val] ¬ data.symbols.Fetch[s1];
IF ( NOT found ) THEN RETURN;
[] ¬ data.symbols.Store[s2, val];
};
};
Simple Formatting
Bold: PROC [line: ROPE, data: Data] ~ { DoAddOneLook[line, 'b, data]; };
Italics: PROC [line: ROPE, data: Data] ~ { DoAddOneLook[line, 'i, data]; };
Small: PROC [line: ROPE, data: Data] ~ { DoAddOneLook[line, 's, data]; };
SmallBold: PROC [line: ROPE, data: Data] ~ { DoAddTwoLook[line, 's, 'b, data]; };
JoinBI: PROC [line: ROPE, data: Data] ~ { JoinTwoLooks[line, 'b, 'i, data]; };
JoinBR: PROC [line: ROPE, data: Data] ~ { JoinTwoLooks[line, 'b, ' , data]; };
JoinIB: PROC [line: ROPE, data: Data] ~ { JoinTwoLooks[line, 'i, 'b, data]; };
JoinIR: PROC [line: ROPE, data: Data] ~ { JoinTwoLooks[line, 'i, 'r, data]; };
JoinRB: PROC [line: ROPE, data: Data] ~ { JoinTwoLooks[line, 'r, 'b, data]; };
JoinRI: PROC [line: ROPE, data: Data] ~ { JoinTwoLooks[line, 'r, 'i, data]; };
Operations
Comments: PROC [line: ROPE, data: Data] ~ {
IF NOT data.muteComments THEN data.txt ¬ Rope.Cat[data.txt, blank, line, "\n"];
};
BadOp: PROC [op: ROPE, data: Data] ~ {
IF data.badOps = NIL THEN data.badOps ¬ op
ELSE data.badOps ¬ data.badOps.Concat[Rope.Concat[" ", op]];
};
AddText: PROC [line: ROPE, data: Data] ~ {
token: ROPE ¬ Filter[line, data];
IF data.escC THEN {
data.escC ¬ FALSE;
IF NOT data.fill THEN data.txt ¬ Rope.Cat[data.txt, token, blank];
}
ELSE data.txt ¬ Rope.Cat[data.txt, token, IF data.fill THEN blank ELSE "\n"];
};
Title: PROC [line: ROPE, data: Data] ~ {
tokenTiltle, tokenChapter, tokenDate: ROPE;
ris: IO.STREAM ¬ IO.RIS[line];
Validate[data];
tokenTiltle ¬ MyGetToken[ris, data ! IO.EndOfStream => CONTINUE];
tokenChapter ¬ MyGetToken[ris, data ! IO.EndOfStream => CONTINUE];
tokenDate ¬ MyGetToken[ris, data ! IO.EndOfStream => CONTINUE];
data.txt ¬ IO.PutFR["\t\t\t%g(%g)", [rope[tokenTiltle]], [rope[tokenChapter]]];
data.level ¬ 1;
data.format ¬ title;
Validate[data];
data.txt ¬ tokenDate;
data.level ¬ 2;
data.format ¬ abstract;
Validate[data];
Break[data];
};
Head: PROC [line: ROPE, data: Data] ~ {
ris: IO.STREAM ¬ IO.RIS[line];
Validate[data];
data.level ¬ 1;
data.format ¬ head;
GetSixWords[ris, data ! IO.EndOfStream => CONTINUE];
Break[data];
};
SubHead: PROC [line: ROPE, data: Data] ~ {
ris: IO.STREAM ¬ IO.RIS[line];
Validate[data];
data.level ¬ 2;
data.format ¬ head;
GetSixWords[ris, data ! IO.EndOfStream => CONTINUE];
Break[data];
};
Item: PROC [line: ROPE, data: Data] ~ {
Validate[data];
data.level ¬ 3;
data.format ¬ IF data.fill THEN item ELSE block;
IF line#NIL THEN {
ris: IO.STREAM ¬ IO.RIS[line];
txt: ROPE ¬ MyGetToken[ris, data];
data.txt ¬ IO.PutFR1["%g\t", [rope[txt]]];
};
};
Fill: PROC [data: Data] ~ { data.fill ¬ TRUE; Break[data]; };
NoFill: PROC [data: Data] ~ { data.fill ¬ FALSE; Break[data]; };
Break: PROC [data: Data] ~ {
Validate[data];
data.level ¬ 2;
data.format ¬ IF data.fill THEN body ELSE block;
};
BreakAndPush: PROC [data: Data] ~ {
Validate[data];
data.level ¬ 3;
data.format ¬ IF data.fill THEN body ELSE block;
};
BreakAndPop: PROC [data: Data] ~ {
Validate[data];
data.level ¬ 2;
data.format ¬ IF data.fill THEN body ELSE block;
};
Validate: PROC [data: Data] ~ {
IF data.nodeStack=NIL THEN RETURN;
IF NOT Rope.IsEmpty[data.txt] THEN {
node: TiogaFileOps.Ref;
SELECT data.level FROM
1 => {
node ¬ TiogaFileOps.InsertAsLastChild[data.root];
data.nodeStack ¬ LIST[node];
};
2 => {
node ¬ TiogaFileOps.InsertAsLastChild[data.nodeStack.first];
data.nodeStack.rest ¬ LIST[node];
};
3 => {
IF data.nodeStack.rest=NIL THEN {
data.nodeStack.rest ¬ LIST[TiogaFileOps.InsertAsLastChild[data.nodeStack.first]];
TiogaFileOps.SetContents[data.nodeStack.rest.first, blank];
TiogaFileOps.SetFormat[data.nodeStack.rest.first, "body"];
};
node ¬ TiogaFileOps.InsertAsLastChild[data.nodeStack.rest.first];
data.nodeStack.rest.rest ¬ LIST[node];
};
ENDCASE => {
node ¬ TiogaFileOps.InsertAsLastChild[data.nodeStack.first];
data.nodeStack.rest ¬ LIST[node];
};
TiogaFileOps.SetContents[node, data.txt];
CloseLook[' , NIL, data];
UNTIL data.props=NIL DO
l: Look ¬ data.props.first;
TiogaFileOps.AddLooks[x: node, start: l.start, len: l.len, look: l.look, root: data.root];
data.props ¬ data.props.rest;
ENDLOOP;
data.txt ¬ NIL;
TiogaFileOps.SetFormat[node, data.format];
}
ELSE data.props ¬ NIL;
data.level ¬ 2;
};
Break Procs
EscapeProc: IO.BreakProc =
{ RETURN[SELECT char FROM '\\ => break, ENDCASE => other] };
LineProc: IO.BreakProc =
{ RETURN[SELECT char FROM '\l, '\n => sepr, ENDCASE => other] };
LiteralProc: IO.BreakProc =
{ RETURN[SELECT char FROM '" => break, ENDCASE => other] };
QuoteProc: IO.BreakProc =
{ RETURN[SELECT char FROM '' => break, ENDCASE => other] };
SpaceProc: IO.BreakProc =
{ RETURN[SELECT char FROM ' , '\t, '\l, '\n => sepr, ENDCASE => other] };
Actual conversion
CreateRoot: PROC RETURNS [TiogaFileOps.Ref] ~ {
root: TiogaFileOps.Ref ~ TiogaFileOps.CreateRoot[];
node: REF ~ root;
textnode: TextNode.Ref ~ NARROW[node];
NodeProps.PutProp[n: textnode, name: $NewlineDelimiter, value: Rope.Flatten["\n"]];
RETURN [root]
};
MakeTitle: PROC [fileName: ROPE] RETURNS [ROPE] ~ {
Produce a meaningful title for the viewer, by extracting manual page name and section number from the filename.
ENABLE {
RuntimeError.BoundsFault => GOTO default;
};
nameEnd: INT ~ fileName.SkipTo[skip: "."] - 1;
sectionStart: INT ~ nameEnd + 2;
sectionEnd: INT ~ fileName.SkipTo[pos: sectionStart, skip: "."] - 1;
nameStart: INT ¬ nameEnd;
name, section: ROPE;
WHILE nameStart > 0 DO
IF fileName.Fetch[nameStart] = '/ THEN EXIT;
nameStart ¬ nameStart - 1;
ENDLOOP;
nameStart ¬ nameStart + 1;
name ¬ fileName.Substr[start: nameStart, len: nameEnd-nameStart+1];
section ¬ fileName.Substr[start: sectionStart, len: sectionEnd-sectionStart+1];
RETURN [IO.PutFR["%g (%g)", IO.rope[name], IO.rope[section]]];
EXITS
default => RETURN [fileName];
};
FormatManPage: PROC [fileName: ROPE, tempName: ROPE, stderr: IO.STREAM] ~ {
root: TiogaFileOps.Ref ~ CreateRoot[];
title: ROPE ~ MakeTitle[fileName];
viewer: ViewerClasses.Viewer ~ ViewerOps.CreateViewer[$Text, [name: title, label: title]];
s: IO.STREAM ~ OpenReadStream[fileName];
Parse[s, stderr, root];
ViewerOps.SetViewer[viewer, root, FALSE, $TiogaDocument];
ViewerOps.OpenIcon[viewer];
TiogaFileOps.Store[root, tempName]; -- cache the result
viewer.file ¬ tempName;
};
LoadFromCache: PROC [fileName: ROPE, tempName: ROPE, stderr: IO.STREAM] ~ {
root: TiogaFileOps.Ref ~ CreateRoot[];
title: ROPE ~ MakeTitle[fileName];
viewer: ViewerClasses.Viewer ~ ViewerOps.CreateViewer[$Text, [name: title, label: title]];
ViewerOps.SetViewer[viewer, root, FALSE, $TiogaDocument];
ViewerOps.OpenIcon[viewer];
TiogaMenuOps.Load[viewer, tempName];
viewer.name ¬ title;
ViewerOps.PaintViewer[viewer, caption];
};
FormatManPage2: PROC [fileName: ROPE, tempName: ROPE, stderr: IO.STREAM] ~ {
root: TiogaFileOps.Ref ~ CreateRoot[];
s: IO.STREAM ~ StreamOpen[fileName];
Parse[s, stderr, root];
TiogaFileOps.Store[root, tempName]; -- cache the result
[] ¬ TiogaMenuOps.Open[fileName: tempName];
};
Profile Operations
ModeFromProfile: PROC RETURNS [ATOM] ~ {
choice: ROPE ~ UserProfile.Token[key: "Man.SearchRule", default: "All"];
IF Rope.Equal[choice, "First", FALSE] THEN RETURN [$First]
ELSE IF Rope.Equal[choice, "List", FALSE] THEN RETURN [$List]
ELSE RETURN [$All];
};
List Production
ReportFile: PROC [cmd: Commander.Handle, fileName: ROPE, seen: LIST OF ROPE ¬ NIL] RETURNS [seenOut: LIST OF ROPE] ~ {
name: ROPE;
section: ROPE;
inList: BOOL ¬ FALSE;
seenOut ¬ seen;
[name, section] ¬ PageNameFromFile[fileName ! NonStandard => GOTO Failed];
FOR each: LIST OF ROPE ¬ seenOut, each.rest WHILE each # NIL DO
IF Rope.IsPrefix[section, each.first, FALSE] THEN {
inList ¬ TRUE;
EXIT;
};
ENDLOOP;
IF inList THEN {
IO.PutF1[cmd.out, "localman %g\n", IO.rope[fileName]];
}
ELSE {
IO.PutFL[cmd.out, "man %g %g\n", LIST[IO.rope[section], IO.rope[name]]];
seenOut ¬ CONS[section, seenOut];
};
EXITS
Failed => IO.PutF1[cmd.out, "Nonstandard: %g\n", IO.rope[fileName]];
};
Commander Operations
TooMany: SIGNAL ~ CODE;
ManCmd: Commander.CommandProc ~ {
ENABLE TooMany => { msg ¬ "too many matches" ; RESUME };
stderr: IO.STREAM ~ cmd.err;
args: CommanderOps.ArgumentVector ~ CommanderOps.Parse[cmd
! CommanderOps.Failed => { msg ¬ errorMsg; GO TO Failed }];
IF ( args.argc < 2 ) THEN { msg ¬ "wrong arguments"; GOTO Failed };
{
firstChar: CHARACTER ~ args[1].Fetch[0];
cmdName: ROPE ¬ NIL;
mode: ATOM ¬ ModeFromProfile[];
section: ROPE ¬ NIL;
sectionsSeen: LIST OF ROPE ¬ NIL;
fileNames: LIST OF ROPE;
IF firstChar = '- THEN {
SELECT args[1].Fetch[1 ! RuntimeError.BoundsFault => { msg ¬ "wrong arguments"; GOTO Failed }] FROM
'f, 'F => mode ¬ $First;
'a, 'A => mode ¬ $All;
'l, 'L => mode ¬ $List;
ENDCASE => { msg ¬ "wrong arguments"; GOTO Failed };
IF ( args.argc < 3 ) THEN { msg ¬ "wrong arguments"; GOTO Failed }
ELSE cmdName ¬ args[2];
}
ELSE IF Ascii.Digit[firstChar] THEN {
section ¬ args[1];
mode ¬ $Section; -- Internal mode to indicate that a specific section is selected
IF ( args.argc < 3 ) THEN { msg ¬ "wrong arguments"; GOTO Failed }
ELSE cmdName ¬ args[2];
}
ELSE {
cmdName ¬ args[1];
};
IF cmdName = NIL THEN {
msg ¬ "wrong arguments";
GOTO Failed;
};
fileNames ¬ FindMatches[cmd, cmdName, mode, section];
IF ( fileNames = NIL ) THEN { msg ¬ cmdName.Concat[": not found"]; GOTO Failed };
FOR tail: LIST OF ROPE ¬ fileNames, tail.rest WHILE ( tail # NIL ) DO
fileName: ROPE ~ tail.first;
IF mode = $List THEN {
sectionsSeen ¬ ReportFile[cmd, fileName, sectionsSeen];
}
ELSE {
tempName: ROPE ~ TemporaryFile[fileName.Concat[".tioga"]];
found: BOOL ¬ TRUE;
FileInfo[tempName ! FileError => { found ¬ FALSE; CONTINUE }];
IF ( NOT found )
THEN FormatManPage[fileName, tempName, stderr
! FileError => { msg ¬ huh; GOTO Failed }]
ELSE LoadFromCache[fileName, tempName, stderr];
}
ENDLOOP;
};
EXITS Failed => { result ¬ $Failed };
};
LocalManCmd: Commander.CommandProc ~ {
stderr: IO.STREAM ~ cmd.err;
args: CommanderOps.ArgumentVector ~ CommanderOps.Parse[cmd
! CommanderOps.Failed => { msg ¬ errorMsg; GO TO Failed }];
IF ( args.argc < 2 ) THEN { msg ¬ "wrong arguments"; GOTO Failed };
{
fileName: ROPE ~ args[1];
tempName: ROPE ~ TemporaryFile[fileName.Concat[".man.tioga"]];
FormatManPage2[fileName, tempName, stderr
! FileError => { msg ¬ huh; GOTO Failed }]
};
EXITS Failed => { result ¬ $Failed };
};
Help
UsageMsg: ROPE =
"Tioga access to the Unix(tm) man pages.
 Usage: man [-f | -a | -l | sectionNumber] name
The first argument (optional) specifies the mode. The switches are:
 -f  First page: lowest section number, earliest directory in $MANPATH
 -a  All pages
 -l  List of pages: produces a list of the commands to access each specific page
Alternatively, a sectionNumber may be specified to access the page from that section.
The second argument is the name of the desired page.
";
Init
CreateTab: PROC RETURNS [tab: SymTab.Ref] ~ {
cc: PROC [name: ROPE, cmd: Cmd] ~ INLINE {
ref: REF Cmd ~ NEW[Cmd ¬ cmd];
[] ¬ tab.Store[name, ref];
};
tab ¬ SymTab.Create[];
cc["B", b];
cc["BI", bi];
cc["BR", br];
cc["DT", notImplem];
cc["HP", hp];
cc["I", i];
cc["IB", ib];
cc["IP", ip];
cc["IR", ir];
cc["IX", notImplem];
cc["LP", lp];
cc["PD", notImplem];
cc["PP", pp];
cc["RE", re];
cc["RB", rb];
cc["RI", ri];
cc["RS", rs];
cc["SB", sb];
cc["SH", sh];
cc["SM", sm];
cc["SS", ss];
cc["TH", th];
cc["TP", tp];
cc["TX", tx];
cc["as", as];
cc["br", pp];
cc["ds", ds];
cc["fi", fi];
cc["ft", ft];
cc["hy", notImplem];
cc["ne", notImplem];
cc["nf", nf];
cc["nh", notImplem];
cc["nx", nx];
cc["rm", rm];
cc["rn", rn];
cc["so", so];
cc["sp", hp];
cc["ta", notImplem];
cc["ti", tp];
cc["\\""", comment];
};
Commander.Register["man", ManCmd, UsageMsg];
Commander.Register["LocalMan", LocalManCmd, "file\n Tioga access to a (local) man page"];
}.