aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbamse16 <marius@marius.me.uk>2009-04-11 09:14:42 +0000
committerbamse16 <marius@marius.me.uk>2009-04-11 09:14:42 +0000
commit1824ae6360c9ce1897e75404163d39df08ee5fbf (patch)
tree0eca1f6f4cb42e08f25e44a4683aecfb5881aac2
parent41f8cde09ff77996339cabc71517496976beee2e (diff)
downloadsequelpro-1824ae6360c9ce1897e75404163d39df08ee5fbf.tar.gz
sequelpro-1824ae6360c9ce1897e75404163d39df08ee5fbf.tar.bz2
sequelpro-1824ae6360c9ce1897e75404163d39df08ee5fbf.zip
Added printing support via WebKit WebView
-rw-r--r--Resources/sequel-pro-print-template.html75
-rw-r--r--Source/CustomQuery.h3
-rw-r--r--Source/CustomQuery.m10
-rw-r--r--Source/DeepMutableCopy.h10
-rw-r--r--Source/ICUTemplateMatcher.h44
-rw-r--r--Source/ICUTemplateMatcher.m192
-rw-r--r--Source/MGTemplateEngine.h104
-rw-r--r--Source/MGTemplateEngine.m673
-rw-r--r--Source/MGTemplateFilter.h14
-rw-r--r--Source/MGTemplateMarker.h41
-rw-r--r--Source/MGTemplateStandardFilters.h15
-rw-r--r--Source/MGTemplateStandardFilters.m97
-rw-r--r--Source/MGTemplateStandardMarkers.h24
-rw-r--r--Source/MGTemplateStandardMarkers.m620
-rw-r--r--Source/NSArray_DeepMutableCopy.h12
-rw-r--r--Source/NSArray_DeepMutableCopy.m42
-rw-r--r--Source/NSDictionary_DeepMutableCopy.h12
-rw-r--r--Source/NSDictionary_DeepMutableCopy.m43
-rw-r--r--Source/RegexKitLite.h130
-rw-r--r--Source/RegexKitLite.m354
-rw-r--r--Source/TableContent.h4
-rw-r--r--Source/TableContent.m22
-rw-r--r--Source/TableDocument.h5
-rw-r--r--Source/TableDocument.m122
-rw-r--r--Source/TableSource.h1
-rw-r--r--Source/TableSource.m19
-rw-r--r--sequel-pro.xcodeproj/project.pbxproj68
27 files changed, 2745 insertions, 11 deletions
diff --git a/Resources/sequel-pro-print-template.html b/Resources/sequel-pro-print-template.html
new file mode 100644
index 00000000..b0eb0960
--- /dev/null
+++ b/Resources/sequel-pro-print-template.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html dir="ltr" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml" lang="en">
+<head>
+<title>Sequel Pro</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<style type="text/css" media="all">
+html {
+ font-size:82%;
+}
+input, select, textarea {
+ font-size:1em;
+}
+div.item label {
+ white-space:nowrap;
+}
+.nowrap {
+ white-space:nowrap;
+}
+div.nowrap {
+ margin:0;
+ padding:0;
+}
+body, table, th, td {
+ background-color:#FFFFFF;
+ color:#000000;
+}
+img {
+ border:0 none;
+}
+table, th, td {
+ border:0.1em solid #000000;
+}
+
+table {
+ border-collapse:collapse;
+ border-spacing:0;
+ width: 100%;
+}
+
+th, td {
+ padding:0.2em;
+}
+th {
+ background-color:#E5E5E5;
+ font-weight:bold;
+}
+
+tr > td {
+ text-align: left;
+}
+</style>
+</head>
+
+<body>
+<p>
+ <b>Connection:</b> {{c.username}}{% if c.username %}@{% /if %}{{c.hostname}}{% if c.port %}:{% /if %}{{c.port}}/{{c.database}}<br>
+ <b>Generated on:</b> {% now | date_format: "dd MMM yyyy 'at' HH:mm:ss" %} by {{c.version}}<br>
+ {% if c.query %}<b>SQL query:</b> {{c.query}}{% /if %}
+ <br>
+</p>
+<table id="table_results" class="data">
+<thead><tr>
+{% for column in columns %}<th>{{ column }}</th>{% /for %}
+</tr></thead>
+
+<tbody>
+{% for row in rows %}
+<tr>
+ {% for cell in row %}<td>{{ cell }}</td>{% /for %}
+</tr>
+{% /for %}
+</tbody>
+</table>
+
+</body></html> \ No newline at end of file
diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h
index c2eac75f..e2ef7e59 100644
--- a/Source/CustomQuery.h
+++ b/Source/CustomQuery.h
@@ -61,6 +61,8 @@
NSMutableArray *queryFavorites;
CMMCPConnection *mySQLConnection;
+
+ NSString *usedQuery;
}
// IBAction methods
@@ -88,5 +90,6 @@
- (void)setConnection:(CMMCPConnection *)theConnection;
- (void)setFavorites;
- (void)doPerformQueryService:(NSString *)query;
+- (NSString *)usedQuery;
@end
diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m
index 05eed36f..ba9349f9 100644
--- a/Source/CustomQuery.m
+++ b/Source/CustomQuery.m
@@ -396,6 +396,10 @@ sets the tableView columns corresponding to the mysql-result
}
}
+ if(usedQuery)
+ [usedQuery release];
+ usedQuery = [[NSString stringWithString:[queries componentsJoinedByString:@";\n"]] retain];
+
//perform empty query if no query is given
if ( [queries count] == 0 ) {
theResult = [mySQLConnection queryString:@""];
@@ -668,6 +672,10 @@ inserts the query in the textView and performs query
[self runAllQueries:self];
}
+- (NSString *)usedQuery
+{
+ return usedQuery;
+}
#pragma mark -
#pragma mark TableView datasource methods
@@ -1062,6 +1070,7 @@ traps enter key and
{
self = [super init];
prefs = nil;
+ usedQuery = [[NSString stringWithString:@""] retain];
return self;
}
@@ -1070,6 +1079,7 @@ traps enter key and
[queryResult release];
[prefs release];
[queryFavorites release];
+ [usedQuery release];
[super dealloc];
}
diff --git a/Source/DeepMutableCopy.h b/Source/DeepMutableCopy.h
new file mode 100644
index 00000000..a1c92e21
--- /dev/null
+++ b/Source/DeepMutableCopy.h
@@ -0,0 +1,10 @@
+/*
+ * DeepMutableCopy.h
+ *
+ * Created by Matt Gemmell on 02/05/2008.
+ * Copyright 2008 Instinctive Code. All rights reserved.
+ *
+ */
+
+#import "NSArray_DeepMutableCopy.h"
+#import "NSDictionary_DeepMutableCopy.h"
diff --git a/Source/ICUTemplateMatcher.h b/Source/ICUTemplateMatcher.h
new file mode 100644
index 00000000..d02c0a31
--- /dev/null
+++ b/Source/ICUTemplateMatcher.h
@@ -0,0 +1,44 @@
+//
+// ICUTemplateMatcher.h
+//
+// Created by Matt Gemmell on 19/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "MGTemplateEngine.h"
+
+/*
+ This is an example Matcher for MGTemplateEngine, implemented using libicucore on Leopard,
+ via the RegexKitLite library: http://regexkit.sourceforge.net/#RegexKitLite
+
+ This project includes everything you need, as long as you're building on Mac OS X 10.5 or later.
+
+ Other matchers can easily be implemented using the MGTemplateEngineMatcher protocol,
+ if you prefer to use another regex framework, or use another matching method entirely.
+ */
+
+@interface ICUTemplateMatcher : NSObject <MGTemplateEngineMatcher> {
+ MGTemplateEngine *engine;
+ NSString *markerStart;
+ NSString *markerEnd;
+ NSString *exprStart;
+ NSString *exprEnd;
+ NSString *filterDelimiter;
+ NSString *templateString;
+ NSString *regex;
+}
+
+@property(assign) MGTemplateEngine *engine; // weak ref
+@property(retain) NSString *markerStart;
+@property(retain) NSString *markerEnd;
+@property(retain) NSString *exprStart;
+@property(retain) NSString *exprEnd;
+@property(retain) NSString *filterDelimiter;
+@property(retain) NSString *templateString;
+@property(retain) NSString *regex;
+
++ (ICUTemplateMatcher *)matcherWithTemplateEngine:(MGTemplateEngine *)theEngine;
+
+- (NSArray *)argumentsFromString:(NSString *)argString;
+
+@end
diff --git a/Source/ICUTemplateMatcher.m b/Source/ICUTemplateMatcher.m
new file mode 100644
index 00000000..dbeb49aa
--- /dev/null
+++ b/Source/ICUTemplateMatcher.m
@@ -0,0 +1,192 @@
+//
+// ICUTemplateMatcher.m
+//
+// Created by Matt Gemmell on 19/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "ICUTemplateMatcher.h"
+#import "RegexKitLite.h"
+
+
+@implementation ICUTemplateMatcher
+
+
++ (ICUTemplateMatcher *)matcherWithTemplateEngine:(MGTemplateEngine *)theEngine
+{
+ return [[[ICUTemplateMatcher alloc] initWithTemplateEngine:theEngine] autorelease];
+}
+
+
+- (id)initWithTemplateEngine:(MGTemplateEngine *)theEngine
+{
+ if (self = [super init]) {
+ self.engine = theEngine; // weak ref
+ }
+
+ return self;
+}
+
+
+- (void)dealloc
+{
+ self.engine = nil;
+ self.templateString = nil;
+ self.markerStart = nil;
+ self.markerEnd = nil;
+ self.exprStart = nil;
+ self.exprEnd = nil;
+ self.filterDelimiter = nil;
+ self.regex = nil;
+
+ [super dealloc];
+}
+
+
+- (void)engineSettingsChanged
+{
+ // This method is a good place to cache settings from the engine.
+ self.markerStart = engine.markerStartDelimiter;
+ self.markerEnd = engine.markerEndDelimiter;
+ self.exprStart = engine.expressionStartDelimiter;
+ self.exprEnd = engine.expressionEndDelimiter;
+ self.filterDelimiter = engine.filterDelimiter;
+ self.templateString = engine.templateContents;
+
+ // Note: the \Q ... \E syntax causes everything inside it to be treated as literals.
+ // This help us in the case where the marker/filter delimiters have special meaning
+ // in regular expressions; notably the "$" character in the default marker start-delimiter.
+ // Note: the (?m) syntax makes ICU enable multiline matching.
+ NSString *basePattern = @"(\\Q%@\\E)(?:\\s+)?(.*?)(?:(?:\\s+)?\\Q%@\\E(?:\\s+)?(.*?))?(?:\\s+)?\\Q%@\\E";
+ NSString *mrkrPattern = [NSString stringWithFormat:basePattern, self.markerStart, self.filterDelimiter, self.markerEnd];
+ NSString *exprPattern = [NSString stringWithFormat:basePattern, self.exprStart, self.filterDelimiter, self.exprEnd];
+ self.regex = [NSString stringWithFormat:@"(?m)(?:%@|%@)", mrkrPattern, exprPattern];
+}
+
+
+- (NSDictionary *)firstMarkerWithinRange:(NSRange)range
+{
+ NSRange matchRange = [self.templateString rangeOfRegex:self.regex options:RKLNoOptions inRange:range capture:0 error:NULL];
+ NSMutableDictionary *markerInfo = nil;
+ if (matchRange.length > 0) {
+ markerInfo = [NSMutableDictionary dictionary];
+ [markerInfo setObject:[NSValue valueWithRange:matchRange] forKey:MARKER_RANGE_KEY];
+
+ // Found a match. Obtain marker string.
+ NSString *matchString = [self.templateString substringWithRange:matchRange];
+ NSRange localRange = NSMakeRange(0, [matchString length]);
+ //NSLog(@"mtch: \"%@\"", matchString);
+
+ // Find type of match
+ NSString *matchType = nil;
+ NSRange mrkrSubRange = [matchString rangeOfRegex:regex options:RKLNoOptions inRange:localRange capture:1 error:NULL];
+ BOOL isMarker = (mrkrSubRange.length > 0); // only matches if match has marker-delimiters
+ int offset = 0;
+ if (isMarker) {
+ matchType = MARKER_TYPE_MARKER;
+ } else {
+ matchType = MARKER_TYPE_EXPRESSION;
+ offset = 3;
+ }
+ [markerInfo setObject:matchType forKey:MARKER_TYPE_KEY];
+
+ // Split marker string into marker-name and arguments.
+ NSRange markerRange = NSMakeRange(0, [matchString length]);
+ markerRange = [matchString rangeOfRegex:regex options:RKLNoOptions inRange:localRange capture:2 + offset error:NULL];
+
+ if (markerRange.length > 0) {
+ NSString *markerString = [matchString substringWithRange:markerRange];
+ NSArray *markerComponents = [self argumentsFromString:markerString];
+ if (markerComponents && [markerComponents count] > 0) {
+ [markerInfo setObject:[markerComponents objectAtIndex:0] forKey:MARKER_NAME_KEY];
+ int count = [markerComponents count];
+ if (count > 1) {
+ [markerInfo setObject:[markerComponents subarrayWithRange:NSMakeRange(1, count - 1)]
+ forKey:MARKER_ARGUMENTS_KEY];
+ }
+ }
+
+ // Check for filter.
+ NSRange filterRange = [matchString rangeOfRegex:regex options:RKLNoOptions inRange:localRange capture:3 + offset error:NULL];
+ if (filterRange.length > 0) {
+ // Found a filter. Obtain filter string.
+ NSString *filterString = [matchString substringWithRange:filterRange];
+
+ // Convert first : plus any immediately-following whitespace into a space.
+ localRange = NSMakeRange(0, [filterString length]);
+ NSString *space = @" ";
+ NSRange filterArgDelimRange = [filterString rangeOfRegex:@":(?:\\s+)?" options:RKLNoOptions inRange:localRange
+ capture:0 error:NULL];
+ if (filterArgDelimRange.length > 0) {
+ // Replace found text with space.
+ filterString = [NSString stringWithFormat:@"%@%@%@",
+ [filterString substringWithRange:NSMakeRange(0, filterArgDelimRange.location)],
+ space,
+ [filterString substringWithRange:NSMakeRange(NSMaxRange(filterArgDelimRange),
+ localRange.length - NSMaxRange(filterArgDelimRange))]];
+ }
+
+ // Split into filter-name and arguments.
+ NSArray *filterComponents = [self argumentsFromString:filterString];
+ if (filterComponents && [filterComponents count] > 0) {
+ [markerInfo setObject:[filterComponents objectAtIndex:0] forKey:MARKER_FILTER_KEY];
+ int count = [filterComponents count];
+ if (count > 1) {
+ [markerInfo setObject:[filterComponents subarrayWithRange:NSMakeRange(1, count - 1)]
+ forKey:MARKER_FILTER_ARGUMENTS_KEY];
+ }
+ }
+ }
+ }
+ }
+
+ return markerInfo;
+}
+
+
+- (NSArray *)argumentsFromString:(NSString *)argString
+{
+ // Extract arguments from argString, taking care not to break single- or double-quoted arguments,
+ // including those containing \-escaped quotes.
+ NSString *argsPattern = @"\"(.*?)(?<!\\\\)\"|'(.*?)(?<!\\\\)'|(\\S+)";
+ NSMutableArray *args = [NSMutableArray array];
+
+ int location = 0;
+ while (location != NSNotFound) {
+ NSRange searchRange = NSMakeRange(location, [argString length] - location);
+ NSRange entireRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions
+ inRange:searchRange capture:0 error:NULL];
+ NSRange matchedRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions
+ inRange:searchRange capture:1 error:NULL];
+ if (matchedRange.length == 0) {
+ matchedRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions
+ inRange:searchRange capture:2 error:NULL];
+ if (matchedRange.length == 0) {
+ matchedRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions
+ inRange:searchRange capture:3 error:NULL];
+ }
+ }
+
+ location = NSMaxRange(entireRange) + ((entireRange.length == 0) ? 1 : 0);
+ if (matchedRange.length > 0) {
+ [args addObject:[argString substringWithRange:matchedRange]];
+ } else {
+ location = NSNotFound;
+ }
+ }
+
+ return args;
+}
+
+
+@synthesize engine;
+@synthesize markerStart;
+@synthesize markerEnd;
+@synthesize exprStart;
+@synthesize exprEnd;
+@synthesize filterDelimiter;
+@synthesize templateString;
+@synthesize regex;
+
+
+@end
diff --git a/Source/MGTemplateEngine.h b/Source/MGTemplateEngine.h
new file mode 100644
index 00000000..f0eb9a43
--- /dev/null
+++ b/Source/MGTemplateEngine.h
@@ -0,0 +1,104 @@
+//
+// MGTemplateEngine.h
+//
+// Created by Matt Gemmell on 11/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+// Keys in blockInfo dictionaries passed to delegate methods.
+#define BLOCK_NAME_KEY @"name" // NSString containing block name (first word of marker)
+#define BLOCK_END_NAMES_KEY @"endNames" // NSArray containing names of possible ending-markers for block
+#define BLOCK_ARGUMENTS_KEY @"args" // NSArray of further arguments in block start marker
+#define BLOCK_START_MARKER_RANGE_KEY @"startMarkerRange" // NSRange (as NSValue) of block's starting marker
+#define BLOCK_VARIABLES_KEY @"vars" // NSDictionary of variables
+
+#define TEMPLATE_ENGINE_ERROR_DOMAIN @"MGTemplateEngineErrorDomain"
+
+@class MGTemplateEngine;
+@protocol MGTemplateEngineDelegate
+@optional
+- (void)templateEngine:(MGTemplateEngine *)engine blockStarted:(NSDictionary *)blockInfo;
+- (void)templateEngine:(MGTemplateEngine *)engine blockEnded:(NSDictionary *)blockInfo;
+- (void)templateEngineFinishedProcessingTemplate:(MGTemplateEngine *)engine;
+- (void)templateEngine:(MGTemplateEngine *)engine encounteredError:(NSError *)error isContinuing:(BOOL)continuing;
+@end
+
+// Keys in marker dictionaries returned from Matcher methods.
+#define MARKER_NAME_KEY @"name" // NSString containing marker name (first word of marker)
+#define MARKER_TYPE_KEY @"type" // NSString, either MARKER_TYPE_EXPRESSION or MARKER_TYPE_MARKER
+#define MARKER_TYPE_MARKER @"marker"
+#define MARKER_TYPE_EXPRESSION @"expression"
+#define MARKER_ARGUMENTS_KEY @"args" // NSArray of further arguments in marker, if any
+#define MARKER_FILTER_KEY @"filter" // NSString containing name of filter attached to marker, if any
+#define MARKER_FILTER_ARGUMENTS_KEY @"filterArgs" // NSArray of filter arguments, if any
+#define MARKER_RANGE_KEY @"range" // NSRange (as NSValue) of marker's range
+
+@protocol MGTemplateEngineMatcher
+@required
+- (id)initWithTemplateEngine:(MGTemplateEngine *)engine;
+- (void)engineSettingsChanged; // always called at least once before beginning to process a template.
+- (NSDictionary *)firstMarkerWithinRange:(NSRange)range;
+@end
+
+#import "MGTemplateMarker.h"
+#import "MGTemplateFilter.h"
+
+@interface MGTemplateEngine : NSObject {
+@public
+ NSString *markerStartDelimiter; // default: {%
+ NSString *markerEndDelimiter; // default: %}
+ NSString *expressionStartDelimiter; // default: {{
+ NSString *expressionEndDelimiter; // default: }}
+ NSString *filterDelimiter; // default: | example: {{ myVar|uppercase }}
+ NSString *literalStartMarker; // default: literal
+ NSString *literalEndMarker; // default: /literal
+@private
+ NSMutableArray *_openBlocksStack;
+ NSMutableDictionary *_globals;
+ int _outputDisabledCount;
+ int _templateLength;
+ NSMutableDictionary *_filters;
+ NSMutableDictionary *_markers;
+ NSMutableDictionary *_templateVariables;
+ BOOL _literal;
+@public
+ NSRange remainingRange;
+ id <MGTemplateEngineDelegate> delegate;
+ id <MGTemplateEngineMatcher> matcher;
+ NSString *templateContents;
+}
+
+@property(retain) NSString *markerStartDelimiter;
+@property(retain) NSString *markerEndDelimiter;
+@property(retain) NSString *expressionStartDelimiter;
+@property(retain) NSString *expressionEndDelimiter;
+@property(retain) NSString *filterDelimiter;
+@property(retain) NSString *literalStartMarker;
+@property(retain) NSString *literalEndMarker;
+@property(assign, readonly) NSRange remainingRange;
+@property(assign) id <MGTemplateEngineDelegate> delegate; // weak ref
+@property(retain) id <MGTemplateEngineMatcher> matcher;
+@property(retain, readonly) NSString *templateContents;
+
+// Creation.
++ (NSString *)version;
++ (MGTemplateEngine *)templateEngine;
+
+// Managing persistent values.
+- (void)setObject:(id)anObject forKey:(id)aKey;
+- (void)addEntriesFromDictionary:(NSDictionary *)dict;
+- (id)objectForKey:(id)aKey;
+
+// Configuration and extensibility.
+- (void)loadMarker:(NSObject <MGTemplateMarker> *)marker;
+- (void)loadFilter:(NSObject <MGTemplateFilter> *)filter;
+
+// Utilities.
+- (NSObject *)resolveVariable:(NSString *)var;
+- (NSDictionary *)templateVariables;
+
+// Processing templates.
+- (NSString *)processTemplate:(NSString *)templateString withVariables:(NSDictionary *)variables;
+- (NSString *)processTemplateInFileAtPath:(NSString *)templatePath withVariables:(NSDictionary *)variables;
+
+@end
diff --git a/Source/MGTemplateEngine.m b/Source/MGTemplateEngine.m
new file mode 100644
index 00000000..bf99afec
--- /dev/null
+++ b/Source/MGTemplateEngine.m
@@ -0,0 +1,673 @@
+//
+// MGTemplateEngine.m
+//
+// Created by Matt Gemmell on 11/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "MGTemplateEngine.h"
+#import "MGTemplateStandardMarkers.h"
+#import "MGTemplateStandardFilters.h"
+#import "DeepMutableCopy.h"
+
+
+#define DEFAULT_MARKER_START @"{%"
+#define DEFAULT_MARKER_END @"%}"
+#define DEFAULT_EXPRESSION_START @"{{" // should always be different from marker-start
+#define DEFAULT_EXPRESSION_END @"}}"
+#define DEFAULT_FILTER_START @"|"
+#define DEFAULT_LITERAL_START @"literal"
+#define DEFAULT_LITERAL_END @"/literal"
+// example: {% markername arg1 arg2|filter:arg1 arg2 %}
+
+#define GLOBAL_ENGINE_GROUP @"engine" // name of dictionary in globals containing engine settings
+#define GLOBAL_ENGINE_DELIMITERS @"delimiters" // name of dictionary in GLOBAL_ENGINE_GROUP containing delimiters
+#define GLOBAL_DELIM_MARKER_START @"markerStart" // name of key in GLOBAL_ENGINE_DELIMITERS containing marker start delimiter
+#define GLOBAL_DELIM_MARKER_END @"markerEnd"
+#define GLOBAL_DELIM_EXPR_START @"expressionStart"
+#define GLOBAL_DELIM_EXPR_END @"expressionEnd"
+#define GLOBAL_DELIM_FILTER @"filter"
+
+@interface MGTemplateEngine (PrivateMethods)
+
+- (NSObject *)valueForVariable:(NSString *)var parent:(NSObject **)parent parentKey:(NSString **)parentKey;
+- (void)setValue:(NSObject *)newValue forVariable:(NSString *)var forceCurrentStackFrame:(BOOL)inStackFrame;
+- (void)reportError:(NSString *)errorStr code:(int)code continuing:(BOOL)continuing;
+- (void)reportBlockBoundaryStarted:(BOOL)started;
+- (void)reportTemplateProcessingFinished;
+
+@end
+
+
+@implementation MGTemplateEngine
+
+
+#pragma mark Creation and destruction
+
+
++ (NSString *)version
+{
+ // 1.0.0 20 May 2008
+ return @"1.0.0";
+}
+
+
++ (MGTemplateEngine *)templateEngine
+{
+ return [[[MGTemplateEngine alloc] init] autorelease];
+}
+
+
+- (id)init
+{
+ if (self = [super init]) {
+ _openBlocksStack = [[NSMutableArray alloc] init];
+ _globals = [[NSMutableDictionary alloc] init];
+ _markers = [[NSMutableDictionary alloc] init];
+ _filters = [[NSMutableDictionary alloc] init];
+ _templateVariables = [[NSMutableDictionary alloc] init];
+ _outputDisabledCount = 0; // i.e. not disabled.
+ self.markerStartDelimiter = DEFAULT_MARKER_START;
+ self.markerEndDelimiter = DEFAULT_MARKER_END;
+ self.expressionStartDelimiter = DEFAULT_EXPRESSION_START;
+ self.expressionEndDelimiter = DEFAULT_EXPRESSION_END;
+ self.filterDelimiter = DEFAULT_FILTER_START;
+ self.literalStartMarker = DEFAULT_LITERAL_START;
+ self.literalEndMarker = DEFAULT_LITERAL_END;
+
+ // Load standard markers and filters.
+ [self loadMarker:[[[MGTemplateStandardMarkers alloc] initWithTemplateEngine:self] autorelease]];
+ [self loadFilter:[[[MGTemplateStandardFilters alloc] init] autorelease]];
+ }
+
+ return self;
+}
+
+
+- (void)dealloc
+{
+ [_openBlocksStack release];
+ _openBlocksStack = nil;
+ [_globals release];
+ _globals = nil;
+ [_filters release];
+ _filters = nil;
+ [_markers release];
+ _markers = nil;
+ self.delegate = nil;
+ [templateContents release];
+ templateContents = nil;
+ [_templateVariables release];
+ _templateVariables = nil;
+ self.markerStartDelimiter = nil;
+ self.markerEndDelimiter = nil;
+ self.expressionStartDelimiter = nil;
+ self.expressionEndDelimiter = nil;
+ self.filterDelimiter = nil;
+ self.literalStartMarker = nil;
+ self.literalEndMarker = nil;
+
+ [super dealloc];
+}
+
+
+#pragma mark Managing persistent values.
+
+
+- (void)setObject:(id)anObject forKey:(id)aKey
+{
+ [_globals setObject:anObject forKey:aKey];
+}
+
+
+- (void)addEntriesFromDictionary:(NSDictionary *)dict
+{
+ [_globals addEntriesFromDictionary:dict];
+}
+
+
+- (id)objectForKey:(id)aKey
+{
+ return [_globals objectForKey:aKey];
+}
+
+
+#pragma mark Configuration and extensibility.
+
+
+- (void)loadMarker:(NSObject <MGTemplateMarker> *)marker
+{
+ if (marker) {
+ // Obtain claimed markers.
+ NSArray *markers = [marker markers];
+ if (markers) {
+ for (NSString *markerName in markers) {
+ NSObject *existingHandler = [_markers objectForKey:markerName];
+ if (!existingHandler) {
+ // Set this MGTemplateMaker instance as the handler for markerName.
+ [_markers setObject:marker forKey:markerName];
+ }
+ }
+ }
+ }
+}
+
+
+- (void)loadFilter:(NSObject <MGTemplateFilter> *)filter
+{
+ if (filter) {
+ // Obtain claimed filters.
+ NSArray *filters = [filter filters];
+ if (filters) {
+ for (NSString *filterName in filters) {
+ NSObject *existingHandler = [_filters objectForKey:filterName];
+ if (!existingHandler) {
+ // Set this MGTemplateFilter instance as the handler for filterName.
+ [_filters setObject:filter forKey:filterName];
+ }
+ }
+ }
+ }
+}
+
+
+#pragma mark Delegate
+
+
+- (void)reportError:(NSString *)errorStr code:(int)code continuing:(BOOL)continuing
+{
+ if (delegate) {
+ NSString *errStr = NSLocalizedString(errorStr, nil);
+ if (!continuing) {
+ errStr = [NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"Fatal Error", nil), errStr];
+ }
+ SEL selector = @selector(templateEngine:encounteredError:isContinuing:);
+ if ([(NSObject *)delegate respondsToSelector:selector]) {
+ NSError *error = [NSError errorWithDomain:TEMPLATE_ENGINE_ERROR_DOMAIN
+ code:code
+ userInfo:[NSDictionary dictionaryWithObject:errStr
+ forKey:NSLocalizedDescriptionKey]];
+ [(NSObject <MGTemplateEngineDelegate> *)delegate templateEngine:self
+ encounteredError:error
+ isContinuing:continuing];
+ }
+ }
+}
+
+
+- (void)reportBlockBoundaryStarted:(BOOL)started
+{
+ if (delegate) {
+ SEL selector = (started) ? @selector(templateEngine:blockStarted:) : @selector(templateEngine:blockEnded:);
+ if ([(NSObject *)delegate respondsToSelector:selector]) {
+ [(NSObject *)delegate performSelector:selector withObject:self withObject:[_openBlocksStack lastObject]];
+ }
+ }
+}
+
+
+- (void)reportTemplateProcessingFinished
+{
+ if (delegate) {
+ SEL selector = @selector(templateEngineFinishedProcessingTemplate:);
+ if ([(NSObject *)delegate respondsToSelector:selector]) {
+ [(NSObject *)delegate performSelector:selector withObject:self];
+ }
+ }
+}
+
+
+#pragma mark Utilities.
+
+
+- (NSObject *)valueForVariable:(NSString *)var parent:(NSObject **)parent parentKey:(NSString **)parentKey
+{
+ // Returns value for given variable-path, and returns by reference the parent object the variable
+ // is contained in, and the key used on that parent object to access the variable.
+ // e.g. for var "thing.stuff.2", where thing = NSDictionary and stuff = NSArray,
+ // parent would be a pointer to the "stuff" array, and parentKey would be "2".
+
+ NSString *dot = @".";
+ NSArray *dotBits = [var componentsSeparatedByString:dot];
+ NSObject *result = nil;
+ NSObject *currObj = nil;
+
+ // Check to see if there's a top-level entry for first part of var in templateVariables.
+ NSString *firstVar = [dotBits objectAtIndex:0];
+
+ if ([_templateVariables objectForKey:firstVar]) {
+ currObj = _templateVariables;
+ } else if ([_globals objectForKey:firstVar]) {
+ currObj = _globals;
+ } else {
+ // Attempt to find firstVar in stack variables.
+ NSEnumerator *stack = [_openBlocksStack reverseObjectEnumerator];
+ NSDictionary *stackFrame = nil;
+ while (stackFrame = [stack nextObject]) {
+ NSDictionary *vars = [stackFrame objectForKey:BLOCK_VARIABLES_KEY];
+ if (vars && [vars objectForKey:firstVar]) {
+ currObj = vars;
+ break;
+ }
+ }
+ }
+
+ if (!currObj) {
+ return nil;
+ }
+
+ // Try raw KVC.
+ @try {
+ result = [currObj valueForKeyPath:var];
+ }
+ @catch (NSException *exception) {
+ // do nothing
+ }
+
+ if (result) {
+ // Got it with regular KVC. Work out parent and parentKey if necessary.
+ if (parent || parentKey) {
+ if ([dotBits count] > 1) {
+ if (parent) {
+ *parent = [currObj valueForKeyPath:[[dotBits subarrayWithRange:NSMakeRange(0, [dotBits count] - 1)]
+ componentsJoinedByString:dot]];
+ }
+ if (parentKey) {
+ *parentKey = [dotBits lastObject];
+ }
+ } else {
+ if (parent) {
+ *parent = currObj;
+ }
+ if (parentKey) {
+ *parentKey = var;
+ }
+ }
+ }
+ } else {
+ // Try iterative checking for array indices.
+ int numKeys = [dotBits count];
+ if (numKeys > 1) { // otherwise no point in checking
+ NSObject *thisParent = currObj;
+ NSString *thisKey = nil;
+ for (int i = 0; i < numKeys; i++) {
+ thisKey = [dotBits objectAtIndex:i];
+ NSObject *newObj = nil;
+ @try {
+ newObj = [currObj valueForKeyPath:thisKey];
+ }
+ @catch (NSException *e) {
+ // do nothing
+ }
+ // Check to see if this is an array which we can index into.
+ if (!newObj && [currObj isKindOfClass:[NSArray class]]) {
+ NSCharacterSet *numbersSet = [NSCharacterSet decimalDigitCharacterSet];
+ NSScanner *scanner = [NSScanner scannerWithString:thisKey];
+ NSString *digits;
+ BOOL scanned = [scanner scanCharactersFromSet:numbersSet intoString:&digits];
+ if (scanned && digits && [digits length] > 0) {
+ int index = [digits intValue];
+ if (index >= 0 && index < [((NSArray *)currObj) count]) {
+ newObj = [((NSArray *)currObj) objectAtIndex:index];
+ }
+ }
+ }
+ thisParent = currObj;
+ currObj = newObj;
+ if (!currObj) {
+ break;
+ }
+ }
+ result = currObj;
+ if (parent || parentKey) {
+ if (parent) {
+ *parent = thisParent;
+ }
+ if (parentKey) {
+ *parentKey = thisKey;
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+
+- (void)setValue:(NSObject *)newValue forVariable:(NSString *)var forceCurrentStackFrame:(BOOL)inStackFrame
+{
+ NSObject *parent = nil;
+ NSString *parentKey = nil;
+ NSObject *currValue;
+ currValue = [self valueForVariable:var parent:&parent parentKey:&parentKey];
+ if (!inStackFrame && currValue && (currValue != newValue)) {
+ // Set new value appropriately.
+ if ([parent isKindOfClass:[NSMutableArray class]]) {
+ [(NSMutableArray *)parent replaceObjectAtIndex:[parentKey intValue] withObject:newValue];
+ } else {
+ // Try using setValue:forKey:
+ @try {
+ [parent setValue:newValue forKey:parentKey];
+ }
+ @catch (NSException *e) {
+ // do nothing
+ }
+ }
+ } else if (!currValue || inStackFrame) {
+ // Put the variable into the current block-stack frame, or _templateVariables otherwise.
+ NSMutableDictionary *vars;
+ if ([_openBlocksStack count] > 0) {
+ vars = [[_openBlocksStack lastObject] objectForKey:BLOCK_VARIABLES_KEY];
+ } else {
+ vars = _templateVariables;
+ }
+ if ([vars respondsToSelector:@selector(setValue:forKey:)]) {
+ [vars setValue:newValue forKey:var];
+ }
+ }
+}
+
+
+- (NSObject *)resolveVariable:(NSString *)var
+{
+ NSObject *parent = nil;
+ NSString *key = nil;
+ NSObject *result = [self valueForVariable:var parent:&parent parentKey:&key];
+ //NSLog(@"var: %@, parent: %@, key: %@, result: %@", var, parent, key, result);
+ return result;
+}
+
+
+- (NSDictionary *)templateVariables
+{
+ return [NSDictionary dictionaryWithDictionary:_templateVariables];
+}
+
+
+#pragma mark Processing templates.
+
+
+- (NSString *)processTemplate:(NSString *)templateString withVariables:(NSDictionary *)variables
+{
+ // Set up environment.
+ [_openBlocksStack release];
+ _openBlocksStack = [[NSMutableArray alloc] init];
+ [_globals setObject:[NSDictionary dictionaryWithObjectsAndKeys:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ self.markerStartDelimiter, GLOBAL_DELIM_MARKER_START,
+ self.markerEndDelimiter, GLOBAL_DELIM_MARKER_END,
+ self.expressionStartDelimiter, GLOBAL_DELIM_EXPR_START,
+ self.expressionEndDelimiter, GLOBAL_DELIM_EXPR_END,
+ self.filterDelimiter, GLOBAL_DELIM_FILTER,
+ nil], GLOBAL_ENGINE_DELIMITERS,
+ nil]
+ forKey:GLOBAL_ENGINE_GROUP];
+ [_globals setObject:[NSNumber numberWithBool:YES] forKey:@"true"];
+ [_globals setObject:[NSNumber numberWithBool:NO] forKey:@"false"];
+ [_globals setObject:[NSNumber numberWithBool:YES] forKey:@"YES"];
+ [_globals setObject:[NSNumber numberWithBool:NO] forKey:@"NO"];
+ [_globals setObject:[NSNumber numberWithBool:YES] forKey:@"yes"];
+ [_globals setObject:[NSNumber numberWithBool:NO] forKey:@"no"];
+ _outputDisabledCount = 0;
+ [templateContents release];
+ templateContents = [templateString retain];
+ _templateLength = [templateString length];
+ [_templateVariables release];
+ _templateVariables = [variables deepMutableCopy];
+ remainingRange = NSMakeRange(0, [templateString length]);
+ _literal = NO;
+
+ // Ensure we have a matcher.
+ if (!matcher) {
+ [self reportError:@"No matcher has been configured for the template engine" code:7 continuing:NO];
+ return nil;
+ }
+
+ // Tell our matcher to take note of our settings.
+ [matcher engineSettingsChanged];
+ NSMutableString *output = [NSMutableString string];
+
+ while (remainingRange.location != NSNotFound) {
+ NSDictionary *matchInfo = [matcher firstMarkerWithinRange:remainingRange];
+ if (matchInfo) {
+ // Append output before marker if appropriate.
+ NSRange matchRange = [[matchInfo objectForKey:MARKER_RANGE_KEY] rangeValue];
+ if (_outputDisabledCount == 0) {
+ NSRange preMarkerRange = NSMakeRange(remainingRange.location, matchRange.location - remainingRange.location);
+ [output appendFormat:@"%@", [templateContents substringWithRange:preMarkerRange]];
+ }
+
+ // Adjust remainingRange.
+ remainingRange.location = NSMaxRange(matchRange);
+ remainingRange.length = _templateLength - remainingRange.location;
+
+ // Process the marker we found.
+ //NSLog(@"Match: %@", matchInfo);
+ NSString *matchMarker = [matchInfo objectForKey:MARKER_NAME_KEY];
+
+ // Deal with literal mode.
+ if ([matchMarker isEqualToString:self.literalStartMarker]) {
+ if (_literal && _outputDisabledCount == 0) {
+ // Output this tag literally.
+ [output appendFormat:@"%@", [templateContents substringWithRange:matchRange]];
+ } else {
+ // Enable literal mode.
+ _literal = YES;
+ }
+ continue;
+ } else if ([matchMarker isEqualToString:self.literalEndMarker]) {
+ // Disable literal mode.
+ _literal = NO;
+ continue;
+ } else if (_literal && _outputDisabledCount == 0) {
+ [output appendFormat:@"%@", [templateContents substringWithRange:matchRange]];
+ continue;
+ }
+
+ // Check to see if the match is a marker.
+ BOOL isMarker = [[matchInfo objectForKey:MARKER_TYPE_KEY] isEqualToString:MARKER_TYPE_MARKER];
+ NSObject <MGTemplateMarker> *markerHandler = nil;
+ NSObject *val = nil;
+ if (isMarker) {
+ markerHandler = [_markers objectForKey:matchMarker];
+
+ // Process marker with handler.
+ BOOL blockStarted = NO;
+ BOOL blockEnded = NO;
+ BOOL outputEnabled = (_outputDisabledCount == 0);
+ BOOL outputWasEnabled = outputEnabled;
+ NSRange nextRange = remainingRange;
+ NSDictionary *newVariables = nil;
+ NSDictionary *blockInfo = nil;
+
+ // If markerHandler is same as that of current block, send blockInfo.
+ if ([_openBlocksStack count] > 0) {
+ NSDictionary *currBlock = [_openBlocksStack lastObject];
+ NSString *currBlockStartMarker = [currBlock objectForKey:BLOCK_NAME_KEY];
+ if ([_markers objectForKey:currBlockStartMarker] == markerHandler) {
+ blockInfo = currBlock;
+ }
+ }
+
+ // Call marker's handler.
+ val = [markerHandler markerEncountered:matchMarker
+ withArguments:[matchInfo objectForKey:MARKER_ARGUMENTS_KEY]
+ inRange:matchRange
+ blockStarted:&blockStarted blockEnded:&blockEnded
+ outputEnabled:&outputEnabled nextRange:&nextRange
+ currentBlockInfo:blockInfo newVariables:&newVariables];
+
+ if (outputEnabled != outputWasEnabled) {
+ if (outputEnabled) {
+ _outputDisabledCount--;
+ } else {
+ _outputDisabledCount++;
+ }
+ }
+ remainingRange = nextRange;
+
+ // Check to see if remainingRange is valid.
+ if (NSMaxRange(remainingRange) > [self.templateContents length]) {
+ [self reportError:[NSString stringWithFormat:@"Marker handler \"%@\" specified an invalid range to resume processing from",
+ matchMarker]
+ code:5 continuing:NO];
+ break;
+ }
+
+ BOOL forceVarsToStack = NO;
+ if (blockStarted && blockEnded) {
+ // This is considered an error on the part of the marker-handler. Report to delegate.
+ [self reportError:[NSString stringWithFormat:@"Marker \"%@\" reported that a block simultaneously began and ended",
+ matchMarker]
+ code:0 continuing:YES];
+ } else if (blockStarted) {
+ NSArray *endMarkers = [markerHandler endMarkersForMarker:matchMarker];
+ if (!endMarkers) {
+ // Report error to delegate.
+ [self reportError:[NSString stringWithFormat:@"Marker \"%@\" started a block but did not supply any suitable end-markers",
+ matchMarker]
+ code:4 continuing:YES];
+ continue;
+ }
+
+ // A block has begun. Create relevant stack frame.
+ NSMutableDictionary *frame = [NSMutableDictionary dictionary];
+ [frame setObject:matchMarker forKey:BLOCK_NAME_KEY];
+ [frame setObject:endMarkers forKey:BLOCK_END_NAMES_KEY];
+ NSArray *arguments = [matchInfo objectForKey:MARKER_ARGUMENTS_KEY];
+ if (!arguments) {
+ arguments = [NSArray array];
+ }
+ [frame setObject:arguments forKey:BLOCK_ARGUMENTS_KEY];
+ [frame setObject:[matchInfo objectForKey:MARKER_RANGE_KEY] forKey:BLOCK_START_MARKER_RANGE_KEY];
+ [frame setObject:[NSMutableDictionary dictionary] forKey:BLOCK_VARIABLES_KEY];
+ [_openBlocksStack addObject:frame];
+
+ forceVarsToStack = YES;
+
+ // Report block start to delegate.
+ [self reportBlockBoundaryStarted:YES];
+ } else if (blockEnded) {
+ if (!blockInfo ||
+ ([_openBlocksStack count] > 0 &&
+ ![(NSArray *)[[_openBlocksStack lastObject] objectForKey:BLOCK_END_NAMES_KEY] containsObject:matchMarker])) {
+ // The marker-handler just told us a block ended, but the current block was not
+ // started by that marker-handler. This means a syntax error exists in the template,
+ // specifically an unterminated block (the current block).
+ // This is considered an unrecoverable error.
+ NSString *errMsg;
+ if ([_openBlocksStack count] == 0) {
+ errMsg = [NSString stringWithFormat:@"Marker \"%@\" reported that a non-existent block ended",
+ matchMarker];
+ } else {
+ NSString *currBlockName = [[_openBlocksStack lastObject] objectForKey:BLOCK_NAME_KEY];
+ errMsg = [NSString stringWithFormat:@"Marker \"%@\" reported that a block ended, \
+but current block was started by \"%@\" marker",
+ matchMarker, currBlockName];
+ }
+ [self reportError:errMsg code:1 continuing:YES];
+ break;
+ }
+
+ // Report block end to delegate before removing stack frame, so we can send info dict.
+ [self reportBlockBoundaryStarted:NO];
+
+ // Remove relevant stack frame.
+ if ([_openBlocksStack count] > 0) {
+ [_openBlocksStack removeLastObject];
+ }
+ }
+
+ // Process newVariables
+ if (newVariables) {
+ //NSLog(@"new vars %@", newVariables);
+ for (NSString *key in newVariables) {
+ [self setValue:[newVariables objectForKey:key] forVariable:key forceCurrentStackFrame:forceVarsToStack];
+ }
+ }
+
+ } else {
+ // Check to see if the first word of the match is a variable.
+ val = [self resolveVariable:matchMarker];
+ }
+
+ // Prepare result for output, if we have a result.
+ if (val && _outputDisabledCount == 0) {
+ // Process filter if specified.
+ NSString *filter = [matchInfo objectForKey:MARKER_FILTER_KEY];
+ if (filter) {
+ NSObject <MGTemplateFilter> *filterHandler = [_filters objectForKey:filter];
+ if (filterHandler) {
+ val = [filterHandler filterInvoked:filter
+ withArguments:[matchInfo objectForKey:MARKER_FILTER_ARGUMENTS_KEY] onValue:val];
+ }
+ }
+
+ // Output result.
+ [output appendFormat:@"%@", val];
+ } else if ((!val && !isMarker && _outputDisabledCount == 0) || (isMarker && !markerHandler)) {
+ // Call delegate's error-reporting method, if implemented.
+ [self reportError:[NSString stringWithFormat:@"\"%@\" is not a valid %@",
+ matchMarker, (isMarker) ? @"marker" : @"variable"]
+ code:((isMarker) ? 2 : 3) continuing:YES];
+ }
+ } else {
+ // Append output to end of template.
+ if (_outputDisabledCount == 0) {
+ [output appendFormat:@"%@", [templateContents substringWithRange:remainingRange]];
+ }
+
+ // Check to see if there are open blocks left over.
+ int openBlocks = [_openBlocksStack count];
+ if (openBlocks > 0) {
+ NSString *errMsg = [NSString stringWithFormat:@"Finished processing template, but %d %@ left open (%@).",
+ openBlocks,
+ (openBlocks == 1) ? @"block was" : @"blocks were",
+ [[_openBlocksStack valueForKeyPath:BLOCK_NAME_KEY] componentsJoinedByString:@", "]];
+ [self reportError:errMsg code:6 continuing:YES];
+ }
+
+ // Ensure we terminate the loop.
+ remainingRange.location = NSNotFound;
+ }
+ }
+
+ // Tell all marker-handlers we're done.
+ [[_markers allValues] makeObjectsPerformSelector:@selector(engineFinishedProcessingTemplate)];
+
+ // Inform delegate we're done.
+ [self reportTemplateProcessingFinished];
+
+ return output;
+}
+
+
+- (NSString *)processTemplateInFileAtPath:(NSString *)templatePath withVariables:(NSDictionary *)variables
+{
+ NSString *result = nil;
+ NSStringEncoding enc;
+ NSString *templateString = [NSString stringWithContentsOfFile:templatePath usedEncoding:&enc error:NULL];
+ if (templateString) {
+ result = [self processTemplate:templateString withVariables:variables];
+ }
+ return result;
+}
+
+
+#pragma mark Properties
+
+
+@synthesize markerStartDelimiter;
+@synthesize markerEndDelimiter;
+@synthesize expressionStartDelimiter;
+@synthesize expressionEndDelimiter;
+@synthesize filterDelimiter;
+@synthesize literalStartMarker;
+@synthesize literalEndMarker;
+@synthesize remainingRange;
+@synthesize delegate;
+@synthesize matcher;
+@synthesize templateContents;
+
+
+@end
diff --git a/Source/MGTemplateFilter.h b/Source/MGTemplateFilter.h
new file mode 100644
index 00000000..44c23423
--- /dev/null
+++ b/Source/MGTemplateFilter.h
@@ -0,0 +1,14 @@
+/*
+ * MGTemplateFilter.h
+ *
+ * Created by Matt Gemmell on 12/05/2008.
+ * Copyright 2008 Instinctive Code. All rights reserved.
+ *
+ */
+
+@protocol MGTemplateFilter
+
+- (NSArray *)filters;
+- (NSObject *)filterInvoked:(NSString *)filter withArguments:(NSArray *)args onValue:(NSObject *)value;
+
+@end
diff --git a/Source/MGTemplateMarker.h b/Source/MGTemplateMarker.h
new file mode 100644
index 00000000..ccdec72d
--- /dev/null
+++ b/Source/MGTemplateMarker.h
@@ -0,0 +1,41 @@
+/*
+ * MGTemplateMarker.h
+ *
+ * Created by Matt Gemmell on 12/05/2008.
+ * Copyright 2008 Instinctive Code. All rights reserved.
+ *
+ */
+
+#import "MGTemplateEngine.h"
+
+@protocol MGTemplateMarker
+@required
+- (id)initWithTemplateEngine:(MGTemplateEngine *)engine; // to avoid retain cycles, use a weak reference for engine.
+- (NSArray *)markers; // array of markers (each unique across all markers) this object handles.
+- (NSArray *)endMarkersForMarker:(NSString *)marker; // returns the possible corresponding end-markers for a marker which has just started a block.
+- (NSObject *)markerEncountered:(NSString *)marker withArguments:(NSArray *)args inRange:(NSRange)markerRange
+ blockStarted:(BOOL *)blockStarted blockEnded:(BOOL *)blockEnded
+ outputEnabled:(BOOL *)outputEnabled nextRange:(NSRange *)nextRange
+ currentBlockInfo:(NSDictionary *)blockInfo newVariables:(NSDictionary **)newVariables;
+/* Notes for -markerEncountered:... method
+ Arguments:
+ marker: marker encountered by the template engine
+ args: arguments to the marker, in order
+ markerRange: the range of the marker encountered in the engine's templateString
+ blockStarted: pointer to BOOL. Set it to YES if the marker just started a block.
+ blockEnded: pointer to BOOL. Set it to YES if the marker just ended a block.
+ Note: you should never set both blockStarted and blockEnded in the same call.
+ outputEnabled: pointer to BOOL, indicating whether the engine is currently outputting. Can be changed to switch output on/off.
+ nextRange: the next range in the engine's templateString which will be searched. Can be modified if necessary.
+ currentBlockInfo: information about the current block, if the block was started by this handler; otherwise nil.
+ Note: if supplied, will include a dictionary of variables set for the current block.
+ newVariables: variables to set in the template context. If blockStarted is YES, these will be scoped only within the new block.
+ Note: if currentBlockInfo was specified, variables set in the return dictionary will override/update any variables of
+ the same name in currentBlockInfo's variables. This is for ease of updating loop-counters or such.
+ Returns:
+ A return value to insert into the template output, or nil if nothing should be inserted.
+ */
+
+- (void)engineFinishedProcessingTemplate;
+
+@end
diff --git a/Source/MGTemplateStandardFilters.h b/Source/MGTemplateStandardFilters.h
new file mode 100644
index 00000000..335b0713
--- /dev/null
+++ b/Source/MGTemplateStandardFilters.h
@@ -0,0 +1,15 @@
+//
+// MGTemplateStandardFilters.h
+//
+// Created by Matt Gemmell on 13/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "MGTemplateFilter.h"
+
+
+@interface MGTemplateStandardFilters : NSObject <MGTemplateFilter> {
+
+}
+
+@end
diff --git a/Source/MGTemplateStandardFilters.m b/Source/MGTemplateStandardFilters.m
new file mode 100644
index 00000000..45f1d51d
--- /dev/null
+++ b/Source/MGTemplateStandardFilters.m
@@ -0,0 +1,97 @@
+//
+// MGTemplateStandardFilters.m
+//
+// Created by Matt Gemmell on 13/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "MGTemplateStandardFilters.h"
+
+
+#define UPPERCASE @"uppercase"
+#define LOWERCASE @"lowercase"
+#define CAPITALIZED @"capitalized"
+#define DATE_FORMAT @"date_format"
+#define COLOR_FORMAT @"color_format"
+
+
+@implementation MGTemplateStandardFilters
+
+
+- (NSArray *)filters
+{
+ return [NSArray arrayWithObjects:
+ UPPERCASE, LOWERCASE, CAPITALIZED,
+ DATE_FORMAT, COLOR_FORMAT,
+ nil];
+}
+
+
+- (NSObject *)filterInvoked:(NSString *)filter withArguments:(NSArray *)args onValue:(NSObject *)value
+{
+ if ([filter isEqualToString:UPPERCASE]) {
+ return [[NSString stringWithFormat:@"%@", value] uppercaseString];
+
+ } else if ([filter isEqualToString:LOWERCASE]) {
+ return [[NSString stringWithFormat:@"%@", value] lowercaseString];
+
+ } else if ([filter isEqualToString:CAPITALIZED]) {
+ return [[NSString stringWithFormat:@"%@", value] capitalizedString];
+
+ } else if ([filter isEqualToString:DATE_FORMAT]) {
+ // Formats NSDates according to Unicode syntax:
+ // http://unicode.org/reports/tr35/tr35-4.html#Date_Format_Patterns
+ // e.g. "dd MM yyyy" etc.
+ if ([value isKindOfClass:[NSDate class]] && [args count] == 1) {
+ NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
+ [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ NSString *format = [args objectAtIndex:0];
+ [dateFormatter setDateFormat:format];
+ return [dateFormatter stringFromDate:(NSDate *)value];
+ }
+
+ } else if ([filter isEqualToString:COLOR_FORMAT]) {
+#if TARGET_OS_IPHONE
+ if ([value isKindOfClass:[UIColor class]] && [args count] == 1) {
+#else
+ if ([value isKindOfClass:[NSColor class]] && [args count] == 1) {
+#endif
+ NSString *format = [[args objectAtIndex:0] lowercaseString];
+ if ([format isEqualToString:@"hex"]) {
+ // Output color in hex format RRGGBB (without leading # character).
+#if TARGET_OS_IPHONE
+ CGColorRef color = [(UIColor *)value CGColor];
+ CGColorSpaceRef colorSpace = CGColorGetColorSpace(color);
+ CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(colorSpace);
+
+ if (colorSpaceModel != kCGColorSpaceModelRGB)
+ return @"000000";
+
+ const CGFloat *components = CGColorGetComponents(color);
+ NSString *colorHex = [NSString stringWithFormat:@"%02x%02x%02x",
+ (int)(components[0] * 255),
+ (int)(components[1] * 255),
+ (int)(components[2] * 255)];
+ return colorHex;
+#else
+ NSColor *color = [(NSColor *)value colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
+ if (!color) { // happens if the colorspace couldn't be converted
+ return @"000000"; // black
+ } else {
+ NSString *colorHex = [NSString stringWithFormat:@"%02x%02x%02x",
+ (int)([color redComponent] * 255),
+ (int)([color greenComponent] * 255),
+ (int)([color blueComponent] * 255)];
+ return colorHex;
+ }
+#endif
+ }
+ }
+
+ }
+
+ return value;
+}
+
+
+@end
diff --git a/Source/MGTemplateStandardMarkers.h b/Source/MGTemplateStandardMarkers.h
new file mode 100644
index 00000000..2830197a
--- /dev/null
+++ b/Source/MGTemplateStandardMarkers.h
@@ -0,0 +1,24 @@
+//
+// MGTemplateStandardMarkers.h
+//
+// Created by Matt Gemmell on 13/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "MGTemplateEngine.h"
+#import "MGTemplateMarker.h"
+
+@interface MGTemplateStandardMarkers : NSObject <MGTemplateMarker> {
+ MGTemplateEngine *engine; // weak ref
+ NSMutableArray *forStack;
+ NSMutableArray *sectionStack;
+ NSMutableArray *ifStack;
+ NSMutableArray *commentStack;
+ NSMutableDictionary *cycles;
+}
+
+- (BOOL)currentBlock:(NSDictionary *)blockInfo matchesTopOfStack:(NSMutableArray *)stack;
+- (BOOL)argIsNumeric:(NSString *)arg intValue:(int *)val checkVariables:(BOOL)checkVars;
+- (BOOL)argIsTrue:(NSString *)arg;
+
+@end
diff --git a/Source/MGTemplateStandardMarkers.m b/Source/MGTemplateStandardMarkers.m
new file mode 100644
index 00000000..df4fcf3e
--- /dev/null
+++ b/Source/MGTemplateStandardMarkers.m
@@ -0,0 +1,620 @@
+//
+// MGTemplateStandardMarkers.m
+//
+// Created by Matt Gemmell on 13/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "MGTemplateStandardMarkers.h"
+#import "MGTemplateFilter.h"
+
+//==============================================================================
+
+#define FOR_START @"for"
+#define FOR_END @"/for"
+
+#define FOR_TYPE_ENUMERATOR @"in" // e.g. for thing in things
+#define FOR_TYPE_RANGE @"to" // e.g. for 1 to 5
+#define FOR_REVERSE @"reversed"
+
+#define FOR_LOOP_VARS @"currentLoop"
+#define FOR_LOOP_CURR_INDEX @"currentIndex"
+#define FOR_LOOP_START_INDEX @"startIndex"
+#define FOR_LOOP_END_INDEX @"endIndex"
+#define FOR_PARENT_LOOP @"parentLoop"
+
+#define STACK_START_MARKER_RANGE @"markerRange"
+#define STACK_START_REMAINING_RANGE @"remainingRange"
+#define FOR_STACK_ENUMERATOR @"enumerator"
+#define FOR_STACK_ENUM_VAR @"enumeratorVariable"
+#define FOR_STACK_DISABLED_OUTPUT @"disabledOutput"
+
+//==============================================================================
+
+#define SECTION_START @"section"
+#define SECTION_END @"/section"
+
+//==============================================================================
+
+#define IF_START @"if"
+#define ELSE @"else"
+#define IF_END @"/if"
+
+#define IF_VARS @"currentIf"
+#define DISABLE_OUTPUT @"shouldDisableOutput"
+#define IF_ARG_TRUE @"argumentTrue"
+#define IF_ELSE_SEEN @"elseEncountered"
+
+//==============================================================================
+
+#define NOW @"now"
+
+//==============================================================================
+
+#define COMMENT_START @"comment"
+#define COMMENT_END @"/comment"
+
+//==============================================================================
+
+#define LOAD @"load"
+
+//==============================================================================
+
+#define CYCLE @"cycle"
+#define CYCLE_INDEX @"lastIndex"
+#define CYCLE_VALUES @"value"
+
+//==============================================================================
+
+#define SET @"set"
+
+//==============================================================================
+
+
+@implementation MGTemplateStandardMarkers
+
+
+- (id)initWithTemplateEngine:(MGTemplateEngine *)theEngine
+{
+ if (self = [super init]) {
+ engine = theEngine;
+ forStack = [[NSMutableArray alloc] init];
+ sectionStack = [[NSMutableArray alloc] init];
+ ifStack = [[NSMutableArray alloc] init];
+ commentStack = [[NSMutableArray alloc] init];
+ cycles = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+
+- (void)dealloc
+{
+ engine = nil;
+ [forStack release];
+ forStack = nil;
+ [sectionStack release];
+ sectionStack = nil;
+ [ifStack release];
+ ifStack = nil;
+ [commentStack release];
+ commentStack = nil;
+ [cycles release];
+ cycles = nil;
+
+ [super dealloc];
+}
+
+
+- (NSArray *)markers
+{
+ return [NSArray arrayWithObjects:
+ FOR_START, FOR_END,
+ SECTION_START, SECTION_END,
+ IF_START, ELSE, IF_END,
+ NOW,
+ COMMENT_START, COMMENT_END,
+ LOAD,
+ CYCLE,
+ SET,
+ nil];
+}
+
+
+- (NSArray *)endMarkersForMarker:(NSString *)marker
+{
+ if ([marker isEqualToString:FOR_START]) {
+ return [NSArray arrayWithObjects:FOR_END, nil];
+ } else if ([marker isEqualToString:SECTION_START]) {
+ return [NSArray arrayWithObjects:SECTION_END, nil];
+ } else if ([marker isEqualToString:IF_START]) {
+ return [NSArray arrayWithObjects:IF_END, ELSE, nil];
+ } else if ([marker isEqualToString:COMMENT_START]) {
+ return [NSArray arrayWithObjects:COMMENT_END, nil];
+ }
+ return nil;
+}
+
+
+- (NSObject *)markerEncountered:(NSString *)marker withArguments:(NSArray *)args inRange:(NSRange)markerRange
+ blockStarted:(BOOL *)blockStarted blockEnded:(BOOL *)blockEnded
+ outputEnabled:(BOOL *)outputEnabled nextRange:(NSRange *)nextRange
+ currentBlockInfo:(NSDictionary *)blockInfo newVariables:(NSDictionary **)newVariables
+{
+ if ([marker isEqualToString:FOR_START]) {
+ if (args && [args count] >= 3) {
+ // Determine which type of loop this is.
+ BOOL isRange = YES;
+ if ([[args objectAtIndex:1] isEqualToString:FOR_TYPE_ENUMERATOR]) {
+ isRange = NO;
+ }
+ BOOL reversed = NO;
+ if ([args count] == 4 && [[args objectAtIndex:3] isEqualToString:FOR_REVERSE]) {
+ reversed = YES;
+ }
+
+ // Determine if we have acceptable parameters.
+ NSObject *loopEnumObject = nil;
+ BOOL valid = NO;
+ NSString *startArg = [args objectAtIndex:0];
+ NSString *endArg = [args objectAtIndex:2];
+ int startIndex, endIndex;
+ if (isRange) {
+ // Check to see if either the arg itself is numeric, or it corresponds to a numeric variable.
+ valid = [self argIsNumeric:startArg intValue:&startIndex checkVariables:YES];
+ if (valid) {
+ valid = [self argIsNumeric:endArg intValue:&endIndex checkVariables:YES];
+ if (valid) {
+ // Check startIndex and endIndex are sensible.
+ valid = (startIndex <= endIndex);
+ }
+ }
+ } else {
+ startIndex = 0;
+
+ // Check that endArg is a collection.
+ NSObject *obj = [engine resolveVariable:endArg];
+ if (obj && [obj respondsToSelector:@selector(objectEnumerator)] && [obj respondsToSelector:@selector(count)]) {
+ endIndex = [(NSArray *)obj count];
+ if (endIndex > 0) {
+ loopEnumObject = obj;
+ valid = YES;
+ }
+ }
+ }
+
+ if (valid) {
+ *blockStarted = YES;
+
+ // Set up for-stack frame for this loop.
+ NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE,
+ [NSValue valueWithRange:*nextRange], STACK_START_REMAINING_RANGE,
+ nil];
+ [forStack addObject:stackFrame];
+
+ // Set up variables for the block.
+ int currentIndex = (reversed) ? endIndex : startIndex;
+ NSMutableDictionary *loopVars = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithInt:startIndex], FOR_LOOP_START_INDEX,
+ [NSNumber numberWithInt:endIndex], FOR_LOOP_END_INDEX,
+ [NSNumber numberWithInt:currentIndex], FOR_LOOP_CURR_INDEX,
+ [NSNumber numberWithBool:reversed], FOR_REVERSE,
+ nil];
+ NSMutableDictionary *blockVars = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ loopVars, FOR_LOOP_VARS,
+ nil];
+
+ // Add enumerator variable if appropriate.
+ if (!isRange) {
+ NSEnumerator *enumerator;
+ if (reversed && [loopEnumObject respondsToSelector:@selector(reverseObjectEnumerator)]) {
+ enumerator = [(NSArray *)loopEnumObject reverseObjectEnumerator];
+ } else {
+ enumerator = [(NSArray *)loopEnumObject objectEnumerator];
+ }
+ [stackFrame setObject:enumerator forKey:FOR_STACK_ENUMERATOR];
+ [stackFrame setObject:startArg forKey:FOR_STACK_ENUM_VAR];
+ [blockVars setObject:[enumerator nextObject] forKey:startArg];
+ }
+
+ // Add parentLoop if it exists.
+ if (blockInfo) {
+ NSDictionary *parentLoop;
+ parentLoop = (NSDictionary *)[engine resolveVariable:FOR_LOOP_VARS]; // in case parent loop isn't in the first parent stack-frame.
+ if (parentLoop) {
+ [loopVars setObject:parentLoop forKey:FOR_PARENT_LOOP];
+ }
+ }
+
+ *newVariables = blockVars;
+ } else {
+ // Disable output for this block.
+ *blockStarted = YES;
+ NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:YES], FOR_STACK_DISABLED_OUTPUT,
+ [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE,
+ [NSValue valueWithRange:*nextRange], STACK_START_REMAINING_RANGE,
+ nil];
+ [forStack addObject:stackFrame];
+ *outputEnabled = NO;
+ }
+ }
+
+ } else if ([marker isEqualToString:FOR_END]) {
+ // Decide whether to loop back or terminate.
+ if ([self currentBlock:blockInfo matchesTopOfStack:forStack]) {
+ NSMutableDictionary *frame = [forStack lastObject];
+
+ // Check to see if this was a block with an invalid looping condition.
+ NSNumber *disabledOutput = (NSNumber *)[frame objectForKey:FOR_STACK_DISABLED_OUTPUT];
+ if (disabledOutput && [disabledOutput boolValue]) {
+ *outputEnabled = YES;
+ *blockEnded = YES;
+ [forStack removeLastObject];
+ }
+
+ // This is the same loop that's on top of our stack. Check to see if we need to loop back.
+ BOOL loop = NO;
+ NSDictionary *blockVars = [blockInfo objectForKey:BLOCK_VARIABLES_KEY];
+ if ([blockVars count] == 0) {
+ *blockEnded = YES;
+ return nil;
+ }
+ NSMutableDictionary *loopVars = [[[blockVars objectForKey:FOR_LOOP_VARS] mutableCopy] autorelease];
+ BOOL reversed = [[loopVars objectForKey:FOR_REVERSE] boolValue];
+ NSEnumerator *loopEnum = [frame objectForKey:FOR_STACK_ENUMERATOR];
+ NSObject *newEnumValue = nil;
+ int currentIndex = [[loopVars objectForKey:FOR_LOOP_CURR_INDEX] intValue];
+ if (loopEnum) {
+ // Enumerator type.
+ newEnumValue = [loopEnum nextObject];
+ if (newEnumValue) {
+ loop = YES;
+ }
+ } else {
+ // Range type.
+ if (reversed) {
+ int minIndex = [[loopVars objectForKey:FOR_LOOP_START_INDEX] intValue];
+ if (currentIndex > minIndex) {
+ loop = YES;
+ }
+ } else {
+ int maxIndex = [[loopVars objectForKey:FOR_LOOP_END_INDEX] intValue];
+ if (currentIndex < maxIndex) {
+ loop = YES;
+ }
+ }
+ }
+
+ if (loop) {
+ // Set remainingRange from stack dict
+ *nextRange = [[frame objectForKey:STACK_START_REMAINING_RANGE] rangeValue];
+
+ // Set new currentIndex
+ if (reversed) {
+ currentIndex--;
+ } else {
+ currentIndex++;
+ }
+ [loopVars setObject:[NSNumber numberWithInt:currentIndex] forKey:FOR_LOOP_CURR_INDEX];
+
+ // Set new val for enumVar if specified
+ NSMutableDictionary *newVars = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ loopVars, FOR_LOOP_VARS,
+ nil];
+ if (newEnumValue) {
+ [newVars setObject:newEnumValue forKey:[frame objectForKey:FOR_STACK_ENUM_VAR]];
+ }
+
+ *newVariables = newVars;
+ } else {
+ // Don't need to do much here, since:
+ // 1. Each blockStack frame for a "for" has its own currentLoop dict.
+ // 2. Parent loop's enum-vars are still in place in the parent stack's vars.
+
+ // End block.
+ *blockEnded = YES;
+ [forStack removeLastObject];
+ }
+
+ // Return immediately.
+ return nil;
+ }
+ *blockEnded = YES;
+
+ } else if ([marker isEqualToString:SECTION_START]) {
+ if (args && [args count] == 1) {
+ *blockStarted = YES;
+
+ // Set up for-stack frame for this section.
+ NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE,
+ nil];
+ [sectionStack addObject:stackFrame];
+ }
+
+ } else if ([marker isEqualToString:SECTION_END]) {
+ if ([self currentBlock:blockInfo matchesTopOfStack:sectionStack]) {
+ // This is the same section that's on top of our stack. Remove from stack.
+ [sectionStack removeLastObject];
+ }
+ *blockEnded = YES;
+
+ } else if ([marker isEqualToString:IF_START]) {
+ if (args && ([args count] == 1 || [args count] == 3)) {
+ *blockStarted = YES;
+
+ // Determine appropriate values for outputEnabled and for our if-stack frame.
+ BOOL elseEncountered = NO;
+ BOOL argTrue = NO;
+ if ([args count] == 1) {
+ argTrue = [self argIsTrue:[args objectAtIndex:0]];
+ } else if ([args count] == 2 && [[[args objectAtIndex:0] lowercaseString] isEqualToString:@"not"]) {
+ // e.g. if not x
+ argTrue = ![self argIsTrue:[args objectAtIndex:1]];
+ } else if ([args count] == 3) {
+ // Assumed to be of the form: operand comparison operand, e.g. x == y
+ NSString *firstArg = [args objectAtIndex:0];
+ NSString *secondArg = [args objectAtIndex:2];
+ BOOL firstTrue = [self argIsTrue:firstArg];
+ BOOL secondTrue = [self argIsTrue:secondArg];
+ int num1, num2;
+ BOOL firstNumeric, secondNumeric;
+ firstNumeric = [self argIsNumeric:firstArg intValue:&num1 checkVariables:YES];
+ secondNumeric = [self argIsNumeric:secondArg intValue:&num2 checkVariables:YES];
+ if (!firstNumeric) {
+ num1 = ([engine resolveVariable:firstArg]) ? 1 : 0;
+ }
+ if (!secondNumeric) {
+ num2 = ([engine resolveVariable:secondArg]) ? 1 : 0;
+ }
+ NSString *op = [[args objectAtIndex:1] lowercaseString];
+
+ if ([op isEqualToString:@"and"] || [op isEqualToString:@"&&"]) {
+ argTrue = (firstTrue && secondTrue);
+ } else if ([op isEqualToString:@"or"] || [op isEqualToString:@"||"]) {
+ argTrue = (firstTrue || secondTrue);
+ } else if ([op isEqualToString:@"="] || [op isEqualToString:@"=="]) {
+ argTrue = (num1 == num2);
+ } else if ([op isEqualToString:@"!="] || [op isEqualToString:@"<>"]) {
+ argTrue = (num1 != num2);
+ } else if ([op isEqualToString:@">"]) {
+ argTrue = (num1 > num2);
+ } else if ([op isEqualToString:@"<"]) {
+ argTrue = (num1 < num2);
+ } else if ([op isEqualToString:@">="]) {
+ argTrue = (num1 >= num2);
+ } else if ([op isEqualToString:@"<="]) {
+ argTrue = (num1 <= num2);
+ } else if ([op isEqualToString:@"\%"]) {
+ argTrue = ((num1 % num2) > 0);
+ }
+ }
+
+ BOOL shouldDisableOutput = *outputEnabled;
+ if (shouldDisableOutput && !argTrue) {
+ *outputEnabled = NO;
+ }
+
+ // Create variables.
+ NSMutableDictionary *ifVars = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:argTrue], IF_ARG_TRUE,
+ [NSNumber numberWithBool:shouldDisableOutput], DISABLE_OUTPUT,
+ [NSNumber numberWithBool:elseEncountered], IF_ELSE_SEEN,
+ nil];
+
+ // Set up for-stack frame for this if-statement.
+ NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE,
+ ifVars, IF_VARS,
+ nil];
+ [ifStack addObject:stackFrame];
+ }
+
+ } else if ([marker isEqualToString:ELSE]) {
+ if ([self currentBlock:blockInfo matchesTopOfStack:ifStack]) {
+ NSMutableDictionary *frame = [[ifStack lastObject] objectForKey:IF_VARS];
+ BOOL elseSeen = [[frame objectForKey:IF_ELSE_SEEN] boolValue];
+ BOOL argTrue = [[frame objectForKey:IF_ARG_TRUE] boolValue];
+ BOOL modifyOutput = [[frame objectForKey:DISABLE_OUTPUT] boolValue];
+
+ if (!elseSeen) {
+ if (modifyOutput) {
+ // Only make changes if we've not already seen an 'else' for this block,
+ // and if we're modifying output state at all.
+ *outputEnabled = !argTrue; // either turning it off, or turning it back on.
+ }
+
+ // Note that we've now seen the else marker.
+ [frame setObject:[NSNumber numberWithBool:YES] forKey:IF_ELSE_SEEN];
+ }
+ }
+
+ } else if ([marker isEqualToString:IF_END]) {
+ if ([self currentBlock:blockInfo matchesTopOfStack:ifStack]) {
+ NSMutableDictionary *frame = [[ifStack lastObject] objectForKey:IF_VARS];
+ BOOL modifyOutput = [[frame objectForKey:DISABLE_OUTPUT] boolValue];
+ if (modifyOutput) {
+ // If we're modifying output, it was enabled when this block started.
+ // Thus, it should be enabled after the block ends.
+ // If it's already enabled, this will have no harmful effect.
+ *outputEnabled = YES;
+ }
+
+ // End block.
+ [ifStack removeLastObject];
+ *blockEnded = YES;
+ }
+ *blockEnded = YES;
+
+ } else if ([marker isEqualToString:NOW]) {
+ return [NSDate date];
+
+ } else if ([marker isEqualToString:COMMENT_START]) {
+ // Work out if we need to start a block.
+ if (!args || [args count] == 0) {
+ *blockStarted = YES;
+
+ // Determine appropriate values for outputEnabled and for our stack frame.
+ BOOL shouldDisableOutput = *outputEnabled;
+ if (shouldDisableOutput) {
+ *outputEnabled = NO;
+ }
+
+ // Set up for-stack frame for this if-statement.
+ NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE,
+ [NSNumber numberWithBool:shouldDisableOutput], DISABLE_OUTPUT,
+ nil];
+ [commentStack addObject:stackFrame];
+ }
+
+ } else if ([marker isEqualToString:COMMENT_END]) {
+ // Check this is block on top of stack.
+ if ([self currentBlock:blockInfo matchesTopOfStack:commentStack]) {
+ NSMutableDictionary *frame = [commentStack lastObject];
+ BOOL modifyOutput = [[frame objectForKey:DISABLE_OUTPUT] boolValue];
+ if (modifyOutput) {
+ // If we're modifying output, it was enabled when this block started.
+ // Thus, it should be enabled after the block ends.
+ // If it's already enabled, this will have no harmful effect.
+ *outputEnabled = YES;
+ }
+
+ // End block.
+ [commentStack removeLastObject];
+ *blockEnded = YES;
+ }
+ *blockEnded = YES;
+
+ } else if ([marker isEqualToString:LOAD]) {
+ if (args && [args count] > 0) {
+ for (NSString *className in args) {
+ Class class = NSClassFromString(className);
+ if (class && [(id)class isKindOfClass:[NSObject class]]) {
+ if ([class conformsToProtocol:@protocol(MGTemplateFilter)]) {
+ // Instantiate and load filter.
+ NSObject <MGTemplateFilter> *obj = [[[class alloc] init] autorelease];
+ [engine loadFilter:obj];
+ } else if ([class conformsToProtocol:@protocol(MGTemplateMarker)]) {
+ // Instantiate and load marker.
+ NSObject <MGTemplateMarker> *obj = [[[class alloc] initWithTemplateEngine:engine] autorelease];
+ [engine loadMarker:obj];
+ }
+ }
+ }
+ }
+
+ } else if ([marker isEqualToString:CYCLE]) {
+ if (args && [args count] > 0) {
+ // Check to see if it's an existing cycle.
+ NSString *rangeKey = NSStringFromRange(markerRange);
+ NSMutableDictionary *cycle = [cycles objectForKey:rangeKey];
+ if (cycle) {
+ NSArray *vals = [cycle objectForKey:CYCLE_VALUES];
+ int currIndex = [[cycle objectForKey:CYCLE_INDEX] intValue];
+ currIndex++;
+ if (currIndex >= [vals count]) {
+ currIndex = 0;
+ }
+ [cycle setObject:[NSNumber numberWithInt:currIndex] forKey:CYCLE_INDEX];
+ return [vals objectAtIndex:currIndex];
+ } else {
+ // New cycle. Create and output appropriately.
+ cycle = [NSMutableDictionary dictionaryWithCapacity:2];
+ [cycle setObject:[NSNumber numberWithInt:0] forKey:CYCLE_INDEX];
+ [cycle setObject:args forKey:CYCLE_VALUES];
+ [cycles setObject:cycle forKey:rangeKey];
+ return [args objectAtIndex:0];
+ }
+ }
+ } else if ([marker isEqualToString:SET]) {
+ if (args && [args count] == 2 && *outputEnabled) {
+ // Set variable arg1 to value arg2.
+ NSDictionary *newVar = [NSDictionary dictionaryWithObject:[args objectAtIndex:1]
+ forKey:[args objectAtIndex:0]];
+ if (newVar) {
+ *newVariables = newVar;
+ }
+ }
+ }
+
+ return nil;
+}
+
+
+- (BOOL)currentBlock:(NSDictionary *)blockInfo matchesTopOfStack:(NSMutableArray *)stack
+{
+ if (blockInfo && [stack count] > 0) { // end-tag should always have blockInfo, and correspond to a stack frame.
+ NSDictionary *frame = [stack lastObject];
+ NSRange stackSectionRange = [[frame objectForKey:STACK_START_MARKER_RANGE] rangeValue];
+ NSRange thisSectionRange = [[blockInfo objectForKey:BLOCK_START_MARKER_RANGE_KEY] rangeValue];
+ if (NSEqualRanges(stackSectionRange, thisSectionRange)) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+
+- (BOOL)argIsTrue:(NSString *)arg
+{
+ BOOL argTrue = NO;
+ if (arg) {
+ NSObject *val = [engine resolveVariable:arg];
+ if (val) {
+ if ([val isKindOfClass:[NSNumber class]]) {
+ argTrue = [(NSNumber *)val boolValue];
+ } else {
+ argTrue = YES;
+ }
+ }
+ }
+ return argTrue;
+}
+
+
+- (BOOL)argIsNumeric:(NSString *)arg intValue:(int *)val checkVariables:(BOOL)checkVars
+{
+ BOOL numeric = NO;
+ int value = 0;
+
+ if (arg && [arg length] > 0) {
+ if ([[arg substringToIndex:1] isEqualToString:@"0"] || [arg intValue] != 0) {
+ numeric = YES;
+ value = [arg intValue];
+ } else if (checkVars) {
+ // Check to see if arg is a variable with an intValue.
+ NSObject *argObj = [engine resolveVariable:arg];
+ NSString *argStr = [NSString stringWithFormat:@"%@", argObj];
+ if (argObj && [argObj respondsToSelector:@selector(intValue)] &&
+ [self argIsNumeric:argStr intValue:&value checkVariables:NO]) { // avoid recursion
+ numeric = YES;
+ }
+ }
+ }
+
+ if (val) {
+ *val = value;
+ }
+ return numeric;
+}
+
+
+- (void)engineFinishedProcessingTemplate
+{
+ // Clean up stacks etc.
+ [forStack release];
+ forStack = [[NSMutableArray alloc] init];
+ [sectionStack release];
+ sectionStack = [[NSMutableArray alloc] init];
+ [ifStack release];
+ ifStack = [[NSMutableArray alloc] init];
+ [commentStack release];
+ commentStack = [[NSMutableArray alloc] init];
+ cycles = [[NSMutableDictionary alloc] init];
+}
+
+
+@end
diff --git a/Source/NSArray_DeepMutableCopy.h b/Source/NSArray_DeepMutableCopy.h
new file mode 100644
index 00000000..9f619d03
--- /dev/null
+++ b/Source/NSArray_DeepMutableCopy.h
@@ -0,0 +1,12 @@
+//
+// NSArray_DeepMutableCopy.h
+//
+// Created by Matt Gemmell on 02/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+@interface NSArray (DeepMutableCopy)
+
+- (NSMutableArray *)deepMutableCopy;
+
+@end
diff --git a/Source/NSArray_DeepMutableCopy.m b/Source/NSArray_DeepMutableCopy.m
new file mode 100644
index 00000000..72f64c06
--- /dev/null
+++ b/Source/NSArray_DeepMutableCopy.m
@@ -0,0 +1,42 @@
+//
+// NSArray_DeepMutableCopy.m
+//
+// Created by Matt Gemmell on 02/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "NSArray_DeepMutableCopy.h"
+
+
+@implementation NSArray (DeepMutableCopy)
+
+
+- (NSMutableArray *)deepMutableCopy;
+{
+ NSMutableArray *newArray;
+ unsigned int index, count;
+
+ count = [self count];
+ newArray = [[NSMutableArray allocWithZone:[self zone]] initWithCapacity:count];
+ for (index = 0; index < count; index++) {
+ id anObject;
+
+ anObject = [self objectAtIndex:index];
+ if ([anObject respondsToSelector:@selector(deepMutableCopy)]) {
+ anObject = [anObject deepMutableCopy];
+ [newArray addObject:anObject];
+ [anObject release];
+ } else if ([anObject respondsToSelector:@selector(mutableCopyWithZone:)]) {
+ anObject = [anObject mutableCopyWithZone:nil];
+ [newArray addObject:anObject];
+ [anObject release];
+ } else {
+ [newArray addObject:anObject];
+ }
+ }
+
+ return newArray;
+}
+
+
+@end
diff --git a/Source/NSDictionary_DeepMutableCopy.h b/Source/NSDictionary_DeepMutableCopy.h
new file mode 100644
index 00000000..7f94acde
--- /dev/null
+++ b/Source/NSDictionary_DeepMutableCopy.h
@@ -0,0 +1,12 @@
+//
+// NSDictionary_DeepMutableCopy.h
+//
+// Created by Matt Gemmell on 02/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+@interface NSDictionary (DeepMutableCopy)
+
+- (NSMutableDictionary *)deepMutableCopy;
+
+@end
diff --git a/Source/NSDictionary_DeepMutableCopy.m b/Source/NSDictionary_DeepMutableCopy.m
new file mode 100644
index 00000000..a5a49333
--- /dev/null
+++ b/Source/NSDictionary_DeepMutableCopy.m
@@ -0,0 +1,43 @@
+//
+// NSDictionary_DeepMutableCopy.m
+//
+// Created by Matt Gemmell on 02/05/2008.
+// Copyright 2008 Instinctive Code. All rights reserved.
+//
+
+#import "NSDictionary_DeepMutableCopy.h"
+
+
+@implementation NSDictionary (DeepMutableCopy)
+
+
+- (NSMutableDictionary *)deepMutableCopy;
+{
+ NSMutableDictionary *newDictionary;
+ NSEnumerator *keyEnumerator;
+ id anObject;
+ id aKey;
+
+ newDictionary = [self mutableCopy];
+ // Run through the new dictionary and replace any objects that respond to -deepMutableCopy or -mutableCopy with copies.
+ keyEnumerator = [[newDictionary allKeys] objectEnumerator];
+ while ((aKey = [keyEnumerator nextObject])) {
+ anObject = [newDictionary objectForKey:aKey];
+ if ([anObject respondsToSelector:@selector(deepMutableCopy)]) {
+ anObject = [anObject deepMutableCopy];
+ [newDictionary setObject:anObject forKey:aKey];
+ [anObject release];
+ } else if ([anObject respondsToSelector:@selector(mutableCopyWithZone:)]) {
+ anObject = [anObject mutableCopyWithZone:nil];
+ [newDictionary setObject:anObject forKey:aKey];
+ [anObject release];
+ } else {
+ [newDictionary setObject:anObject forKey:aKey];
+ }
+ }
+
+ return newDictionary;
+}
+
+
+@end
diff --git a/Source/RegexKitLite.h b/Source/RegexKitLite.h
new file mode 100644
index 00000000..0338f582
--- /dev/null
+++ b/Source/RegexKitLite.h
@@ -0,0 +1,130 @@
+//
+// RegexKitLite.h
+// http://regexkit.sourceforge.net/
+// Licensesd under the terms of the BSD License, as specified below.
+//
+
+/*
+ Copyright (c) 2008, John Engelhart
+
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ * Neither the name of the Zang Industries nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifdef __OBJC__
+
+#import <Foundation/NSObjCRuntime.h>
+#import <Foundation/NSRange.h>
+#import <Foundation/NSString.h>
+
+#endif // __OBJC__
+
+#include <limits.h>
+#include <sys/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// For Mac OS X < 10.5.
+#ifndef NSINTEGER_DEFINED
+#define NSINTEGER_DEFINED
+#ifdef __LP64__ || NS_BUILD_32_LIKE_64
+typedef long NSInteger;
+typedef unsigned long NSUInteger;
+#define NSIntegerMin LONG_MIN
+#define NSIntegerMax LONG_MAX
+#define NSUIntegerMax ULONG_MAX
+#else
+typedef int NSInteger;
+typedef unsigned int NSUInteger;
+#define NSIntegerMin INT_MIN
+#define NSIntegerMax INT_MAX
+#define NSUIntegerMax UINT_MAX
+#endif
+#endif // NSINTEGER_DEFINED
+
+#ifndef _REGEXKITLITE_H_
+#define _REGEXKITLITE_H_
+
+#ifdef __OBJC__
+
+@class NSError;
+
+// NSError error domains and user info keys.
+extern NSString * const RKLICURegexErrorDomain;
+
+extern NSString * const RKLICURegexErrorNameErrorKey;
+extern NSString * const RKLICURegexLineErrorKey;
+extern NSString * const RKLICURegexOffsetErrorKey;
+extern NSString * const RKLICURegexPreContextErrorKey;
+extern NSString * const RKLICURegexPostContextErrorKey;
+extern NSString * const RKLICURegexRegexErrorKey;
+extern NSString * const RKLICURegexRegexOptionsErrorKey;
+
+// These must be idential to their ICU regex counterparts. See http://www.icu-project.org/userguide/regexp.html
+enum {
+ RKLNoOptions = 0,
+ RKLCaseless = 2,
+ RKLComments = 4,
+ RKLDotAll = 32,
+ RKLMultiline = 8,
+ RKLUnicodeWordBoundaries = 256
+};
+typedef uint32_t RKLRegexOptions;
+
+@interface NSString (RegexKitLiteAdditions)
+
++ (void)clearStringCache;
+
++ (NSInteger)captureCountForRegex:(NSString *)regexString;
++ (NSInteger)captureCountForRegex:(NSString *)regexString options:(RKLRegexOptions)options error:(NSError **)error;
+
+- (BOOL)isMatchedByRegex:(NSString *)regexString;
+- (BOOL)isMatchedByRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range error:(NSError **)error;
+
+- (NSRange)rangeOfRegex:(NSString *)regexString;
+- (NSRange)rangeOfRegex:(NSString *)regexString capture:(NSInteger)capture;
+- (NSRange)rangeOfRegex:(NSString *)regexString inRange:(NSRange)range;
+- (NSRange)rangeOfRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error;
+
+- (NSString *)stringByMatching:(NSString *)regexString;
+- (NSString *)stringByMatching:(NSString *)regexString capture:(NSInteger)capture;
+- (NSString *)stringByMatching:(NSString *)regexString inRange:(NSRange)range;
+- (NSString *)stringByMatching:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error;
+
+@end
+
+#endif // _REGEXKITLITE_H_
+
+#endif // __OBJC__
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
diff --git a/Source/RegexKitLite.m b/Source/RegexKitLite.m
new file mode 100644
index 00000000..9e77bd28
--- /dev/null
+++ b/Source/RegexKitLite.m
@@ -0,0 +1,354 @@
+//
+// RegexKitLite.m
+// http://regexkit.sourceforge.net/
+// Licensesd under the terms of the BSD License, as specified below.
+//
+
+/*
+ Copyright (c) 2008, John Engelhart
+
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ * Neither the name of the Zang Industries nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#import <CoreFoundation/CFBase.h>
+#import <CoreFoundation/CFString.h>
+#import <Foundation/NSDictionary.h>
+#import <Foundation/NSError.h>
+#import <Foundation/NSException.h>
+#import <libkern/OSAtomic.h>
+#import <string.h>
+#import <stdlib.h>
+#import "RegexKitLite.h"
+
+#ifndef RKL_CACHE_SIZE
+#define RKL_CACHE_SIZE 23
+#endif
+
+#ifndef RKL_FIXED_LENGTH
+#define RKL_FIXED_LENGTH 2048
+#endif
+
+// Ugly macros to keep other parts clean.
+
+#define NSRangeInsideRange(inside, within) ({NSRange _inside = (inside), _within = (within); (((_inside.location - _within.location) <= _within.length) && ((NSMaxRange(_inside) - _within.location) <= _within.length));})
+#define NSEqualRanges(range1, range2) ({NSRange _r1 = (range1), _r2 = (range2); ((_r1.location == _r2.location) && (_r1.length == _r2.length));})
+#define NSMakeRange(loc, len) ((NSRange){(NSUInteger)(loc), (NSUInteger)(len)})
+#define CFMakeRange(loc, len) ((CFRange){(CFIndex)(loc), (CFIndex)(len)})
+#define NSMaxRange(r) ({NSRange _r = (r); _r.location + _r.length;})
+#define NSNotFoundRange ((NSRange){NSNotFound, 0})
+#define NSMaxiumRange ((NSRange){0, NSUIntegerMax})
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
+#define CFAutorelease(obj) ({CFTypeRef _obj = (obj); (_obj == NULL) ? NULL : [(id)CFMakeCollectable(_obj) autorelease]; })
+#else
+#define CFAutorelease(obj) ({CFTypeRef _obj = (obj); (_obj == NULL) ? NULL : [(id)(_obj) autorelease]; })
+#endif
+
+#define RKLMakeString(str, hash, len, uc) ((RKLString){(str), (hash), (len), (UniChar *)(uc)})
+#define RKLClearCacheSlotLastString(ce) ({ ce->last = RKLMakeString(NULL, 0, 0, NULL); ce->lastFindRange = NSNotFoundRange; ce->lastMatchRange = NSNotFoundRange; })
+#define RKLGetRangeForCapture(regex, status, capture, range) ({ range.location = (NSUInteger)uregex_start(regex, capture, &status); range.length = (NSUInteger)uregex_end(regex, capture, &status) - range.location; status; })
+#define RKLInternalException [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"An internal error occured at %@:%d", [NSString stringWithUTF8String:__FILE__], __LINE__] userInfo:NULL]
+
+// Exported symbols. Error domains, keys, etc.
+NSString * const RKLICURegexErrorDomain = @"RKLICURegexErrorDomain";
+
+NSString * const RKLICURegexErrorNameErrorKey = @"RKLICURegexErrorName";
+NSString * const RKLICURegexLineErrorKey = @"RKLICURegexLine";
+NSString * const RKLICURegexOffsetErrorKey = @"RKLICURegexOffset";
+NSString * const RKLICURegexPreContextErrorKey = @"RKLICURegexPreContext";
+NSString * const RKLICURegexPostContextErrorKey = @"RKLICURegexPostContext";
+NSString * const RKLICURegexRegexErrorKey = @"RKLICURegexRegex";
+NSString * const RKLICURegexRegexOptionsErrorKey = @"RKLICURegexRegexOptions";
+
+// Type / struct definitions
+
+typedef struct uregex uregex; // Opaque ICU regex type.
+
+#define U_PARSE_CONTEXT_LEN 16
+
+typedef struct UParseError {
+ int32_t line;
+ int32_t offset;
+ unichar preContext[U_PARSE_CONTEXT_LEN];
+ unichar postContext[U_PARSE_CONTEXT_LEN];
+} UParseError;
+
+typedef struct {
+ void *string; // Used ONLY for pointer equality tests! Never messaged!
+ CFHashCode hash;
+ NSUInteger length;
+ UniChar *uniChar;
+} RKLString;
+
+typedef struct {
+ NSString *regexString;
+ RKLRegexOptions regexOptions;
+ uregex *icu_regex;
+ NSInteger captureCount;
+
+ RKLString last;
+ NSRange lastFindRange;
+ NSRange lastMatchRange;
+} RKLCacheSlot;
+
+// ICU functions. See http://www.icu-project.org/apiref/icu4c/uregex_8h.html Tweaked slightly from the originals, but functionally identical.
+const char * u_errorName (int32_t status);
+int32_t u_strlen (const UniChar *s);
+void uregex_close (uregex *regexp);
+int32_t uregex_end (uregex *regexp, int32_t groupNum, int32_t *status);
+BOOL uregex_find (uregex *regexp, int32_t location, int32_t *status);
+BOOL uregex_findNext (uregex *regexp, int32_t *status);
+int32_t uregex_groupCount (uregex *regexp, int32_t *status);
+uregex * uregex_open (const UniChar *pattern, int32_t patternLength, RKLRegexOptions flags, UParseError *parseError, int32_t *status);
+void uregex_setText (uregex *regexp, const UniChar *text, int32_t textLength, int32_t *status);
+int32_t uregex_start (uregex *regexp, int32_t groupNum, int32_t *status);
+
+static RKLCacheSlot *getCachedRegex (NSString *regexString, RKLRegexOptions regexOptions, NSError **error);
+static NSError *RKLNSErrorForRegex (NSString *regexString, RKLRegexOptions regexOptions, UParseError *parseError, int status);
+
+// Compile unit local global variables
+static OSSpinLock cacheSpinLock = OS_SPINLOCK_INIT;
+static RKLCacheSlot RKLCache[RKL_CACHE_SIZE];
+static RKLCacheSlot *lastCacheSlot;
+static void *lastRegexString;
+static UniChar fixedUniChar[(RKL_FIXED_LENGTH * sizeof(UniChar))];
+static RKLString fixedString = {NULL, 0, 0, &fixedUniChar[0]};
+static RKLString dynamicString;
+
+// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity.
+// IMPORTANT! Should only be called with cacheSpinLock already locked!
+// ----------
+
+static RKLCacheSlot *getCachedRegex(NSString *regexString, RKLRegexOptions regexOptions, NSError **error) {
+ CFHashCode regexHash = CFHash(regexString);
+ RKLCacheSlot *cacheSlot = &RKLCache[regexHash % RKL_CACHE_SIZE]; // Retrieve the cache slot for this regex.
+ UParseError parseError = (UParseError){-1, -1, {0}, {0}};
+ UniChar *regexUniChar = NULL;
+ CFIndex regexLength = 0;
+ int32_t status = 0;
+
+ // Return the cached entry if it's a match, otherwise clear the slot and create a new ICU regex in its place.
+ if((cacheSlot->regexOptions == regexOptions) && (cacheSlot->icu_regex != NULL) && (cacheSlot->regexString != NULL) && (CFEqual(regexString, cacheSlot->regexString) == YES)) { lastCacheSlot = cacheSlot; lastRegexString = regexString; return(cacheSlot); }
+
+ RKLClearCacheSlotLastString(cacheSlot); // Clear any cached string state for this cache slot.
+ if(cacheSlot->regexString != NULL) { CFRelease(cacheSlot->regexString); cacheSlot->regexString = NULL; cacheSlot->regexOptions = 0; }
+ if(cacheSlot->icu_regex != NULL) { uregex_close(cacheSlot->icu_regex); cacheSlot->icu_regex = NULL; cacheSlot->captureCount = -1; }
+
+ cacheSlot->regexString = (NSString *)CFStringCreateCopy(NULL, (CFStringRef)regexString); // Get a cheap immutable copy.
+ cacheSlot->regexOptions = regexOptions;
+ regexLength = CFStringGetLength((CFStringRef)regexString); // In UTF16 code pairs.
+
+ // Try to quickly obtain the regex string in UTF16 format. Otherwise allocate enough space on the stack and convert to UTF16 using the stack buffer.
+ if((regexUniChar = (UniChar *)CFStringGetCharactersPtr((CFStringRef)regexString)) == NULL) {
+ if((regexUniChar = alloca(regexLength * sizeof(UniChar))) == NULL) { return(NULL); }
+ CFStringGetCharacters((CFStringRef)regexString, CFRangeMake(0, regexLength), regexUniChar);
+ }
+
+ // Create the ICU regex. If there is a problem, create a NSError if requested.
+ if(((cacheSlot->icu_regex = uregex_open(regexUniChar, (int32_t)regexLength, regexOptions, &parseError, &status)) == NULL) && (status > 0)) {
+ if(error != NULL) { *error = RKLNSErrorForRegex(regexString, regexOptions, &parseError, status); }
+ return(NULL);
+ }
+
+ cacheSlot->captureCount = (NSUInteger)uregex_groupCount(cacheSlot->icu_regex, &status);
+ lastCacheSlot = cacheSlot;
+ lastRegexString = regexString;
+
+ return(cacheSlot);
+}
+
+static NSError *RKLNSErrorForRegex(NSString *regexString, RKLRegexOptions regexOptions, UParseError *parseError, int status) {
+ NSNumber *regexOptionsNumber = [NSNumber numberWithInt:regexOptions];
+ NSNumber *lineNumber = [NSNumber numberWithInt:parseError->line];
+ NSNumber *offsetNumber = [NSNumber numberWithInt:parseError->offset];
+ NSString *preContextString = [NSString stringWithCharacters:&parseError->preContext[0] length:u_strlen(&parseError->preContext[0])];
+ NSString *postContextString = [NSString stringWithCharacters:&parseError->postContext[0] length:u_strlen(&parseError->postContext[0])];
+ NSString *errorNameString = [NSString stringWithUTF8String:u_errorName(status)];
+ NSString *reasonString = [NSString stringWithFormat:@"The error %@ occured at line %d, column %d: %@<<HERE>>%@", errorNameString, parseError->line, parseError->offset, preContextString, postContextString];
+
+ // If line == -1, parseError doesn't contain any useful information. Set lineNumber to NULL,
+ // which will stop adding objects to the dictionary at that point, ignoring everything after.
+ if(parseError->line == -1) { reasonString = [NSString stringWithFormat:@"The error %@ occured.", errorNameString]; lineNumber = NULL; }
+
+ return([NSError errorWithDomain:RKLICURegexErrorDomain code:(NSInteger)status userInfo:[NSDictionary dictionaryWithObjectsAndKeys: @"There was an error compiling the regular expression.", @"NSLocalizedDescription", reasonString, @"NSLocalizedFailureReason", regexString, RKLICURegexRegexErrorKey, regexOptionsNumber, RKLICURegexRegexOptionsErrorKey, lineNumber, RKLICURegexLineErrorKey, offsetNumber, RKLICURegexOffsetErrorKey, preContextString, RKLICURegexPreContextErrorKey, postContextString, RKLICURegexPostContextErrorKey, errorNameString, RKLICURegexErrorNameErrorKey, NULL]]);
+}
+
+@implementation NSString (RegexKitLiteAdditions)
+
++ (void)clearStringCache
+{
+ OSSpinLockLock(&cacheSpinLock);
+ fixedString = RKLMakeString(NULL, 0, 0, fixedString.uniChar);
+ dynamicString = RKLMakeString(NULL, 0, 0, reallocf(dynamicString.uniChar, 0));
+ NSUInteger x = 0;
+ for(x = 0; x < RKL_CACHE_SIZE; x++) { RKLClearCacheSlotLastString((&RKLCache[x])); }
+ OSSpinLockUnlock(&cacheSpinLock);
+}
+
++ (NSInteger)captureCountForRegex:(NSString *)regexString
+{
+ return([self captureCountForRegex:regexString options:RKLNoOptions error:NULL]);
+}
+
++ (NSInteger)captureCountForRegex:(NSString *)regexString options:(RKLRegexOptions)options error:(NSError **)error
+{
+ if(error != NULL) { *error = NULL; }
+ if(regexString == NULL) { [NSException raise:NSInvalidArgumentException format:@"The regular expression argument is NULL."]; }
+
+ RKLCacheSlot *cacheSlot = NULL;
+ NSInteger captureCount = -1;
+
+ OSSpinLockLock(&cacheSpinLock);
+ if((cacheSlot = getCachedRegex(regexString, options, error)) != NULL) { captureCount = cacheSlot->captureCount; }
+ OSSpinLockUnlock(&cacheSpinLock);
+
+ return(captureCount);
+}
+
+- (BOOL)isMatchedByRegex:(NSString *)regexString
+{
+ return([self isMatchedByRegex:regexString options:RKLNoOptions inRange:NSMaxiumRange error:NULL]);
+}
+
+- (BOOL)isMatchedByRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range error:(NSError **)error
+{
+ return(([self rangeOfRegex:regexString options:options inRange:range capture:0 error:error].location == NSNotFound) ? NO : YES);
+}
+
+- (NSString *)stringByMatching:(NSString *)regexString
+{
+ return([self stringByMatching:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:0 error:NULL]);
+}
+
+- (NSString *)stringByMatching:(NSString *)regexString capture:(NSInteger)capture
+{
+ return([self stringByMatching:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:capture error:NULL]);
+}
+
+- (NSString *)stringByMatching:(NSString *)regexString inRange:(NSRange)range
+{
+ return([self stringByMatching:regexString options:RKLNoOptions inRange:range capture:0 error:NULL]);
+}
+
+- (NSString *)stringByMatching:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error
+{
+ NSRange matchedRange = [self rangeOfRegex:regexString options:options inRange:range capture:capture error:error];
+ return((matchedRange.location == NSNotFound) ? NULL : CFAutorelease(CFStringCreateWithSubstring(NULL, (CFStringRef)self, CFMakeRange(matchedRange.location, matchedRange.length))));
+}
+
+- (NSRange)rangeOfRegex:(NSString *)regexString
+{
+ return([self rangeOfRegex:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:0 error:NULL]);
+}
+
+- (NSRange)rangeOfRegex:(NSString *)regexString capture:(NSInteger)capture
+{
+ return([self rangeOfRegex:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:capture error:NULL]);
+}
+
+- (NSRange)rangeOfRegex:(NSString *)regexString inRange:(NSRange)range
+{
+ return([self rangeOfRegex:regexString options:RKLNoOptions inRange:range capture:0 error:NULL]);
+}
+
+
+// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity.
+// ----------
+
+- (NSRange)rangeOfRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error
+{
+ if(error != NULL) { *error = NULL; }
+ if(regexString == NULL) { [NSException raise:NSInvalidArgumentException format:@"The regular expression argument is NULL."]; }
+
+ NSRange captureRange = NSNotFoundRange;
+ CFIndex stringLength = CFStringGetLength((CFStringRef)self); // In UTF16 code pairs.
+ RKLCacheSlot *cacheSlot = NULL;
+ NSException *exception = NULL;
+ int32_t status = 0;
+
+ if(range.length == NSUIntegerMax) { range.length = stringLength; } // For convenience.
+ if((NSUInteger)stringLength < NSMaxRange(range)) { [NSException raise:NSRangeException format:@"The search range exceeds the strings bounds."]; }
+
+ // IMPORTANT! Once we have obtained the lock, code MUST exit via 'goto exitNow;' to unlock the lock! NO EXCEPTIONS!
+
+ OSSpinLockLock(&cacheSpinLock); // Grab the lock and get cache entry.
+ // Fast path the common case where this regex is the same one used last time.
+ // On a miss, do full lookup with getCachedRegex(), which compiles the regex if it's not in the cache.
+ if((lastCacheSlot != NULL) && (options == lastCacheSlot->regexOptions) && (CFEqual(regexString, lastCacheSlot->regexString) == YES)) { cacheSlot = lastCacheSlot; }
+ else if((cacheSlot = getCachedRegex(regexString, options, error)) == NULL) { goto exitNow; }
+ if(cacheSlot->icu_regex == NULL) { exception = RKLInternalException; goto exitNow; } // assertion check.
+
+ if((capture < 0) || (capture > cacheSlot->captureCount)) { exception = [NSException exceptionWithName:NSInvalidArgumentException reason:@"The capture argument is not valid." userInfo:NULL]; goto exitNow; }
+
+ RKLString selfString = RKLMakeString(self, CFHash(self), stringLength, CFStringGetCharactersPtr((CFStringRef)self));
+ // *string will point to the most approrpiate buffer. If selfString contains a valid uniChar pointer, that's used.
+ // Otherwise, use the strings length to determine if the fixed or dynamically sized conversion buffer should be used.
+ RKLString *string = (selfString.uniChar != NULL) ? &selfString : (stringLength < RKL_FIXED_LENGTH) ? &fixedString : &dynamicString;
+
+ // Check if this regex is already set to this string.
+ if((cacheSlot->last.uniChar == string->uniChar) && (cacheSlot->last.string == selfString.string) && (cacheSlot->last.hash == selfString.hash) && (cacheSlot->last.length == selfString.length) && (cacheSlot->last.string != NULL)) { goto alreadySetText; }
+
+ // If we didn't get direct UTF16 access, perform any required UTF16 conversions if the current buffer doesn't match this string.
+ if((string != &selfString) && ((string->string != self) || (string->length != selfString.length) || (string->hash != selfString.hash))) {
+ *string = RKLMakeString(self, selfString.hash, selfString.length, string->uniChar);
+ // If this is the dynamically sized buffer, resize the allocation to the correct size.
+ if((stringLength >= RKL_FIXED_LENGTH) && ((string->uniChar = reallocf(string->uniChar, (selfString.length * sizeof(UniChar)))) == NULL)) { goto exitNow; }
+ CFStringGetCharacters((CFStringRef)self, CFRangeMake(0, string->length), string->uniChar); // Convert to a UTF16 string.
+ }
+
+ RKLClearCacheSlotLastString(cacheSlot); // Clear the cached state for this regex.
+ if(string->uniChar == NULL) { exception = RKLInternalException; goto exitNow; } // assertion check.
+ uregex_setText(cacheSlot->icu_regex, string->uniChar, string->length, &status); // "set" the ICU regex to this string.
+ if(status != 0) { goto exitNow; }
+ cacheSlot->last = *string; // Cache the last string we set this regex to.
+
+ alreadySetText:
+ if((NSEqualRanges(range, cacheSlot->lastFindRange) == NO)) { // Perform a 'find' if the current range is different than the last find range.
+ // Using uregex_findNext can be a slight performance win.
+ BOOL useFindNext = (range.location == (NSMaxRange(cacheSlot->lastMatchRange) + ((cacheSlot->lastMatchRange.length == 0) ? 1 : 0))) ? YES : NO;
+
+ cacheSlot->lastFindRange = NSNotFoundRange; // Cleared the cached search/find range.
+ if(useFindNext == NO) { if((uregex_find (cacheSlot->icu_regex, range.location, &status) == NO) || (status != 0)) { goto exitNow; } }
+ else { if((uregex_findNext(cacheSlot->icu_regex, &status) == NO) || (status != 0)) { goto exitNow; } }
+
+ if(RKLGetRangeForCapture(cacheSlot->icu_regex, status, 0, cacheSlot->lastMatchRange) != 0) { goto exitNow; }
+ cacheSlot->lastFindRange = range; // Cache the successful search/find range.
+ }
+
+ if(NSRangeInsideRange(cacheSlot->lastMatchRange, range) == NO) { goto exitNow; } // If the regex matched outside the requested range, exit.
+ if(capture == 0) { captureRange = cacheSlot->lastMatchRange; } else { RKLGetRangeForCapture(cacheSlot->icu_regex, status, capture, captureRange); }
+
+ exitNow: // A bit of advice...
+ OSSpinLockUnlock(&cacheSpinLock); // Always... no, no... never... forget to unlock your locks.
+ if(exception != NULL) { [exception raise]; } // I think the young people enjoy it when I "get down" verbally, don't you?
+ if(status > 0) { [NSException raise:NSInternalInconsistencyException format:@"ICU regular expression error #%d, %s", status, u_errorName(status)]; }
+ return((status == 0) ? captureRange : NSNotFoundRange);
+}
+
+@end
diff --git a/Source/TableContent.h b/Source/TableContent.h
index cc242aba..6fa157a1 100644
--- a/Source/TableContent.h
+++ b/Source/TableContent.h
@@ -58,7 +58,7 @@
CMMCPConnection *mySQLConnection;
id editData;
- NSString *selectedTable;
+ NSString *selectedTable, *usedQuery;
NSMutableArray *fullResult, *filteredResult, *keys;
NSMutableDictionary *oldRow;
NSString *compareType, *sortField;
@@ -75,6 +75,8 @@
- (IBAction)filterTable:(id)sender;
- (IBAction)showAll:(id)sender;
- (IBAction)toggleFilterField:(id)sender;
+- (NSString *)usedQuery;
+- (void)setUsedQuery:(NSString *)query;
//edit methods
- (IBAction)addRow:(id)sender;
diff --git a/Source/TableContent.m b/Source/TableContent.m
index dfe9b24c..2a359d07 100644
--- a/Source/TableContent.m
+++ b/Source/TableContent.m
@@ -50,6 +50,7 @@
sortField = nil;
areShowingAllRows = false;
currentlyEditingRow = -1;
+ usedQuery = [[NSString stringWithString:@""] retain];
return self;
}
@@ -297,6 +298,8 @@
[limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
}
+ [self setUsedQuery:query];
+
queryResult = [mySQLConnection queryString:query];
if ( queryResult == nil ) {
NSLog(@"Loading table data for %@ failed, query string was: %@", aTable, query);
@@ -393,6 +396,9 @@
[limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
[limitRowsField selectText:self];
}
+
+ [self setUsedQuery:queryString];
+
queryResult = [mySQLConnection queryString:queryString];
// [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]];
[fullResult setArray:[self fetchResultAsArray:queryResult]];
@@ -605,6 +611,8 @@
[limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]];
}
+ [self setUsedQuery:queryString];
+
theResult = [mySQLConnection queryString:queryString];
[filteredResult setArray:[self fetchResultAsArray:theResult]];
@@ -654,6 +662,18 @@
[argumentField setEnabled:(![[[compareField selectedItem] title] hasSuffix:@"NULL"])];
}
+- (NSString *)usedQuery
+{
+ return usedQuery;
+}
+
+- (void)setUsedQuery:(NSString *)query
+{
+ if(usedQuery)
+ [usedQuery release];
+ usedQuery = [[NSString stringWithString:query] retain];
+}
+
#pragma mark Edit methods
@@ -1659,6 +1679,7 @@
[limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]];
}
+ [self setUsedQuery:queryString];
queryResult = [mySQLConnection queryString:queryString];
// [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]];
[fullResult setArray:[self fetchResultAsArray:queryResult]];
@@ -2189,6 +2210,7 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn
[compareType release];
if (sortField) [sortField release];
[prefs release];
+ [usedQuery release];
[super dealloc];
}
diff --git a/Source/TableDocument.h b/Source/TableDocument.h
index f58faad5..ad66ef55 100644
--- a/Source/TableDocument.h
+++ b/Source/TableDocument.h
@@ -25,6 +25,7 @@
#import <Cocoa/Cocoa.h>
#import <MCPKit_bundled/MCPKit_bundled.h>
+#import <WebKit/WebKit.h>
@class CMMCPConnection, CMMCPResult;
@@ -96,6 +97,8 @@
NSToolbar *mainToolbar;
NSToolbarItem *chooseDatabaseToolbarItem;
+
+ WebView *printWebView;
}
//start sheet
@@ -119,6 +122,8 @@
sshPort:(NSString *)sshPort; // no-longer in use
- (NSMutableArray *)favorites;
+- (NSString *)getHTMLforPrint;
+
//alert sheets method
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo;
diff --git a/Source/TableDocument.m b/Source/TableDocument.m
index 19825570..49962fcc 100644
--- a/Source/TableDocument.m
+++ b/Source/TableDocument.m
@@ -42,6 +42,10 @@
#import "CMMCPConnection.h"
#import "CMMCPResult.h"
+//used for printing
+#import "MGTemplateEngine.h"
+#import "ICUTemplateMatcher.h"
+
NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocumentFavoritesControllerSelectionIndexDidChange";
NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFavoritesControllerFavoritesDidChange";
@@ -60,7 +64,8 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocum
chooseDatabaseButton = nil;
chooseDatabaseToolbarItem = nil;
}
-
+ printWebView = [[WebView alloc] init];
+ [printWebView setFrameLoadDelegate:self];
return self;
}
@@ -97,14 +102,116 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocum
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
-- (NSPrintOperation *)printOperationWithSettings:(NSDictionary *)ps error:(NSError **)e
-{
+- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
+{
+ //because I need the webFrame loaded (for preview), I've moved the actuall printing here.
NSPrintInfo *printInfo = [self printInfo];
[printInfo setHorizontalPagination:NSFitPagination];
[printInfo setVerticalPagination:NSAutoPagination];
- NSPrintOperation *printOp = [NSPrintOperation printOperationWithView:[[tableTabView selectedTabViewItem] view] printInfo:printInfo];
- return printOp;
+ [printInfo setVerticallyCentered:NO];
+ [printInfo setTopMargin:30];
+ [printInfo setBottomMargin:30];
+ [printInfo setLeftMargin:10];
+ [printInfo setRightMargin:10];
+
+ NSPrintOperation *op = [NSPrintOperation
+ printOperationWithView:[[[printWebView mainFrame] frameView] documentView]
+ printInfo:printInfo];
+
+ //add ability to select orientation to print panel
+ NSPrintPanel *printPanel = [op printPanel];
+ [printPanel setOptions:[printPanel options] + NSPrintPanelShowsOrientation + NSPrintPanelShowsScaling];
+ [op setPrintPanel:printPanel];
+
+ [op runOperationModalForWindow:tableWindow
+ delegate:self
+ didRunSelector:
+ @selector(printOperationDidRun:success:contextInfo:)
+ contextInfo:NULL];
+
+}
+
+- (IBAction)printDocument:(id)sender
+{
+ //here load the printing document. The actual printing is done in the doneLoading delegate.
+ [[printWebView mainFrame] loadHTMLString:[self getHTMLforPrint] baseURL:nil];
+}
+
+- (void)printOperationDidRun:(NSPrintOperation *)printOperation
+ success:(BOOL)success
+ contextInfo:(void *)info
+{
+ //selector for print... maybe we can get rid of this?
+}
+
+- (NSString *)getHTMLforPrint
+{
+ // Set up template engine with your chosen matcher.
+ MGTemplateEngine *engine = [MGTemplateEngine templateEngine];
+ [engine setMatcher:[ICUTemplateMatcher matcherWithTemplateEngine:engine]];
+
+ NSString *versionForPrint = [NSString stringWithFormat:@"%@ %@ (build %@)",
+ [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"],
+ [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"],
+ [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]
+ ];
+
+ NSMutableDictionary *connection = [[NSMutableDictionary alloc] init];
+ if([[self user] length])
+ [connection setValue:[self user] forKey:@"username"];
+ [connection setValue:[self host] forKey:@"hostname"];
+ if([[portField stringValue] length])
+ [connection setValue:[portField stringValue] forKey:@"port"];
+ [connection setValue:selectedDatabase forKey:@"database"];
+ [connection setValue:versionForPrint forKey:@"version"];
+
+ NSArray *columns, *rows;
+ columns = rows = [[NSArray alloc] init];
+
+ if ( [tableTabView indexOfTabViewItem:[tableTabView selectedTabViewItem]] == 0 ){
+ if([[tableSourceInstance tableStructureForPrint] count] > 0)
+ columns = [[NSArray alloc] initWithArray:[[tableSourceInstance tableStructureForPrint] objectAtIndex:0] copyItems:YES];
+ if([[tableSourceInstance tableStructureForPrint] count] > 1)
+ rows = [[NSArray alloc] initWithArray:
+ [[tableSourceInstance tableStructureForPrint] objectsAtIndexes:
+ [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[tableSourceInstance tableStructureForPrint] count]-1)]
+ ]
+ ];
+ }
+ else if ( [tableTabView indexOfTabViewItem:[tableTabView selectedTabViewItem]] == 1 ){
+ if([[tableContentInstance currentResult] count] > 0)
+ columns = [[NSArray alloc] initWithArray:[[tableContentInstance currentResult] objectAtIndex:0] copyItems:YES];
+ if([[tableContentInstance currentResult] count] > 1)
+ rows = [[NSArray alloc] initWithArray:
+ [[tableContentInstance currentResult] objectsAtIndexes:
+ [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[tableContentInstance currentResult] count]-1)]
+ ]
+ ];
+ [connection setValue:[tableContentInstance usedQuery] forKey:@"query"];
+ }
+ else if ( [tableTabView indexOfTabViewItem:[tableTabView selectedTabViewItem]] == 2 ){
+ if([[customQueryInstance currentResult] count] > 0)
+ columns = [[NSArray alloc] initWithArray:[[customQueryInstance currentResult] objectAtIndex:0] copyItems:YES];
+ if([[customQueryInstance currentResult] count] > 1)
+ rows = [[NSArray alloc] initWithArray:
+ [[customQueryInstance currentResult] objectsAtIndexes:
+ [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[customQueryInstance currentResult] count]-1)]
+ ]
+ ];
+ [connection setValue:[customQueryInstance usedQuery] forKey:@"query"];
+ }
+
+ [engine setObject:connection forKey:@"c"];
+ // Get path to template.
+ NSString *templatePath = [[NSBundle mainBundle] pathForResource:@"sequel-pro-print-template" ofType:@"html"];
+ NSDictionary *print_data = [NSDictionary dictionaryWithObjectsAndKeys:
+ columns, @"columns",
+ rows, @"rows",
+ nil];
+ // Process the template and display the results.
+ NSString *result = [engine processTemplateInFileAtPath:templatePath withVariables:print_data];
+ return result;
}
- (CMMCPConnection *)sharedConnection
@@ -1740,10 +1847,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocum
* Invoked when the document window is about to close
*/
- (void)windowWillClose:(NSNotification *)aNotification
-{
- //reset print settings, so we're not prompted about saving them
- [self setPrintInfo:[NSPrintInfo sharedPrintInfo]];
-
+{
if ([mySQLConnection isConnected]) [self closeConnection];
if ([[[SPQueryConsole sharedQueryConsole] window] isVisible]) [self toggleConsole:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
diff --git a/Source/TableSource.h b/Source/TableSource.h
index e97602f5..58964827 100644
--- a/Source/TableSource.h
+++ b/Source/TableSource.h
@@ -96,6 +96,7 @@
- (NSString *)defaultValueForField:(NSString *)field;
- (NSArray *)fieldNames;
- (NSDictionary *)enumFields;
+- (NSArray *)tableStructureForPrint;
//tableView datasource methods
- (int)numberOfRowsInTableView:(NSTableView *)aTableView;
diff --git a/Source/TableSource.m b/Source/TableSource.m
index 92aa9811..de6bca6c 100644
--- a/Source/TableSource.m
+++ b/Source/TableSource.m
@@ -91,7 +91,7 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab
[tableFields setArray:[self fetchResultAsArray:tableSourceResult]];
[tableSourceResult release];
- indexResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW INDEX FROM %@", [selectedTable backtickQuotedString]]] retain];
+ indexResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"NSPrintPanelShowsPageSetupAccessory %@", [selectedTable backtickQuotedString]]] retain];
// [indexes setArray:[[self fetchResultAsArray:indexResult] retain]];
[indexes setArray:[self fetchResultAsArray:indexResult]];
[indexResult release];
@@ -832,6 +832,23 @@ returns a dictionary containing enum/set field names as key and possible values
return [NSDictionary dictionaryWithDictionary:enumFields];
}
+- (NSArray *)tableStructureForPrint
+{
+ CMMCPResult *queryResult;
+ NSMutableArray *tempResult = [NSMutableArray array];
+ int i;
+
+ queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]];
+
+ if ([queryResult numOfRows]) [queryResult dataSeek:0];
+ [tempResult addObject:[queryResult fetchFieldNames]];
+ for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) {
+ [tempResult addObject:[queryResult fetchRowAsArray]];
+ }
+
+ return tempResult;
+}
+
#pragma mark TableView datasource methods
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj
index c9676fe4..83d5c308 100644
--- a/sequel-pro.xcodeproj/project.pbxproj
+++ b/sequel-pro.xcodeproj/project.pbxproj
@@ -55,6 +55,16 @@
17E6420A0EF020CB001BC333 /* DBView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 17E642060EF020CB001BC333 /* DBView.xib */; };
17E6423B0EF0216C001BC333 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17E642390EF0216C001BC333 /* Credits.rtf */; };
17E6423E0EF0218B001BC333 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 17E6423C0EF0218B001BC333 /* InfoPlist.strings */; };
+ 296DC89F0F8FD336002A3258 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296DC89E0F8FD336002A3258 /* WebKit.framework */; };
+ 296DC8B60F909194002A3258 /* MGTemplateEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8A70F909194002A3258 /* MGTemplateEngine.m */; };
+ 296DC8B70F909194002A3258 /* RegexKitLite.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8AB0F909194002A3258 /* RegexKitLite.m */; };
+ 296DC8B80F909194002A3258 /* ICUTemplateMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8AC0F909194002A3258 /* ICUTemplateMatcher.m */; };
+ 296DC8B90F909194002A3258 /* MGTemplateStandardMarkers.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8AD0F909194002A3258 /* MGTemplateStandardMarkers.m */; };
+ 296DC8BA0F909194002A3258 /* NSArray_DeepMutableCopy.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8AE0F909194002A3258 /* NSArray_DeepMutableCopy.m */; };
+ 296DC8BB0F909194002A3258 /* NSDictionary_DeepMutableCopy.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8B10F909194002A3258 /* NSDictionary_DeepMutableCopy.m */; };
+ 296DC8BC0F909194002A3258 /* MGTemplateStandardFilters.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8B40F909194002A3258 /* MGTemplateStandardFilters.m */; };
+ 296DC8BF0F9091DF002A3258 /* libicucore.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 296DC8BE0F9091DF002A3258 /* libicucore.dylib */; };
+ 296DC8D20F90950C002A3258 /* sequel-pro-print-template.html in Resources */ = {isa = PBXBuildFile; fileRef = 296DC8D10F90950C002A3258 /* sequel-pro-print-template.html */; };
4DECC3350EC2A170008D359E /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3320EC2A170008D359E /* Sparkle.framework */; };
4DECC3360EC2A170008D359E /* MCPKit_bundled.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3330EC2A170008D359E /* MCPKit_bundled.framework */; };
4DECC3370EC2A170008D359E /* Growl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3340EC2A170008D359E /* Growl.framework */; };
@@ -225,6 +235,26 @@
17E642070EF020CB001BC333 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = English.lproj/DBView.xib; sourceTree = "<group>"; };
17E6423A0EF0216C001BC333 /* English */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = English; path = Interfaces/English.lproj/Credits.rtf; sourceTree = SOURCE_ROOT; };
17E6423D0EF0218B001BC333 /* English */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = English; path = Interfaces/English.lproj/InfoPlist.strings; sourceTree = SOURCE_ROOT; };
+ 296DC89E0F8FD336002A3258 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = /System/Library/Frameworks/WebKit.framework; sourceTree = "<absolute>"; };
+ 296DC8A50F909194002A3258 /* MGTemplateMarker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTemplateMarker.h; sourceTree = "<group>"; };
+ 296DC8A60F909194002A3258 /* MGTemplateFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTemplateFilter.h; sourceTree = "<group>"; };
+ 296DC8A70F909194002A3258 /* MGTemplateEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTemplateEngine.m; sourceTree = "<group>"; };
+ 296DC8A80F909194002A3258 /* MGTemplateEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTemplateEngine.h; sourceTree = "<group>"; };
+ 296DC8A90F909194002A3258 /* ICUTemplateMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICUTemplateMatcher.h; sourceTree = "<group>"; };
+ 296DC8AA0F909194002A3258 /* DeepMutableCopy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DeepMutableCopy.h; sourceTree = "<group>"; };
+ 296DC8AB0F909194002A3258 /* RegexKitLite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegexKitLite.m; sourceTree = "<group>"; };
+ 296DC8AC0F909194002A3258 /* ICUTemplateMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICUTemplateMatcher.m; sourceTree = "<group>"; };
+ 296DC8AD0F909194002A3258 /* MGTemplateStandardMarkers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTemplateStandardMarkers.m; sourceTree = "<group>"; };
+ 296DC8AE0F909194002A3258 /* NSArray_DeepMutableCopy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSArray_DeepMutableCopy.m; sourceTree = "<group>"; };
+ 296DC8AF0F909194002A3258 /* NSArray_DeepMutableCopy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSArray_DeepMutableCopy.h; sourceTree = "<group>"; };
+ 296DC8B00F909194002A3258 /* RegexKitLite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RegexKitLite.h; sourceTree = "<group>"; };
+ 296DC8B10F909194002A3258 /* NSDictionary_DeepMutableCopy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSDictionary_DeepMutableCopy.m; sourceTree = "<group>"; };
+ 296DC8B20F909194002A3258 /* NSDictionary_DeepMutableCopy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSDictionary_DeepMutableCopy.h; sourceTree = "<group>"; };
+ 296DC8B30F909194002A3258 /* MGTemplateStandardMarkers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTemplateStandardMarkers.h; sourceTree = "<group>"; };
+ 296DC8B40F909194002A3258 /* MGTemplateStandardFilters.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTemplateStandardFilters.m; sourceTree = "<group>"; };
+ 296DC8B50F909194002A3258 /* MGTemplateStandardFilters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTemplateStandardFilters.h; sourceTree = "<group>"; };
+ 296DC8BE0F9091DF002A3258 /* libicucore.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libicucore.dylib; path = usr/lib/libicucore.dylib; sourceTree = SDKROOT; };
+ 296DC8D10F90950C002A3258 /* sequel-pro-print-template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "sequel-pro-print-template.html"; sourceTree = "<group>"; };
2A37F4C4FDCFA73011CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = "<absolute>"; };
2A37F4C5FDCFA73011CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
4DECC3320EC2A170008D359E /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Frameworks/Sparkle.framework; sourceTree = "<group>"; };
@@ -301,6 +331,8 @@
4DECC3360EC2A170008D359E /* MCPKit_bundled.framework in Frameworks */,
4DECC3370EC2A170008D359E /* Growl.framework in Frameworks */,
B5EAC0FD0EC87FF900CC579C /* Security.framework in Frameworks */,
+ 296DC89F0F8FD336002A3258 /* WebKit.framework in Frameworks */,
+ 296DC8BF0F9091DF002A3258 /* libicucore.dylib in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -310,6 +342,8 @@
1058C7A6FEA54F5311CA2CBB /* Linked Frameworks */ = {
isa = PBXGroup;
children = (
+ 296DC8BE0F9091DF002A3258 /* libicucore.dylib */,
+ 296DC89E0F8FD336002A3258 /* WebKit.framework */,
4DECC3320EC2A170008D359E /* Sparkle.framework */,
4DECC3330EC2A170008D359E /* MCPKit_bundled.framework */,
4DECC3340EC2A170008D359E /* Growl.framework */,
@@ -391,6 +425,7 @@
17E641430EF01E90001BC333 /* Resources */ = {
isa = PBXGroup;
children = (
+ 296DC8D10F90950C002A3258 /* sequel-pro-print-template.html */,
17E6418B0EF01FF7001BC333 /* Images */,
1703EF2A0F0B0742005BBE7E /* english_help */,
B58731270F838C9E00087794 /* PreferenceDefaults.plist */,
@@ -466,6 +501,7 @@
17E6416E0EF01F3B001BC333 /* Other */ = {
isa = PBXGroup;
children = (
+ 296DC8A40F90914B002A3258 /* MGTemplateEngine */,
17E6416F0EF01F4C001BC333 /* Keychain */,
17E641700EF01F52001BC333 /* MCPKit */,
58FEF15E0F23D60A00518E8E /* Parsing */,
@@ -596,6 +632,30 @@
name = Products;
sourceTree = "<group>";
};
+ 296DC8A40F90914B002A3258 /* MGTemplateEngine */ = {
+ isa = PBXGroup;
+ children = (
+ 296DC8A50F909194002A3258 /* MGTemplateMarker.h */,
+ 296DC8A60F909194002A3258 /* MGTemplateFilter.h */,
+ 296DC8A70F909194002A3258 /* MGTemplateEngine.m */,
+ 296DC8A80F909194002A3258 /* MGTemplateEngine.h */,
+ 296DC8A90F909194002A3258 /* ICUTemplateMatcher.h */,
+ 296DC8AA0F909194002A3258 /* DeepMutableCopy.h */,
+ 296DC8AB0F909194002A3258 /* RegexKitLite.m */,
+ 296DC8AC0F909194002A3258 /* ICUTemplateMatcher.m */,
+ 296DC8AD0F909194002A3258 /* MGTemplateStandardMarkers.m */,
+ 296DC8AE0F909194002A3258 /* NSArray_DeepMutableCopy.m */,
+ 296DC8AF0F909194002A3258 /* NSArray_DeepMutableCopy.h */,
+ 296DC8B00F909194002A3258 /* RegexKitLite.h */,
+ 296DC8B10F909194002A3258 /* NSDictionary_DeepMutableCopy.m */,
+ 296DC8B20F909194002A3258 /* NSDictionary_DeepMutableCopy.h */,
+ 296DC8B30F909194002A3258 /* MGTemplateStandardMarkers.h */,
+ 296DC8B40F909194002A3258 /* MGTemplateStandardFilters.m */,
+ 296DC8B50F909194002A3258 /* MGTemplateStandardFilters.h */,
+ );
+ name = MGTemplateEngine;
+ sourceTree = "<group>";
+ };
2A37F4AAFDCFA73011CA2CEA /* sequel-pro */ = {
isa = PBXGroup;
children = (
@@ -751,6 +811,7 @@
B58731280F838C9E00087794 /* PreferenceDefaults.plist in Resources */,
B52460DB0F8EF93B00171639 /* Console.xib in Resources */,
B52461030F8EF9F500171639 /* ConnectionView.xib in Resources */,
+ 296DC8D20F90950C002A3258 /* sequel-pro-print-template.html in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -811,6 +872,13 @@
B57747DC0F7A89D0003B34F9 /* SPFavoriteTextFieldCell.m in Sources */,
B52460D70F8EF92300171639 /* SPArrayAdditions.m in Sources */,
B52460D80F8EF92300171639 /* SPTextViewAdditions.m in Sources */,
+ 296DC8B60F909194002A3258 /* MGTemplateEngine.m in Sources */,
+ 296DC8B70F909194002A3258 /* RegexKitLite.m in Sources */,
+ 296DC8B80F909194002A3258 /* ICUTemplateMatcher.m in Sources */,
+ 296DC8B90F909194002A3258 /* MGTemplateStandardMarkers.m in Sources */,
+ 296DC8BA0F909194002A3258 /* NSArray_DeepMutableCopy.m in Sources */,
+ 296DC8BB0F909194002A3258 /* NSDictionary_DeepMutableCopy.m in Sources */,
+ 296DC8BC0F909194002A3258 /* MGTemplateStandardFilters.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};