1 <?php
2 /**
3 * Contains ScriptFilter and Result class for Alphred to work with script filters
4 *
5 * PHP version 5
6 *
7 * @package Alphred
8 * @copyright Shawn Patrick Rice 2014
9 * @license http://opensource.org/licenses/MIT MIT
10 * @version 1.0.0
11 * @author Shawn Patrick Rice <rice@shawnrice.org>
12 * @link http://www.github.com/shawnrice/alphred
13 * @link http://shawnrice.github.io/alphred
14 * @since File available since Release 1.0.0
15 *
16 */
17
18 namespace Alphred;
19
20 /**
21 * Creates Script Filter XML for Alfred
22 *
23 * Example usage:
24 *
25 * ````php
26 * $script_filter = new Alphred\ScriptFilter( ['error_on_empty'] => true );
27 * $script_filter->add_result( new Alphred\Result([
28 * 'title' => 'This is a title',
29 * 'subtitle' => 'This is a subtitle',
30 * 'valid' => false
31 * ]));
32 * $script_filter->to_xml();
33 * ````
34 *
35 * @uses Result Result items are stored in the script filter
36 * @since 1.0.0
37 *
38 */
39 class ScriptFilter {
40 /**
41 * Constructs a script filter container
42 *
43 * @since 1.0.0
44 *
45 * @param array $options specify options:... see>>>?
46 */
47 public function __construct( $options = [] ) {
48
49 $this->i18n = false;
50 foreach ( ['localize', 'localise', 'i18n' ] as $localize ) :
51 if ( isset( $options[ $localize ] ) && $options[ $localize ] ) {
52 $this->initializei18n();
53 break;
54 }
55 endforeach;
56
57 // We'll just save all the options for later use if necessary
58 $this->options = $options;
59
60 // Create an array to hold the results
61 $this->results = [];
62
63 // Create the XML writer
64 $this->xml = new \XMLWriter();
65
66 }
67
68 /**
69 * Initializes a i18n Alphred object to use internally
70 *
71 * @since 1.0.0
72 * @see \Alphred\i18n
73 *
74 */
75 private function initializei18n() {
76 if ( class_exists( '\Alphred\i18n' ) ) {
77 $this->il18 = new i18n;
78 } else {
79 \Alphred\Log::console( 'Error: cannot find i18n class.', 0 );
80 }
81 }
82
83 /**
84 * Translates a string using the i18n class
85 *
86 * @since 1.0.0
87 * @see \Alphred\i18n
88 *
89 * @param string $string a string to translate
90 * @return string the string, translated if possible
91 */
92 private function translate( $string ) {
93 // Check if the translation is turned on
94 if ( ! $this->i18n ) {
95 // No translation, so just return the string
96 return $string;
97 }
98 // Try to return the translation
99 return $this->i18n->translate( $string );
100 }
101
102
103 /**
104 * Adds a result into the script filter
105 *
106 * @since 1.0.0
107 * @see \Alphred\Result
108 *
109 * @param \Alphred\Result $result an Alphred\Result object
110 */
111 public function add_result( \Alphred\Result $result ) {
112 if ( ! ( is_object( $result ) && ( 'Alphred\Result' == get_class( $result ) ) ) ) {
113 // Double-check that the namespacing doesn't affect the return value of "get_class"
114 // raise an exception instead
115 return false;
116 }
117 array_push( $this->results, $result );
118 }
119
120
121 /**
122 * Returns an array of the results
123 *
124 * @since 1.0.0
125 *
126 * @return array an array of the current result items
127 */
128 public function get_results() {
129 return $this->results;
130 }
131
132 /**
133 * Alias of to_xml()
134 *
135 * @since 1.0.0
136 * @see \Alphred\ScriptFilter::to_xml()
137 */
138 public function print_results() {
139 $this->to_xml();
140 }
141
142 /**
143 * Outputs the script filter in Alfred XML
144 *
145 * @since 1.0.0
146 *
147 */
148 public function to_xml() {
149
150 // If the user requested to have an item when the script filter was empty, then we'll
151 // supply a very generic one
152 if ( isset( $this->options['error_on_empty'] ) ) {
153 if ( 0 === count( $this->get_results() ) ) {
154 // A generic "no results found" response
155 $result = new Result( [
156 'title' => 'Error: No results found.',
157 'icon' => '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/Unsupported.icns',
158 'subtitle' => 'Please try a different query.',
159 'autocomplete' => '',
160 'valid' => false
161 ]);
162 $this->add_result( $result );
163 }
164 }
165
166 // Begin the XML generation
167 $this->xml->openMemory();
168 $this->xml->setIndent( 4 );
169 $this->xml->startDocument( '1.0', 'UTF-8' );
170 $this->xml->startElement( 'items' );
171
172 // Cycle through all results and generate the XML
173 foreach ( $this->results as $result ) :
174 $this->write_item( $result );
175 endforeach;
176 // End the xml document
177 $this->xml->endDocument();
178 // Print out the XML
179 print $this->xml->outputMemory();
180 }
181
182 /**
183 * Writes out the Alfred XML
184 *
185 * @since 1.0.0
186 *
187 * @param object $item An \Alphred\Result object
188 */
189 private function write_item( $item ) {
190 // The information we need is stored in the sub variable, so let's just get that
191 $item = $item->data;
192 // These go in the 'item' part as an attribute
193 $attributes = [ 'uid', 'arg', 'autocomplete' ];
194 // This is either true or false
195 $bool = [ 'valid' ];
196
197 // Start the element
198 $this->xml->startElement( 'item' );
199
200 // Cycle through all the attributes. If they are set, then write them out
201 foreach ( $attributes as $v ) :
202 if ( isset( $item[ $v ] ) ) {
203 $this->xml->writeAttribute( $v, $item[ $v ] );
204 }
205 endforeach;
206
207 // Translate 'valid' from a boolean to the 'yes' or 'no' value that Alfred wants to see
208 if ( isset( $item['valid'] ) && in_array( strtolower( $item['valid'] ), ['yes', 'no', true, false] ) ) {
209 if ( 'no' == strtolower( $item['valid'] ) ) {
210 $item['valid'] = false;
211 }
212 $valid = $item['valid'] ? 'yes' : 'no';
213 $this->xml->writeAttribute( 'valid', $valid );
214 }
215
216 // Cycle through the $item array and set everything. The keys are the, well, keys, and
217 // the values are the values. ( $array => xml )
218 foreach ( $item as $k => $v ) :
219 // Make suure that the bit of data is not in either the $attributes or $bool array
220 if ( ! in_array( $k, array_merge( $attributes, $bool ) ) ) {
221 // Check to see, first, if we need to add attributes by parsing the key
222 if ( false !== strpos( $k, '_' ) && 0 === strpos( $k, 'subtitle' ) ) {
223 $this->xml->startElement( substr( $k, 0, strpos( $k, '_' ) ) );
224 $this->xml->writeAttribute( 'mod', substr( $k, strpos( $k, '_' ) + 1 ) );
225 } else if ( false !== strpos( $k, '_' ) ) {
226 // Add in checks for icon filetype
227 // These are the general sub-items
228 $this->xml->startElement( substr( $k, 0, strpos( $k, '_' ) ) );
229 $this->xml->writeAttribute( 'type', substr( $k, strpos( $k, '_' ) + 1 ) );
230 } else {
231 // There are no attributes, so just start the sub-element
232 $this->xml->startElement( $k );
233 }
234 // Put in the text (value), and translate it for us if we're using the i18n class
235 $this->xml->text( $this->translate( $v ) );
236 // Close the sub-element
237 $this->xml->endElement();
238 }
239 endforeach;
240 // End the item
241 $this->xml->endElement();
242 }
243
244 }
245
246 /**
247 * Result class
248 *
249 * Class object represents an item in the script filter array. The internals of the
250 * class check for validity so that only correct methods can be set.
251 *
252 * @since 1.0.0
253 * @see ScriptFilter::add_result() These items are part of the ScriptFilter
254 *
255 * @method void set_arg( string $arg ) the argument to pass
256 * @method void set_autocomplete( string $autocomplete ) autocomplete text
257 * @method void set_icon( string $icon ) path to icon
258 * @method void set_icon_fileicon( string $fileicon ) path to application
259 * @method void set_icon_filetype( string $filetype ) filetype for icon
260 * @method void set_subtitle( string $subtitle ) subtitle text
261 * @method void set_subtitle_alt( string $subtitle ) alt subtitle text
262 * @method void set_subtitle_cmd( string $subtitle ) cmd subtitle text
263 * @method void set_subtitle_ctrl( string $subtitle ) ctrl subtitle text
264 * @method void set_subtitle_fn( string $subtitle ) fn subtitle text
265 * @method void set_subtitle_shift( string $subtitle ) shift subtitle text
266 * @method void set_text_copy( string $text ) text to pass when copying
267 * @method void set_text_largetype( string $text ) text to pass to large type
268 * @method void set_title( string $title ) title of result
269 * @method void set_uid( string $uid ) uid for result
270 */
271 class Result {
272
273 /**
274 * Possible string methods for a Result
275 *
276 * @var array
277 */
278 private static $string_methods = [
279 'title',
280 'icon',
281 'icon_filetype',
282 'icon_fileicon',
283 'subtitle',
284 'subtitle_shift',
285 'subtitle_fn',
286 'subtitle_ctrl',
287 'subtitle_alt',
288 'subtitle_cmd',
289 'uid',
290 'arg',
291 'text_copy',
292 'text_largetype',
293 'autocomplete'
294 ];
295
296 /**
297 * Possible boolean methods for a Result
298 * @var array
299 */
300 private static $bool_methods = [ 'valid' ];
301
302
303 /**
304 * Creates a Result object
305 *
306 * @param array|string $args the title if string; a list of arguments if an array
307 */
308 public function __construct( $args ) {
309
310 // Create the data storage variable
311 $this->data = [];
312
313 // If it is a string, then it's the title; if it's an array, then it's multiple values
314 if ( is_string( $args ) ) {
315 // Set the title
316 $this->set_title( $args );
317 } else if ( is_array( $args ) ) {
318 // It's an array, so, cycle through each value and set it
319 foreach ( $args as $key => $value ) :
320 $this->set( [ $key => $value ] );
321 endforeach;
322 }
323
324 }
325
326 /**
327 * Sets a multiple values of a result object
328 *
329 * @throws \Alphred\InvalidScriptFilterArgument When trying to set an invalid script filter field
330 *
331 * @param array $options an array of possible options
332 */
333 public function set( $options ) {
334 // Options must be an array of 'key' => 'value', like: 'title' => 'This is a title'
335 if ( ! is_array( $options ) ) {
336 return false;
337 }
338 // Cycle through the options and see if they are in either $string_methods or $bool_methods;
339 // if so, call them via the magic __call(); otherwise, thrown an exception.
340 foreach ( $options as $option => $value ) :
341 $method = "set_{$option}";
342 if ( in_array( $option, self::$string_methods ) || in_array( $option, self::$bool_methods ) ) {
343 $this->$method( $value );
344 } else {
345 // Not valid. Throw an exception.
346 throw new InvalidScriptFilterArgument( "Error: `{$method}` is not valid.", 3 );
347 }
348 endforeach;
349 }
350
351 /**
352 * Magic method to set everything necessary
353 *
354 * @todo Convert the 'false' returns to thrown Exceptions
355 * @throws \Alphred\TooManyArguments when trying to use multiple values
356 * @throws \Alphred\InvalidXMLProperty when trying to set an invalid XML property
357 *
358 * @param string $called method called
359 * @param array $arguments array of arguments
360 * @return bool
361 */
362 public function __call( $called, $arguments ) {
363 // Make sure that the method is supposed to exist
364 if ( 0 !== strpos( $called, 'set_' ) ) {
365 // We should raise an exception here instead.
366 return false;
367 }
368 // There should only be one argument in the arguments array
369 if ( 1 === count( $arguments ) ) {
370 // Remove the "set_" part of the 'method'
371 $method = str_replace( 'set_', '', $called );
372 // If the value is a bool, then check to make sure it's supposed to be a bool
373 if ( is_bool( $arguments[0] ) && ( in_array( $method, self::$bool_methods ) ) ) {
374 // Set the data
375 $this->data[ $method ] = $arguments[0];
376 return true;
377 } else if ( in_array( $method, self::$string_methods ) ) {
378 // Set the data
379 $this->data[ $method ] = $arguments[0];
380 return true;
381 } else {
382 if ( in_array( $method, self::$bool_methods ) ) {
383 throw new ShouldBeBool( "`{$method}` should be passed as bool not string" );
384 } else {
385 throw new InvalidXMLProperty( "`{$method}` is not a valid property for a script filter.", 3 );
386 }
387 }
388 } else {
389 throw new TooManyArguments( "Expecting a single argument when trying to `{$called}` but got multiple.", 3 );
390 }
391 }
392 }