<> <> <> <> <> <> <> <<>> DIRECTORY Ascii USING [Upper, Lower], Basics USING [Comparison], BasicTime USING [Now, Unpack], Commander USING [CommandProc, Register], CommanderOps USING [ArgumentVector, Parse], DFUtilities USING [DirectoryItem, FileItem, IncludeItem, ParseFromStream, ProcessItemProc, SyntaxError], EditSpan USING [Copy], IO USING [STREAM, BreakProc, Close, EndOf, EndOfStream, Error, GetChar, GetTokenRope, PutF, PutF1, PutFR, PutFR1, RIS, SetIndex, SkipWhitespace, int, rope, time], List USING [Nconc1], NodeProps USING [PutProp, ValueFromBool], PFS USING [Error, FileLookup, PATH, PathFromRope, RopeFromPath, StreamOpen], PFSNames USING [Component, ComponentRope, ShortName], Process USING [CheckForAbort], RedBlackTree USING [Compare, Create, DuplicateKey, EnumerateIncreasing, GetKey, Insert, UserData, Size, Table], Rope, SystemNames USING [ReleaseName, UserName], TextNode USING [Level, Location, MakeNodeLoc, nullLocation, nullSpan, Ref, Root, Span, StepForward], Tioga, TiogaFileOps USING [AddLooks, CreateRoot, InsertAsLastChild, InsertNode, NodeBody, SetContents, SetFormat, Store], TiogaIO USING [FromFile], UserProfile USING [Token], ViewerIO USING [CreateViewerStreams]; CatalogImpl: CEDAR PROGRAM IMPORTS Ascii, BasicTime, Commander, CommanderOps, DFUtilities, EditSpan, IO, List, NodeProps, PFS, PFSNames, Process, RedBlackTree, Rope, SystemNames, TextNode, TiogaFileOps, TiogaIO, UserProfile, ViewerIO EXPORTS TiogaFileOps -- export NodeBody to convince Compiler we know that TiogaFileOps.Ref is really TextNode.Ref ~ BEGIN <<>> ROPE: TYPE ~ Rope.ROPE; STREAM: TYPE ~ IO.STREAM; Ref: TYPE ~ Tioga.Node; NodeBody: PUBLIC TYPE ~ Tioga.NodeRep; CatalogCmdProc: Commander.CommandProc ~ { rootName: ROPE; catalogFileName: ROPE; argv: CommanderOps.ArgumentVector ¬ CommanderOps.Parse[cmd]; quietly, releaseMessage: BOOLEAN ¬ FALSE; documentationWarnings: BOOLEAN ¬ TRUE; i: NAT ¬ 1; WHILE i < argv.argc DO IF Rope.Fetch[argv[i], 0] = '- THEN { SELECT Ascii.Lower[Rope.Fetch[argv[i], 1]] FROM 'd => documentationWarnings ¬ FALSE; 'q => quietly ¬ TRUE; 'r => releaseMessage ¬ TRUE; ENDCASE => RETURN [$Failure, Rope.Concat[argv[i], " is an unrecognized option; available options are -quietly, -releaseMessage and -documentationWarnings"]]; } ELSE { IF rootName # NIL THEN RETURN [$Failure, "multiple catalog names supplied; only one allowed."]; rootName ¬ argv[i]; }; i ¬ i+1; ENDLOOP; IF rootName = NIL THEN RETURN; catalogFileName ¬ Catalog[rootName, IF quietly THEN NIL ELSE cmd.out, releaseMessage, documentationWarnings]; RETURN [NIL, Rope.Cat["\n", catalogFileName, " created."]]; }; BaseNameFromFileName: PROC [name: ROPE] RETURNS [ROPE] ~ { path: PFS.PATH ~ PFS.PathFromRope[name]; short: ROPE ¬ PFSNames.ComponentRope[PFSNames.ShortName[path]]; pos: INT ¬ short.Find["-"]; IF pos = -1 THEN pos ¬ short.FindBackward["."]; IF pos = -1 THEN RETURN[short]; RETURN[short.Substr[0, pos]]; }; Catalog: PROC [rootName: ROPE, out: STREAM ¬ NIL, releaseMessage: BOOL ¬ FALSE, documentationWarnings: BOOL ¬ TRUE] RETURNS [catalogFileName: ROPE] ~ { fullRootNamePath: PFS.PATH ~ PFS.FileLookup[PFS.PathFromRope[rootName], LIST["df"]]; rootStream: STREAM ~ PFS.StreamOpen[fullRootNamePath]; fullRootName: ROPE ~ PFS.RopeFromPath[fullRootNamePath]; rootBase: ROPE ~ BaseNameFromFileName[rootName]; -- e.g., "PCedar" catalogBase: ROPE ~ Rope.Concat[rootBase, IF releaseMessage THEN "ReleaseCatalog" ELSE "Catalog"]; catalogName: ROPE ~ Rope.Concat[catalogBase, ".tioga"]; logName: ROPE ~ Rope.Concat[catalogBase, ".log"]; catalogDoc: CatalogDoc ~ InitializeCatalogDoc[rootBase, catalogName]; ProcessItem: DFUtilities.ProcessItemProc ~ { <> WITH item SELECT FROM item: REF DFUtilities.IncludeItem => { dfName: ROPE ~ item.path1; packageName: ROPE ~ BaseNameFromFileName[dfName]; Process.CheckForAbort[]; AddCatalogEntry[doc~catalogDoc, packageName~packageName, dfFileName~dfName, releaseMessage~releaseMessage, documentationWarnings~documentationWarnings]; }; ENDCASE; }; catalogDoc.out ¬ out; catalogDoc.log ¬ ViewerIO.CreateViewerStreams[ name: logName, backingFile: logName, editedStream: FALSE].out; catalogDoc.log.PutF["Catalog of %g, %g\n\n", [rope[fullRootName]], [time[BasicTime.Now[]]] ]; <> DFUtilities.ParseFromStream[rootStream, ProcessItem]; rootStream.Close[]; FinishCatalogDoc[catalogDoc]; RETURN [catalogDoc.fileName] }; CatalogDoc: TYPE ~ REF CatalogDocRec; CatalogDocRec: TYPE ~ RECORD[ out: STREAM, -- for recording our progress log: STREAM, -- for logging errors fileName: ROPE, packageName: ROPE, root: Ref, latestLevel1Node: Ref, latestLevel2Node: Ref, latestLevel3Node: Ref, commandIndex: IndexTable, keywordIndex: IndexTable ]; Header: PROC [base: ROPE] RETURNS [ROPE] ~ { UpperCase: PROC [r: ROPE] RETURNS [ROPE] ~ { upper: PROC [old: CHAR] RETURNS [new: CHAR] ~ { new ¬ Ascii.Upper[old] }; RETURN[Rope.Translate[base: r, translator: upper]]; }; RETURN[IO.PutFR1["%g PACKAGE CATALOG", IO.rope[UpperCase[base]] ]]; }; Footer: PROC RETURNS [ROPE] ~ { RETURN[IO.PutFR1["CEDAR %g  FOR INTERNAL XEROX USE ONLY", IO.rope[SystemNames.ReleaseName[]] ]]; }; InitializeCatalogDoc: PROC [catalogName, fileName: ROPE] RETURNS [doc: CatalogDoc] ~ { PutPropRope: PROC [n: TextNode.Ref, name: ATOM, value: ROPE] ~ { <> NodeProps.PutProp[n: n, name: name, value: value]; }; doc ¬ NEW[CatalogDocRec]; doc.commandIndex ¬ CreateIndexTable[]; doc.keywordIndex ¬ CreateIndexTable[]; doc.fileName ¬ fileName; doc.latestLevel1Node ¬ NIL; doc.root ¬ TiogaFileOps.CreateRoot[]; AppendNode[level~1, doc~doc, r~doc.fileName, format~NIL]; NodeProps.PutProp[doc.latestLevel1Node, $Comment, NodeProps.ValueFromBool[TRUE]]; AppendNode[level~2, doc~doc, format~NIL, r~IO.PutFR["%g %t", IO.rope[UserProfile.Token["EditorComforts.LastEdited", SystemNames.UserName[]]], IO.time[BasicTime.Now[]]]]; NodeProps.PutProp[doc.latestLevel2Node, $Comment, NodeProps.ValueFromBool[TRUE]]; AppendNode[level~1, doc~doc, r~Header[catalogName], 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~Footer[], 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[catalogName, " Package Catalog"], format~"title"]; AppendNode[level~1, doc~doc, r~IO.PutFR1["c Copyright %g by Xerox Corporation. All rights reserved.", IO.int[BasicTime.Unpack[BasicTime.Now[]].year]], 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"]; <> }; 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, releaseMessage: BOOL ¬ FALSE, documentationWarnings: BOOL ¬ TRUE] ~ { lastDirectoryPath: ROPE ¬ NIL; documentationDirectory: BOOL ¬ FALSE; packageDoc: ROPE ~ Rope.Concat[packageName, "Doc.tioga"]; documentationFileName: ROPE ¬ NIL; documentationFiles: ROPE ¬ NIL; listOfCommands: LIST OF REF ANY ¬ NIL; moreDFs: LIST OF ROPE; TryThisDFItem: DFUtilities.ProcessItemProc ~ { <> WITH item SELECT FROM directory: REF DFUtilities.DirectoryItem => { lastDirectoryPath ¬ directory.path1; documentationDirectory ¬ Rope.Match["*", lastDirectoryPath, FALSE]; <> }; incl: REF DFUtilities.IncludeItem => { IF ( Rope.Find[incl.path1, "-Source.df", 0, FALSE] # -1 ) OR ( Rope.Find[incl.path1, "-PCR.df", 0, FALSE] # -1 ) THEN { moreDFs ¬ CONS[incl.path1, moreDFs]; <> }; <> }; file: REF DFUtilities.FileItem => { base, ext, shortName: ROPE ¬ NIL; [base, ext, shortName] ¬ ParseFileName[file.name ! PFS.Error => IF error.group#bug THEN { Log[doc, IO.PutFR1["PFS.Error... %g", IO.rope[error.explanation]]]; GOTO Fail; }; ]; SELECT TRUE FROM Rope.Equal["command", ext, FALSE] => { <> listOfCommands ¬ List.Nconc1[listOfCommands, shortName]; }; Rope.Equal["cm", ext, FALSE] => { <> listOfCommands ¬ List.Nconc1[listOfCommands, shortName]; }; Rope.Equal["tioga", ext, FALSE] OR Rope.Equal["ip", ext, FALSE] OR Rope.Equal["intepress", ext, FALSE] OR documentationDirectory => { IF documentationWarnings AND documentationDirectory THEN Log[doc, IO.PutFR1["%g is in the 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: STREAM ¬ NIL; Process.CheckForAbort[]; BeginPackage[doc, packageName]; dfStream ¬ PFS.StreamOpen[fileName: PFS.PathFromRope[dfFileName] ! PFS.Error => { Log[doc, IO.PutFR1["PFS.Error... %g", IO.rope[error.explanation]]]; GO TO FileProblem; }; ]; DFUtilities.ParseFromStream[in~dfStream, proc~TryThisDFItem, filter~[comments~FALSE, filterA~source, filterB~all, filterC~defining] ! DFUtilities.SyntaxError => { Log[doc, IO.PutFR1["DFUtilities.SyntaxError... %g", IO.rope[reason]]]; CONTINUE; }; ]; dfStream.Close[]; IF moreDFs # NIL THEN FOR dL: LIST OF ROPE ¬ moreDFs, dL.rest UNTIL dL = NIL DO dfs: STREAM; Process.CheckForAbort[]; dfs ¬ PFS.StreamOpen[fileName: PFS.PathFromRope[dL.first] ! PFS.Error => { Log[doc, IO.PutFR1["PFS.Error... %g", IO.rope[error.explanation]]]; CONTINUE; }; ]; IF dfs = NIL THEN LOOP; DFUtilities.ParseFromStream[in~dfs, proc~TryThisDFItem, filter~[comments~FALSE, filterA~source, filterB~all, filterC~defining] ! DFUtilities.SyntaxError => { Log[doc, IO.PutFR1["DFUtilities.SyntaxError... %g", IO.rope[reason]]]; CONTINUE; }; ]; ENDLOOP; Process.CheckForAbort[]; { -- create the catalog entry author, creator, maintainer: ROPE ¬ NIL; abstractSpan: TextNode.Span ¬ TextNode.nullSpan; keywords: ROPE; singlePageDocument: BOOLEAN; IF documentationFileName.IsEmpty THEN { IF NOT Rope.Equal[doc.packageName, "Top", FALSE] THEN Log[doc, "No package documentation."] } ELSE { [abstractSpan: abstractSpan, author: author, creator: creator, maintainer: maintainer, keywords: keywords, singlePageDocument: singlePageDocument] ¬ ExtractStuffFromDocFile[doc, documentationFileName]; IF maintainer.IsEmpty[] THEN { Log[doc, "No maintainer listed in documentation."] }; }; IF releaseMessage AND maintainer.IsEmpty[] AND NOT Rope.Match["*CedarChest*", dfFileName, FALSE] THEN maintainer ¬ "Maintained by: CedarSupport^.pa"; AppendNode[level~2, doc~doc, r~Rope.Cat[packageName, ": ", dfFileName], format~"head"]; <> <> IF (NOT releaseMessage) AND (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 { <> AppendNode[level~3, doc~doc, r~Rope.Concat["Documentation: ", documentationFiles], format~"indent"]; TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~14, look~'b, root~doc.root]; }; <> <> <> <<};>> IF (NOT releaseMessage) AND (NOT keywords.IsEmpty) THEN { CommaSeparated: IO.BreakProc ~ { <> RETURN [SELECT char FROM ', => sepr, ENDCASE => other] }; s: STREAM ¬ IO.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: ROPE ¬ NIL; 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: ROPE ¬ NIL; FOR l: LIST OF REF ANY ¬ listOfCommands, l.rest WHILE l # NIL DO commandName: ROPE ¬ NARROW[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 releaseMessage THEN { field: ROPE ~ "changes"; size: INT ~ Rope.Size[field]; AppendNode[level~3, doc~doc, r~field, format~"indent"]; TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~0, len~1, look~'t, root~doc.root]; TiogaFileOps.AddLooks[x~doc.latestLevel3Node, start~size-1, len~1, look~'t, root~doc.root]; } ELSE 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: ROPE ¬ NIL, singlePageDocument: BOOLEAN ¬ FALSE ] ~ { tiogaDocRoot: Ref ~ TiogaIO.FromFile[PFS.PathFromRope[documentationFile] ! PFS.Error => { Log[doc, IO.PutFR1["PFS.Error... %g", IO.rope[error.explanation]]]; GOTO Quit; }; ].root; prev: Ref ¬ NIL; hasCopyright: BOOL ¬ FALSE; AbstractNode: PROC [node: Ref, begin: BOOL ¬ FALSE] ~ { nodeLoc: TextNode.Location ~ TextNode.MakeNodeLoc[node]; IF abstractSpan.start=TextNode.nullLocation THEN { IF begin OR hasCopyright THEN abstractSpan.start ¬ abstractSpan.end ¬ nodeLoc; <> } ELSE { IF abstractSpan.end.node=prev THEN abstractSpan.end ¬ nodeLoc; <> }; }; FOR node: Ref ¬ Forward[tiogaDocRoot], Forward[prev ¬ node] WHILE node#NIL DO SELECT node.format FROM $authors => { IF NOT author.IsEmpty[] THEN author ¬ author.Concat["; "]; author ¬ author.Concat[node.rope]; }; $abstract => { <> <> <> <> <> <> contents: ROPE ~ node.rope; 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; <> 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] ~ { pos: INT; shortName ¬ PFSNames.ComponentRope[PFSNames.ShortName[PFS.PathFromRope[fileName]] ]; pos ¬ Rope.FindBackward[shortName, "."]; IF pos = -1 THEN RETURN[shortName, NIL, shortName]; base ¬ Rope.Substr[shortName, 0, pos]; ext ¬ Rope.Substr[shortName, pos + 1]; }; 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: STREAM ~ doc.out; IF out#NIL THEN out.PutF1["%g ... ", IO.rope[packageName]]; doc.packageName ¬ packageName; }; Log: PROC [doc: CatalogDoc, msg: ROPE] ~ { log: 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: REF ¬ NEW[PairRec ¬ [term, location]]; RedBlackTree.Insert[table, data, data ! RedBlackTree.DuplicateKey => CONTINUE]; }; GetIndexKey: RedBlackTree.GetKey ~ { <> RETURN [data]; }; CompareIndexKeys: RedBlackTree.Compare ~ { <> 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: BOOL ¬ FALSE] ~ { pair: Pair ¬ NARROW[data]; RETURN [proc[pair.term, pair.location]]; }; RedBlackTree.EnumerateIncreasing[table, ApplyProc]; }; Commander.Register[ key~"Catalog", proc~CatalogCmdProc, doc~"Catalog [-quietly] [-documentationWarnings] [-releaseMessage] make a catalog of software packages stored as DF files in ///Top/*.DF" ]; END.