DIRECTORY Atom USING [MakeAtom], BitTableLookup USING [Failed, Insert, Lookup, Read, Table], Commander USING [CommandProc, Register], CommandTool USING [CurrentWorkingDirectory], Containers USING [ChildXBound, ChildYBound, Create], Basics USING [LongNumber, Comparison, BITAND], FS USING [ComponentPositions, Error, ExpandName, StreamOpen], GVSend USING [AddRecipient, AddToItem, Create, Handle, Send, SendFailed, StartSend, StartSendInfo, StartText], Icons USING [IconFlavor, NewIconFromFile], IO USING [Backup, CharClass, Close, EndOf, EndOfStream, GetChar, GetTokenRope, int, Put, PutChar, PutF, PutFR, PutRope, RIS, rope, SkipWhitespace, STREAM, time], List USING [UniqueSort], Menus USING [AppendMenuEntry, ChangeNumberOfLines, CreateEntry, CreateMenu, GetNumberOfLines, Menu, MenuEntry, MenuLine, MenuProc, MouseButton, ReplaceMenuEntry, SetLine], MenusPrivate USING [menuHLeading, menuHSpace], NodeProps USING [GetProp], Process USING [Detach, DisableTimeout, SecondsToTicks, SetTimeout, Yield], Rope USING [Cat, Compare, Equal, FromRefText, ROPE, Size, Substr], SpellingCorrection USING [BruteForceCorrection], SpellingToolDefinition USING [DefinitionButton, InitializeDictionaryServer, FinalizeDictionaryServer, DefinitionStats], SpellingToolShared USING [FirstWithin, CheckMiddleOfWord, Selection, CorrectTiogaOpsCallWithLocks, MapWordsInSelection, ProcessSelection], TEditScrolling USING [AutoScroll], TEditOpsExtras USING [WorkingDirectoryFromViewer], TextNode USING [Ref], TiogaOps USING [Delete, FetchLooks, GetRope, InsertRope, LastWithin, NoSelection, Ref, Root, SaveForPaste, SelectionGrain, SetLooks, SetSelection, StepBackward], TiogaOpsDefs USING [Location, Ref, Viewer], TypeScript USING [TS, Create], UserCredentials USING [Get], UserProfile USING [Boolean, CallWhenProfileChanges, ListOfTokens, Number, ProfileChangedProc, Token], VFonts USING [CharWidth, FontHeight, StringWidth], ViewerBLT USING [ChangeNumberOfLines], ViewerClasses USING [Viewer], ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc, ViewerEvent], ViewerIO USING [CreateViewerStreams], ViewerOps USING [AddProp, BlinkIcon, CreateViewer, FetchProp, PaintViewer, SetOpenHeight], VM USING [Age, Touch]; SpellingToolImpl: CEDAR MONITOR IMPORTS Atom, Basics, BitTableLookup, Commander, CommandTool, Containers, FS, GVSend, Icons, IO, List, Menus, NodeProps, Process, Rope, SpellingCorrection, SpellingToolDefinition, SpellingToolShared, TEditOpsExtras, TEditScrolling, TiogaOps, TypeScript, UserCredentials, UserProfile, VFonts, ViewerBLT, ViewerEvents, ViewerIO, ViewerOps, VM = BEGIN OPEN SpellingToolShared; ROPE: TYPE = Rope.ROPE; RopeList: TYPE = LIST OF ROPE; STREAM: TYPE = IO.STREAM; Viewer: TYPE = ViewerClasses.Viewer; ViewerList: TYPE = LIST OF Viewer; LocalDataList: TYPE = LIST OF LocalData; LocalData: TYPE = REF LocalDataRep; LocalDataRep: TYPE = RECORD [ BitTableFilename: ROPE _ NIL, globalAuxilliaryWordlistFilenames: RopeList _ NIL, wDir: ROPE _ NIL, localAuxilliaryWordlistFilenamesRope: ROPE _ NIL, auxilliaryWordlistFilenames: RopeList _ NIL, bitTable: BitTableLookup.Table _ NIL, users: ViewerList _ NIL ]; FileDataList: TYPE = LIST OF FileData; FileData: TYPE = REF FileDataRep; FileDataRep: TYPE = RECORD [ filename: ROPE, localDatas: LocalDataList _ NIL, wordsAddedList: RopeList _ NIL, menuEntry: Menus.MenuEntry _ NIL ]; installationDirectory: ROPE _ CommandTool.CurrentWorkingDirectory[]; localAuxilliaryWordlistFilenamesProperty: ATOM = $Wordlists; viewerLocalDataKey: ATOM _ Atom.MakeAtom[IO.PutFR["SpellingTool (started %g) Viewer to LocalData", IO.time[]]]; spellingToolIcon: Icons.IconFlavor = Icons.NewIconFromFile["SpellingTool.icons", 0]; localDataCache: LocalDataList _ NIL; fileDatas: FileDataList _ NIL; dirty: BOOL _ FALSE; toolExists: BOOL _ FALSE; toolMenu: Menus.Menu _ Menus.CreateMenu[lines: 1]; permMessage: PUBLIC STREAM; permMessageIn: STREAM; reportStatistics: BOOL _ FALSE; reportStatisticsTo: ROPE _ "Nix.pa"; reportCount: INT _ 0; totalCheckCommands, totalCorrectionsApplied, totalCorrectionsCnt, totalCorrectionsProposedCnt, totalDefinitionsCnt, totalDefinitionsWorkedCount, totalInsertionsCnt, totalMaintainerNotifications, totalWordsCheckedCnt, totalWordsMisspelledCnt, totalWordsProfitablyChecked: INT _ 0; lastCheckCommands, lastCorrectionsApplied, lastCorrectionsCnt, lastCorrectionsProposedCnt, lastDefinitionsCnt, lastDefinitionsWorkedCount, lastInsertionsCnt, lastMaintainerNotifications, lastWordsCheckedCnt, lastWordsMisspelledCnt, lastWordsProfitablyChecked: INT _ 0; globalAuxilliaryWordlistFilenames: RopeList; BitTableFilename: ROPE; insertionInterval: INT _ 60; pluralStripping: BOOL _ TRUE; SpellCheckWithin: INTERNAL PROC [s: Selection, toEnd, forwards, wrapAround: BOOL] = { wordStart, wordEnd: TiogaOpsDefs.Location; loc: TiogaOpsDefs.Location _ s.start; misspelled: BOOL; wordsChecked: INT _ 0; localData: LocalData _ GetLocalData[s]; SpellingCheckerProc: INTERNAL PROC [word: REF TEXT] RETURNS [misspelled: BOOL] = { misspelled _ ~localData.bitTable.Lookup[word]; IF misspelled THEN misspelled _ HandleMisspelling[word]; wordsChecked _ wordsChecked + 1; IF LOOPHOLE[Basics.BITAND[511, LOOPHOLE[wordsChecked, Basics.LongNumber].lowbits], CARDINAL] = 0B THEN Process.Yield[]; }; HandleMisspelling: INTERNAL PROC [word: REF TEXT] RETURNS [misspelled: BOOL _ TRUE] = { s: INT _ word.length; lc, sc: CHAR; IF ~pluralStripping THEN RETURN; IF s = 1 THEN { misspelled _ FALSE; RETURN; }; IF s <= 4 THEN RETURN; lc _ word[s-1]; sc _ word[s-2]; IF lc = 's THEN { IF sc = 's THEN RETURN; IF sc = '\' THEN { word.length _ s - 2; misspelled _ ~localData.bitTable.Lookup[word]; word.length _ s + 2; } ELSE { word.length _ s - 1; misspelled _ ~localData.bitTable.Lookup[word]; word.length _ s + 1; }; }; }; Locker: INTERNAL PROC [root: TiogaOpsDefs.Ref] = { TiogaOps.SetSelection[s.viewer, wordStart, wordEnd, char, TRUE, TRUE, primary]; }; IF NOT Valid[localData] THEN { permMessage.PutRope["Can't check.\n"]; RETURN}; VM.Touch[localData.bitTable.vmHandle.interval]; IF s.end.where = -1 THEN { s.end.node _ TiogaOps.StepBackward[s.end.node]; s.end.where _ (TiogaOps.GetRope[s.end.node]).Size[]-1; }; IF toEnd THEN { IF forwards THEN { s.end.node _ TiogaOps.LastWithin[TiogaOps.Root[s.start.node]]; s.end.where _ (TiogaOps.GetRope[s.end.node]).Size[]-1; } ELSE { s.start.node _ SpellingToolShared.FirstWithin[TiogaOps.Root[s.start.node]]; s.start.where _ 0; }; }; [misspelled, wordStart, wordEnd] _ MapWordsInSelection[s.start, s.end, SpellingCheckerProc, forwards]; IF ~misspelled AND wrapAround THEN { IF loc.where = 0 THEN { s.end.node _ TiogaOps.StepBackward[loc.node]; s.end.where _ (TiogaOps.GetRope[s.end.node]).Size[]-1; } ELSE { s.end.node _ loc.node; s.end.where _ loc.where - 1; }; s.start.node _ SpellingToolShared.FirstWithin[TiogaOps.Root[s.end.node]]; s.start.where _ 0; [misspelled, wordStart, wordEnd] _ MapWordsInSelection[s.start, s.end, SpellingCheckerProc, forwards]; }; totalWordsCheckedCnt _ totalWordsCheckedCnt + wordsChecked; IF misspelled THEN { totalWordsProfitablyChecked _ totalWordsProfitablyChecked + wordsChecked; totalWordsMisspelledCnt _ totalWordsMisspelledCnt + 1; CorrectTiogaOpsCallWithLocks[Locker ! TiogaOps.NoSelection => {Locker[NIL]; GOTO noSelection}]; TEditScrolling.AutoScroll[s.viewer]; ProposeCorrections[localData, Rope.Substr[TiogaOps.GetRope[wordStart.node], wordStart.where, wordEnd.where-wordStart.where+1]]; } ELSE { IF Menus.GetNumberOfLines[container.menu] # 1 THEN { Menus.ChangeNumberOfLines[container.menu, 1]; ViewerBLT.ChangeNumberOfLines[container, 1]; ViewerOps.PaintViewer[container, menu]; }; IF wordsChecked = 0 THEN { IO.PutF[permMessage, "The selection did not contain any words.\n"]; ViewerOps.BlinkIcon[s.viewer, 1, 1]; } ELSE IF wordsChecked = 1 THEN IO.PutF[permMessage, "The selected word is correctly spelled.\n"] ELSE { IO.PutF[permMessage, "No misspellings found in %g words.\n", IO.int[wordsChecked]]; ViewerOps.BlinkIcon[s.viewer, 1, 1]; }; }; VM.Age[localData.bitTable.vmHandle.interval]; EXITS noSelection => NULL; }; InsertWordOrInsertWordAndSpell: INTERNAL PROC [fd: FileData, andSpell, forwards, wrapAround: BOOL] = { s: Selection; localData: LocalData; misspelled: BOOL; wordStart, wordEnd: TiogaOpsDefs.Location; auxilliaryWordlistFilename: ROPE _ NIL; Locker: INTERNAL PROC [root: TiogaOpsDefs.Ref] = { WordCollector: INTERNAL PROC [word: REF TEXT] RETURNS [stop: BOOL _ FALSE] = { AddWord[fd, word, Rope.FromRefText[word]]; }; s _ ProcessSelection[FALSE, FALSE, forwards].s; localData _ GetLocalData[s]; IF Valid[localData] THEN [misspelled, wordStart, wordEnd] _ MapWordsInSelection[s.start, s.end, WordCollector, forwards] ELSE permMessage.PutRope["Can't check.\n"]; }; CorrectTiogaOpsCallWithLocks[Locker ! TiogaOps.NoSelection => GOTO noSelection]; IF NOT Valid[localData] THEN RETURN; IF insertionInterval = 0 THEN SaveInsertions[]; IF andSpell THEN { IF forwards THEN { s.start.node _ s.end.node; s.start.where _ s.end.where + 1; } ELSE { s.end.node _ s.start.node; s.end.where _ s.start.where - 1; }; SpellCheckWithin[s, TRUE, forwards, wrapAround]; }; EXITS noSelection => IO.PutF[permMessage, "Make a selection.\n"]; }; TextNodeRoot: PROC [tiogaOpsNode: TiogaOps.Ref] RETURNS [textNode: TextNode.Ref] = { tiogaOpsRoot: TiogaOps.Ref _ TiogaOps.Root[tiogaOpsNode]; TRUSTED {textNode _ LOOPHOLE[tiogaOpsRoot]}; }; Valid: INTERNAL PROC [localData: LocalData] RETURNS [valid: BOOL] = { valid _ localData # NIL; }; GetLocalData: INTERNAL PROC [s: Selection] RETURNS [localData: LocalData] = { localAuxilliaryWordlistFilenamesRope: ROPE _ NARROW[NodeProps.GetProp[TextNodeRoot[s.start.node], localAuxilliaryWordlistFilenamesProperty]]; auxilliaryWordlistFilenames: RopeList _ NIL; listComputed: BOOL _ FALSE; WholeList: PROC RETURNS [rl: RopeList] = INLINE { IF NOT listComputed THEN { auxilliaryWordlistFilenames _ ParseToList[wDir, localAuxilliaryWordlistFilenamesRope, globalAuxilliaryWordlistFilenames]; listComputed _ TRUE}; rl _ auxilliaryWordlistFilenames}; wDir: ROPE _ TEditOpsExtras.WorkingDirectoryFromViewer[s.viewer]; ValidCache: INTERNAL PROC RETURNS [valid: BOOL] = { valid _ Rope.Equal[localData.BitTableFilename, BitTableFilename, FALSE] AND ( (Rope.Equal[ localData.localAuxilliaryWordlistFilenamesRope, localAuxilliaryWordlistFilenamesRope, FALSE] AND Rope.Equal[localData.wDir, wDir, FALSE] AND ListEqual[ localData.globalAuxilliaryWordlistFilenames, globalAuxilliaryWordlistFilenames]) OR ListEqual[WholeList[], localData.auxilliaryWordlistFilenames]); }; oldLocalData: LocalData; UseIt: INTERNAL PROC RETURNS [ld: LocalData] = { ViewerOps.AddProp[s.viewer, viewerLocalDataKey, ld _ localData]; IF oldLocalData # NIL THEN NoteNonUse[oldLocalData, s.viewer]; IF localData # NIL THEN localData.users _ CONS[s.viewer, localData.users]; }; oldLocalData _ localData _ NARROW[ViewerOps.FetchProp[s.viewer, viewerLocalDataKey]]; IF localData = NIL OR (NOT Valid[localData]) OR (NOT ValidCache[]) THEN { FOR ldc: LocalDataList _ localDataCache, ldc.rest WHILE ldc # NIL DO localData _ ldc.first; IF Valid[localData] AND ValidCache[] THEN RETURN [UseIt[]]; ENDLOOP; [] _ WholeList[]; WaitTillClean[]; localData _ ComputeLocalData[wDir, localAuxilliaryWordlistFilenamesRope, auxilliaryWordlistFilenames]; [] _ UseIt[]; }; }; ParseToList: PROC [wDir, rope: ROPE, andUnion: RopeList] RETURNS [list: RopeList] = { Add: PROC [r: ROPE, l: RopeList] RETURNS [new: RopeList] = { IF l = NIL THEN RETURN [LIST[r]]; SELECT Rope.Compare[r, l.first, FALSE] FROM less => RETURN [CONS[r, l]]; equal => RETURN [l]; greater => RETURN [CONS[l.first, Add[r, l.rest]]]; ENDCASE => ERROR; }; stream: STREAM _ IO.RIS[rope]; list _ andUnion; FOR i: INT _ stream.SkipWhitespace[], stream.SkipWhitespace[] WHILE NOT stream.EndOf[] DO raw: ROPE _ stream.GetTokenRope[SepByWhite].token; r: ROPE _ FS.ExpandName[name: raw, wDir: wDir].fullFName; list _ Add[r, list]; ENDLOOP; stream.Close[]; }; ListEqual: PROC [l1, l2: RopeList] RETURNS [equal: BOOL] = { DO IF l1 = l2 THEN RETURN [TRUE]; IF l1 = NIL OR l2 = NIL THEN RETURN [FALSE]; IF NOT Rope.Equal[l1.first, l2.first, FALSE] THEN RETURN [FALSE]; l1 _ l1.rest; l2 _ l2.rest; ENDLOOP; }; ComputeLocalData: INTERNAL PROC [wDir, localAuxilliaryWordlistFilenamesRope: ROPE, auxilliaryWordlistFilenames: RopeList] RETURNS [localData: LocalData] = { success: BOOL _ TRUE; localData _ NEW [LocalDataRep _ [ BitTableFilename: BitTableFilename, globalAuxilliaryWordlistFilenames: globalAuxilliaryWordlistFilenames, wDir: wDir, localAuxilliaryWordlistFilenamesRope: localAuxilliaryWordlistFilenamesRope, auxilliaryWordlistFilenames: auxilliaryWordlistFilenames]]; [success, localData.bitTable] _ GetBitFile[localData.BitTableFilename]; IF NOT success THEN RETURN [NIL]; FOR f: RopeList _ localData.auxilliaryWordlistFilenames, f.rest UNTIL f = NIL DO fd: FileData _ FindFileData[f.first]; fd.localDatas _ CONS[localData, fd.localDatas]; MergeWordFile[f.first, localData.bitTable]; ENDLOOP; localDataCache _ CONS[localData, localDataCache]; }; FindFileData: INTERNAL PROC [filename: ROPE] RETURNS [fd: FileData] = { FOR fdl: FileDataList _ fileDatas, fdl.rest WHILE fdl # NIL DO IF fdl.first.filename.Equal[filename, FALSE] THEN RETURN [fdl.first]; ENDLOOP; fd _ NEW [FileDataRep _ [filename: filename]]; fileDatas _ CONS[fd, fileDatas]; IF toolExists THEN MaybeAddWordlistInserter[fd, TRUE]; }; MaybeAddWordlistInserter: PROC [fd: FileData, paint: BOOL] = { fullFName, shortName, ext: ROPE; cp: FS.ComponentPositions; IF fd.menuEntry # NIL THEN ERROR; [fullFName, cp, ] _ FS.ExpandName[fd.filename]; IF cp.server.length = 0 THEN { shortName _ fullFName.Substr[start: cp.base.start, len: cp.base.length]; ext _ fullFName.Substr[start: cp.ext.start, len: cp.ext.length]; IF NOT ext.Equal["Wordlist", FALSE] THEN shortName _ shortName.Cat[".", ext]; Menus.AppendMenuEntry[ menu: toolMenu, line: 0, entry: fd.menuEntry _ Menus.CreateEntry[ name: shortName, proc: InsertWord, clientData: fd] ]; IF paint THEN ViewerOps.PaintViewer[container, menu]; }; }; SepByWhite: PROC [char: CHAR] RETURNS [IO.CharClass] --IO.BreakProc-- = { RETURN [SELECT char FROM IN [0C .. ' ] => sepr, ENDCASE => other]; }; generalDestroyRegistration: ViewerEvents.EventRegistration _ ViewerEvents.RegisterEventProc[proc: NoteDestruction, event: destroy]; NoteDestruction: ENTRY PROC [viewer: Viewer, event: ViewerEvents.ViewerEvent, before: BOOL] RETURNS [abort: BOOL _ FALSE] --ViewerEvents.EventProc-- = { ENABLE UNWIND => NULL; localData: LocalData _ NARROW[ViewerOps.FetchProp[viewer, viewerLocalDataKey]]; IF event = destroy AND localData # NIL THEN { ViewerOps.AddProp[viewer, viewerLocalDataKey, NIL]; NoteNonUse[localData, viewer]; }; }; NoteNonUse: INTERNAL PROC [localData: LocalData, user: Viewer] = { localData.users _ FilterViewerList[user, localData.users]; IF localData.users = NIL THEN { myFileDatas: FileDataList _ NIL; FOR rl: RopeList _ localData.auxilliaryWordlistFilenames, rl.rest WHILE rl # NIL DO myFileDatas _ CONS[FindFileData[rl.first], myFileDatas]; ENDLOOP; FOR myFileDatas _ myFileDatas, myFileDatas.rest WHILE myFileDatas # NIL DO fd: FileData _ myFileDatas.first; fd.localDatas _ FilterLocalDataList[localData, fd.localDatas]; IF fd.localDatas = NIL THEN { IF fd.menuEntry # NIL THEN Unenter[fd, TRUE]; IF fd.wordsAddedList = NIL THEN fileDatas _ FilterFileDataList[fd, fileDatas]; }; ENDLOOP; localDataCache _ FilterLocalDataList[localData, localDataCache]; }; }; Unenter: PROC [fd: FileData, paint: BOOL] = { Menus.ReplaceMenuEntry[menu: toolMenu, oldEntry: fd.menuEntry, newEntry: NIL]; fd.menuEntry _ NIL; IF paint AND toolExists THEN ViewerOps.PaintViewer[container, menu]; }; FlushCache: INTERNAL PROC = { WHILE localDataCache # NIL DO user: Viewer _ localDataCache.first.users.first; ViewerOps.AddProp[user, viewerLocalDataKey, NIL]; NoteNonUse[localDataCache.first, user]; ENDLOOP; }; FilterViewerList: PROC [member: Viewer, oldList: ViewerList] RETURNS [newList: ViewerList] = { last: ViewerList _ NIL; newList _ oldList; FOR this: ViewerList _ newList, this.rest WHILE this # NIL DO IF this.first = member THEN { IF last = NIL THEN newList _ this.rest ELSE last.rest _ this.rest; RETURN; }; last _ this; ENDLOOP; ERROR; }; FilterLocalDataList: PROC [member: LocalData, oldList: LocalDataList] RETURNS [newList: LocalDataList] = { last: LocalDataList _ NIL; newList _ oldList; FOR this: LocalDataList _ newList, this.rest WHILE this # NIL DO IF this.first = member THEN { IF last = NIL THEN newList _ this.rest ELSE last.rest _ this.rest; RETURN; }; last _ this; ENDLOOP; ERROR; }; FilterFileDataList: PROC [member: FileData, oldList: FileDataList] RETURNS [newList: FileDataList] = { last: FileDataList _ NIL; newList _ oldList; FOR this: FileDataList _ newList, this.rest WHILE this # NIL DO IF this.first = member THEN { IF last = NIL THEN newList _ this.rest ELSE last.rest _ this.rest; RETURN; }; last _ this; ENDLOOP; ERROR; }; GetBitFile: INTERNAL PROC [fileName: ROPE] RETURNS [ succeeded: BOOLEAN _ FALSE, bitTable: BitTableLookup.Table] = { s: STREAM; bitTable _ NIL; IO.PutF[permMessage, "Reading word list from \"%g\"...", IO.rope[fileName]]; s _ FS.StreamOpen[fileName ! FS.Error => { permMessage.PutRope[" file could not be read.\n"]; GOTO openFailed}]; bitTable _ BitTableLookup.Read[s ! BitTableLookup.Failed => { permMessage.PutRope[SELECT why FROM TooLarge => " bit table too large.\n", BadTable => " contained bad table.\n", ENDCASE => ERROR]; GOTO readFailed}]; s.Close[]; succeeded _ TRUE; IO.Put[permMessage, IO.rope[" done.\n"]]; EXITS openFailed => NULL; readFailed => NULL; }; AddWord: INTERNAL PROC [fd: FileData, wordAsText: REF TEXT, wordAsRope: ROPE] = { totalInsertionsCnt _ totalInsertionsCnt + 1; IO.PutF[permMessage, "\"%g\" inserted in %g.\n", IO.rope[wordAsRope], IO.rope[fd.filename]]; fd.wordsAddedList _ CONS[wordAsRope, fd.wordsAddedList]; dirty _ TRUE; FOR ldl: LocalDataList _ fd.localDatas, ldl.rest WHILE ldl # NIL DO ldl.first.bitTable.Insert[wordAsText]; ENDLOOP; }; GetToken: INTERNAL PROC [s: STREAM, word: REF TEXT] RETURNS [w: REF TEXT] = { UnPrintable: INTERNAL PROC [c: CHAR] RETURNS [unPrintable: BOOL] = INLINE { unPrintable _ c IN ['\000 .. '\040]; }; c: CHAR; w _ word; WHILE UnPrintable[c _ s.GetChar[]] DO ENDLOOP; s.Backup[c]; w.length _ w.maxLength; FOR i: CARDINAL _ 0, i+1 DO c _ s.GetChar[ ! IO.EndOfStream => GOTO done]; IF UnPrintable[c] THEN GOTO done; IF i >= w.maxLength THEN { w _ NEW[TEXT[2*w.maxLength+1]]; w.length _ w.maxLength; }; w[i] _ c; REPEAT done => w.length _ i; ENDLOOP; }; MergeWordFile: INTERNAL PROC [fileName: ROPE, bitTable: BitTableLookup.Table] = { wordFile: STREAM; IO.PutF[permMessage, "Adding words from file \"%g\"...", IO.rope[fileName]]; wordFile _ FS.StreamOpen[fileName ! FS.Error => GOTO openFailed]; DO word: REF TEXT _ NEW[TEXT[30]]; word _ GetToken[wordFile, word ! IO.EndOfStream => EXIT]; bitTable.Insert[word]; ENDLOOP; IO.Close[wordFile]; IO.Put[permMessage, IO.rope[" done.\n"]]; EXITS openFailed => IO.Put[permMessage, IO.rope[" file did not exist.\nThis file will be created automatically if you insert words into it.\n"]]; }; SaveInsertions: INTERNAL PROC [] = { dirty _ FALSE; FOR fdl: FileDataList _ fileDatas, fdl.rest WHILE fdl # NIL DO fd: FileData _ fdl.first; NILList: PROC = { fd.wordsAddedList _ NIL; IF fd.localDatas = NIL THEN fileDatas _ FilterFileDataList[fd, fileDatas]; }; IF fd.wordsAddedList # NIL THEN { wordFile: STREAM _ NIL; lock: BOOL; wordFile _ FS.StreamOpen[fd.filename, $append ! FS.Error => {lock _ error.group = lock; CONTINUE}]; IF wordFile # NIL THEN { FOR w: RopeList _ fd.wordsAddedList, w.rest UNTIL w = NIL DO wordFile.Put[IO.rope[w.first]]; wordFile.PutChar['\n]; ENDLOOP; wordFile.Close[]; NILList[]; } ELSE { IO.PutF[permMessage, "The insertion save file \"%g\" could not be written", IO.rope[fd.filename]]; IF lock THEN --try again later-- { IO.PutRope[permMessage, " due to lock conflict --- will try again later.\n"]; dirty _ TRUE; } ELSE { IO.PutRope[permMessage, "; insertions lost.\n"]; NILList[]; }; }; }; ENDLOOP; DoStats[]; }; DoStats: INTERNAL PROC = { [totalDefinitionsCnt, totalDefinitionsWorkedCount] _ SpellingToolDefinition.DefinitionStats[]; IF reportStatistics THEN IF (totalCheckCommands # lastCheckCommands) OR (totalWordsCheckedCnt # lastWordsCheckedCnt) OR (totalWordsProfitablyChecked # lastWordsProfitablyChecked) OR (totalWordsMisspelledCnt # lastWordsMisspelledCnt) OR (totalInsertionsCnt # lastInsertionsCnt) OR (totalCorrectionsCnt # lastCorrectionsCnt) OR (totalCorrectionsProposedCnt # lastCorrectionsProposedCnt) OR (totalCorrectionsApplied # lastCorrectionsApplied) OR (totalDefinitionsCnt # lastDefinitionsCnt) OR (totalDefinitionsWorkedCount # lastDefinitionsWorkedCount) OR (totalMaintainerNotifications # lastMaintainerNotifications) THEN { IF reportCount >= 60 THEN { name, password, stats: Rope.ROPE; reportCount _ 0; [name, password] _ UserCredentials.Get[]; stats _ Rope.Cat[ IO.PutFR["\nNumber of searches: %g\nNumber of words checked: %g\nNumber of words profitably checked: %g\nMisspellings found: %g\nInsertions made: %g\n", IO.int[totalCheckCommands], IO.int[totalWordsCheckedCnt], IO.int[totalWordsProfitablyChecked], IO.int[totalWordsMisspelledCnt], IO.int[totalInsertionsCnt]], IO.PutFR["Words corrected: %g\n", IO.int[totalCorrectionsCnt]], IO.PutFR["Corrections proposed: %g\nCorrections accepted: %g\nDefinitions requested: %g\nDefinitions given: %g\nNotifications sent: %g\n", IO.int[totalCorrectionsProposedCnt], IO.int[totalCorrectionsApplied], IO.int[totalDefinitionsCnt], IO.int[totalDefinitionsWorkedCount], IO.int[totalMaintainerNotifications]]]; IF ~Mail[name, password, reportStatisticsTo, "Spelling Tool statistics.", stats] THEN RETURN; lastCheckCommands _ totalCheckCommands; lastWordsCheckedCnt _ totalWordsCheckedCnt; lastWordsProfitablyChecked _ totalWordsProfitablyChecked; lastWordsMisspelledCnt _ totalWordsMisspelledCnt; lastInsertionsCnt _ totalInsertionsCnt; lastCorrectionsCnt _ totalCorrectionsCnt; lastCorrectionsProposedCnt _ totalCorrectionsProposedCnt; lastCorrectionsApplied _ totalCorrectionsApplied; lastDefinitionsCnt _ totalDefinitionsCnt; lastDefinitionsWorkedCount _ totalDefinitionsWorkedCount; lastMaintainerNotifications _ totalMaintainerNotifications; }; reportCount _ reportCount + 1; }; }; Mail: INTERNAL PROC[sender, password, recipient, subject, message: ROPE] RETURNS[success: BOOLEAN _ FALSE] = BEGIN grapevine: GVSend.Handle; results: GVSend.StartSendInfo; grapevine _ GVSend.Create[]; BEGIN ENABLE GVSend.SendFailed => CONTINUE; results _ grapevine.StartSend[password, sender, NIL, FALSE]; IF results = ok THEN { grapevine.AddRecipient[recipient]; grapevine.StartText[]; grapevine.AddToItem[IO.PutFR["Date: %g\nFrom: %g\nSubject: %g\nTo: %g\n\n%g", IO.time[], IO.rope[sender], IO.rope[subject], IO.rope[recipient], IO.rope[message]]]; grapevine.Send[]}; END; RETURN[TRUE]; END; CleanlinessWanted: CONDITION; CleanlinessAchieved: CONDITION; MakeAdditionsPermanent: ENTRY PROC = TRUSTED { ENABLE UNWIND => NULL; DO IF insertionInterval > 0 THEN Process.SetTimeout[@CleanlinessWanted, Process.SecondsToTicks[insertionInterval]] ELSE Process.DisableTimeout[@CleanlinessWanted]; SaveInsertions[]; BROADCAST CleanlinessAchieved; IF NOT toolExists THEN EXIT; WAIT CleanlinessWanted; ENDLOOP; }; CheckSpelling: ENTRY Menus.MenuProc = { ENABLE UNWIND => NULL; s: Selection; wasExtended: BOOL; Locker: INTERNAL PROC [root: TiogaOpsDefs.Ref] = { [s, wasExtended] _ ProcessSelection[FALSE, TRUE, TRUE]; }; --IF GotBitTable[] THEN-- { CorrectTiogaOpsCallWithLocks[Locker ! TiogaOps.NoSelection => GOTO noSelection]; totalCheckCommands _ totalCheckCommands + 1; IF mouseButton = blue AND ~wasExtended THEN { s.start.node _ s.end.node; s.start.where _ s.end.where + 1; SpellCheckWithin[s, TRUE, TRUE, TRUE]; } ELSE SpellCheckWithin[s, FALSE, TRUE, wasExtended]; }; EXITS noSelection => IO.PutF[permMessage, "Make a selection.\n"]; }; InsertWord: ENTRY Menus.MenuProc = { ENABLE UNWIND => NULL; fd: FileData _ NARROW[clientData]; --IF GotBitTable[] THEN-- { InsertWordOrInsertWordAndSpell[fd, mouseButton # red, TRUE, TRUE]; IF mouseButton # red THEN totalCheckCommands _ totalCheckCommands + 1; }; }; InsertHead: ENTRY Menus.MenuProc = { ENABLE UNWIND => NULL; permMessage.PutRope["Click on name of wordlist you wish to insert in.\n"]; }; Ignore: ENTRY Menus.MenuProc = { ENABLE UNWIND => NULL; s: Selection; Locker: INTERNAL PROC [root: TiogaOpsDefs.Ref] = { s _ ProcessSelection[FALSE, FALSE, TRUE].s; s.start.node _ s.end.node; s.start.where _ s.end.where + 1; SpellingToolShared.CheckMiddleOfWord[s]; }; --IF GotBitTable[] THEN-- { CorrectTiogaOpsCallWithLocks[Locker ! TiogaOps.NoSelection => GOTO noSelection]; totalCheckCommands _ totalCheckCommands + 1; SpellCheckWithin[s, TRUE, TRUE, TRUE]; }; EXITS noSelection => IO.PutF[permMessage, "Make a selection.\n"]; }; theWordCorrected: ROPE _ NIL; lastWordCorrected: ROPE _ NIL; correctionBuffer: REF TEXT _ NEW[TEXT[20]]; ProposeCorrections: INTERNAL PROC [localData: LocalData, theWord: ROPE] = { WordChecker: INTERNAL PROC [w: REF TEXT] RETURNS [inTable: BOOL] = { inTable _ localData.bitTable.Lookup[w]; }; CompareWords: INTERNAL PROC[ref1, ref2: REF ANY] RETURNS [Basics.Comparison] = { RETURN[Rope.Compare[NARROW[ref1], NARROW[ref2], FALSE]]; }; numCorrections: INT _ 0; viewerWidth: INTEGER _ container.ww - 6; oldMenuLines: INTEGER _ Menus.GetNumberOfLines[container.menu]; widthLeft, lineNum: INTEGER; correctionList: RopeList _ NIL; IF theWord = NIL THEN { IO.PutF[permMessage, "No word selected, so no correction done.\n"]; RETURN; }; totalCorrectionsCnt _ totalCorrectionsCnt + 1; lastWordCorrected _ theWord; theWordCorrected _ theWord; [correctionList, correctionBuffer] _ SpellingCorrection.BruteForceCorrection[theWord, WordChecker, correctionBuffer]; TRUSTED {correctionList _ LOOPHOLE[List.UniqueSort[LOOPHOLE[correctionList], CompareWords]];}; IF correctionList # NIL THEN { correctionList _ CONS[Rope.Cat[theWord, " --> "], correctionList]; }; lineNum _ 0; widthLeft _ 0; Menus.ChangeNumberOfLines[container.menu, 2]; IF correctionList = NIL THEN { lineNum _ 1; Menus.SetLine[container.menu, 1, NIL]; Menus.AppendMenuEntry[line: 1, menu: container.menu, entry: Menus.CreateEntry[name: "No corrections for this word.", proc: NoCorrectionButton]]; IO.PutF[permMessage, "\"%g\" may be misspelled; sorry, no corrections.\n", IO.rope[theWordCorrected]]; } ELSE { FOR c: RopeList _ correctionList, c.rest UNTIL c = NIL DO width: INT _ VFonts.StringWidth[c.first]; IF width > widthLeft AND widthLeft < viewerWidth THEN { IF ~(lineNum+2 IN Menus.MenuLine) THEN { IO.PutF[permMessage, "There are more corrections, but they can't fit in the menu.\n"]; EXIT; }; lineNum _ lineNum + 1; widthLeft _ viewerWidth - MenusPrivate.menuHLeading; Menus.SetLine[container.menu, lineNum, NIL]; }; Menus.AppendMenuEntry[ line: lineNum, menu: container.menu, entry: Menus.CreateEntry[ name: c.first, clientData: IF c = correctionList THEN theWord ELSE c.first, proc: ApplyCorrectionButton ] ]; widthLeft _ widthLeft - width - MenusPrivate.menuHSpace; numCorrections _ numCorrections + 1; ENDLOOP; IO.PutF[permMessage, "\"%g\" may be misspelled; %g correction buttons above.\n", IO.rope[theWordCorrected], IO.int[numCorrections-1]]; }; IF lineNum+1 # oldMenuLines THEN ViewerBLT.ChangeNumberOfLines[container, lineNum+1]; totalCorrectionsProposedCnt _ totalCorrectionsProposedCnt + numCorrections - 1; ViewerOps.PaintViewer[container, menu]; }; ApplyCorrectionButton: ENTRY PROC [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift, control: BOOL _ FALSE] --Menus.MenuProc-- = { ENABLE UNWIND => NULL; theWord: ROPE _ NIL; correction: ROPE _ NARROW[clientData]; wordCount: INT _ 0; s: Selection; Locker: INTERNAL PROC [root: TiogaOpsDefs.Ref] = TRUSTED { ExtractOneWord: INTERNAL PROC [word: REF TEXT] RETURNS [stop: BOOLEAN _ FALSE] = TRUSTED { stop _ wordCount > 0; IF wordCount = 0 THEN theWord _ Rope.FromRefText[word]; wordCount _ wordCount + 1; }; s _ ProcessSelection[FALSE, TRUE, TRUE].s; [] _ MapWordsInSelection[s.start, s.end, ExtractOneWord, TRUE]; IF theWord = NIL THEN IO.PutF[permMessage, "No word has been selected.\n"]; }; MakeCorrection: INTERNAL PROC [root: TiogaOpsDefs.Ref] = { looks: ROPE _ TiogaOps.FetchLooks[s.start.node, s.start.where]; TiogaOps.SaveForPaste[]; TiogaOps.Delete[]; TiogaOps.SetLooks[looks]; TiogaOps.InsertRope[correction]; s.end.where _ s.start.where + correction.Size[] - 1; TiogaOps.SetSelection[viewer: s.viewer, start: s.start, end: s.end, pendingDelete: TRUE]; }; CorrectTiogaOpsCallWithLocks[Locker ! TiogaOps.NoSelection => { IO.PutF[permMessage, "No word is selected, so no correction can be applied.\n"]; GOTO leave;}]; IF wordCount = 0 THEN { IO.PutF[permMessage, "No word is selected, so no correction can be applied.\n"]; RETURN; }; IF wordCount > 1 THEN { IO.PutF[permMessage, "More than one word selected, so no correction can be applied.\n"]; RETURN; }; IF ~(Rope.Equal[theWord, lastWordCorrected] OR Rope.Equal[theWord, theWordCorrected]) THEN { IO.PutF[permMessage, "Selected word, \"%g\", is not the same as \"%g\".\n", , IO.rope[theWord], IO.rope[theWordCorrected]]; RETURN; }; totalCorrectionsApplied _ totalCorrectionsApplied + 1; IF ~Rope.Equal[theWord, correction] THEN CorrectTiogaOpsCallWithLocks[MakeCorrection ! TiogaOps.NoSelection => { IO.PutF[permMessage, "There is no selection, so no correction can be applied.\n"]; GOTO leave;}]; lastWordCorrected _ correction; IF mouseButton = blue THEN { s _ ProcessSelection[FALSE,FALSE, TRUE].s; s.start.node _ s.end.node; s.start.where _ s.end.where + 1; --IF GotBitTable[] THEN-- { totalCheckCommands _ totalCheckCommands + 1; SpellCheckWithin[s, TRUE, TRUE, TRUE]; }; }; EXITS leave => NULL; }; ResetButton: ENTRY Menus.MenuProc = { ENABLE UNWIND => NULL; permMessage.PutRope["Freeing bit tables ... "]; FlushCache[]; permMessage.PutRope[" ... ensuring insertions written ... "]; WaitTillClean[]; permMessage.PutRope[" ... done resetting.\n"]; }; NoCorrectionButton: ENTRY Menus.MenuProc = { ENABLE UNWIND => NULL; IO.PutF[permMessage, "\"%g\" may be misspelled; no similar words are in the list.\n", IO.rope[theWordCorrected]]; }; container: Viewer _ NIL; registration: ViewerEvents.EventRegistration; QuitSpellTool: ENTRY ViewerEvents.EventProc = { ENABLE UNWIND => NULL; IF viewer = container AND event = destroy THEN { SpellingToolDefinition.FinalizeDictionaryServer[]; SaveInsertions[]; toolExists _ FALSE; container _ NIL; ViewerEvents.UnRegisterEventProc[registration, destroy]; }; }; DefineMenu: PROC = { Menus.AppendMenuEntry [line: 0, menu: toolMenu, entry: Menus.CreateEntry[name: "Reset", proc: ResetButton, guarded: TRUE]]; Menus.AppendMenuEntry [line: 0, menu: toolMenu, entry: Menus.CreateEntry[name: "Definition", proc: SpellingToolDefinition.DefinitionButton, guarded: TRUE]]; Menus.AppendMenuEntry [line: 0, menu: toolMenu, entry: Menus.CreateEntry[name: "Check", proc: CheckSpelling]]; Menus.AppendMenuEntry [line: 0, menu: toolMenu, entry: Menus.CreateEntry[name: "Insert in:", proc: InsertHead]]; }; NewSpellingTool: ENTRY PROC RETURNS [new: BOOL] = { ENABLE UNWIND => NULL; ymax: INTEGER _ 0; emWidth: INTEGER _ VFonts.CharWidth['M]; emHeight: INTEGER _ VFonts.FontHeight[]; ts: TypeScript.TS; IF toolExists THEN RETURN [FALSE]; toolExists _ new _ TRUE; container _ Containers.Create[[name: "Spelling Tool", iconic: TRUE, column: right, menu: toolMenu, scrollable: FALSE, icon: spellingToolIcon]]; registration _ ViewerEvents.RegisterEventProc[QuitSpellTool, destroy]; ts _ TypeScript.Create[info: [parent: container, border: FALSE, scrollable: TRUE, wy: 2], paint: FALSE]; [permMessageIn, permMessage] _ ViewerIO.CreateViewerStreams[name: "Definitions", viewer: ts, editedStream: FALSE]; TRUSTED { Process.Detach[FORK SpellingToolDefinition.InitializeDictionaryServer[permMessage]]; }; ViewerOps.SetOpenHeight[container, 6*emHeight + 8]; Containers.ChildXBound[container, ts]; Containers.ChildYBound[container, ts]; ViewerOps.PaintViewer[container, all]; }; WaitTillClean: INTERNAL PROC = { WHILE dirty DO BROADCAST CleanlinessWanted; WAIT CleanlinessAchieved; ENDLOOP; }; InitializeProfile: ENTRY UserProfile.ProfileChangedProc = { ENABLE UNWIND => NULL; oldBitTableFilename: ROPE _ BitTableFilename; oldGlobalAuxilliaryWordlistFilenames: RopeList _ globalAuxilliaryWordlistFilenames; globalAuxilliaryWordlistFilenames _ UserProfile.ListOfTokens["SpellingTool.Wordlists", LIST["SpellingTool.Wordlist"]]; FOR rl: RopeList _ globalAuxilliaryWordlistFilenames, rl.rest WHILE rl # NIL DO rl.first _ FS.ExpandName[name: rl.first, wDir: installationDirectory].fullFName; ENDLOOP; BitTableFilename _ FS.ExpandName[ name: UserProfile.Token[ key: "SpellingTool.BitTable", default: "SpellingTool.BitTable"], wDir: installationDirectory ].fullFName; insertionInterval _ UserProfile.Number["SpellingTool.InsertionInterval", 60]; pluralStripping _ UserProfile.Boolean["SpellingTool.PluralStripping", TRUE]; IF NOT (oldBitTableFilename.Equal[BitTableFilename, FALSE] AND ListEqual[ oldGlobalAuxilliaryWordlistFilenames, globalAuxilliaryWordlistFilenames]) THEN FlushCache[]; BROADCAST CleanlinessWanted; }; SpellingToolCommand: Commander.CommandProc = { IF NewSpellingTool[] THEN { UserProfile.CallWhenProfileChanges[InitializeProfile]; TRUSTED {Process.Detach[FORK MakeAdditionsPermanent[]];}; }; }; TRUSTED {Process.SetTimeout[@CleanlinessAchieved, Process.SecondsToTicks[30]]}; DefineMenu[]; Commander.Register[ key: "SpellingTool", proc: SpellingToolCommand, doc: "Makes a Spelling Tool."]; END. CHANGE LOG Created by Nix on February 25, 1985 5:38:53 pm PST, END JFile: SpellingToolImpl.mesa Last Edited by: Nix, December 2, 1983 11:57 am Last Edited by: Spreitzer, March 2, 1985 1:44:59 pm PST For localAuxilliaryWordlistFilenamesRope. |-Contains the alphabetized, duplicate-filtered union of local and globals. |-Includes a ld iff ld.users # NIL & ld.auxilliaryWordlistFilenames includes me |-# NIL iff localDatas#NIL & IsLocal[filename] the property of document's root node that gives wordlist file names the property of a viewer that gives local data the icon The cache of files->bitTable |-Includes a ld iff ld.users # NIL The associated with each file |-Includes a fd iff fd.localDatas#NIL or fd.wordsAddedList#NIL |-TRUE when some FileData have non-empty wordsAddedList. A background process will write these additions to the file, NIL-out the wordsAddedList, and reset the dirty big. Handle for the word list. globalBitTable: BitTableLookup.Table _ NIL; Must now be local to document. |-Includes an insert button for fd iff fd.localDatas#NIL & IsLocal[fd.filename] The place for writing permanent messages, e.g. definitions. Parameters settable in User.Profile. List of the names of the word list files that the user would like merged in to the global word list. The name of the user's bit table file. The number of seconds between writes to the user's word list file. Whether or not to strip s's off the ends of misspelled words. Applies the spelling checker within the selection, which searches for the first misspelled word and then highlights it and returns. If toEnd is TRUE, then it ignores whatever the end of the selection is said to be, and searches to the end of the document. Inserts the selected word into the word list, and if andSpell is TRUE continues searching in the document after the selection for the next misspelled word. Get the right bit table & stuff. Utility for reading in the bit file portion of the word list. Utility for reading a token from the given stream into the given buffer. How's this for device independence? Reads in an auxilliary word file and merges the words therein into the word list. GotBitTable: INTERNAL PROC [] RETURNS [present: BOOLEAN] = { Checks the state of the world to see if it's OK. present _ globalBitTable # NIL; IF ~present THEN IO.PutF[permMessage, "Word table not yet loaded; wait or restart the Spelling Tool.\n"]; }; Saves the words the have been recently inserted into word lists in their files. scope of GVSend.SendFailed end scope Spreitzer, February 25, 1985 5:38:07 pm PST Added VM operations to accommodate Beach's addition of explicit VM management. changes to: DIRECTORY, SpellingToolImpl, SpellCheckWithin Spreitzer, February 27, 1985 11:42:53 am PST Introduced auxilliary wordlist file lists local to document. changes to: , DIRECTORY Edited on February 27, 1985 8:46:37 pm PST, by Spreitzer This entry from NewStuff, rather than EditorComforts changes to: viewerLocalDataKey, LocalDataRep, FileDataRep, localDataCache, fileDatas, dirty, toolMenu, SpellCheckWithin, WordCollector (local of Locker, local of InsertWordOrInsertWordAndSpell), Locker (local of InsertWordOrInsertWordAndSpell), InsertWordOrInsertWordAndSpell, Valid, ValidCache (local of GetLocalData), UseIt (local of GetLocalData), ComputeLocalData, NoteNonUse, Unenter, FlushCache, FilterLocalDataList, GetBitFile, AddWord, SaveInsertions, NILList (local of SaveInsertions), ApplyInsertionButton, NewSpellingTool, FlushCacheButton, NoCorrectionButton, QuitSpellTool, DIRECTORY, STREAM, Viewer, ViewerList, FindFileData, FilterViewerList, ProposeCorrections, container, generalDestroyRegistration, NoteDestruction, Commander, GetLocalData, MaybeAddWordlistInserter, NewSpellingTool, SpellCheckWithin, DIRECTORY, toolMenu, NewSpellingTool, NewSpellingTool, TRUSTED, DefineMenu, FlushCache, ResetButton, InitializeProfile, SpellingToolImpl, localAuxilliaryWordlistFilenamesProperty, ProposeCorrections, Locker (local of ApplyCorrectionButton), ApplyCorrectionButton Edited on March 2, 1985 1:44:16 pm PST, by Spreitzer Changed default source of bit table from [Indigo]SpellingTool.BitTable to SpellingTool.BitTable changes to: InitializeProfile Κ'l˜code– "Cedar" stylešœ™K™.K™7—K˜šΟk ˜ Kšœœ ˜Kšœœ'˜;Kšœ œ˜(Kšœ œ˜,Kšœ œ$˜4Kšœœœ˜.Kšœœ5˜=Kšœœb˜nKšœœ˜*Kšœœpœœ˜‘Kšœœ˜Kšœœ ˜«Kšœ œ˜.Kšœ œ ˜Kšœœ=˜JKšœœ$œ˜BKšœœ˜0Kšœœ[˜wKšœœr˜ŠKšœœ˜"Kšœœ˜2Kšœ œ˜Kšœ œ“˜‘Kšœ œ˜+Kšœ œœ ˜Kšœœ˜Kšœ œT˜eKšœœ&˜2Kšœ œ˜&Kšœœ ˜Kšœ œU˜gKšœ œ˜%Kšœ œK˜ZKšœœ˜K˜—šΟbœœ˜KšœCœœσœ˜Χ—Kš˜Kšœ˜Kšœœœ˜Kš œ œœœœ˜Kšœœœœ˜Kšœœ˜$Kšœ œœœ˜"K˜Kšœœœœ ˜(Kšœ œœ˜#šœœœ˜Kšœœœ˜Kšœ.œ˜2šœœœ˜Kšœ)™)—Kšœ&œœ˜1šœ(œ˜,K™K—Kšœ!œ˜%Kšœ˜K˜—K˜Kšœœœœ ˜&Kšœ œœ ˜!šœ œœ˜Kšœ œ˜šœœ˜ KšœO™O—Kšœœ˜šœ˜ Kšœ.™.—Kšœ˜—K˜K˜Kšœœ)˜D˜KšΟcC™C—Kšœ*œ˜<˜KšŸ.™.—Kšœœœ8œ ˜o˜KšŸ™—KšœT˜T˜KšŸ™—šœ œ˜$K™"—˜K™—šœœ˜Kšœ>™>—šœœœ˜Kšœͺ™ͺ—˜KšŸ™—šœ(œ™,K™—K˜Kšœ œœ˜šœ2˜2KšœO™O—˜Kšœ;™;—Kšœ œœ˜Kšœœ˜K˜Kšœœœ˜Kšœœ ˜$Kšœ œ˜Kšœœ˜—Kšœ„œ˜ŒK˜šœ$™$K™Kšœd™d—Kšœ,˜,˜KšŸ&™&—Kšœœ˜˜KšŸB™B—Kšœœ˜˜KšŸ=™=—Kšœœœ˜K˜K˜K˜K˜šΟnœœœ-œ˜UKšœ€™€K˜*K˜%Kšœ œ˜Kšœ Οs‘œ˜K˜'K˜š œœœœœœœ˜RKšœ.˜.šœ ˜K˜%—K˜ š œœœœ,œ˜fK˜—K˜K˜—š œœœœœœœœ˜WKšœœ˜Kšœœ˜ Kšœœœ˜ šœœ˜Kšœ œ˜Kšœ˜K˜—Kšœœœ˜K˜K˜šœ œ˜Kšœ œœ˜šœ œ˜K˜Kšœ.˜.K˜K˜—šœ˜K˜Kšœ.˜.K˜K˜—K˜—K˜—šΠbnœœœ˜2Kšœ:œœ ˜OK˜K˜—šœœœ˜K˜&Kšœ˜—Kšœ-˜/šœœ˜Kšœ/˜/Kšœ6˜6K˜—šœœ˜šœ œ˜K˜>K˜6K˜—šœ˜K˜KK˜K˜—K˜—K˜fšœ œ œ˜$šœœ˜Kšœ-˜-Kšœ6˜6K˜—šœ˜Kšœ˜Kšœ˜K˜—K˜IK˜K˜fK˜—K˜;šœ œ˜KšœI˜IKšœ6˜6KšœFœœ˜_K˜$K˜K˜—šœ˜šœ,œ˜4Kšœ-˜-Kšœ,˜,Kšœ'˜'K˜—šœœ˜KšœA˜CK˜$K˜—šœœœ˜Kšœ?˜A—šœ˜Kšœ;œ˜SK˜$K˜—K˜—Kšœ+˜-š˜Kšœœ˜—K˜K˜—K˜š œœœ0œ˜fKšœ›™›K˜K˜ K˜Kšœ œ˜K˜*Kšœœœ˜'K˜š’œœœ˜2š  œœœœœœœœ˜NKšœ*˜*K˜K˜—Kšœœœ˜/K˜šœ˜Kšœ`˜dKšœ'˜+—K˜—Kšœ>œ˜PKšœœœœ˜$šœ˜K˜—šœ œ˜šœ œ˜K˜K˜ K˜—šœ˜K˜K˜ K˜—Kšœœ˜0K˜—š˜Kšœœ*˜;—K˜K˜K˜—š  œœœ˜TKšœ9˜9Kšœ œ˜,K˜K˜—š  œœœœ œ˜EKšœœ˜K˜K˜—š  œœœœ˜MK™ Kšœ&œœZ˜Kšœ(œ˜,Kšœœœ˜š  œœœœ˜1šœœœ˜Kšœy˜yKšœœ˜—Kšœ"˜"—Kšœœ7˜Aš   œœœœ œ˜3šœ˜Kšœ9œ˜?š˜šœ˜šœ ˜ Kšœ/˜/Kšœ%˜%Kšœ˜—Kšœ"œ˜+šœ ˜Kšœ,˜,Kšœ#˜#——Kšœ@˜B——K˜—K˜š œœœœ˜0Kšœ@˜@Kšœœœ$˜>Kšœ œœœ˜JKšœ˜—Kšœœ4˜Ušœ œœœœœœ˜Išœ/œœ˜DKšœ˜Kšœœœœ ˜;Kšœ˜—K˜K˜Kšœf˜fK˜ Kšœ˜—K˜K˜—š  œœœœ˜Uš œœœœ˜Kšœ$œœœ ˜EKšœ˜—Kšœœ&˜.Kšœ œ˜ Kšœ œœ˜6K˜K˜—š œœœ˜>Kšœœ˜ Kšœœ˜Kšœœœœ˜!Kšœœ˜/šœœ˜KšœH˜HKšœ@˜@Kšœœœœ%˜M˜K˜K˜˜(Kšœ˜K˜K˜—K˜—Kšœœ(˜5K˜—K˜K˜—š   œœœœœ Ÿœ˜Išœœ˜Kšœ˜Kšœ ˜—K˜K˜—Kšœƒ˜ƒK˜š’œœœ;œœ œœŸœ˜˜Kšœœœ˜Kšœœ2˜Ošœœ œœ˜-Kšœ.œ˜3Kšœ˜K˜—K˜K˜—š  œœœ)˜BK˜:šœœœ˜Kšœœ˜ šœ?œœ˜SKšœœ&˜8Kšœ˜—šœ-œœ˜JK˜!K˜>šœœœ˜Kšœœœ œ˜-Kšœœœ/˜NK˜—Kšœ˜—Kšœ@˜@K˜—K˜K˜—š œœœ˜-KšœIœ˜NKšœœ˜Kšœœ œ(˜DK˜K˜—š  œœœ˜šœœ˜K˜0Kšœ,œ˜1Kšœ'˜'Kšœ˜—K˜K˜—š œœ'œ˜^Kšœœ˜K˜šœ'œœ˜=šœœ˜Kšœœœœ˜BKšœ˜K˜—K˜ Kšœ˜—Kšœ˜K˜K˜—š œœ-œ˜jKšœœ˜K˜šœ*œœ˜@šœœ˜Kšœœœœ˜BKšœ˜K˜—K˜ Kšœ˜—Kšœ˜K˜K˜—š œœ+œ˜fKšœœ˜K˜šœ)œœ˜?šœœ˜Kšœœœœ˜BKšœ˜K˜—K˜ Kšœ˜—Kšœ˜K˜K˜—š  œœœ œœœœ%˜tKšœ=™=K˜Kšœœ˜ Kšœ œ˜Kšœ7œ˜Lšœœ˜šœ ˜ Kšœ2˜2Kšœ˜——šœ"˜"šœ˜šœœ˜#Kšœ&˜&Kšœ&˜&Kšœœ˜—Kšœ˜——K˜ Kšœ œ˜Kšœœ˜)š˜Kšœœ˜Kšœœ˜—K˜K˜—š  œœœœœœ˜QK˜-Kšœ/œœ˜\Kšœœ ˜8Kšœœ˜ šœ.œœ˜CK˜&Kšœ˜—K˜K˜—š œœœœœœœœœ˜MKšœH™Hš  œœœœœœœ˜KKšœ#™#Kšœœ˜$K˜—Kšœœ˜K˜ Kšœœœ˜/K˜ K˜šœœ ˜Kšœœœ˜.Kšœœœ˜!šœœ˜Kšœœœ˜K˜K˜—K˜ š˜K˜—Kšœ˜—˜K˜—K˜—š  œœœ œ%˜QKšœQ™QKšœ œ˜Kšœ7œ˜LKšœ œœ œ ˜Aš˜Kš œœœœœ˜Kšœ!œœ˜9Kšœ˜Kšœ˜—Kšœ˜Kšœœ˜)š˜˜ Kšœœg˜}——K˜K˜K˜—š   œœœœ œ™K˜š œœ˜Kšœœ˜Kšœœœ/˜JK˜—šœœœ˜!Kšœ œœ˜Kšœœ˜ Kšœ œ#œ&œ˜cšœ œœ˜šœ)œœ˜œ˜PKšœ,˜,šœœœ˜-Kšœ˜K˜ Kšœœœœ˜&K˜—šœ˜Kšœœœ˜.—K˜—š˜Kšœœ*˜;—K˜K˜—šž œœ˜$Kšœœœ˜Kšœœ ˜"šŸœ˜Kšœ6œœ˜Bšœœ˜Kšœ,˜,—K˜—K˜K˜K˜—šž œœ˜$Kšœœœ˜KšœJ˜JK˜K˜K˜—šžœœ˜ Kšœœœ˜Kšœ ˜ š’œœœ˜2Kšœœœœ˜+Kšœ˜K˜ K˜(K˜—šŸœ˜Kšœ>œ˜PKšœ,˜,Kšœœœœ˜&K˜—š˜Kšœœ*˜;—K˜K˜K˜—Kšœœœ˜Kšœœœ˜Kš œœœœœ˜+K˜š’œœœ!œ˜Kš  œœœœœœ œ˜DK˜'K˜—š   œœœ œœœ˜PKšœœœœ˜8K˜—Kšœœ˜Kšœ œ˜(Kšœœ*˜?Kšœœ˜Kšœœ˜šœ œœ˜KšœA˜CKšœ˜K˜—K˜.K˜K˜šœ%˜%KšœP˜P—Kšœœœ#˜^šœœœ˜Kšœœ-˜BK˜—Kšœ ˜ Kšœ˜Kšœ-˜-šœœœ˜K˜ Kšœ!œ˜&Kšœ‘˜‘KšœIœ˜fK˜—šœ˜šœ&œœ˜9Kšœœ˜)šœœœ˜7šœ œœ˜(KšœT˜VKšœ˜K˜—K˜Kšœ4˜4Kšœ'œ˜,K˜—šœ˜Kšœ˜Kšœ˜šœ˜Kšœ˜Kšœ œœ œ ˜œ-œ˜KšœF˜FKšœ9œœœ˜hKšœkœ˜ršœ˜ KšœœA˜TKšœ˜—K˜K˜3K˜&K˜&K˜K˜&K˜K˜—K˜š’ œœœ˜ šœ˜Kš œ˜Kšœ˜Kšœ˜—K˜K˜—šžœœ#˜;Kšœœœ˜Kšœœ˜-KšœS˜SKšœWœ˜všœ;œœ˜OKšœ œC˜PKšœ˜—šœœ ˜!˜K˜K˜"—K˜K˜ —K˜MKšœFœ˜Lš˜š˜Kšœ-œ˜3šœ ˜Kšœ%˜%Kšœ#˜#——Kšœ˜—Kš œ˜K˜K˜—šžœ˜.šœœ˜K˜6Kšœœ˜9K˜—K˜K˜—KšœH˜OK˜ šœ˜Kšœ˜Kšœ˜Kšœ˜—˜K˜—K˜K˜šœ˜Kšœ˜ Kšœ/ΟrΠkr˜7—K˜™+K™NKšœ €-™9—™,K™