--------------------------------------------------------------- WORDWRAP.BAS Eric J. Pearson CIS 71641,717 This file describes a Word Wrapping routine that I added to a text-editing program written in PowerBASIC 3.x. It is not intended to be a working program; it simply demonstrates the technique. These routines are hereby released into the public domain (without warranty) and may be used without fees. This "tutorial" file, however, is (C) Copyright 1993 Eric J. Pearson. The editor is based on a "Array Of Lines", i.e. a string array containing the individual lines of the file to be edited, but it could easily be modified for use with a "One Big String" editor. Here are the primary variables that are used: EditLine$(1:32000) - The dynamic array containing the text to be edited. Row% - The current "vertical" position of the cursor, relative to line 1. It is NOT the actual screen Row; if line 1 of the file being edited is being displayed on line 10 of the screen, Row% would still be = 1. Col% - The current "horizontal" position of the cursor. It is NOT the actual screen Column; if the line of text being edited does not start in screen column 1, the Col% would not be affected. The editor keeps track of the top of the current edit window, as well as the left margin (i.e. where on the screen the text being edited is located) and I actually locate the cursor with LOCATE Row%-TopOfWindow%,Col%-LeftMargin%. Examples: EditLine$(Row%) = the line currently being edited. MID$(EditLine$(Row%),Col%,1) = the character at the cursor. LineEndChar$ - Either CHR$(255) or CHR$(174), depending on whether or not the user wants a visible end-of-line marker. In the sample code below, the < character is used to indicate the end of a line of text. WordWrapOK% - A true/false switch indicating whether or not Word Wrapping is "legal" for the file being edited. WordWrap% - A true/false switch indicating whether or not Word Wrapping is currently "turned on". WordWrapCol% - The Col% where text should be wrapped. Default=78. ============================================================================= Conventions: All PowerBASIC keywords are typed in CAPS. Variable names are typed with MixedUpperAndLowerCase, and type declarations (%, $, etc) are always used. True/false switches (integer variables) are either set to %True (-1) or %False (0). This allows very simple coding; instead of... IF WordWrapOK% = %True THEN ...or... IF WordWrapOK% = %False THEN ...I simply use... IF WordWrapOK% THEN ...or... IF NOT WordWrapOK% THEN ... to make the code more readable, smaller, and faster. ============================================================================= Routine #1: Turning on Word Wrap First, I provided a keyboard toggle (Alt-W) to turn word wrapping on/off. CALL Kybd '<------ the MAIN editor keyboard input (presumably in a loop) SELECT CASE Keyboard% CASE ....... '(handle routine keypresses here) CASE %AltW IF WordWrapOK% THEN WordWrap% = NOT WordWrap% 'TOGGLE TRUE/FALSE CALL ShowWrapMode 'see below IF WordWrap% THEN 'WORD WRAPPING HAS JUST BEEN TURNED ON... '-------------- YOUR CODE GOES HERE ------------- 'Display a message asking for the Word Wrap Right 'Margin, and accept user input. Limit values to '40 to 120 if desired. Or you could arbitrarily set 'it to a value appropriate for the size of the 'editing window, without asking the user. WordWrapCol% = some value END IF END IF ============================================================================= Routine #2: It's Time to Wrap I chose to check for Word Wrapping after every keypress. This allows the editor to automatically Wrap any line to which the cursor is moved. Actually, the code is written so that Word Wrapping is checked BEFORE every keypress (which amounts to the same thing) to simplify the structure of the program and to take advantage of any delays between keypresses. A key is pressed and processed normally (inserted into the text, or acted upon if it's a Ctrl or Alt key). Then, before the next key is pressed, the Word Wrap routine checks the wrap status of the current line... IF WordWrap% THEN CALL DoWordWrap(WrapHappened%) IF WrapHappened% THEN 'The Word Wrap routine changed something, so we have to 'redisplay the contents of the screen. CALL ReDisplayEditWindow END IF END IF CALL Kybd '<------ the MAIN editor keyboard input (presumably in a loop) SELECT CASE Keyboard% ...and so on. ============================================================================= Routine #3: The Actual Word Wrap Routine The word wrap routine looks for trailing spaces: if a line ends in a space, it is considered "wrappable" to the following line. If a line ends in a character other than a space, it is not wrappable. Example: (remember that the "<" indicates a LineEndChar$) wrappable lines ÄÄÄÄÄÄÄ¿ ³ This is a test of the emergency broadcast system. The broadcasters of < your area have developed this system to keep you informed in the event < of an emergency.< ³ ÀÄÄÄ end of legal wrapping (no trailing space). The routine automatically wraps both up and down... Example of Wrap Down: If you have the line... This is a test of the emergency broadcast system. The broadcasters of _< ... and the cursor is where the _ is shown, and you type "your", the y, o, and u will fit, but when you type the r the word must be wrapped down to the next line to make... This is a test of the emergency broadcast system. The broadcasters of < your_< Example of Wrap Up: If you have the lines... XXXXXXXXXXXX X XXXXXXXX XXXXXXXXXXXX XXXX XX XXXX XXXXXX XXXXX XXXXXXX < ZZZZZZZZZZ XX XXXX.< ...there is no room for ZZZZZZZZZZ to be wrapped up to the previous line. If the user types AA and a space at the BEGINNING of the SECOND line, however, it will fit on the first line, so it needs to be "wrapped up"... XXXXXXXXXXXX X XXXXXXXX XXXXXXXXXXXX XXXX XX XXXX XXXXXX XXXXX XXXXXXX AA_< ZZZZZZZZZZ XX XXXX.< Wrap Up Example #2: If you have the lines... This is a test of the emergency broadcast system. The broadcasterXXXXXXs < of your...< ... where XXXXXX represents a typo, and if somebody deletes the XXXXXX, the word "of" needs to be wrapped up to the previous line, but not the "your". When wrapping up and down, of course, the cursor must end up in a logical position. And that's the tricky part. Here's the routine... SUB DoWordWrap(WrapHappened%) (add SHARED, LOCAL, and STATIC declarations here, as needed) WrapHappened% = %False IncompleteWrap% = %False 'see below IF Row% = 1 THEN 'THERE'S NO PREVIOUS LINE TO WRAP UP TO. WrapStartLine% = 1 ELSE 'THERE IS A PREVIOUS LINE, SO CHECK IT... IF RIGHT$(EditLine$(Row%-1),2) = " "+LineEndChar$ THEN 'PREVIOUS LINE IS WRAPPABLE (ENDS IN SPACE) WrapStartLine% = Row%-1 ELSE 'PREVIOUS LINE NOT WRAPPABLE. START ON CURRENT LINE. WrapStartLine% = Row% END IF END IF 'We now know where the wrap should start. Assume a 1-line wrap to begin 'with... WrapEndLine% = WrapStartLine% 'We are going to INCR WrapEndLine% if it turns out that more than one line 'needs to be wrapped... 'TO START THE WRAPPING PROCESS, WE BUILD ONE LONG STRING CONTAINING ALL OF 'THE LINES TO BE CHECKED FOR WRAPPING. WrapBlock$ = "" 'clear any previous data (not needed if LOCAL WrapBlock$) WorkLine% = WrapStartLine% DO IF WorkLine% = Row% THEN 'IN THE PROCESS OF BUILDING THE BLOCK, WE ARE 'ADDING THE LINE WHERE THE CURSOR IS LOCATED. IN 'ORDER TO REPOSITION THE CURSOR LATER, WE HAVE TO 'SAVE CURSOR POSITION, IN TERMS OF "HOW MANY CHARS 'INTO THE BLOCK IS THE CURSOR CURRENTLY LOCATED?". CursorOffSet% = LEN(WrapBlock$)+Col% END IF 'ADD LINE TO THE BLOCK (MINUS THE LineEndChar$, WHICH MAY MOVE). WrapBlock$ = WrapBlock$+RTRIM$(EditLine$(WorkLine%),LineEndChar$) 'CHECK THE WRAP BLOCK TO SEE IF IT ENDS IN A SPACE (I.E. IF WE 'SHOULD CONTINUE WRAPPING DOWN) *AND* CHECK THE NEXT LINE TO MAKE 'SURE THAT IT IS NOT A BLANK LINE (WHICH WE SHOULD NOT WRAP). IF RIGHT$(WrapBlock$,1)=" " AND LEN(EditLine$(WorkLine%+1))>1 THEN 'WE NEED TO KEEP WRAPPING DOWN... IF WorkLine%-WrapStartLine% > 30 THEN 'WE'VE REACHED THE 30-LINE LIMIT OF THE WRAP ROUTINE 'BUT THERE ARE MORE LINES THAT COULD BE WRAPPED. SET 'THE WARNING FLAG... IncompleteWrap% = %True '... AND STOP BUILDING THE WrapBlock$... EXIT LOOP ELSE 'WE HAVEN'T REACHED 30 LINES YET, SO KEEP GOING... INCR WorkLine% END IF ELSE 'WE'VE REACHED THE END OF LINES THAT NEED TO BE WRAPPED. EXIT LOOP END IF LOOP WrapEndLine% = WorkLine% 'we now know the first and last Wrap lines. 'OK, WE'VE GOT THE WRAP BLOCK. NOW START TAKING IT APART... WorkLine% = WrapStartLine% DO ParsePt% = LEN(WrapBlock$) 'ParsePt means "parse point" IF ParsePt% > WordWrapCol% THEN 'THE WRAP BLOCK WILL NOT FIT ON ONE LINE... 'Use the REVINSTR% function (REVerse INSTR - see below) 'to find the "Parse Point" where we can break the line... ParsePt% = REVINSTR%(LEFT$(WrapBlock$,WordWrapCol%)," ") IF ParsePt% = 0 THEN 'NO SPACES IN THE BLOCK, SO WE HAVE TO FORCE A 'BREAK IN THE LINE AT THE RIGHT MARGIN... ParsePt% = WordWrapCol% END IF END IF IF WorkLine% > WrapEndLine% THEN 'IN THE PROCESS OF TAKING APART THE WRAP BLOCK, WE'VE 'FOUND THAT THE RESULTING BLOCK WILL BE LONGER THAN THE 'ORIGINAL BLOCK. SO WE NEED TO INSERT A BLANK LINE... INCR WrapEndLine% CALL InsertLine(WrapEndLine%) 'see below END IF 'CHECK TO SEE IF THE LINE CHANGED... IF EditLine$(WorkLine%)<>LEFT$(WrapBlock$,ParsePt%)+LineEndChar$ THEN 'THE WRAPPED LINE IS *NOT* THE SAME AS THE ORIGINAL. EditLine$(WorkLine%)=LEFT$(WrapBlock$,ParsePt%)+LineEndChar$ WrapHappened% = %True ELSE 'THE WRAPPED LINE *IS* THE SAME AS THE ORIGINAL. 'SO CHECK TO SEE IF WE NEED TO KEEP GOING... IF WrapHappened% OR _ '#1 ((NOT WrapHappened% AND (WorkLine%>Row%+2)) THEN '#2 'EITHER #1 WE CHANGED A LINE EARLIER, BUT WE'VE ' REACHED THE POINT WHERE NO FURTHER ' CHANGES ARE NEEDED, ' OR #2 NOTHING HAS BEEN CHANGED, AND WE'VE ' CHECKED FAR ENOUGH. 'EITHER WAY, WE CAN QUIT WRAPPING... EXIT LOOP '(note: exit LOOP not exit IF.) END IF END IF 'WE'RE STILL WRAPPING. REMOVE THE WRAPPED LINE FROM THE WRAP BLOCK... WrapBlock$ = MID$(WrapBlock$,ParsePt%+1) IF LEN(WrapBlock$) > 0 THEN 'THERE'S STILL SOME TEXT LEFT TO BE PROCESSED... INCR WorkLine% '(and iterate the loop) ELSE 'WE'VE EMPTIED THE WRAP BLOCK... 'This FOR/NEXT loop will only do something if the wrapped 'block is one or more lines shorter than the original... FOR Dummy1% = WorkLine%+1 TO WrapEndLine% CALL DeleteLine(WorkLine+1) DECR WrapEndLine% NEXT IF IncompleteWrap% THEN 'We had to wrap all the way to the end of the block, 'and more wrapping still needs to be done... '-------------- YOUR CODE GOES HERE ------------- 'My program displays the following message and waits 'for a keypress... '"PARAGRAPH IS TOO LONG: WORD WRAP INCOMPLETE." '"Move cursor down 30 lines to fix the word" '"wrap. You should try to use paragraphs less" '"than 30 lines long with this word processor." '"(A typical printout page is 60 lines long.)" END IF 'We're done wrapping... EXIT LOOP END IF LOOP IF WrapHappened% THEN 'PUT THE CURSOR (Row% and Col%) WHERE IT BELONGS... Row% = WrapStartLine% Col% = CursorOffSet% DO IF Col% > LEN(EditLine$(Row%))-1 THEN 'The Col% is past the end of line Row%... IF Row% = WrapEndLine% THEN EXIT LOOP 'Move the cursor down to the next line... DECR Col%,LEN(EditLine$(Row%))-1 INCR Row% ELSE EXIT LOOP END IF LOOP END IF 'We're done. END SUB ============================================================================= This is a very fast Reverse INSTR function: it effectively searches backwards from the end of a string to find the first matching character, unlike INSTR, which searches forward from the beginning of a string. FUNCTION REVINSTR%(MainString$,SearchChar$) LOCAL Dummy1%,Dummy2% DO Dummy1% = INSTR(Dummy2%+1,MainString$,SearchChar$) IF Dummy1% = 0 THEN REVINSTR% = Dummy2% EXIT FUNCTION ELSE 'The string contains a SearchChar$... Dummy2% = Dummy1% 'When the loop is iterated we'll check for 'another one, until no more are found. END IF LOOP END FUNCTION ============================================================================= Other Routines: The following (trivial) routines are not included in this file... SUB Kybd - My standard Get One Keypress routine. It returns an integer called Keyboard%. SUB ShowWrapMode - displays "WORD WRAPPING ON" or "word wrapping off" on the editor status line. SUB SaveEditedFile- normal routine modifed to append the values of WordWrap% and WordWrapCol% to the end of the file. SUB LoadEditorFile- normal routine modified to process the values added by SaveEditFile, and to NOT load the extra line into EditLine$(). SUB InsertLine(LineNo%) - uses ARRAY INSERT to add a line in the middle of the EditLine$() array. Also changes MaxRow%. SUB DeleteLine(LineNo%) - uses ARRAY DELETE to delete a line in the middle of the EditLine$() array. Also changes MaxRow%. =============================================================================