<> <<>> <> <> <> <<>> <> <<1. allow several files to be printed with a single command.>> <<2. recognize node formats that have extra lead in front (e.g. unit in Cedar.style), and insert a blank line.>> <<3. Some form of synchronization with Tioga Save button (like the Compile command).>> DIRECTORY Ascii USING [Lower], Commander USING [CommandProc, Handle, Register], CommandTool USING [ArgumentVector, Failed, Parse], Convert USING [AppendInt, Error, IntFromRope], FS USING [Close, ComponentPositions, Create, Delete, ExpandName, Error, ErrorDesc, ErrorFromStream, FileInfo, GetInfo, GetName, nullOpenFile, Open, OpenFile, StreamFromOpenFile], IPOutput USING [BeginMasterFromStream, BeginPage, BeginPreamble, Concat, ConcatT, EndMaster, EndPage, EndPreamble, FGet, FSet, Master, Rotate, ScaleT, SetFont, SetupFont, SetXY, Show, Translate], IO USING [Backup, Close, CR, EndOfStream, Error, FF, GetChar, GetLength, int, PutChar, PutF, PutFR, PutRope, rope, SP, STREAM, TAB, time], IOClasses USING [CreatePipe], PutGet USING [FromFileC, FromFileError], PutGetExtras USING [WritePlain], RefText USING [AppendChar, AppendRope, New, TrustTextAsRope], Rope USING [Cat, Concat, Fetch, Find, IsEmpty, Length, ROPE, Substr], TEditInput USING [FreeTree], TEditProfile USING [sourceExtensions], TextNode USING [Ref], UserProfile USING [Token]; IPrint: CEDAR PROGRAM IMPORTS Ascii, Commander, CommandTool, Convert, FS, IPOutput, IO, IOClasses, PutGet, PutGetExtras, RefText, Rope, TEditInput, TEditProfile, UserProfile = BEGIN ROPE: TYPE = Rope.ROPE; STREAM: TYPE = IO.STREAM; Master: TYPE ~ IPOutput.Master; Micas: TYPE = INT; WritePreamble: PROC[master: Master] ~ { master.BeginPreamble[]; master.SetupFont[n: 0, name: "Xerox/PressFonts/Gacha/MRR", scale: 282]; -- 8 point Gacha master.SetupFont[n: 1, name: "Xerox/PressFonts/Gacha/MRR", scale: 212]; -- 6 point Gacha master.Translate[-110*254/2, -85*254/2]; master.Rotate[90]; master.Concat[]; master.Translate[85*254/2, 110*254/2]; master.Concat[]; master.FSet[11]; -- transformation to rotate the page by 90 degrees master.EndPreamble[]; }; SetGacha: PROC[master: Master, size: NAT] ~ { master.SetFont[SELECT size FROM 8 => 0, 6 => 1 ENDCASE => ERROR]; }; WritePageBeginning: PROC[master: Master] ~ { master.BeginPage[]; master.ScaleT[0.00001]; }; WriteLandscapeTransformation: PROC[master: Master] ~ { master.FGet[11]; master.ConcatT[]; }; WritePageEnding: PROC[master: Master] ~ { master.EndPage[]; }; InterpressFromText: PROC [text: --input-- STREAM, interpress: --output-- STREAM, landscape: BOOL, fileNameForLeaderPage: --only first 49 chars will show-- ROPE, fileNameForHeading: --approximately first 90 chars will show-- ROPE] RETURNS [interpressPages: INT] = { <<-- ! IO.Error [$Failure] (FS errors)>> <<-- Does not close text or interpress streams.>> columnsPerPage: [1 .. 2]; maxCharsPerLine, maxLinesPerColumn: NAT; xCoordinateOfLeftEdge, columnOffset, yCoordinateOfHeadingBaseline, yCoordinateOfFirstBaseline, deltaYCoordinateBetweenLines: Micas; pageStarted: BOOLEAN _ FALSE; ipMaster: Master ~ IPOutput.BeginMasterFromStream[interpress]; WritePreamble[ipMaster]; { <<-- Initialize parameters that depend upon the landscape switch>> ComputeColumnParms: PROC [topMargin, bottomMargin, leftMargin, pageHeight, columnWidthForText, charWidth, deltaYCoordinateBetweenLines: Micas] RETURNS [maxCharsPerLine, maxLinesPerColumn: NAT, xCoordinateOfLeftEdge, yCoordinateOfFirstBaseline: Micas] = { xCoordinateOfLeftEdge _ leftMargin; yCoordinateOfFirstBaseline _ pageHeight - topMargin; maxLinesPerColumn _ (yCoordinateOfFirstBaseline-bottomMargin) / deltaYCoordinateBetweenLines; maxCharsPerLine _ columnWidthForText / charWidth; }; IF landscape THEN { columnsPerPage _ 2; columnOffset _ 13335; deltaYCoordinateBetweenLines _ 254; [maxCharsPerLine, maxLinesPerColumn, xCoordinateOfLeftEdge, yCoordinateOfFirstBaseline] _ ComputeColumnParms[ topMargin: 1905, bottomMargin: 1905, leftMargin: 1270, pageHeight: 21590, columnWidthForText: 12065, charWidth: 129, deltaYCoordinateBetweenLines: deltaYCoordinateBetweenLines]; yCoordinateOfHeadingBaseline _ 952; } ELSE { columnsPerPage _ 1; columnOffset _ 0; deltaYCoordinateBetweenLines _ 338; [maxCharsPerLine, maxLinesPerColumn, xCoordinateOfLeftEdge, yCoordinateOfFirstBaseline] _ ComputeColumnParms[ topMargin: 2581, bottomMargin: 1270, leftMargin: 1905, pageHeight: 27940, columnWidthForText: 18415, charWidth: 173, deltaYCoordinateBetweenLines: deltaYCoordinateBetweenLines]; yCoordinateOfHeadingBaseline _ 26035; } }; { column: INT; xCoordinateOfColumnLeftEdge: Micas; headingBuffer: REF TEXT _ RefText.New[maxCharsPerLine]; PrintHeading: PROC [] = { <<-- Uses headingBuffer, column, columnsPerPage, fileNameForHeading, xCoordinateOfColumnLeftEdge, yCoordinateOfHeadingBaseline, and ipMaster.>> headingBuffer.length _ 0; IF (column-1) MOD columnsPerPage = 0 THEN { -- First column of page start: INT _ fileNameForHeading.Length[] - (maxCharsPerLine-11); IF start > 0 THEN { headingBuffer _ RefText.AppendRope[to: headingBuffer, from: "..."]; start _ start + 3; } ELSE start _ 0; headingBuffer _ RefText.AppendRope[to: headingBuffer, from: fileNameForHeading, start: start]; }; FOR i: NAT IN [headingBuffer.length .. maxCharsPerLine-11) DO headingBuffer _ RefText.AppendChar[to: headingBuffer, from: IO.SP]; ENDLOOP; headingBuffer _ RefText.AppendRope[to: headingBuffer, from: " Page "]; headingBuffer _ Convert.AppendInt[to: headingBuffer, from: column]; ipMaster.SetXY[xCoordinateOfColumnLeftEdge, yCoordinateOfHeadingBaseline]; ipMaster.Show[RefText.TrustTextAsRope[headingBuffer]]; }; indent: BOOL _ FALSE; indentChars: NAT _ 0; lineBuffer: REF TEXT = RefText.New[maxCharsPerLine+1]; PrintColumn: PROC [] RETURNS [eof: BOOL] = { <<-- Uses indent, indentChars, text, ipMaster, PrintHeading, maxCharsPerLine, maxLinesPerColumn, xCoordinateOfColumnLeftEdge, yCoordinateOfFirstBaseline, deltaYCoordinateBetweenLines, pageStarted, landscape, and lineBuffer.>> <<-- Returns eof = TRUE iff end of stream from text and no printing has occurred to ipMaster.>> ff: BOOL; FOR linesPrinted: INT _ 0, linesPrinted.SUCC UNTIL linesPrinted = maxLinesPerColumn DO [eof, ff, indent, indentChars] _ FillLineBuffer[lineBuffer, maxCharsPerLine, indent, indentChars, text]; IF linesPrinted = 0 THEN { IF eof THEN RETURN [TRUE]; IF ff THEN LOOP; IF NOT pageStarted THEN { WritePageBeginning[ipMaster]; IF landscape THEN WriteLandscapeTransformation[ipMaster]; pageStarted _ TRUE; SetGacha[ipMaster, IF landscape THEN 6 ELSE 8]; }; PrintHeading[]; }; IF eof OR ff THEN RETURN [FALSE]; ipMaster.SetXY[xCoordinateOfColumnLeftEdge, yCoordinateOfFirstBaseline - linesPrinted*deltaYCoordinateBetweenLines]; ipMaster.Show[RefText.TrustTextAsRope[lineBuffer]]; ENDLOOP; RETURN [FALSE]; }; FOR column _ 1, column _ column.SUCC DO xCoordinateOfColumnLeftEdge _ xCoordinateOfLeftEdge + ((column-1) MOD columnsPerPage)*columnOffset; IF PrintColumn[].eof THEN EXIT; IF column MOD columnsPerPage = 0 THEN { IF pageStarted THEN WritePageEnding[ipMaster]; pageStarted _ FALSE; }; ENDLOOP; IF pageStarted THEN {WritePageEnding[ipMaster]; pageStarted _ FALSE}; ipMaster.EndMaster[]; RETURN [(column-1+columnsPerPage-1)/columnsPerPage] }; };--InterpressFromText FillLineBuffer: PROC [lineBuffer: REF TEXT, maxCharsPerLine: NAT, indent: BOOL, indentChars: NAT, text: --input-- STREAM] RETURNS [eof, ff: BOOL _ FALSE, nextIndent: BOOL _ FALSE, nextIndentChars: NAT _ 0] = { <<-- lineBuffer is garbage on entry. lineBuffer.maxLength >= maxCharsPerLine.>> <<-- Returns eof = TRUE iff end of stream from text and lineBuffer is empty.>> <<-- Returns ff = TRUE iff form-feed char read from text and lineBuffer is empty.>> spacesPerTab: NAT = 4; extraIndentForFollowingLines: NAT = 2; lineLength, maxLineLength: NAT; OpenLine: PROC [] = { lineLength _ 0; maxLineLength _ IF indent THEN maxCharsPerLine-indentChars ELSE maxCharsPerLine; }; Append: PROC [c: CHAR] = { lineBuffer[lineLength] _ c; lineLength _ lineLength + 1 }; UnAppend: PROC [n: NAT] = { FOR i: NAT DECREASING IN [lineLength-n .. lineLength-1] DO text.Backup[lineBuffer[i]]; ENDLOOP; lineLength _ lineLength - n; }; IsTabStop: PROC [] RETURNS [BOOL] = { RETURN [ (IF indent THEN lineLength+indentChars ELSE lineLength) MOD spacesPerTab = 0]; }; IndexOfLastWhiteSpace: PROC [] RETURNS [n: NAT] = { FOR i: NAT DECREASING IN [1 .. lineLength) DO -- don't look at first char in line IF lineBuffer[i].ORD <= (IO.SP).ORD THEN RETURN [i]; ENDLOOP; <<-- here only if breaking an all-nonwhite line (unlikely)>> RETURN [NAT.LAST] }; IndexOfFirstNonwhiteSpace: PROC [] RETURNS [n: NAT] = { FOR i: NAT IN [0 .. lineLength) DO IF lineBuffer[i].ORD > (IO.SP).ORD THEN RETURN [i]; ENDLOOP; <<-- here only if breaking an all-white line (very unlikely)>> RETURN [0]; }; CloseLine: PROC [] RETURNS [] = { IF indent THEN { FOR i: NAT DECREASING IN [0 .. lineLength) DO lineBuffer[i + indentChars] _ lineBuffer[i]; ENDLOOP; lineLength _ lineLength + indentChars; FOR i: NAT IN [0 .. indentChars) DO lineBuffer[i] _ IO.SP ENDLOOP; }; lineBuffer.length _ lineLength; }; OpenLine[]; DO { <<-- Assert 0 <= lineLength <= maxLineLength>> char: CHAR; { char _ text.GetChar[ ! IO.EndOfStream => GOTO endOfStream]; EXITS endOfStream => { char _ IO.CR; eof _ TRUE } }; IF eof AND lineLength = 0 THEN GOTO done; IF char = IO.FF THEN { IF lineLength = 0 THEN { ff _ TRUE; GOTO done }; text.Backup[char]; char _ IO.CR; }; IF char = IO.CR THEN GOTO done; IF char = IO.TAB THEN { THROUGH [0 .. spacesPerTab) DO Append[IO.SP]; IF IsTabStop[] THEN EXIT; IF lineLength > maxLineLength THEN EXIT; ENDLOOP; } ELSE Append[char]; <<-- Assert 0 < lineLength <= maxLineLength+1>> IF lineLength > maxLineLength THEN { <<-- line won't fit in buffer; must insert extra line break>> i: NAT _ IndexOfLastWhiteSpace[]; IF i = NAT.LAST THEN { <<-- line won't break at white space; break at end and insert "~~">> UnAppend[3]; Append['~]; Append['~]; } ELSE { <<-- put back chars following the last white space; then discard the last white space>> UnAppend[lineLength - (i+1)]; lineLength _ lineLength - 1; }; <<-- Assert 0 < lineLength <= maxLineLength>> nextIndentChars _ IF indent THEN indentChars ELSE MIN[ IndexOfFirstNonwhiteSpace[] + extraIndentForFollowingLines, maxCharsPerLine/2]; nextIndent _ TRUE; GOTO done; }; EXITS done => { CloseLine[]; RETURN } } ENDLOOP; };--FillLineBuffer PlainTextStreamFromNode: PROC [from: TextNode.Ref, to: STREAM] = { <<-- Writes a plain text representation of from and its descendants onto to, then Closes to.>> PutGetExtras.WritePlain[h: to, root: from, restoreDashes: TRUE]; to.Close[]; RETURN; }; InterpressFileFromFile: PROC [from, to: FS.OpenFile, landscape: BOOL, ignoreNodes: BOOL, cmd: Commander.Handle] RETURNS [ok: BOOL] = { <<-- Writes a plain text representation of file onto a interpress file named interpressFileName.>> <<-- Avoids producing internal Tioga document format if ignoreNodes and for plain text files.>> <<-- ! IO.Error[$Failure] (FS errors)>> fromStream: STREAM = from.StreamFromOpenFile[ accessRights: $read, streamOptions: [tiogaRead: TRUE, commitAndReopenTransOnFlush: TRUE, truncatePagesOnClose: TRUE, finishTransOnClose: TRUE, closeFSOpenFileOnClose: FALSE]]; toStream: STREAM = to.StreamFromOpenFile[ accessRights: $write, streamOptions: [commitAndReopenTransOnFlush: TRUE, truncatePagesOnClose: TRUE, finishTransOnClose: TRUE, closeFSOpenFileOnClose: FALSE]]; fromName, fromAttachedToName, toName: ROPE; fileNameForLeaderPage, fileNameForHeading: ROPE; [fromName, fromAttachedToName] _ from.GetName[]; [toName, ] _ to.GetName[]; fileNameForLeaderPage _ fromName; fileNameForHeading _ IO.PutFR["%g of %g", IO.rope[IF fromAttachedToName.IsEmpty[] THEN fromName ELSE fromAttachedToName], IO.time[from.GetInfo[].created]]; { interpressPages: INT; IF ignoreNodes OR fromStream.GetLength[] = from.GetInfo[].bytes THEN { <<-- Read plain text directly from from.>> cmd.out.PutF["Reading text file %g, writing Interpress file %g ... ", IO.rope[fromName], IO.rope[toName]]; interpressPages _ InterpressFromText[ fromStream, toStream, landscape, fileNameForLeaderPage, fileNameForHeading]; } ELSE { <<-- Built Tioga tree structure from from, then fork a process to produce a plain text version.>> rootNode: TextNode.Ref; pushStream, pullStream: STREAM; producer: PROCESS _ NIL; cmd.out.PutF["Reading Tioga file %g, creating tree structure... ", IO.rope[fromName]]; rootNode _ PutGet.FromFileC[from ! PutGet.FromFileError => { cmd.out.PutRope["Tioga error reading file\n"]; GOTO fail }]; cmd.out.PutF["Reading tree structure, writing Interpress file %g ... ", IO.rope[toName]]; [push: pushStream, pull: pullStream] _ IOClasses.CreatePipe[bufferByteCount: 1000]; producer _ FORK PlainTextStreamFromNode[from: rootNode, to: pushStream]; interpressPages _ InterpressFromText[ pullStream, toStream, landscape, fileNameForLeaderPage, fileNameForHeading]; TRUSTED { JOIN producer; producer _ NIL }; TEditInput.FreeTree[rootNode]; pullStream.Close[]; }; fromStream.Close[]; toStream.Close[]; cmd.out.PutF[" %g Interpress pages written.\n", IO.int[interpressPages]]; RETURN [ok: TRUE] EXITS fail => { RETURN [ok: FALSE] } } }; <> <> <<-- Sends press file, giving feedback on its progress to cmd and checking for process abort.>> <<-- "For:" name on break page will be that of the logged-in user.>> <> <> <> <> <> <> <> <> <<};>> <> <<{>> < GOTO abortTransmission];>> < {>> <> <> <<-- this proc is called again with state = $aborted; then ABORTED is raised by SendPressFile>> <<};>> <<};>> <<};--SendPressFileProgress>> <> <> <> <> < CONTINUE];>> <> <> <> <> <<};>> <> <<};>> FileCheck: PROC [fileName: ROPE] RETURNS [exists: BOOL, fullFName: ROPE] = { <<-- Returns exists: TRUE iff file exists. If exists then fullFName is filled in to make later lookup faster.>> [fullFName: fullFName] _ FS.FileInfo[name: fileName ! FS.Error => IF error.group = $user THEN GOTO doesNotExist]; RETURN [exists: TRUE, fullFName: fullFName] EXITS doesNotExist => RETURN [exists: FALSE, fullFName: NIL]; }; CompleteFileName: PROC [fileName: ROPE] RETURNS [fullFName: ROPE] = { <<-- Returns NIL if can't find a completion that corresponds to an existing file>> exists: BOOL; [exists, fullFName] _ FileCheck[fileName]; IF exists OR fileName.Find["!"] >= 0 OR fileName.Find["."] >= 0 THEN RETURN; FOR extList: LIST OF ROPE _ TEditProfile.sourceExtensions, extList.rest UNTIL extList = NIL DO f: ROPE = fileName.Concat[extList.first]; [exists, fullFName] _ FileCheck[f]; IF exists THEN RETURN; ENDLOOP; }; InterpressFileName: PROC [fileName: ROPE, newDir: ROPE] RETURNS [ROPE] = { cp: FS.ComponentPositions; base: ROPE; [fullFName: fileName, cp: cp] _ FS.ExpandName[name: fileName]; base _ fileName.Substr[cp.base.start, cp.base.length]; RETURN [Rope.Cat[newDir, base, ".interpress"]]; }; DoPrint: Commander.CommandProc = { <> argv: CommandTool.ArgumentVector _ NIL; i: NAT; fileName: ROPE; landscape: BOOL _ TRUE; ignoreNodes: BOOL _ FALSE; nCopies: INT _ 1; host: ROPE _ UserProfile.Token[key: "Hardcopy.InterpressPrinter", default: NIL]; hostSpecified: BOOL _ NOT host.IsEmpty[]; interpressFileName: ROPE _ NIL; retainInterpressFile: BOOL _ FALSE; { -- interpret the command line, modifying fileName, ... , retainInterpressFile. IsParm: PROC [i: NAT] RETURNS [BOOL] = { RETURN [i # argv.argc-1 AND NOT argv[i].IsEmpty[] AND argv[i].Fetch[0] # '-]; }; argv _ CommandTool.Parse[cmd ! CommandTool.Failed => { msg _ errorMsg; CONTINUE; }]; IF argv = NIL THEN RETURN[$Failure, msg]; IF argv.argc < 2 THEN RETURN[$Failure, printHelpText]; FOR i _ 1, i.SUCC UNTIL i = argv.argc-1 DO IF argv[i].Length[] # 2 OR argv[i].Fetch[0] # '- THEN GOTO switchSyntaxError; SELECT Ascii.Lower[argv[i].Fetch[1]] FROM 'p => landscape _ FALSE; 't => ignoreNodes _ TRUE; 'c => { i _ i.SUCC; nCopies _ Convert.IntFromRope[argv[i] ! Convert.Error => GOTO numberSyntaxError]; }; 'h => { hostSpecified _ TRUE; IF IsParm[i.SUCC] THEN { i _ i.SUCC; host _ argv[i] } ELSE host _ NIL; }; 'r => { retainInterpressFile _ TRUE; IF IsParm[i.SUCC] THEN { i _ i.SUCC; interpressFileName _ argv[i] } ELSE interpressFileName _ NIL; }; ENDCASE => GOTO switchSyntaxError; ENDLOOP; fileName _ argv[i]; EXITS switchSyntaxError => RETURN[ $Failure, IO.PutFR["Unrecognized switch: \"%g\"\n", IO.rope[argv[i]]]]; numberSyntaxError => RETURN[ $Failure, IO.PutFR["Unrecognized number: \"%g\"\n", IO.rope[argv[i]]]]; }; { -- Validate fileName (add file extension if not specified) and check hostSpecified completeFileName: ROPE _ CompleteFileName[fileName]; IF completeFileName.IsEmpty[] THEN RETURN[$Failure, IO.PutFR["Could not find file: \"%g\"\n", IO.rope[fileName]]]; fileName _ completeFileName; <> <> IF host.IsEmpty THEN retainInterpressFile _ TRUE; }; { -- do the real work <<-- File error handing is done at this level>> inputFile, interpressFile: FS.OpenFile _ FS.nullOpenFile; { ENABLE { IO.Error => { error: FS.ErrorDesc; IF ec # $Failure THEN GOTO cantHandle; error _ FS.ErrorFromStream[stream ! IO.Error => GOTO cantHandle]; cmd.out.PutF["FS error: %g\n", IO.rope[error.explanation]]; GOTO cleanupAfterError; EXITS cantHandle => REJECT }; FS.Error => { cmd.out.PutF["FS error: %g\n", IO.rope[error.explanation]]; GOTO cleanupAfterError; }; ABORTED => { GOTO cleanupAfterError; }; }; IF --PressPrinter.IsAPressFile[fileName]--FALSE THEN { cmd.out.PutF["File %g is already in press format\n", IO.rope[fileName]]; interpressFileName _ fileName; retainInterpressFile _ TRUE; } ELSE { inputFile, interpressFile: FS.OpenFile; IF interpressFileName.IsEmpty[] THEN interpressFileName _ InterpressFileName[ fileName, IF retainInterpressFile THEN "" ELSE "[]<>Temp>"]; inputFile _ FS.Open[fileName, $read]; interpressFile _ FS.Create[name: interpressFileName, setKeep: retainInterpressFile, keep: IF retainInterpressFile THEN 2 ELSE 1]; IF NOT InterpressFileFromFile[from: inputFile, to: interpressFile, landscape: landscape, ignoreNodes: ignoreNodes, cmd: cmd] THEN GOTO cleanupAfterError; inputFile.Close[]; inputFile _ FS.nullOpenFile; interpressFile.Close[]; interpressFile _ FS.nullOpenFile; }; <> <<[result, msg] _ SendPressFile[interpressFileName, host, nCopies, cmd];>> IF result # NIL THEN GOTO cleanupAfterError; IF NOT retainInterpressFile THEN { cmd.out.PutF["Deleting file %g ... ", IO.rope[interpressFileName]]; FS.Delete[interpressFileName]; cmd.out.PutChar['\n]; }; cmd.out.PutRope["Finished Print.\n"]; RETURN [result: NIL, msg: NIL]; EXITS cleanupAfterError => { IF inputFile # FS.nullOpenFile THEN inputFile.Close[]; IF interpressFile # FS.nullOpenFile THEN interpressFile.Close[]; cmd.out.PutRope["Aborted Print.\n"]; RETURN [result: $Failure, msg: NIL]; }; }; }; };--DoPrint printHelpText: ROPE = "Usage: IPrint {-p} {-h {hostName}} {-r {pfName}} {-c nCopies} {-t} file\n\t-p portrait mode, single column Gacha 8 (default is landscape, two columns Gacha 6)\n\t-h {hostName} name of printer, empty sends to no printer (default is Hardcopy.InterpressPrinter entry of user profile)\n\t-r {pfName} retain interpress file, naming it pfName if specified\n\t-c nCopies number of copies to print\n\t-t print text only, without indenting to show Tioga nodes\n\n\tThe file extension defaults according to the SourceFileExtensions entry of user profile, like the Open command. If the file is already in interpress format, it is simply sent to a printer.\n"; Commander.Register["IPrint", DoPrint, printHelpText]; END. <>