├── Preview.png ├── HighlightedWebView ├── en.lproj │ ├── InfoPlist.strings │ └── Credits.rtf ├── HighlightedWebView-Prefix.pch ├── DHAppDelegate.h ├── main.m ├── DHAppDelegate.m └── HighlightedWebView-Info.plist ├── DHHighlightedWebView ├── DHWebView.h ├── DHHighlightedWebView.h ├── DHScrollbarHighlighter.h ├── DHSearchQuery.h ├── Info.plist ├── DHSearchQuery.m ├── DHMatchedText.h ├── DHMatchedText.m ├── DHScrollbarHighlighter.m └── DHWebView.m ├── HighlightedWebView.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── bogdan.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings ├── xcuserdata │ └── bogdan.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── HighlightedWebView.xcscheme └── project.pbxproj ├── README.md └── HighlightedWebView.podspec /Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kapeli/HighlightedWebView/HEAD/Preview.png -------------------------------------------------------------------------------- /HighlightedWebView/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHWebView.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface DHWebView : WebView 5 | 6 | - (void)highlightQuery:(NSString *)aQuery caseSensitive:(BOOL)isCaseSensitive; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /HighlightedWebView/HighlightedWebView-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'HighlightedWebView' target in the 'HighlightedWebView' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /HighlightedWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HighlightedWebView.xcodeproj/project.xcworkspace/xcuserdata/bogdan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kapeli/HighlightedWebView/HEAD/HighlightedWebView.xcodeproj/project.xcworkspace/xcuserdata/bogdan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /HighlightedWebView/DHAppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "DHWebView.h" 3 | 4 | @interface DHAppDelegate : NSObject { 5 | } 6 | 7 | @property (assign) IBOutlet DHWebView *webView; 8 | 9 | - (IBAction)search:(id)sender; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /HighlightedWebView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // HighlightedWebView 4 | // 5 | // Created by Bogdan Popescu on 05/05/2012. 6 | // Copyright (c) 2012 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **)argv); 14 | } 15 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHHighlightedWebView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DHHighlightedWebView.h 3 | // DHHighlightedWebView 4 | // 5 | // Created by James Howard on 9/7/17. 6 | // 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for DHHighlightedWebView. 12 | FOUNDATION_EXPORT double DHHighlightedWebViewVersionNumber; 13 | 14 | //! Project version string for DHHighlightedWebView. 15 | FOUNDATION_EXPORT const unsigned char DHHighlightedWebViewVersionString[]; 16 | 17 | #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /HighlightedWebView/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /HighlightedWebView.xcodeproj/xcuserdata/bogdan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | HighlightedWebView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 43F7F37C1554F75D00879D86 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /HighlightedWebView.xcodeproj/project.xcworkspace/xcuserdata/bogdan.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 6 | 7 | IDEWorkspaceUserSettings_HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 8 | 9 | IDEWorkspaceUserSettings_SnapshotAutomaticallyBeforeSignificantChanges 10 | 11 | SnapshotAutomaticallyBeforeSignificantChanges 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHScrollbarHighlighter.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface DHScrollbarHighlighter : NSView { 5 | WebView *parentView; 6 | NSArray *matches; 7 | NSMutableArray *highlightRects; 8 | } 9 | 10 | @property (retain) WebView *parentView; 11 | @property (retain) NSArray *matches; 12 | @property (retain) NSMutableArray *highlightRects; 13 | 14 | + (DHScrollbarHighlighter *)highlighterWithWebView:(WebView *)aWebView andMatches:(NSArray *)matches; 15 | - (id)initWithWebView:(WebView *)aWebView andMatches:(NSArray *)matches; 16 | - (void)calculatePositions; 17 | - (int)topPositionForElement:(DOMHTMLElement *)element; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | License 2 | ============== 3 | Do whatever you want with this code. I only ask that you check out [Dash](http://kapeli.com/dash), my Snippet Manager and Documentation Browser app. 4 | 5 | HighlightedWebView 6 | ================== 7 | 8 | Drop-in WebView subclass that adds Safari-style in-page search-result highlighting. 9 | 10 | ![Screenshot](https://github.com/Kapeli/HighlightedWebView/raw/master/Preview.png) 11 | 12 | Features: 13 | ========= 14 | * Highlight search results. 15 | * Show position of search results by highlighting the scrollbar. 16 | * Maintain text size (zoom level) between launches. 17 | 18 | Project Goals: 19 | ============== 20 | * Avoid JavaScript. 21 | * Avoid blocking the main thread as much as possible. 22 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHSearchQuery.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface DHSearchQuery : NSObject { 4 | NSString *query; 5 | BOOL isCaseSensitive; 6 | NSMutableDictionary *selectionAfterHighlight; 7 | NSMutableDictionary *selectionAfterClear; 8 | } 9 | 10 | @property (retain) NSString *query; 11 | @property (assign) BOOL isCaseSensitive; 12 | @property (retain) NSMutableDictionary *selectionAfterHighlight; 13 | @property (retain) NSMutableDictionary *selectionAfterClear; 14 | 15 | + (DHSearchQuery *)searchQueryWithQuery:(NSString *)aQuery caseSensitive:(BOOL)caseSensitive; 16 | - (id)initWithQuery:(NSString *)aQuery caseSensitive:(BOOL)caseSensitive; 17 | - (BOOL)isEqualTo:(DHSearchQuery *)object; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /DHHighlightedWebView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /HighlightedWebView/DHAppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "DHAppDelegate.h" 2 | 3 | @implementation DHAppDelegate 4 | 5 | @synthesize webView; 6 | 7 | - (void)controlTextDidChange:(NSNotification *)obj 8 | { 9 | NSTextView *field = [[obj userInfo] objectForKey:@"NSFieldEditor"]; 10 | [webView highlightQuery:[field string] caseSensitive:NO]; 11 | } 12 | 13 | - (IBAction)search:(id)sender 14 | { 15 | NSString *query = [sender stringValue]; 16 | [webView searchFor:query direction:YES caseSensitive:NO wrap:YES]; 17 | } 18 | 19 | - (void)dealloc 20 | { 21 | [super dealloc]; 22 | } 23 | 24 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 25 | { 26 | // [webView setMainFrameURL:@"file:///Users/bogdan/Library/Application%20Support/Dash/DocSets/Android/Android.docset/Contents/Resources/Documents/docs/reference/android/widget/AbsListView.html"]; 27 | [webView setMainFrameURL:@"http://kapeli.com/dash"]; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /HighlightedWebView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "HighlightedWebView" 3 | s.version = "1.0.0" 4 | s.summary = "WebView subclass that highlights all search results (Safari-style wannabe)" 5 | 6 | s.description = <<-DESC 7 | `WebView` subclass that highlights all search results (Safari-style wannabe). 8 | DESC 9 | 10 | s.homepage = "https://github.com/Kapeli/HighlightedWebView" 11 | s.screenshot = "https://github.com/Kapeli/HighlightedWebView/raw/master/Preview.png" 12 | s.license = { :type => "MIT", :file => "README.md" } 13 | s.author = {"Bogdan Popescu" => "support@kapeli.com"} 14 | s.social_media_url = "http://twitter.com/kapeli" 15 | 16 | s.platform = :osx 17 | s.source = { :git => "https://github.com/Kapeli/HighlightedWebView.git", :tag => "1.0.0" } 18 | 19 | s.source_files = "HighlightedWebView/*.{h,m}" 20 | s.exclude_files = "HighlightedWebView/DHAppDelegate.h", "HighlightedWebView/DHAppDelegate.m", "HighlightedWebView/main.m" 21 | 22 | s.frameworks = "Foundation", "Cocoa" 23 | end 24 | -------------------------------------------------------------------------------- /HighlightedWebView/HighlightedWebView-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.kapeli.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2012 __MyCompanyName__. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHSearchQuery.m: -------------------------------------------------------------------------------- 1 | #import "DHSearchQuery.h" 2 | 3 | @implementation DHSearchQuery 4 | 5 | @synthesize query; 6 | @synthesize isCaseSensitive; 7 | @synthesize selectionAfterHighlight; 8 | @synthesize selectionAfterClear; 9 | 10 | + (DHSearchQuery *)searchQueryWithQuery:(NSString *)aQuery caseSensitive:(BOOL)caseSensitive 11 | { 12 | return [[[DHSearchQuery alloc] initWithQuery:aQuery caseSensitive:caseSensitive] autorelease]; 13 | } 14 | 15 | - (id)initWithQuery:(NSString *)aQuery caseSensitive:(BOOL)caseSensitive 16 | { 17 | self = [super init]; 18 | if(self) 19 | { 20 | self.query = [NSString stringWithString:aQuery]; 21 | self.isCaseSensitive = caseSensitive; 22 | self.selectionAfterClear = [NSMutableDictionary dictionary]; 23 | self.selectionAfterHighlight = [NSMutableDictionary dictionary]; 24 | } 25 | return self; 26 | } 27 | 28 | - (BOOL)isEqualTo:(DHSearchQuery *)object 29 | { 30 | if(!object) 31 | { 32 | return NO; 33 | } 34 | if(isCaseSensitive != object.isCaseSensitive) 35 | { 36 | return NO; 37 | } 38 | if(isCaseSensitive) 39 | { 40 | return [query isEqualToString:object.query]; 41 | } 42 | return [query isCaseInsensitiveLike:object.query]; 43 | } 44 | 45 | - (void)dealloc 46 | { 47 | [selectionAfterClear release]; 48 | [selectionAfterHighlight release]; 49 | [query release]; 50 | [super dealloc]; 51 | } 52 | 53 | @end -------------------------------------------------------------------------------- /DHHighlightedWebView/DHMatchedText.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "DHSearchQuery.h" 4 | 5 | @interface DHMatchedText : NSObject { 6 | DOMText *text; 7 | NSString *originalText; 8 | NSRange effectiveRange; 9 | DOMNode *highlightedSpan; 10 | NSMutableArray *foundRanges; 11 | DOMNode *firstMatch; 12 | NSInteger focusedRangeIndex; 13 | DOMElement *focusedSpan; 14 | } 15 | 16 | @property (retain) DOMText *text; 17 | @property (retain) NSString *originalText; 18 | @property (assign) NSRange effectiveRange; 19 | @property (retain) DOMNode *highlightedSpan; 20 | @property (retain) NSMutableArray *foundRanges; 21 | @property (retain) DOMNode *firstMatch; 22 | @property (retain) DOMElement *focusedSpan; 23 | 24 | + (DHMatchedText *)matchedTextWithDOMText:(DOMText *)aText andRange:(NSRange)aRange; 25 | - (id)initWithDOMText:(DOMText *)aText andRange:(NSRange)aRange; 26 | - (void)highlightDOMNode; 27 | - (void)clearHighlight; 28 | 29 | @property (nonatomic, assign) NSInteger focusedRangeIndex; // returns NSNotFound if nothing is focused within foundRanges 30 | 31 | @end 32 | 33 | // This is the "alternate" style (the one in the screenshot), abandoned because it resizes the elements and also looks weird 34 | // with elements that span across multiple nodes. 35 | 36 | // static NSString *DHHighlightSpan = @"-webkit-border-radius: 7px; -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.6); background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(244, 234, 38, 0.7)), to(rgba(237, 206, 0, 0.7)) ); border: 1px solid rgba(244, 234, 38, 0.8);display:inline;position:static;margin:0px 0px 0px 0px;padding:0px 0px 0px 0px;"; 37 | 38 | static NSString *DHHighlightSpan = @"background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(244, 234, 38, 0.5)), to(rgba(237, 206, 0, 0.5)) ) !important;display:inline !important;position:static !important;margin:0px 0px 0px 0px !important;padding:0px 0px 0px 0px !important;opacity:1.0 !important; float:inherit !important; font:inherit !important;"; 39 | 40 | static NSString *DHFocusSpan = @"background: Highlight; !important;display:inline !important;position:static !important;margin:0px 0px 0px 0px !important;padding:0px 0px 0px 0px !important;opacity:1.0 !important; float:inherit !important; font:inherit !important;"; 41 | 42 | static NSString *DHSpanWrap = @"display:inline !important;position:static !important;margin:0px 0px 0px 0px !important;padding:0px 0px 0px 0px !important;opacity:1.0 !important; float:inherit !important; font:inherit !important;"; 43 | -------------------------------------------------------------------------------- /HighlightedWebView.xcodeproj/xcuserdata/bogdan.xcuserdatad/xcschemes/HighlightedWebView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 14 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHMatchedText.m: -------------------------------------------------------------------------------- 1 | #import "DHMatchedText.h" 2 | 3 | @implementation DHMatchedText 4 | 5 | @synthesize text; 6 | @synthesize originalText; 7 | @synthesize effectiveRange; 8 | @synthesize highlightedSpan; 9 | @synthesize foundRanges; 10 | @synthesize firstMatch; 11 | @synthesize focusedRangeIndex; 12 | @synthesize focusedSpan; 13 | 14 | + (DHMatchedText *)matchedTextWithDOMText:(DOMText *)aText andRange:(NSRange)aRange 15 | { 16 | return [[[DHMatchedText alloc] initWithDOMText:aText andRange:aRange] autorelease]; 17 | } 18 | 19 | - (id)initWithDOMText:(DOMText *)aText andRange:(NSRange)aRange 20 | { 21 | self = [super init]; 22 | if(self) 23 | { 24 | self.text = aText; 25 | self.effectiveRange = aRange; 26 | self.foundRanges = [NSMutableArray array]; 27 | self.originalText = [NSString stringWithString:[text nodeValue]]; 28 | focusedRangeIndex = NSNotFound; 29 | } 30 | return self; 31 | } 32 | 33 | - (void)highlightDOMNode 34 | { 35 | self.firstMatch = nil; 36 | self.focusedSpan = nil; 37 | if(!foundRanges.count) 38 | { 39 | return; 40 | } 41 | [foundRanges sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { 42 | NSRange range1 = [obj1 rangeValue]; 43 | NSRange range2 = [obj2 rangeValue]; 44 | if(range1.location < range2.location) 45 | { 46 | return NSOrderedAscending; 47 | } 48 | return NSOrderedDescending; 49 | }]; 50 | DOMNode *parent = [text parentNode]; 51 | DOMDocument *document = [text ownerDocument]; 52 | DOMHTMLElement *spanWrap = (DOMHTMLElement*)[document createElement:@"span"]; 53 | [spanWrap setAttribute:@"style" value:DHSpanWrap]; 54 | [parent replaceChild:spanWrap oldChild:text]; 55 | 56 | NSRange previousRange = NSMakeRange(0, 0); 57 | NSInteger i = 0; 58 | for(NSValue *foundRange in foundRanges) 59 | { 60 | NSRange range = [foundRange rangeValue]; 61 | range = NSMakeRange(range.location - effectiveRange.location, range.length); 62 | if(previousRange.location + previousRange.length < range.location) 63 | { 64 | DOMText *aText = [document createTextNode:[originalText substringWithRange:NSMakeRange(previousRange.location+previousRange.length, range.location-previousRange.location-previousRange.length)]]; 65 | [spanWrap appendChild:aText]; 66 | } 67 | DOMElement *aSpan = [document createElement:@"span"]; 68 | [aSpan setAttribute:@"style" value:i==focusedRangeIndex?DHFocusSpan:DHHighlightSpan]; 69 | if (i == focusedRangeIndex) { 70 | self.focusedSpan = aSpan; 71 | } 72 | if(text) 73 | { 74 | [text setNodeValue:[originalText substringWithRange:range]]; 75 | [aSpan appendChild:text]; 76 | self.firstMatch = text; 77 | self.text = nil; 78 | } 79 | else 80 | { 81 | DOMText *aText = [document createTextNode:[originalText substringWithRange:range]]; 82 | [aSpan appendChild:aText]; 83 | } 84 | [spanWrap appendChild:aSpan]; 85 | previousRange = range; 86 | i++; 87 | } 88 | if(previousRange.location + previousRange.length < originalText.length) 89 | { 90 | DOMText *aText = [document createTextNode:[originalText substringWithRange:NSMakeRange(previousRange.location+previousRange.length, originalText.length-previousRange.location-previousRange.length)]]; 91 | [spanWrap appendChild:aText]; 92 | } 93 | self.highlightedSpan = spanWrap; 94 | } 95 | 96 | - (void)clearHighlight 97 | { 98 | if(!highlightedSpan) 99 | { 100 | return; 101 | } 102 | DOMNode *parent = [highlightedSpan parentNode]; 103 | DOMDocument *document = [highlightedSpan ownerDocument]; 104 | DOMText *original = [document createTextNode:originalText]; 105 | self.text = original; 106 | [parent replaceChild:text oldChild:highlightedSpan]; 107 | self.highlightedSpan = nil; 108 | self.focusedSpan = nil; 109 | focusedRangeIndex = NSNotFound; 110 | } 111 | 112 | - (void)setFocusedRangeIndex:(NSInteger)idx { 113 | NSAssert(idx == NSNotFound || (idx >= 0 && idx < foundRanges.count), @"focusedRangeIndex must be in foundRanges, or NSNotFound"); 114 | if (focusedRangeIndex != idx) { 115 | [self clearHighlight]; 116 | focusedRangeIndex = idx; 117 | [self highlightDOMNode]; 118 | [focusedSpan scrollIntoViewIfNeeded:YES]; 119 | } 120 | } 121 | 122 | - (void)dealloc 123 | { 124 | [text release]; 125 | [firstMatch release]; 126 | [highlightedSpan release]; 127 | [originalText release]; 128 | [foundRanges release]; 129 | [focusedSpan release]; 130 | [super dealloc]; 131 | } 132 | @end 133 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHScrollbarHighlighter.m: -------------------------------------------------------------------------------- 1 | #import "DHScrollbarHighlighter.h" 2 | #import "DHMatchedText.h" 3 | 4 | @implementation DHScrollbarHighlighter 5 | 6 | @synthesize parentView; 7 | @synthesize matches; 8 | @synthesize highlightRects; 9 | 10 | + (DHScrollbarHighlighter *)highlighterWithWebView:(WebView *)aWebView andMatches:(NSArray *)someMatches 11 | { 12 | return [[[self alloc] initWithWebView:aWebView andMatches:someMatches] autorelease]; 13 | } 14 | 15 | - (id)initWithWebView:(WebView *)aWebView andMatches:(NSArray *)someMatches 16 | { 17 | NSScrollView *scrollView = [[[[aWebView mainFrame] frameView] documentView] enclosingScrollView]; 18 | if(scrollView && scrollView.verticalScroller && scrollView.verticalScroller.frame.origin.x > 0 && !scrollView.verticalScroller.isHidden) 19 | { 20 | NSRect scrollerFrame = [scrollView verticalScroller].frame; 21 | NSRect horizontalRect = ([scrollView horizontalScroller].frame.origin.x >= 0 && !scrollView.horizontalScroller.isHidden) ? [scrollView horizontalScroller].frame : NSZeroRect; 22 | NSRect knobRect = [[scrollView verticalScroller] rectForPart:NSScrollerKnobSlot]; 23 | self = [super initWithFrame:NSMakeRect(scrollerFrame.origin.x+knobRect.origin.x, scrollerFrame.origin.y+horizontalRect.size.height, knobRect.size.width, scrollerFrame.size.height)]; 24 | if(self) 25 | { 26 | self.parentView = aWebView; 27 | self.matches = someMatches; 28 | self.highlightRects = [NSMutableArray array]; 29 | [self setAutoresizingMask:NSViewMinXMargin | NSViewHeightSizable]; 30 | [self calculatePositions]; 31 | [self setWantsLayer:YES]; 32 | [parentView addSubview:self]; 33 | } 34 | return self; 35 | } 36 | [self release]; 37 | return nil; 38 | } 39 | 40 | - (void)calculatePositions 41 | { 42 | float documentHeight = [[[[parentView mainFrame] frameView] documentView] bounds].size.height; 43 | float ownHeight = self.frame.size.height; 44 | float ownWidth = self.frame.size.width; 45 | NSMutableArray *rects = [NSMutableArray array]; 46 | for(DHMatchedText *matchedText in matches) 47 | { 48 | DOMHTMLElement *wrapperSpan = (DOMHTMLElement*)[matchedText highlightedSpan]; 49 | int top = [self topPositionForElement:wrapperSpan]; 50 | float flippedY = top / documentHeight * ownHeight; 51 | float actualY = lroundf(ownHeight-flippedY); 52 | actualY = (actualY <= 2) ? 2 : (actualY > ownHeight - 3) ? ownHeight-3 : actualY; 53 | [rects addObject:[NSValue valueWithRect:NSMakeRect(0, actualY-1, ownWidth, 2)]]; 54 | } 55 | for(NSValue *rectValue in rects) 56 | { 57 | NSRect rect = [rectValue rectValue]; 58 | BOOL didIntersect = NO; 59 | for(NSValue *otherRectValue in highlightRects) 60 | { 61 | NSRect otherRect = [otherRectValue rectValue]; 62 | if(NSIntersectsRect(rect, otherRect)) 63 | { 64 | didIntersect = YES; 65 | break; 66 | } 67 | } 68 | if(!didIntersect) 69 | { 70 | [highlightRects addObject:rectValue]; 71 | } 72 | } 73 | } 74 | 75 | - (void)drawRect:(NSRect)dirtyRect 76 | { 77 | NSColor *inner = [NSColor colorWithCalibratedRed:1.0000f green:0.8667f blue:0.0000f alpha:1.0000f]; 78 | for(NSValue *highlightRect in highlightRects) 79 | { 80 | NSRect rect = [highlightRect rectValue]; 81 | [inner set]; 82 | NSRectFill(rect); 83 | } 84 | } 85 | 86 | - (int)topPositionForElement:(DOMHTMLElement *)element 87 | { 88 | int top = 0; 89 | DOMHTMLElement *o = element; 90 | DOMElement *offsetParent = o.offsetParent; 91 | DOMElement *el = o; 92 | while(el.parentNode) 93 | { 94 | el = (DOMElement*)el.parentNode; 95 | if([el respondsToSelector:@selector(offsetParent)] && el.offsetParent) 96 | { 97 | top -= el.scrollTop; 98 | } 99 | if(el == offsetParent) 100 | { 101 | top += o.offsetTop; 102 | if(el.clientTop && ![el.nodeName isCaseInsensitiveLike:@"TABLE"]) 103 | { 104 | top += el.clientTop; 105 | } 106 | o = (DOMHTMLElement*)el; 107 | if([o respondsToSelector:@selector(offsetParent)]) 108 | { 109 | if(!o.offsetParent) 110 | { 111 | if(o.offsetTop) 112 | { 113 | top += o.offsetTop; 114 | } 115 | } 116 | offsetParent = o.offsetParent; 117 | } 118 | else 119 | { 120 | offsetParent = nil; 121 | } 122 | } 123 | } 124 | return top; 125 | } 126 | 127 | - (void)dealloc 128 | { 129 | [parentView release]; 130 | [matches release]; 131 | [highlightRects release]; 132 | [super dealloc]; 133 | } 134 | 135 | @end 136 | -------------------------------------------------------------------------------- /HighlightedWebView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1A7E9B4D1F61B6BA00098FC0 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A7E9B4C1F61B6BA00098FC0 /* QuartzCore.framework */; }; 11 | 1A7E9B571F61BF0500098FC0 /* DHHighlightedWebView.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7E9B551F61BF0500098FC0 /* DHHighlightedWebView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 1A7E9B5A1F61BF0500098FC0 /* DHHighlightedWebView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A7E9B531F61BF0500098FC0 /* DHHighlightedWebView.framework */; }; 13 | 1A7E9B5B1F61BF0500098FC0 /* DHHighlightedWebView.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1A7E9B531F61BF0500098FC0 /* DHHighlightedWebView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 1A7E9B5F1F61BF2000098FC0 /* DHWebView.h in Headers */ = {isa = PBXBuildFile; fileRef = 43F7F39E1554F77000879D86 /* DHWebView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 15 | 1A7E9B601F61BF2000098FC0 /* DHWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F7F39F1554F77000879D86 /* DHWebView.m */; }; 16 | 1A7E9B611F61BF2000098FC0 /* DHScrollbarHighlighter.h in Headers */ = {isa = PBXBuildFile; fileRef = 433DA6B2155A95340023C7D7 /* DHScrollbarHighlighter.h */; }; 17 | 1A7E9B621F61BF2000098FC0 /* DHScrollbarHighlighter.m in Sources */ = {isa = PBXBuildFile; fileRef = 433DA6B3155A95340023C7D7 /* DHScrollbarHighlighter.m */; }; 18 | 1A7E9B631F61BF2000098FC0 /* DHSearchQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = 43F7F3AE155508BB00879D86 /* DHSearchQuery.h */; }; 19 | 1A7E9B641F61BF2000098FC0 /* DHSearchQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F7F3AF155508BB00879D86 /* DHSearchQuery.m */; }; 20 | 1A7E9B651F61BF2000098FC0 /* DHMatchedText.h in Headers */ = {isa = PBXBuildFile; fileRef = 43F7F3B8155566D700879D86 /* DHMatchedText.h */; }; 21 | 1A7E9B661F61BF2000098FC0 /* DHMatchedText.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F7F3B9155566D700879D86 /* DHMatchedText.m */; }; 22 | 43F1FF7D1558185F0043B796 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F1FF7C1558185F0043B796 /* WebKit.framework */; }; 23 | 43F7F3821554F75D00879D86 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F7F3811554F75D00879D86 /* Cocoa.framework */; }; 24 | 43F7F38C1554F75D00879D86 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 43F7F38A1554F75D00879D86 /* InfoPlist.strings */; }; 25 | 43F7F38E1554F75D00879D86 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F7F38D1554F75D00879D86 /* main.m */; }; 26 | 43F7F3921554F75D00879D86 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 43F7F3901554F75D00879D86 /* Credits.rtf */; }; 27 | 43F7F3951554F75D00879D86 /* DHAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F7F3941554F75D00879D86 /* DHAppDelegate.m */; }; 28 | 43F7F3981554F75D00879D86 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 43F7F3961554F75D00879D86 /* MainMenu.xib */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | 1A7E9B581F61BF0500098FC0 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 43F7F3741554F75D00879D86 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = 1A7E9B521F61BF0500098FC0; 37 | remoteInfo = DHHighlightedWebView; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXCopyFilesBuildPhase section */ 42 | 43D497F1155730A000F64F00 /* CopyFiles */ = { 43 | isa = PBXCopyFilesBuildPhase; 44 | buildActionMask = 2147483647; 45 | dstPath = ""; 46 | dstSubfolderSpec = 10; 47 | files = ( 48 | 1A7E9B5B1F61BF0500098FC0 /* DHHighlightedWebView.framework in CopyFiles */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXCopyFilesBuildPhase section */ 53 | 54 | /* Begin PBXFileReference section */ 55 | 1A7E9B4C1F61B6BA00098FC0 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 56 | 1A7E9B531F61BF0500098FC0 /* DHHighlightedWebView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DHHighlightedWebView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 1A7E9B551F61BF0500098FC0 /* DHHighlightedWebView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DHHighlightedWebView.h; sourceTree = ""; }; 58 | 1A7E9B561F61BF0500098FC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | 433DA6B2155A95340023C7D7 /* DHScrollbarHighlighter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHScrollbarHighlighter.h; sourceTree = ""; }; 60 | 433DA6B3155A95340023C7D7 /* DHScrollbarHighlighter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHScrollbarHighlighter.m; sourceTree = ""; }; 61 | 43F1FF7C1558185F0043B796 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 62 | 43F7F37D1554F75D00879D86 /* HighlightedWebView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HighlightedWebView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | 43F7F3811554F75D00879D86 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 64 | 43F7F3841554F75D00879D86 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 65 | 43F7F3851554F75D00879D86 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 66 | 43F7F3861554F75D00879D86 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 67 | 43F7F3891554F75D00879D86 /* HighlightedWebView-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "HighlightedWebView-Info.plist"; sourceTree = ""; }; 68 | 43F7F38B1554F75D00879D86 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 69 | 43F7F38D1554F75D00879D86 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 70 | 43F7F38F1554F75D00879D86 /* HighlightedWebView-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "HighlightedWebView-Prefix.pch"; sourceTree = ""; }; 71 | 43F7F3911554F75D00879D86 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 72 | 43F7F3931554F75D00879D86 /* DHAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DHAppDelegate.h; sourceTree = ""; }; 73 | 43F7F3941554F75D00879D86 /* DHAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DHAppDelegate.m; sourceTree = ""; }; 74 | 43F7F3971554F75D00879D86 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 75 | 43F7F39E1554F77000879D86 /* DHWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHWebView.h; sourceTree = ""; }; 76 | 43F7F39F1554F77000879D86 /* DHWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHWebView.m; sourceTree = ""; }; 77 | 43F7F3AE155508BB00879D86 /* DHSearchQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHSearchQuery.h; sourceTree = ""; }; 78 | 43F7F3AF155508BB00879D86 /* DHSearchQuery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHSearchQuery.m; sourceTree = ""; }; 79 | 43F7F3B8155566D700879D86 /* DHMatchedText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHMatchedText.h; sourceTree = ""; }; 80 | 43F7F3B9155566D700879D86 /* DHMatchedText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHMatchedText.m; sourceTree = ""; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | 1A7E9B4F1F61BF0500098FC0 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 43F7F37A1554F75D00879D86 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | 1A7E9B5A1F61BF0500098FC0 /* DHHighlightedWebView.framework in Frameworks */, 96 | 1A7E9B4D1F61B6BA00098FC0 /* QuartzCore.framework in Frameworks */, 97 | 43F1FF7D1558185F0043B796 /* WebKit.framework in Frameworks */, 98 | 43F7F3821554F75D00879D86 /* Cocoa.framework in Frameworks */, 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | /* End PBXFrameworksBuildPhase section */ 103 | 104 | /* Begin PBXGroup section */ 105 | 1A7E9B541F61BF0500098FC0 /* DHHighlightedWebView */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 43F7F39E1554F77000879D86 /* DHWebView.h */, 109 | 43F7F39F1554F77000879D86 /* DHWebView.m */, 110 | 433DA6B2155A95340023C7D7 /* DHScrollbarHighlighter.h */, 111 | 433DA6B3155A95340023C7D7 /* DHScrollbarHighlighter.m */, 112 | 43F7F3AE155508BB00879D86 /* DHSearchQuery.h */, 113 | 43F7F3AF155508BB00879D86 /* DHSearchQuery.m */, 114 | 43F7F3B8155566D700879D86 /* DHMatchedText.h */, 115 | 43F7F3B9155566D700879D86 /* DHMatchedText.m */, 116 | 1A7E9B551F61BF0500098FC0 /* DHHighlightedWebView.h */, 117 | 1A7E9B561F61BF0500098FC0 /* Info.plist */, 118 | ); 119 | path = DHHighlightedWebView; 120 | sourceTree = ""; 121 | }; 122 | 43F7F3721554F75D00879D86 = { 123 | isa = PBXGroup; 124 | children = ( 125 | 43F7F3871554F75D00879D86 /* HighlightedWebView */, 126 | 1A7E9B541F61BF0500098FC0 /* DHHighlightedWebView */, 127 | 43F7F3801554F75D00879D86 /* Frameworks */, 128 | 43F7F37E1554F75D00879D86 /* Products */, 129 | ); 130 | sourceTree = ""; 131 | }; 132 | 43F7F37E1554F75D00879D86 /* Products */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 43F7F37D1554F75D00879D86 /* HighlightedWebView.app */, 136 | 1A7E9B531F61BF0500098FC0 /* DHHighlightedWebView.framework */, 137 | ); 138 | name = Products; 139 | sourceTree = ""; 140 | }; 141 | 43F7F3801554F75D00879D86 /* Frameworks */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 1A7E9B4C1F61B6BA00098FC0 /* QuartzCore.framework */, 145 | 43F7F3811554F75D00879D86 /* Cocoa.framework */, 146 | 43F7F3831554F75D00879D86 /* Other Frameworks */, 147 | ); 148 | name = Frameworks; 149 | sourceTree = ""; 150 | }; 151 | 43F7F3831554F75D00879D86 /* Other Frameworks */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 43F1FF7C1558185F0043B796 /* WebKit.framework */, 155 | 43F7F3841554F75D00879D86 /* AppKit.framework */, 156 | 43F7F3851554F75D00879D86 /* CoreData.framework */, 157 | 43F7F3861554F75D00879D86 /* Foundation.framework */, 158 | ); 159 | name = "Other Frameworks"; 160 | sourceTree = ""; 161 | }; 162 | 43F7F3871554F75D00879D86 /* HighlightedWebView */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 43F7F3931554F75D00879D86 /* DHAppDelegate.h */, 166 | 43F7F3941554F75D00879D86 /* DHAppDelegate.m */, 167 | 43F7F3961554F75D00879D86 /* MainMenu.xib */, 168 | 43F7F3881554F75D00879D86 /* Supporting Files */, 169 | ); 170 | path = HighlightedWebView; 171 | sourceTree = ""; 172 | }; 173 | 43F7F3881554F75D00879D86 /* Supporting Files */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | 43F7F3891554F75D00879D86 /* HighlightedWebView-Info.plist */, 177 | 43F7F38A1554F75D00879D86 /* InfoPlist.strings */, 178 | 43F7F38D1554F75D00879D86 /* main.m */, 179 | 43F7F38F1554F75D00879D86 /* HighlightedWebView-Prefix.pch */, 180 | 43F7F3901554F75D00879D86 /* Credits.rtf */, 181 | ); 182 | name = "Supporting Files"; 183 | sourceTree = ""; 184 | }; 185 | /* End PBXGroup section */ 186 | 187 | /* Begin PBXHeadersBuildPhase section */ 188 | 1A7E9B501F61BF0500098FC0 /* Headers */ = { 189 | isa = PBXHeadersBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | 1A7E9B5F1F61BF2000098FC0 /* DHWebView.h in Headers */, 193 | 1A7E9B651F61BF2000098FC0 /* DHMatchedText.h in Headers */, 194 | 1A7E9B631F61BF2000098FC0 /* DHSearchQuery.h in Headers */, 195 | 1A7E9B571F61BF0500098FC0 /* DHHighlightedWebView.h in Headers */, 196 | 1A7E9B611F61BF2000098FC0 /* DHScrollbarHighlighter.h in Headers */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXHeadersBuildPhase section */ 201 | 202 | /* Begin PBXNativeTarget section */ 203 | 1A7E9B521F61BF0500098FC0 /* DHHighlightedWebView */ = { 204 | isa = PBXNativeTarget; 205 | buildConfigurationList = 1A7E9B5E1F61BF0500098FC0 /* Build configuration list for PBXNativeTarget "DHHighlightedWebView" */; 206 | buildPhases = ( 207 | 1A7E9B4E1F61BF0500098FC0 /* Sources */, 208 | 1A7E9B4F1F61BF0500098FC0 /* Frameworks */, 209 | 1A7E9B501F61BF0500098FC0 /* Headers */, 210 | 1A7E9B511F61BF0500098FC0 /* Resources */, 211 | ); 212 | buildRules = ( 213 | ); 214 | dependencies = ( 215 | ); 216 | name = DHHighlightedWebView; 217 | productName = DHHighlightedWebView; 218 | productReference = 1A7E9B531F61BF0500098FC0 /* DHHighlightedWebView.framework */; 219 | productType = "com.apple.product-type.framework"; 220 | }; 221 | 43F7F37C1554F75D00879D86 /* HighlightedWebView */ = { 222 | isa = PBXNativeTarget; 223 | buildConfigurationList = 43F7F39B1554F75D00879D86 /* Build configuration list for PBXNativeTarget "HighlightedWebView" */; 224 | buildPhases = ( 225 | 43F7F3791554F75D00879D86 /* Sources */, 226 | 43F7F37A1554F75D00879D86 /* Frameworks */, 227 | 43F7F37B1554F75D00879D86 /* Resources */, 228 | 43D497F1155730A000F64F00 /* CopyFiles */, 229 | ); 230 | buildRules = ( 231 | ); 232 | dependencies = ( 233 | 1A7E9B591F61BF0500098FC0 /* PBXTargetDependency */, 234 | ); 235 | name = HighlightedWebView; 236 | productName = HighlightedWebView; 237 | productReference = 43F7F37D1554F75D00879D86 /* HighlightedWebView.app */; 238 | productType = "com.apple.product-type.application"; 239 | }; 240 | /* End PBXNativeTarget section */ 241 | 242 | /* Begin PBXProject section */ 243 | 43F7F3741554F75D00879D86 /* Project object */ = { 244 | isa = PBXProject; 245 | attributes = { 246 | CLASSPREFIX = DH; 247 | LastUpgradeCheck = 0430; 248 | TargetAttributes = { 249 | 1A7E9B521F61BF0500098FC0 = { 250 | CreatedOnToolsVersion = 8.3.3; 251 | ProvisioningStyle = Automatic; 252 | }; 253 | }; 254 | }; 255 | buildConfigurationList = 43F7F3771554F75D00879D86 /* Build configuration list for PBXProject "HighlightedWebView" */; 256 | compatibilityVersion = "Xcode 3.2"; 257 | developmentRegion = English; 258 | hasScannedForEncodings = 0; 259 | knownRegions = ( 260 | en, 261 | ); 262 | mainGroup = 43F7F3721554F75D00879D86; 263 | productRefGroup = 43F7F37E1554F75D00879D86 /* Products */; 264 | projectDirPath = ""; 265 | projectRoot = ""; 266 | targets = ( 267 | 43F7F37C1554F75D00879D86 /* HighlightedWebView */, 268 | 1A7E9B521F61BF0500098FC0 /* DHHighlightedWebView */, 269 | ); 270 | }; 271 | /* End PBXProject section */ 272 | 273 | /* Begin PBXResourcesBuildPhase section */ 274 | 1A7E9B511F61BF0500098FC0 /* Resources */ = { 275 | isa = PBXResourcesBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | 43F7F37B1554F75D00879D86 /* Resources */ = { 282 | isa = PBXResourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | 43F7F38C1554F75D00879D86 /* InfoPlist.strings in Resources */, 286 | 43F7F3921554F75D00879D86 /* Credits.rtf in Resources */, 287 | 43F7F3981554F75D00879D86 /* MainMenu.xib in Resources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | /* End PBXResourcesBuildPhase section */ 292 | 293 | /* Begin PBXSourcesBuildPhase section */ 294 | 1A7E9B4E1F61BF0500098FC0 /* Sources */ = { 295 | isa = PBXSourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | 1A7E9B621F61BF2000098FC0 /* DHScrollbarHighlighter.m in Sources */, 299 | 1A7E9B661F61BF2000098FC0 /* DHMatchedText.m in Sources */, 300 | 1A7E9B601F61BF2000098FC0 /* DHWebView.m in Sources */, 301 | 1A7E9B641F61BF2000098FC0 /* DHSearchQuery.m in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | 43F7F3791554F75D00879D86 /* Sources */ = { 306 | isa = PBXSourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | 43F7F38E1554F75D00879D86 /* main.m in Sources */, 310 | 43F7F3951554F75D00879D86 /* DHAppDelegate.m in Sources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXSourcesBuildPhase section */ 315 | 316 | /* Begin PBXTargetDependency section */ 317 | 1A7E9B591F61BF0500098FC0 /* PBXTargetDependency */ = { 318 | isa = PBXTargetDependency; 319 | target = 1A7E9B521F61BF0500098FC0 /* DHHighlightedWebView */; 320 | targetProxy = 1A7E9B581F61BF0500098FC0 /* PBXContainerItemProxy */; 321 | }; 322 | /* End PBXTargetDependency section */ 323 | 324 | /* Begin PBXVariantGroup section */ 325 | 43F7F38A1554F75D00879D86 /* InfoPlist.strings */ = { 326 | isa = PBXVariantGroup; 327 | children = ( 328 | 43F7F38B1554F75D00879D86 /* en */, 329 | ); 330 | name = InfoPlist.strings; 331 | sourceTree = ""; 332 | }; 333 | 43F7F3901554F75D00879D86 /* Credits.rtf */ = { 334 | isa = PBXVariantGroup; 335 | children = ( 336 | 43F7F3911554F75D00879D86 /* en */, 337 | ); 338 | name = Credits.rtf; 339 | sourceTree = ""; 340 | }; 341 | 43F7F3961554F75D00879D86 /* MainMenu.xib */ = { 342 | isa = PBXVariantGroup; 343 | children = ( 344 | 43F7F3971554F75D00879D86 /* en */, 345 | ); 346 | name = MainMenu.xib; 347 | sourceTree = ""; 348 | }; 349 | /* End PBXVariantGroup section */ 350 | 351 | /* Begin XCBuildConfiguration section */ 352 | 1A7E9B5C1F61BF0500098FC0 /* Debug */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | CLANG_ANALYZER_NONNULL = YES; 356 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 357 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 358 | CLANG_CXX_LIBRARY = "libc++"; 359 | CLANG_ENABLE_MODULES = YES; 360 | CLANG_ENABLE_OBJC_ARC = NO; 361 | CLANG_WARN_BOOL_CONVERSION = YES; 362 | CLANG_WARN_CONSTANT_CONVERSION = YES; 363 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 364 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 365 | CLANG_WARN_EMPTY_BODY = YES; 366 | CLANG_WARN_ENUM_CONVERSION = YES; 367 | CLANG_WARN_INFINITE_RECURSION = YES; 368 | CLANG_WARN_INT_CONVERSION = YES; 369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 370 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 371 | CLANG_WARN_UNREACHABLE_CODE = YES; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | CODE_SIGN_IDENTITY = "-"; 374 | COMBINE_HIDPI_IMAGES = YES; 375 | CURRENT_PROJECT_VERSION = 1; 376 | DEBUG_INFORMATION_FORMAT = dwarf; 377 | DEFINES_MODULE = YES; 378 | DYLIB_COMPATIBILITY_VERSION = 1; 379 | DYLIB_CURRENT_VERSION = 1; 380 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 381 | ENABLE_STRICT_OBJC_MSGSEND = YES; 382 | ENABLE_TESTABILITY = YES; 383 | FRAMEWORK_VERSION = A; 384 | GCC_NO_COMMON_BLOCKS = YES; 385 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 386 | GCC_WARN_UNDECLARED_SELECTOR = YES; 387 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 388 | GCC_WARN_UNUSED_FUNCTION = YES; 389 | INFOPLIST_FILE = DHHighlightedWebView/Info.plist; 390 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 391 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 392 | MACOSX_DEPLOYMENT_TARGET = 10.12; 393 | MTL_ENABLE_DEBUG_INFO = YES; 394 | PRODUCT_BUNDLE_IDENTIFIER = dh.DHHighlightedWebView; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | SKIP_INSTALL = YES; 397 | VERSIONING_SYSTEM = "apple-generic"; 398 | VERSION_INFO_PREFIX = ""; 399 | }; 400 | name = Debug; 401 | }; 402 | 1A7E9B5D1F61BF0500098FC0 /* Release */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | CLANG_ANALYZER_NONNULL = YES; 406 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 407 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 408 | CLANG_CXX_LIBRARY = "libc++"; 409 | CLANG_ENABLE_MODULES = YES; 410 | CLANG_ENABLE_OBJC_ARC = NO; 411 | CLANG_WARN_BOOL_CONVERSION = YES; 412 | CLANG_WARN_CONSTANT_CONVERSION = YES; 413 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 414 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 415 | CLANG_WARN_EMPTY_BODY = YES; 416 | CLANG_WARN_ENUM_CONVERSION = YES; 417 | CLANG_WARN_INFINITE_RECURSION = YES; 418 | CLANG_WARN_INT_CONVERSION = YES; 419 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | CODE_SIGN_IDENTITY = "-"; 424 | COMBINE_HIDPI_IMAGES = YES; 425 | COPY_PHASE_STRIP = NO; 426 | CURRENT_PROJECT_VERSION = 1; 427 | DEFINES_MODULE = YES; 428 | DYLIB_COMPATIBILITY_VERSION = 1; 429 | DYLIB_CURRENT_VERSION = 1; 430 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 431 | ENABLE_NS_ASSERTIONS = NO; 432 | ENABLE_STRICT_OBJC_MSGSEND = YES; 433 | FRAMEWORK_VERSION = A; 434 | GCC_NO_COMMON_BLOCKS = YES; 435 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 436 | GCC_WARN_UNDECLARED_SELECTOR = YES; 437 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 438 | GCC_WARN_UNUSED_FUNCTION = YES; 439 | INFOPLIST_FILE = DHHighlightedWebView/Info.plist; 440 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 441 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 442 | MACOSX_DEPLOYMENT_TARGET = 10.12; 443 | MTL_ENABLE_DEBUG_INFO = NO; 444 | PRODUCT_BUNDLE_IDENTIFIER = dh.DHHighlightedWebView; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | SKIP_INSTALL = YES; 447 | VERSIONING_SYSTEM = "apple-generic"; 448 | VERSION_INFO_PREFIX = ""; 449 | }; 450 | name = Release; 451 | }; 452 | 43F7F3991554F75D00879D86 /* Debug */ = { 453 | isa = XCBuildConfiguration; 454 | buildSettings = { 455 | ALWAYS_SEARCH_USER_PATHS = NO; 456 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 457 | COPY_PHASE_STRIP = NO; 458 | GCC_C_LANGUAGE_STANDARD = gnu99; 459 | GCC_DYNAMIC_NO_PIC = NO; 460 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 461 | GCC_OPTIMIZATION_LEVEL = 0; 462 | GCC_PREPROCESSOR_DEFINITIONS = ( 463 | "DEBUG=1", 464 | "$(inherited)", 465 | ); 466 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 467 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 468 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 469 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 470 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 471 | GCC_WARN_UNUSED_VARIABLE = YES; 472 | MACOSX_DEPLOYMENT_TARGET = 10.6; 473 | ONLY_ACTIVE_ARCH = YES; 474 | SDKROOT = macosx; 475 | }; 476 | name = Debug; 477 | }; 478 | 43F7F39A1554F75D00879D86 /* Release */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | ALWAYS_SEARCH_USER_PATHS = NO; 482 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 483 | COPY_PHASE_STRIP = YES; 484 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 485 | GCC_C_LANGUAGE_STANDARD = gnu99; 486 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 487 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 488 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 489 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 490 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 491 | GCC_WARN_UNUSED_VARIABLE = YES; 492 | MACOSX_DEPLOYMENT_TARGET = 10.6; 493 | SDKROOT = macosx; 494 | }; 495 | name = Release; 496 | }; 497 | 43F7F39C1554F75D00879D86 /* Debug */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | CODE_SIGN_IDENTITY = "-"; 501 | FRAMEWORK_SEARCH_PATHS = ( 502 | "$(inherited)", 503 | "\"$(SRCROOT)\"", 504 | ); 505 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 506 | GCC_PREFIX_HEADER = "HighlightedWebView/HighlightedWebView-Prefix.pch"; 507 | INFOPLIST_FILE = "HighlightedWebView/HighlightedWebView-Info.plist"; 508 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 509 | LIBRARY_SEARCH_PATHS = ( 510 | "$(inherited)", 511 | "\"$(SRCROOT)\"", 512 | ); 513 | PRODUCT_NAME = "$(TARGET_NAME)"; 514 | WRAPPER_EXTENSION = app; 515 | }; 516 | name = Debug; 517 | }; 518 | 43F7F39D1554F75D00879D86 /* Release */ = { 519 | isa = XCBuildConfiguration; 520 | buildSettings = { 521 | CODE_SIGN_IDENTITY = "-"; 522 | FRAMEWORK_SEARCH_PATHS = ( 523 | "$(inherited)", 524 | "\"$(SRCROOT)\"", 525 | ); 526 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 527 | GCC_PREFIX_HEADER = "HighlightedWebView/HighlightedWebView-Prefix.pch"; 528 | INFOPLIST_FILE = "HighlightedWebView/HighlightedWebView-Info.plist"; 529 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 530 | LIBRARY_SEARCH_PATHS = ( 531 | "$(inherited)", 532 | "\"$(SRCROOT)\"", 533 | ); 534 | PRODUCT_NAME = "$(TARGET_NAME)"; 535 | WRAPPER_EXTENSION = app; 536 | }; 537 | name = Release; 538 | }; 539 | /* End XCBuildConfiguration section */ 540 | 541 | /* Begin XCConfigurationList section */ 542 | 1A7E9B5E1F61BF0500098FC0 /* Build configuration list for PBXNativeTarget "DHHighlightedWebView" */ = { 543 | isa = XCConfigurationList; 544 | buildConfigurations = ( 545 | 1A7E9B5C1F61BF0500098FC0 /* Debug */, 546 | 1A7E9B5D1F61BF0500098FC0 /* Release */, 547 | ); 548 | defaultConfigurationIsVisible = 0; 549 | }; 550 | 43F7F3771554F75D00879D86 /* Build configuration list for PBXProject "HighlightedWebView" */ = { 551 | isa = XCConfigurationList; 552 | buildConfigurations = ( 553 | 43F7F3991554F75D00879D86 /* Debug */, 554 | 43F7F39A1554F75D00879D86 /* Release */, 555 | ); 556 | defaultConfigurationIsVisible = 0; 557 | defaultConfigurationName = Release; 558 | }; 559 | 43F7F39B1554F75D00879D86 /* Build configuration list for PBXNativeTarget "HighlightedWebView" */ = { 560 | isa = XCConfigurationList; 561 | buildConfigurations = ( 562 | 43F7F39C1554F75D00879D86 /* Debug */, 563 | 43F7F39D1554F75D00879D86 /* Release */, 564 | ); 565 | defaultConfigurationIsVisible = 0; 566 | defaultConfigurationName = Release; 567 | }; 568 | /* End XCConfigurationList section */ 569 | }; 570 | rootObject = 43F7F3741554F75D00879D86 /* Project object */; 571 | } 572 | -------------------------------------------------------------------------------- /DHHighlightedWebView/DHWebView.m: -------------------------------------------------------------------------------- 1 | #import "DHWebView.h" 2 | #import "DHMatchedText.h" 3 | #import "DHSearchQuery.h" 4 | #import "DHScrollbarHighlighter.h" 5 | 6 | #import 7 | 8 | @interface DHWebView () { 9 | NSTimer *workerTimer; 10 | DHSearchQuery *currentQuery; 11 | NSMutableArray *highlightedMatches; 12 | NSMutableArray *matchedTexts; 13 | NSMutableString *entirePageContent; 14 | DHScrollbarHighlighter *scrollHighlighter; 15 | } 16 | 17 | @property (retain) NSTimer *workerTimer; 18 | @property (retain) DHSearchQuery *currentQuery; 19 | @property (retain) DHSearchQuery *focusQuery; 20 | @property (retain) NSMutableArray *highlightedMatches; 21 | @property (retain) NSMutableArray *matchedTexts; 22 | @property (retain) NSMutableString *entirePageContent; 23 | @property (retain) DHScrollbarHighlighter *scrollHighlighter; 24 | 25 | - (void)highlightQuery:(DHSearchQuery *)query; 26 | - (void)startClearingHighlights; 27 | - (void)clearHighlights; 28 | - (void)traverseNodes:(NSMutableArray *)nodes; 29 | - (void)highlightMatches; 30 | - (void)timeredHighlightOfMatches:(NSMutableArray *)matches; 31 | - (void)invalidateTimers; 32 | - (NSString *)normalizeWhitespaces:(NSString *)aString; 33 | - (void)selectRangeUsingEncodedDictionary:(NSMutableDictionary *)dictionary; 34 | - (void)clearSelection; 35 | - (void)tryToGuessSelection:(NSDictionary *)fromDict; 36 | - (void)didStartProvisionalLoad; 37 | 38 | @end 39 | 40 | const CFTimeInterval MaxWorkBudgetTime = 0.015; 41 | 42 | // Perform work within WorkBudget. 43 | // workUnit returns NO if there's more work to be done, YES if finished. 44 | // If it takes longer than MaxWorkBudgetTime to finish, continuation will be called to configure a timer to finish the work later. 45 | static void BudgetWork(BOOL (^workUnit)(), void (^continuation)()) { 46 | double start = CACurrentMediaTime(), stop; 47 | BOOL finished; 48 | do { 49 | finished = workUnit(); 50 | stop = CACurrentMediaTime(); 51 | } while (!finished && (stop - start) < MaxWorkBudgetTime); 52 | if (!finished) { 53 | continuation(); 54 | } 55 | } 56 | 57 | @implementation DHWebView 58 | 59 | @synthesize focusQuery; 60 | @synthesize currentQuery; 61 | @synthesize workerTimer; 62 | @synthesize highlightedMatches; 63 | @synthesize matchedTexts; 64 | @synthesize entirePageContent; 65 | @synthesize scrollHighlighter; 66 | 67 | - (BOOL)searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag 68 | { 69 | if(!string.length) 70 | { 71 | self.currentQuery = nil; 72 | self.focusQuery = nil; 73 | [self startClearingHighlights]; 74 | return NO; 75 | } 76 | DHSearchQuery *query = [DHSearchQuery searchQueryWithQuery:string caseSensitive:caseFlag]; 77 | 78 | [self highlightQuery:query]; 79 | 80 | if (highlightedMatches.count == 0) { 81 | return NO; // couldn't find anything 82 | } 83 | 84 | BOOL didFocus = NO; 85 | BOOL focusNext = NO; 86 | 87 | NSEnumerator *e = forward ? highlightedMatches.objectEnumerator : highlightedMatches.reverseObjectEnumerator; 88 | 89 | for (DHMatchedText *highlighted in e) { 90 | if (didFocus && highlighted.focusedRangeIndex != NSNotFound) { 91 | highlighted.focusedRangeIndex = NSNotFound; 92 | } 93 | if (focusNext && highlighted.foundRanges.count) { 94 | highlighted.focusedRangeIndex = forward ? 0 : highlighted.foundRanges.count - 1; 95 | focusNext = NO; 96 | didFocus = YES; 97 | } else if (highlighted.focusedRangeIndex != NSNotFound) { 98 | if (forward && highlighted.focusedRangeIndex + 1 < highlighted.foundRanges.count) { 99 | highlighted.focusedRangeIndex += 1; 100 | didFocus = YES; 101 | } else if (!forward && highlighted.focusedRangeIndex > 0) { 102 | highlighted.focusedRangeIndex -= 1; 103 | didFocus = YES; 104 | } else { 105 | highlighted.focusedRangeIndex = NSNotFound; 106 | focusNext = YES; 107 | } 108 | } 109 | } 110 | 111 | if (!didFocus && (!focusNext || wrapFlag)) { 112 | [[highlightedMatches objectAtIndex:0] setFocusedRangeIndex:0]; 113 | } 114 | 115 | return YES; 116 | } 117 | 118 | - (void)highlightQuery:(NSString *)aQuery caseSensitive:(BOOL)isCaseSensitive 119 | { 120 | DHSearchQuery *query = [DHSearchQuery searchQueryWithQuery:aQuery caseSensitive:isCaseSensitive]; 121 | [self highlightQuery:query]; 122 | } 123 | 124 | - (void)highlightQuery:(DHSearchQuery *)query 125 | { 126 | if([currentQuery isEqualTo:query]) 127 | { 128 | return; 129 | } 130 | self.currentQuery = query; 131 | [self startClearingHighlights]; 132 | } 133 | 134 | - (void)startClearingHighlights 135 | { 136 | [self.scrollHighlighter removeFromSuperview]; 137 | self.scrollHighlighter = nil; 138 | [self invalidateTimers]; 139 | [self clearHighlights]; 140 | } 141 | 142 | - (void)clearHighlights 143 | { 144 | DOMRange *range = [self selectedDOMRange]; 145 | BudgetWork(^{ 146 | if(!highlightedMatches.count) 147 | { 148 | self.highlightedMatches = [NSMutableArray array]; 149 | self.matchedTexts = [NSMutableArray array]; 150 | self.entirePageContent = [NSMutableString string]; 151 | if(!currentQuery.query.length) 152 | { 153 | return YES; 154 | } 155 | DOMDocument *document = [self mainFrameDocument]; 156 | DOMHTMLElement *body = [document body]; 157 | if(!body) 158 | { 159 | return YES; 160 | } 161 | [self tryToGuessSelection:currentQuery.selectionAfterClear]; 162 | [self traverseNodes:[NSMutableArray arrayWithObject:body]]; 163 | return YES; 164 | } 165 | DHMatchedText *match = [highlightedMatches objectAtIndex:0]; 166 | [match retain]; 167 | [highlightedMatches removeObjectAtIndex:0]; 168 | if(![currentQuery.selectionAfterClear objectForKey:@"startContainer"] || ![currentQuery.selectionAfterClear objectForKey:@"endContainer"]) 169 | { 170 | DOMNode *expectedStart = [currentQuery.selectionAfterClear objectForKey:@"expectedStart"]; 171 | DOMNode *expectedEnd = [currentQuery.selectionAfterClear objectForKey:@"expectedEnd"]; 172 | if(range || expectedStart || expectedEnd) 173 | { 174 | if(range) 175 | { 176 | [currentQuery.selectionAfterClear setObject:[range startContainer] forKey:@"expectedStart"]; 177 | [currentQuery.selectionAfterClear setObject:[NSNumber numberWithInt:[range startOffset]] forKey:@"expectedStartOffset"]; 178 | [currentQuery.selectionAfterClear setObject:[range endContainer] forKey:@"expectedEnd"]; 179 | [currentQuery.selectionAfterClear setObject:[NSNumber numberWithInt:[range endOffset]] forKey:@"expectedEndOffset"]; 180 | } 181 | int rangeStartOffset = (!range) ? [[currentQuery.selectionAfterClear objectForKey:@"expectedStartOffset"] intValue] : [range startOffset]; 182 | int rangeEndOffset = (!range) ? [[currentQuery.selectionAfterClear objectForKey:@"expectedEndOffset"] intValue] : [range endOffset]; 183 | DOMNode *startContainer = (range) ? [range startContainer] : expectedStart; 184 | DOMNode *endContainer = (range) ? [range endContainer] : expectedEnd; 185 | int startOffset = -1; 186 | int endOffset = -1; 187 | int offset = 0; 188 | for(int i = 0; i < match.highlightedSpan.childNodes.length; i++) 189 | { 190 | DOMNode *child = [match.highlightedSpan.childNodes item:i]; 191 | if(child.nodeType == DOM_ELEMENT_NODE) 192 | { 193 | for(int j = 0; j < child.childNodes.length; j++) 194 | { 195 | DOMNode *anotherChild = [child.childNodes item:j]; 196 | if(anotherChild == startContainer) 197 | { 198 | [currentQuery.selectionAfterClear removeObjectForKey:@"expectedStart"]; 199 | startOffset = offset + rangeStartOffset; 200 | } 201 | if(anotherChild == endContainer) 202 | { 203 | [currentQuery.selectionAfterClear removeObjectForKey:@"expectedEnd"]; 204 | endOffset = offset + rangeEndOffset; 205 | } 206 | offset += anotherChild.nodeValue.length; 207 | } 208 | } 209 | else 210 | { 211 | if(child == startContainer) 212 | { 213 | [currentQuery.selectionAfterClear removeObjectForKey:@"expectedStart"]; 214 | startOffset = offset + rangeStartOffset; 215 | } 216 | if(child == endContainer) 217 | { 218 | [currentQuery.selectionAfterClear removeObjectForKey:@"expectedEnd"]; 219 | endOffset = offset + rangeEndOffset; 220 | } 221 | offset += child.nodeValue.length; 222 | } 223 | } 224 | [match clearHighlight]; 225 | if(startOffset != -1) 226 | { 227 | [currentQuery.selectionAfterClear setObject:match.text forKey:@"startContainer"]; 228 | [currentQuery.selectionAfterClear setObject:[NSNumber numberWithInt:startOffset] forKey:@"startOffset"]; 229 | } 230 | if(endOffset != -1) 231 | { 232 | [currentQuery.selectionAfterClear setObject:match.text forKey:@"endContainer"]; 233 | [currentQuery.selectionAfterClear setObject:[NSNumber numberWithInt:endOffset] forKey:@"endOffset"]; 234 | } 235 | if([currentQuery.selectionAfterClear objectForKey:@"startContainer"] && [currentQuery.selectionAfterClear objectForKey:@"endContainer"]) 236 | { 237 | @try { 238 | DOMRange *range = [[self mainFrameDocument] createRange]; 239 | [range setStart:[currentQuery.selectionAfterClear objectForKey:@"startContainer"] offset:[[currentQuery.selectionAfterClear objectForKey:@"startOffset"] intValue]]; 240 | [range setEnd:[currentQuery.selectionAfterClear objectForKey:@"endContainer"] offset:[[currentQuery.selectionAfterClear objectForKey:@"endOffset"] intValue]]; 241 | [self setSelectedDOMRange:range affinity:NSSelectionAffinityUpstream]; 242 | } 243 | @catch (NSException *exception) { 244 | } 245 | @finally { 246 | } 247 | } 248 | } 249 | else 250 | { 251 | [match clearHighlight]; 252 | } 253 | } 254 | else 255 | { 256 | [match clearHighlight]; 257 | } 258 | [match release]; 259 | 260 | return NO; // !finished 261 | }, ^{ 262 | self.workerTimer = [NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(clearHighlights) userInfo:nil repeats:NO]; 263 | }); 264 | } 265 | 266 | static BOOL shouldSearchDOMElement(DOMElement *e) { 267 | NSString *tagName = [e tagName]; 268 | if ([tagName isEqualToString:@"INPUT"] || [tagName isEqualToString:@"TEXTAREA"] || [tagName isEqualToString:@"STYLE"] || [tagName isEqualToString:@"HEAD"] || [tagName isEqualToString:@"SCRIPT"]) 269 | { 270 | return NO; 271 | } 272 | return YES; 273 | } 274 | 275 | - (void)traverseNodes:(NSMutableArray *)nodes 276 | { 277 | BudgetWork(^BOOL{ 278 | if(!nodes.count) 279 | { 280 | [self highlightMatches]; 281 | return YES; // all done 282 | } 283 | DOMNode *node = [nodes objectAtIndex:0]; 284 | [node retain]; 285 | [nodes removeObjectAtIndex:0]; 286 | if(node.nodeType == DOM_TEXT_NODE || node.nodeType == DOM_CDATA_SECTION_NODE) 287 | { 288 | DOMText *textNode = (DOMText *)node; 289 | 290 | NSString *content = [self normalizeWhitespaces:[textNode nodeValue]]; 291 | if(content.length) 292 | { 293 | DHMatchedText *matchedText = [DHMatchedText matchedTextWithDOMText:textNode andRange:NSMakeRange(entirePageContent.length, content.length)]; 294 | [entirePageContent appendString:content]; 295 | [matchedTexts addObject:matchedText]; 296 | } 297 | } 298 | if(node.nodeType == DOM_ELEMENT_NODE) 299 | { 300 | if (shouldSearchDOMElement((DOMElement *)node)) 301 | { 302 | DOMNodeList *childNodes = [node childNodes]; 303 | for(int i = 0; i < childNodes.length; i++) 304 | { 305 | [nodes insertObject:[childNodes item:i] atIndex:i]; 306 | } 307 | } 308 | } 309 | [node release]; 310 | return NO; // not finished yet 311 | }, ^{ 312 | self.workerTimer = [NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(traverseWithTimer:) userInfo:nodes repeats:NO]; 313 | }); 314 | } 315 | 316 | - (void)traverseWithTimer:(NSTimer *)timer 317 | { 318 | NSMutableArray *userInfo = [timer userInfo]; 319 | [self traverseNodes:userInfo]; 320 | } 321 | 322 | - (void)highlightMatches 323 | { 324 | NSMutableArray *foundRanges = [NSMutableArray array]; 325 | NSRange foundRange; 326 | NSInteger scanLocation = 0; 327 | do 328 | { 329 | NSStringCompareOptions options = ([currentQuery isCaseSensitive]) ? NSLiteralSearch : NSCaseInsensitiveSearch; 330 | foundRange = [entirePageContent rangeOfString:[currentQuery query] options:options range:NSMakeRange(scanLocation, entirePageContent.length-scanLocation)]; 331 | if(foundRange.location != NSNotFound) 332 | { 333 | scanLocation = foundRange.location+foundRange.length; 334 | [foundRanges addObject:[NSValue valueWithRange:foundRange]]; 335 | } 336 | } 337 | while (foundRange.location != NSNotFound); 338 | 339 | NSEnumerator *matchesEnumerator = [matchedTexts objectEnumerator]; 340 | DHMatchedText *currentMatch = [matchesEnumerator nextObject]; 341 | NSMutableSet *foundMatches = [NSMutableSet set]; 342 | for(NSValue *foundRange in foundRanges) 343 | { 344 | NSRange actualRange = [foundRange rangeValue]; 345 | do 346 | { 347 | NSRange intersectionRange = NSIntersectionRange([currentMatch effectiveRange], actualRange); 348 | if(intersectionRange.length > 0) 349 | { 350 | [foundMatches addObject:currentMatch]; 351 | [[currentMatch foundRanges] addObject:[NSValue valueWithRange:intersectionRange]]; 352 | if(intersectionRange.location+intersectionRange.length >= actualRange.location+actualRange.length) 353 | { 354 | break; 355 | } 356 | else 357 | { 358 | currentMatch = [matchesEnumerator nextObject]; 359 | } 360 | } 361 | else 362 | { 363 | currentMatch = [matchesEnumerator nextObject]; 364 | } 365 | } while (currentMatch); 366 | } 367 | if(foundMatches.count) 368 | { 369 | [self timeredHighlightOfMatches:[NSMutableArray arrayWithArray:[foundMatches allObjects]]]; 370 | } 371 | } 372 | 373 | - (void)timeredHighlightOfMatches:(NSMutableArray *)matches 374 | { 375 | DOMRange *range = [self selectedDOMRange]; 376 | if([matches isKindOfClass:[NSTimer class]]) 377 | { 378 | matches = [(NSTimer*)matches userInfo]; 379 | } 380 | 381 | BudgetWork(^BOOL{ 382 | if(!matches.count) 383 | { 384 | // sort all highlightedMatches so they're in the same order as they would be in an inorder traversal of the DOM 385 | [highlightedMatches sortUsingComparator:^NSComparisonResult(DHMatchedText *a, DHMatchedText *b) { 386 | NSRange ar = [a.foundRanges.firstObject rangeValue]; 387 | NSRange br = [b.foundRanges.firstObject rangeValue]; 388 | if (ar.location < br.location) { 389 | return NSOrderedAscending; 390 | } else if (ar.location > br.location) { 391 | return NSOrderedDescending; 392 | } else if (ar.length < br.length) { 393 | return NSOrderedAscending; 394 | } else if (ar.length > br.length) { 395 | return NSOrderedDescending; 396 | } else { 397 | return NSOrderedSame; 398 | } 399 | }]; 400 | 401 | [self tryToGuessSelection:currentQuery.selectionAfterHighlight]; 402 | self.scrollHighlighter = [DHScrollbarHighlighter highlighterWithWebView:self andMatches:highlightedMatches]; 403 | return YES; // all done 404 | } 405 | DHMatchedText *last = [matches lastObject]; 406 | [highlightedMatches addObject:last]; 407 | [matches removeLastObject]; 408 | 409 | if(![currentQuery.selectionAfterHighlight objectForKey:@"startContainer"] || ![currentQuery.selectionAfterHighlight objectForKey:@"endContainer"]) 410 | { 411 | DOMNode *expectedStart = [currentQuery.selectionAfterHighlight objectForKey:@"expectedStart"]; 412 | DOMNode *expectedEnd = [currentQuery.selectionAfterHighlight objectForKey:@"expectedEnd"]; 413 | if(range || expectedStart || expectedEnd) 414 | { 415 | if(range) 416 | { 417 | [currentQuery.selectionAfterHighlight setObject:[range startContainer] forKey:@"expectedStart"]; 418 | [currentQuery.selectionAfterHighlight setObject:[NSNumber numberWithInt:[range startOffset]] forKey:@"expectedStartOffset"]; 419 | [currentQuery.selectionAfterHighlight setObject:[range endContainer] forKey:@"expectedEnd"]; 420 | [currentQuery.selectionAfterHighlight setObject:[NSNumber numberWithInt:[range endOffset]] forKey:@"expectedEndOffset"]; 421 | } 422 | int startOffset = (!range) ? [[currentQuery.selectionAfterHighlight objectForKey:@"expectedStartOffset"] intValue] : [range startOffset]; 423 | int endOffset = (!range) ? [[currentQuery.selectionAfterHighlight objectForKey:@"expectedEndOffset"] intValue] : [range endOffset]; 424 | BOOL isStart = (range) ? [range startContainer] == last.text : expectedStart == last.text; 425 | BOOL isEnd = (range) ? [range endContainer] == last.text : expectedEnd == last.text; 426 | 427 | [last highlightDOMNode]; 428 | if(isStart) 429 | { 430 | [currentQuery.selectionAfterHighlight removeObjectForKey:@"expectedStart"]; 431 | [currentQuery.selectionAfterHighlight setObject:last.highlightedSpan forKey:@"startContainer"]; 432 | [currentQuery.selectionAfterHighlight setObject:[NSNumber numberWithInt:startOffset] forKey:@"startOffset"]; 433 | } 434 | if(isEnd) 435 | { 436 | [currentQuery.selectionAfterHighlight removeObjectForKey:@"expectedEnd"]; 437 | [currentQuery.selectionAfterHighlight setObject:last.highlightedSpan forKey:@"endContainer"]; 438 | [currentQuery.selectionAfterHighlight setObject:[NSNumber numberWithInt:endOffset] forKey:@"endOffset"]; 439 | } 440 | if([currentQuery.selectionAfterHighlight objectForKey:@"startContainer"] && [currentQuery.selectionAfterHighlight objectForKey:@"endContainer"]) 441 | { 442 | [self selectRangeUsingEncodedDictionary:currentQuery.selectionAfterHighlight]; 443 | } 444 | } 445 | else 446 | { 447 | [last highlightDOMNode]; 448 | } 449 | } 450 | else 451 | { 452 | [last highlightDOMNode]; 453 | } 454 | return NO; // more work to do 455 | }, ^{ 456 | self.workerTimer = [NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(timeredHighlightOfMatches:) userInfo:matches repeats:NO]; 457 | }); 458 | 459 | } 460 | 461 | - (void)invalidateTimers 462 | { 463 | if([self.workerTimer isValid]) 464 | { 465 | [workerTimer invalidate]; 466 | } 467 | self.workerTimer = nil; 468 | } 469 | 470 | - (NSString *)normalizeWhitespaces:(NSString *)aString 471 | { 472 | // Normalize the whitespaces so we can avoid characters like "thin whitespace" (U+2009). 473 | // Yes, I tried using NSWidthInsensitiveSearch, but it only works for comparisons, not searching, hence the name (GG Apple!) 474 | NSRange foundRange; 475 | NSInteger scanLocation = 0; 476 | NSMutableString *string = [NSMutableString stringWithString:aString]; 477 | do 478 | { 479 | foundRange = [string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] options:NSLiteralSearch range:NSMakeRange(scanLocation, string.length-scanLocation)]; 480 | if(foundRange.location != NSNotFound) 481 | { 482 | scanLocation = foundRange.location+foundRange.length; 483 | [string replaceCharactersInRange:foundRange withString:@" "]; 484 | } 485 | } while (foundRange.location != NSNotFound); 486 | return string; 487 | } 488 | 489 | - (void)selectRangeUsingEncodedDictionary:(NSMutableDictionary *)dictionary 490 | { 491 | DOMNode *start = [dictionary objectForKey:@"startContainer"]; 492 | DOMNode *end = [dictionary objectForKey:@"endContainer"]; 493 | int startOffset = [[dictionary objectForKey:@"startOffset"] intValue]; 494 | int endOffset = [[dictionary objectForKey:@"endOffset"] intValue]; 495 | DOMRange *newRange = [[self mainFrameDocument] createRange]; 496 | 497 | int offset = 0; 498 | for(int i = 0; i < [start childNodes].length; i++) 499 | { 500 | DOMNode *child = [[start childNodes] item:i]; 501 | offset += (child.nodeType == DOM_ELEMENT_NODE) ? [(DOMHTMLElement*)child innerText].length : child.nodeValue.length; 502 | if(startOffset < offset) 503 | { 504 | @try { 505 | [newRange setStartBefore:child]; 506 | } 507 | @catch (NSException *exception) { 508 | } 509 | break; 510 | } 511 | } 512 | offset = 0; 513 | for(int i = 0; i < [end childNodes].length; i++) 514 | { 515 | DOMNode *child = [[end childNodes] item:i]; 516 | offset += (child.nodeType == DOM_ELEMENT_NODE) ? [(DOMHTMLElement*)child innerText].length : child.nodeValue.length; 517 | if(endOffset <= offset) 518 | { 519 | @try { 520 | [newRange setEndAfter:child]; 521 | } 522 | @catch (NSException *exception) { 523 | } 524 | break; 525 | } 526 | } 527 | @try { 528 | [self setSelectedDOMRange:newRange affinity:NSSelectionAffinityUpstream]; 529 | } 530 | @catch (NSException *exception) { 531 | } 532 | } 533 | 534 | - (void)clearSelection 535 | { 536 | DOMRange *range = [self selectedDOMRange]; 537 | if(range) 538 | { 539 | [range setEnd:[range startContainer] offset:[range startOffset]]; 540 | [self setSelectedDOMRange:range affinity:NSSelectionAffinityUpstream]; 541 | } 542 | } 543 | 544 | - (void)tryToGuessSelection:(NSDictionary *)fromDict 545 | { 546 | if([self selectedDOMRange]) 547 | { 548 | return; 549 | } 550 | @try { 551 | DOMNode *expectedStart = [fromDict objectForKey:@"expectedStart"]; 552 | DOMNode *expectedEnd = [fromDict objectForKey:@"expectedEnd"]; 553 | int expectedStartOffset = [[fromDict objectForKey:@"expectedStartOffset"] intValue]; 554 | int expectedEndOffset = [[fromDict objectForKey:@"expectedEndOffset"] intValue]; 555 | DOMNode *start = [fromDict objectForKey:@"startContainer"]; 556 | DOMNode *end = [fromDict objectForKey:@"endContainer"]; 557 | int startOffset = (fromDict == currentQuery.selectionAfterHighlight) ? 0 : [[fromDict objectForKey:@"startOffset"] intValue]; 558 | int endOffset = (fromDict == currentQuery.selectionAfterHighlight) ? 0 : [[fromDict objectForKey:@"endOffset"] intValue]; 559 | DOMDocument *document = [self mainFrameDocument]; 560 | if(expectedStart && end) 561 | { 562 | DOMRange *range = [document createRange]; 563 | [range setStart:expectedStart offset:expectedStartOffset]; 564 | [range setEnd:end offset:endOffset]; 565 | [self setSelectedDOMRange:range affinity:NSSelectionAffinityUpstream]; 566 | } 567 | else if(expectedEnd && start) 568 | { 569 | DOMRange *range = [document createRange]; 570 | [range setStart:start offset:startOffset]; 571 | [range setEnd:expectedEnd offset:expectedEndOffset]; 572 | [self setSelectedDOMRange:range affinity:NSSelectionAffinityUpstream]; 573 | } 574 | else if(expectedStart && expectedEnd) 575 | { 576 | DOMRange *range = [[self mainFrameDocument] createRange]; 577 | [range setStart:expectedStart offset:expectedStartOffset]; 578 | [range setEnd:expectedEnd offset:expectedEndOffset]; 579 | [self setSelectedDOMRange:range affinity:NSSelectionAffinityUpstream]; 580 | } 581 | } 582 | @catch (NSException *exception) { 583 | } 584 | } 585 | 586 | - (BOOL)maintainsInactiveSelection 587 | { 588 | return YES; 589 | } 590 | 591 | // You should call this from your delegate, I couldn't find a way of knowing when a new page is loaded 592 | - (void)didStartProvisionalLoad 593 | { 594 | [self invalidateTimers]; 595 | [scrollHighlighter removeFromSuperview]; 596 | self.scrollHighlighter = nil; 597 | self.currentQuery = nil; 598 | self.highlightedMatches = [NSMutableArray array]; 599 | self.matchedTexts = [NSMutableArray array]; 600 | self.entirePageContent = [NSMutableString string]; 601 | } 602 | 603 | - (void)makeTextLarger:(id)sender 604 | { 605 | [super makeTextLarger:sender]; 606 | [[NSUserDefaults standardUserDefaults] setFloat:[self textSizeMultiplier] forKey:@"DHHighlightedWebViewTextSizeMultiplier"]; 607 | } 608 | 609 | - (void)makeTextSmaller:(id)sender 610 | { 611 | [super makeTextSmaller:sender]; 612 | [[NSUserDefaults standardUserDefaults] setFloat:[self textSizeMultiplier] forKey:@"DHHighlightedWebViewTextSizeMultiplier"]; 613 | } 614 | 615 | - (void)awakeFromNib 616 | { 617 | float textSizeMultiplier = [[NSUserDefaults standardUserDefaults] floatForKey:@"DHHighlightedWebViewTextSizeMultiplier"]; 618 | if(textSizeMultiplier > 0) 619 | { 620 | [self setTextSizeMultiplier:textSizeMultiplier]; 621 | } 622 | } 623 | 624 | - (void)dealloc 625 | { 626 | [self invalidateTimers]; 627 | [focusQuery release]; 628 | [currentQuery release]; 629 | [highlightedMatches release]; 630 | [matchedTexts release]; 631 | [entirePageContent release]; 632 | [scrollHighlighter release]; 633 | [super dealloc]; 634 | } 635 | 636 | @end 637 | --------------------------------------------------------------------------------