CatalogImpl.mesa
Copyright © 1985 by Xerox Corporation. All rights reserved.
Rick Beach, October 5, 1985 11:52:43 am PDT
Doug Wyatt, October 14, 1985 6:10:53 pm PDT
DIRECTORY
Ascii USING [Upper, Lower],
Basics USING [Comparison],
BasicTime USING [Now],
Commander USING [CommandProc, Register],
CommandTool USING [ArgumentVector, Parse],
DefaultRemoteNames USING [Get],
DFUtilities USING [DirectoryItem, FileItem, ParseFromStream, ProcessItemProc, SyntaxError],
EditSpan USING [Copy],
FS USING [ComponentPositions, EnumerateForNames, Error, ExpandName, NameProc, StreamOpen],
IO USING [BreakProc, Close, EndOf, EndOfStream, Error, GetChar, GetTokenRope, PutF, PutF1, PutFR, PutFR1, RIS, rope, SetIndex, SkipWhitespace, STREAM, time],
List USING [Nconc1],
NodeProps USING [PutProp, true],
Process USING [CheckForAbort],
PutGet USING [FromFile],
RedBlackTree USING [Compare, Create, DuplicateKey, EnumerateIncreasing, GetKey, Insert, UserData, Size, Table],
Rope USING [Cat, Compare, Concat, Equal, Fetch, Find, IsEmpty, Length, Match, ROPE, Substr, Translate],
TextNode USING [Body, Level, Location, MakeNodeLoc, NodeRope, nullLocation, nullSpan, Ref, Root, Span, StepForward],
TiogaFileOps USING [AddLooks, CreateRoot, InsertAsLastChild, InsertNode, NodeBody, SetContents, SetFormat, Store],
UserCredentials USING [Get],
UserProfile USING [Token],
ViewerIO USING [CreateViewerStreams];
CatalogImpl: CEDAR PROGRAM
IMPORTS Ascii, BasicTime, Commander, CommandTool, DefaultRemoteNames, DFUtilities, EditSpan, FS, IO, List, NodeProps, RedBlackTree, Process, PutGet, Rope, TextNode, TiogaFileOps, UserCredentials, UserProfile, ViewerIO
EXPORTS TiogaFileOps -- export NodeBody to convince Compiler we know that TiogaFileOps.Ref is really TextNode.Ref
~ BEGIN
ROPE: TYPE ~ Rope.ROPE;
Ref: TYPE ~ REF NodeBody;
NodeBody: PUBLIC TYPE ~ TextNode.Body;
CatalogCmdProc: Commander.CommandProc ~ {
systemHost: ROPE ← DefaultRemoteNames.Get[].systemHost;
serverName: ROPE ← Rope.Substr[systemHost, 1, systemHost.Length-2]; -- strip off the brackets
catalogName: ROPE;
catalogFileName: ROPE;
argv: CommandTool.ArgumentVector ← CommandTool.Parse[cmd];
quietly: BOOLEANFALSE;
i: NAT ← 1;
WHILE i < argv.argc DO
IF Rope.Fetch[argv[i], 0] = '- THEN {
SELECT Ascii.Lower[Rope.Fetch[argv[i], 1]] FROM
's => {
i ← i+1; -- consume following option as the server name
IF i >= argv.argc THEN
RETURN [$Failure, "-server option must be followed by a server name; please supply one."];
serverName ← argv[i];
};
'q => quietly ← TRUE;
ENDCASE => RETURN [$Failure, Rope.Concat[argv[i], " is an unrecognized option; use only -server or -quietly"]];
}
ELSE {
IF catalogName # NIL THEN
RETURN [$Failure, "multiple catalog names supplied; only one allowed."];
catalogName ← argv[i];
};
i ← i+1;
ENDLOOP;
IF catalogName = NIL THEN RETURN;
catalogFileName ← Catalog[serverName, catalogName, IF quietly THEN NIL ELSE cmd.out];
RETURN [NIL, Rope.Cat["\n", catalogFileName, " created."]];
};
Catalog: PROC [serverName: ROPE, catalogName: ROPE, out: IO.STREAMNIL]
RETURNS [catalogFileName: ROPE] ~ {
dfFilePattern: ROPE ← Rope.Cat["/", serverName, "/", catalogName, "/Top/*.DF!H"];
TryThisDFFile: FS.NameProc ~ {
PROC [fullFName: ROPE] RETURNS [continue: BOOLEAN];
base, ext: ROPE;
[base, ext] ← ParseFileName[fullFName];
Process.CheckForAbort[];
IF Rope.Equal[s1~ext, s2~"DF", case~FALSE] THEN {
AddCatalogEntry[doc~catalogDoc, packageName~base, dfFileName~fullFName];
};
RETURN[continue~TRUE];
};
catalogDoc: CatalogDoc ~ InitializeCatalogDoc[catalogName];
logName: ROPE ~ Rope.Concat[catalogName, "Catalog.log"];
catalogDoc.out ← out;
catalogDoc.log ← ViewerIO.CreateViewerStreams[
name: logName, backingFile: logName, editedStream: FALSE].out;
IO.PutF[catalogDoc.log, "Catalog of %g, %g\n\n", IO.rope[catalogName], IO.time[]];
FS.EnumerateForNames[pattern~dfFilePattern, proc~TryThisDFFile];
FinishCatalogDoc[catalogDoc];
RETURN [catalogDoc.fileName]
};
CatalogDoc: TYPE ~ REF CatalogDocRec;
CatalogDocRec: TYPE ~ RECORD[
out: IO.STREAM, -- for recording our progress
log: IO.STREAM, -- for logging errors
fileName: ROPE,
packageName: ROPE,
root: Ref,
latestLevel1Node: Ref,
latestLevel2Node: Ref,
latestLevel3Node: Ref,
commandIndex: IndexTable,
keywordIndex: IndexTable
];
InitializeCatalogDoc: PROC [catalogName: ROPE] RETURNS [doc: CatalogDoc] ~ {
UpperCase: PROC [r: ROPE] RETURNS [ROPE] ~ {
upper: PROC [old: CHAR] RETURNS [new: CHAR] ~ { new ← Ascii.Upper[old] };
RETURN[Rope.Translate[base: r, translator: upper]];
};
PutPropRope: PROC [n: TextNode.Ref, name: ATOM, value: ROPE] ~ {
Use this to force the type of a string literal to be ROPE rather than REF TEXT.
NodeProps.PutProp[n: n, name: name, value: value];
};
doc ← NEW[CatalogDocRec];
doc.commandIndex ← CreateIndexTable[];
doc.keywordIndex ← CreateIndexTable[];
doc.fileName ← Rope.Concat[catalogName, "Catalog.tioga"];
doc.latestLevel1Node ← NIL;
doc.root ← TiogaFileOps.CreateRoot[];
PutPropRope[doc.root, $Postfix, "(firstPageNumber) 0 .def (firstHeadersAfterPage) 0 .def"];
AppendNode[level~1, doc~doc, r~doc.fileName, format~NIL];
NodeProps.PutProp[doc.latestLevel1Node, $Comment, NodeProps.true];
AppendNode[level~2, doc~doc, format~NIL, r~IO.PutFR["%g %t", IO.rope[UserProfile.Token["EditorComforts.LastEdited", UserCredentials.Get[].name]], IO.time[BasicTime.Now[]]]];
NodeProps.PutProp[doc.latestLevel2Node, $Comment, NodeProps.true];
AppendNode[level~1, doc~doc, r~Rope.Concat[UpperCase[catalogName], " PACKAGE CATALOG"], format~"unleaded"];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~0, len~LAST[INT], look~'s, root~doc.root];
PutPropRope[doc.latestLevel1Node, $Mark, "centerHeader"];
AppendNode[level~1, doc~doc, r~"CEDAR 6.0 — FOR INTERNAL XEROX USE ONLY", format~"unleaded"];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~0, len~LAST[INT], look~'s, root~doc.root];
PutPropRope[doc.latestLevel1Node, $Mark, "centerFooter"];
AppendNode[level~1, doc~doc, r~Rope.Concat["Package Catalog\nfor ", catalogName], format~"title"];
AppendNode[level~1, doc~doc, r~"c Copyright 1985 by Xerox Corporation. All rights reserved.", format~"abstract"];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~0, len~1, look~'m, root~doc.root];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~0, len~LAST[INT], look~'s, root~doc.root];
AppendNode[level~1, doc~doc, r~"Abstract: This catalog is a list of interesting packages and tools. The catalog is automatically created from the collection of maintainer-supplied entries.", format~"abstract"];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~0, len~8, look~'b, root~doc.root];
AppendNode[level~1, doc~doc, r~"XEROX\t\t\tXerox Corporation\n\t\t\t\tPalo Alto Research Center\n\t\t\t\t3333 Coyote Hill Road\n\t\t\t\tPalo Alto, California 94304\n\nFor Internal Xerox Use Only", format~"boilerplate"];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~0, len~5, look~'q, root~doc.root];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~5, len~LAST[INT], look~'o, root~doc.root];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~115, len~LAST[INT], look~'b, root~doc.root];
TiogaFileOps.AddLooks[x~doc.latestLevel1Node, start~115, len~LAST[INT], look~'x, root~doc.root];
AppendNode[level~1, doc~doc, r~"Catalog Components", format~"head"];
each component is added underneath this heading
};
FinishCatalogDoc: PROC [doc: CatalogDoc] ~ {
Process.CheckForAbort[];
IF NOT IsTableEmpty[doc.commandIndex] THEN AddIndex[doc, command];
IF NOT IsTableEmpty[doc.keywordIndex] THEN AddIndex[doc, keyword];
Process.CheckForAbort[];
[] ← TiogaFileOps.Store[filename~doc.fileName, x~doc.root];
IF doc.log#NIL THEN IO.Close[doc.log];
};
AddCatalogEntry: PROC [doc: CatalogDoc, packageName: ROPE, dfFileName: ROPE] ~ {
lastDirectoryPath: ROPENIL;
packageDoc: ROPE ~ Rope.Concat[packageName, "Doc.tioga"];
documentationFileName: ROPENIL;
documentationFiles: ROPENIL;
listOfCommands: LIST OF REF ANYNIL;
TryThisDFItem: DFUtilities.ProcessItemProc ~ {
ProcessItemProc: TYPE = PROC [item: REF ANY] RETURNS [stop: BOOLFALSE];
WITH item SELECT FROM
directory: REF DFUtilities.DirectoryItem => {
lastDirectoryPath ← directory.path1;
};
file: REF DFUtilities.FileItem => {
base, ext, shortName: ROPENIL;
[base, ext, shortName] ← ParseFileName[file.name !
FS.Error => IF error.group#bug THEN {
Log[doc, IO.PutFR1["FS.Error... %g", IO.rope[error.explanation]]];
GOTO Fail;
};
];
SELECT TRUE FROM
Rope.Equal["load", ext, FALSE] => {
command name is the name of the .Load file
listOfCommands ← List.Nconc1[listOfCommands, base];
};
Rope.Equal["tioga", ext, FALSE] --AND Rope.Match["*Doc", base, FALSE]-- => {
IF NOT Rope.Match["*Documentation*", lastDirectoryPath, FALSE] THEN {
Log[doc, IO.PutFR1["%g is not in the proper Documentation directory.", IO.rope[shortName]]];
};
IF documentationFiles.IsEmpty THEN documentationFiles ← shortName
ELSE documentationFiles ← Rope.Cat[documentationFiles, ", ", shortName];
IF Rope.Equal[shortName, packageDoc, FALSE] THEN
documentationFileName ← Rope.Concat[lastDirectoryPath, file.name];
RETURN;
};
ENDCASE => NULL;
EXITS Fail => NULL;
};
ENDCASE => NULL;
};
dfStream: IO.STREAMNIL;
Process.CheckForAbort[];
BeginPackage[doc, packageName];
dfStream ← FS.StreamOpen[fileName~dfFileName !
FS.Error => {
Log[doc, IO.PutFR1["FS.Error... %g", IO.rope[error.explanation]]];
GO TO FileProblem;
};
];
DFUtilities.ParseFromStream[in~dfStream, proc~TryThisDFItem, filter~[comments~FALSE, filterA~all, filterB~public, filterC~all] !
DFUtilities.SyntaxError => {
Log[doc, IO.PutFR1["DFUtilities.SyntaxError... %g", IO.rope[reason]]];
CONTINUE;
};
];
IO.Close[dfStream];
Process.CheckForAbort[];
{ -- create the catalog entry
author, creator, maintainer: ROPENIL;
abstractSpan: TextNode.Span ← TextNode.nullSpan;
keywords: ROPE;
singlePageDocument: BOOLEAN;
IF documentationFileName.IsEmpty THEN Log[doc, "No package documentation."]
ELSE {
[abstractSpan: abstractSpan, author: author, creator: creator, maintainer: maintainer, keywords: keywords, singlePageDocument: singlePageDocument] ← ExtractStuffFromDocFile[doc, documentationFileName];
IF author.IsEmpty[] AND maintainer.IsEmpty[] THEN {
Log[doc, "No author or maintainer listed in documentation."]
};
};
AppendNode[level~2, doc~doc, r~packageName, format~"head"];
AppendNode[level~3, doc~doc, r~Rope.Cat["DF file: ", packageName, ".df"], format~"indent"];
TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~8, look~'b, root~doc.root];
IF NOT creator.IsEmpty THEN {
AppendNode[level~3, doc~doc, r~creator, format~"indent"];
TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~11 --Created by:--, look~'b, root~doc.root];
};
IF NOT maintainer.IsEmpty THEN {
AppendNode[level~3, doc~doc, r~maintainer, format~"indent"];
TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~14 --Maintained by:--, look~'b, root~doc.root];
};
IF NOT documentationFiles.IsEmpty AND
NOT (Rope.Equal[documentationFiles, packageDoc, FALSE] AND singlePageDocument) THEN {
the list of documentation files either has multiple files (since its not just PackageDoc.Tioga) or points to a long document (since its not a single page document)
AppendNode[level~3, doc~doc, r~Rope.Cat["Documentation: ", documentationFiles], format~"indent"];
TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~14, look~'b, root~doc.root];
};
IF NOT author.IsEmpty THEN {
AppendNode[level~3, doc~doc, r~Rope.Cat["Author: ", author], format~"indent"];
TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~7 --Author:--, look~'b, root~doc.root];
};
IF NOT keywords.IsEmpty THEN {
CommaSeparated: IO.BreakProc ~ {
PROC [char: CHAR] RETURNS [CharClass];
RETURN [SELECT char FROM
', => sepr,
ENDCASE => other]
};
s: IO.STREAMIO.RIS[keywords];
firstBlank: INT ← Rope.Find[keywords, " "];
IO.SetIndex[self~s, index~firstBlank]; -- position past "Keywords:"
[] ← IO.SkipWhitespace[s];
WHILE NOT IO.EndOf[s] DO
ENABLE IO.Error, IO.EndOfStream => EXIT;
keyword: ROPENIL;
keyword ← IO.GetTokenRope[stream~s, breakProc~CommaSeparated].token;
IF NOT keyword.IsEmpty THEN
InsertIndexTerm[doc.keywordIndex, keyword, packageName];
[] ← IO.GetChar[s];
[] ← IO.SkipWhitespace[s];
ENDLOOP;
IO.Close[s];
AppendNode[level~3, doc~doc, r~keywords, format~"indent"];
TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~10, look~'b, root~doc.root];
};
{
contents: ROPENIL;
FOR l: LIST OF REF ANY ← listOfCommands, l.rest WHILE l # NIL DO
commandName: ROPENARROW[l.first];
InsertIndexTerm[doc.commandIndex, commandName, packageName];
contents ← IF contents.IsEmpty
THEN Rope.Concat["Commands: ", commandName]
ELSE Rope.Cat[contents, ", ", commandName];
ENDLOOP;
IF NOT contents.IsEmpty THEN {
AppendNode[level~3, doc~doc, r~contents, format~"indent"];
TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~9, look~'b, root~doc.root];
};
};
IF abstractSpan # TextNode.nullSpan THEN {
[] ← EditSpan.Copy[dest~TextNode.MakeNodeLoc[doc.latestLevel3Node], source~abstractSpan, destRoot~doc.root, sourceRoot~TextNode.Root[abstractSpan.start.node]];
FOR node: Ref ← Forward[doc.latestLevel3Node], Forward[node] WHILE TextNode.Level[node] >= 3 DO
TiogaFileOps.SetFormat[node, "indent"];
ENDLOOP;
};
};
EXITS
FileProblem => NULL;
};
ExtractStuffFromDocFile: PROC [
doc: CatalogDoc, documentationFile: ROPE
] RETURNS [
abstractSpan: TextNode.Span ← TextNode.nullSpan,
author, creator, maintainer, keywords: ROPENIL,
singlePageDocument: BOOLEANFALSE
] ~ {
tiogaDocRoot: Ref ~ PutGet.FromFile[documentationFile !
FS.Error => {
Log[doc, IO.PutFR1["FS.Error... %g", IO.rope[error.explanation]]];
GOTO Quit;
};
];
prev: Ref ← NIL;
hasCopyright: BOOLFALSE;
AbstractNode: PROC [node: Ref, begin: BOOLFALSE] ~ {
nodeLoc: TextNode.Location ~ TextNode.MakeNodeLoc[node];
IF abstractSpan.start=TextNode.nullLocation THEN {
IF begin OR hasCopyright THEN abstractSpan.start ← abstractSpan.end ← nodeLoc;
Begin with this node if it says "Abstract: ..." or we've seen the copyright notice.
}
ELSE {
IF abstractSpan.end.node=prev THEN abstractSpan.end ← nodeLoc;
Continue the abstract if this node is contiguous.
};
};
RopeOf: PROC [n: Ref] RETURNS [ROPE] ~ { RETURN[TextNode.NodeRope[n]] };
FOR node: Ref ← Forward[tiogaDocRoot], Forward[prev ← node] WHILE node#NIL DO
SELECT node.formatName FROM
$authors => {
IF NOT author.IsEmpty[] THEN author ← author.Concat["; "];
author ← author.Concat[RopeOf[node]];
};
$abstract => {
Documentation may contain several abstract format nodes
The following heuristics are used to determine the span of the abstract:
Begin the abstract if the node begins with "Abstract"
Begin the abstract at the node following the copyright notice which is a node matching "Copyright *" or "* Copyright *"
End the abstract if the node does not have abstract format
End the abstract if the node begins with "Keyword", "Created by", or "Mainained by"
contents: ROPE ~ RopeOf[node];
shortContents: ROPE ~ contents.Substr[len: 100];
SELECT TRUE FROM
Rope.Match[pattern: "Copyright*", object: shortContents, case: FALSE]
OR Rope.Match[pattern: "* Copyright *", object: shortContents, case: FALSE] => {
hasCopyright ← TRUE;
};
Rope.Match[pattern: "Abstract*", object: shortContents, case: FALSE] => {
AbstractNode[node, TRUE];
};
Rope.Match[pattern: "Created by*", object: shortContents, case: FALSE] => {
creator ← contents;
};
Rope.Match[pattern: "Maintained by*", object: shortContents, case: FALSE] => {
maintainer ← contents;
};
Rope.Match[pattern: "Keyword*", object: shortContents, case: FALSE] => {
keywords ← contents;
};
ENDCASE => {
AbstractNode[node];
};
};
$boilerplate => {
peek: Ref ~ Forward[node]; -- peek to see if there are any following nodes
IF peek=NIL THEN singlePageDocument ← TRUE;
collect head nodes here for cumulative table of contents
EXIT;
};
ENDCASE;
ENDLOOP;
EXITS Quit => NULL;
};
AddIndex: PROC [doc: CatalogDoc, kindOf: {command, keyword}] ~ {
indexTable: IndexTable ← IF kindOf = command THEN doc.commandIndex ELSE doc.keywordIndex;
lastTerm: ROPE;
PerIndexTerm: EnumerateProc ~ {
IF NOT Rope.Equal[term, lastTerm, FALSE]
THEN AppendNode[level~2, doc~doc, r~Rope.Cat[term, ": ", location], format~"item"]
ELSE doc.latestLevel2Node.rope ← Rope.Cat[doc.latestLevel2Node.rope, ", ", location];
lastTerm ← term;
RETURN [FALSE];
};
AppendNode[level~1, doc~doc, r~IF kindOf = command THEN "Command Index" ELSE "Keyword Index", format~"head"];
EnumerateIndexTerms[indexTable, PerIndexTerm];
};
ParseFileName: PROC [fileName: ROPE] RETURNS [base, ext, shortName: ROPE] ~ {
cp: FS.ComponentPositions;
fullName: ROPE;
[fullName, cp] ← FS.ExpandName[fileName];
base ← Rope.Substr[fullName, cp.base.start, cp.base.length];
ext ← Rope.Substr[fullName, cp.ext.start, cp.ext.length];
shortName ← Rope.Substr[fullName, cp.base.start, cp.ext.start+cp.ext.length-cp.base.start];
};
AppendNode: PROC [doc: CatalogDoc, level: NAT, r: ROPE, format: ROPE] ~ {
InnerAppendNode: PROC [subtree: Ref, latest: Ref, r: ROPE, format: ROPE] RETURNS [newNode: Ref] ~ {
IF subtree = NIL THEN ERROR;
newNode ← IF latest = NIL THEN
TiogaFileOps.InsertNode[x~subtree, child~TRUE]
ELSE
TiogaFileOps.InsertAsLastChild[x~subtree, prevLast~latest];
TiogaFileOps.SetContents[x~newNode, txt~r];
TiogaFileOps.SetFormat[x~newNode, format~format];
};
SELECT level FROM
1 => {
doc.latestLevel1Node ← InnerAppendNode[doc.root, doc.latestLevel1Node, r, format];
doc.latestLevel2Node ← doc.latestLevel3Node ← NIL;
};
2 => {
doc.latestLevel2Node ← InnerAppendNode[doc.latestLevel1Node, doc.latestLevel2Node, r, format];
doc.latestLevel3Node ← NIL;
};
3 => {
doc.latestLevel3Node ← InnerAppendNode[doc.latestLevel2Node, doc.latestLevel3Node, r, format];
};
ENDCASE;
};
Forward: PROC [node: Ref] RETURNS [next: Ref] ~ {
RETURN [TextNode.StepForward[node]];
};
BeginPackage: PROC [doc: CatalogDoc, packageName: ROPE] ~ {
out: IO.STREAM ~ doc.out;
IF out#NIL THEN out.PutF1["%g ... ", IO.rope[packageName]];
doc.packageName ← packageName;
};
Log: PROC [doc: CatalogDoc, msg: ROPE] ~ {
log: IO.STREAM ~ doc.log;
IF log#NIL THEN log.PutF["%g: %g\n", IO.rope[doc.packageName], IO.rope[msg]];
};
IndexTable: TYPE ~ RedBlackTree.Table;
CreateIndexTable: PROC [] RETURNS [IndexTable] ~ {
RETURN [RedBlackTree.Create[getKey: GetIndexKey, compare: CompareIndexKeys]];
};
IsTableEmpty: PROC [table: IndexTable] RETURNS [BOOLEAN] ~ {
RETURN [RedBlackTree.Size[table] = 0];
};
Pair: TYPE ~ REF PairRec;
PairRec: TYPE ~ RECORD[
term: ROPE,
location: ROPE
];
InsertIndexTerm: PROC [table: IndexTable, term: ROPE, location: ROPE] ~ {
data: REFNEW[PairRec ← [term, location]];
RedBlackTree.Insert[table, data, data ! RedBlackTree.DuplicateKey => CONTINUE];
};
GetIndexKey: RedBlackTree.GetKey ~ {
PROC [data: UserData] RETURNS [Key]
RETURN [data];
};
CompareIndexKeys: RedBlackTree.Compare ~ {
PROC [k: Key, data: UserData] RETURNS [Basics.Comparison]
pair1: Pair ← NARROW[k];
pair2: Pair ← NARROW[data];
comparison: Basics.Comparison ← Rope.Compare[pair1.term, pair2.term, FALSE];
IF comparison = equal THEN comparison ← Rope.Compare[pair1.location, pair2.location, FALSE];
RETURN [comparison];
};
EnumerateProc: TYPE ~ PROC [term: ROPE, location: ROPE] RETURNS [BOOLEAN];
EnumerateIndexTerms: PROC [table: IndexTable, proc: EnumerateProc] ~ {
ApplyProc: PROC [data: RedBlackTree.UserData] RETURNS [stop: BOOLFALSE] ~ {
pair: Pair ← NARROW[data];
RETURN [proc[pair.term, pair.location]];
};
RedBlackTree.EnumerateIncreasing[table, ApplyProc];
};
Commander.Register[
key~"Catalog",
proc~CatalogCmdProc,
doc~"Catalog [-quietly] [-server <ServerName>] <CatalogDirectoryName> make a catalog of software packages stored as DF files in /<ServerName>/<CatalogDirectoryName>/Top/*.DF"
];
END.