├── screenshot.png
├── phpunit.xml
├── lib
├── wp-pluggable.php
├── okfn-base.php
├── okfn-annot-content-policy.php
├── okfn-annot-factory.php
├── okfn-utils.php
├── okfn-annot-settings.php
└── okfn-annot-injector.php
├── templates
├── annotator-instance.js
└── settings.html
├── Guardfile
├── test
├── test-bootstrap.php
├── lib
│ ├── test-helpers.php
│ └── okfn-test-case.php
├── okfn-utilsTest.php
├── okfn-annot-settingsTest.php
├── okfn-annot-factoryTest.php
└── okfn-annot-injectorTest.php
├── okfn-annotator.php
├── readme.txt
├── vendor
├── javascripts
│ ├── json2.min.js
│ └── annotator
│ │ └── annotator-full.min.js
├── Mustache.php
└── stylesheets
│ └── annotator.min.css
├── deploy.sh
└── README.md
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openannotation/annotator-wordpress/HEAD/screenshot.png
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
Settings saved.
8 |
34 |
35 |
36 | ### Bugs and feature requests
37 |
38 | You are welcome to submit bug reports as well as ideas and feature
39 | requests using the [GitHub issue tracker](https://github.com/okfn/annotator-wordpress/issues).
40 |
41 | ### Running the unit tests
42 |
43 | The plugin comes with a fairly decent test suite. To run the tests you will need [PEAR](http://pear.php.net/), the PHP package manager.
44 | Once installed PEAR, you will also need to install the latest version of the PHPUnit package. This can be done by issuing the following commands:
45 |
46 | pear channel-discover pear.phpunit.de
47 | pear channel-discover components.ez.no
48 | pear channel-discover pear.symfony-project.com
49 | pear install phpunit/PHPUnit
50 |
51 | The test suite has to be run from within the project root
52 | directory by executing the phpunit command with no options or
53 | arguments.
54 |
55 | phpunit
56 |
57 | The test suite can also be configured to run automatically whenever the library code changes. This is desirable while developing, as it allows to spot
58 | regressions sooner rather than later, and to keep tests up to date. In order
59 | to automate the execution of the tests you will need to install and run
60 | [guard](https://github.com/guard/guard), a Ruby tool that allows to bind custom
61 | commands to file system events. The 'Guardfile' provided here contains a few useful configuration directives that allow doing this with PHPUnit tests.
62 |
63 |
--------------------------------------------------------------------------------
/lib/okfn-utils.php:
--------------------------------------------------------------------------------
1 | array(
70 | * 'a1' => array(
71 | * 'a1a' => 'hi!'
72 | * )
73 | * ),
74 | * 'b'
75 | * ));
76 | *
77 | * echo $object->a->a1->a1a //=> hi!
78 | *
79 | * returns a multi-dimensional object
80 | */
81 |
82 | static function objectify($array) {
83 |
84 | if (is_array($array)) {
85 | return (object) array_map(array(__CLASS__, __FUNCTION__), $array);
86 | } else {
87 | return $array;
88 | }
89 | }
90 |
91 | /*
92 | * Generates a unique identifier by combining a prefix and an autoincremental integer
93 | *
94 | * prefix - a string to be prepended to the numeric id (optional).
95 | *
96 | * returns the unique identifer string.
97 | *
98 | */
99 | static function unique_id($prefix='id_') {
100 | static $id;
101 | $id = isset($id) ? $id + 1 : 0;
102 | return "{$prefix}{$id}";
103 | }
104 |
105 | /*
106 | * Gets the current page URL.
107 | *
108 | *
109 | * Returns a URL string.
110 | *
111 | */
112 |
113 | static function current_url() {
114 | return 'http://' . preg_replace('/\/$/', '', $_SERVER['HTTP_HOST']) .
115 | $_SERVER['REQUEST_URI'];
116 | }
117 |
118 | }
119 |
120 | ?>
121 |
--------------------------------------------------------------------------------
/test/okfn-annot-injectorTest.php:
--------------------------------------------------------------------------------
1 | constructor_arguments = array(
13 | $this->mockHelper('OkfnAnnotSettings',array(),array(),false),
14 | $this->mockHelper('OkfnAnnotContentPolicy',array('url_is_annotatable'),array(),false)
15 | );
16 | }
17 |
18 |
19 | public function testAllAssetPathsShouldBeCorrect(){
20 | $injector = new OkfnAnnotInjector(new stdClass, new stdClass);
21 | $conf = $injector->get_conf();
22 |
23 | foreach(array('javascripts', 'stylesheets') as $asset_type) {
24 | foreach($conf->$asset_type as $asset) {
25 | $this->assertFileExists(dirname(dirname(__FILE__)) ."/{$conf->assets_basepath}{$asset->file}");
26 | }
27 | }
28 | }
29 |
30 | public function testShouldLoadJavascriptDependencies() {
31 | list($settings_mock, $content_policy_mock) = $this->constructor_arguments;
32 | $injector_mock = $this->mockHelper('OkfnAnnotInjector', $this->methods_to_mock, $this->constructor_arguments);
33 |
34 | $content_policy_mock->expects($this->any())
35 | ->method('url_is_annotatable')
36 | ->will($this->returnValue(true));
37 |
38 | $injector_mock->expects($this->once())
39 | ->method('load_javascript_dependencies');
40 |
41 | $injector_mock->inject();
42 | }
43 |
44 |
45 | public function testShouldLoadCorrectlyBothJavascriptAndStylesheetAssets() {
46 | list($settings_mock, $content_policy_mock) = $this->constructor_arguments;
47 | $injector_mock = $this->mockHelper('OkfnAnnotInjector',$this->methods_to_mock, $this->constructor_arguments);
48 |
49 |
50 | $content_policy_mock->expects($this->any())
51 | ->method('url_is_annotatable')
52 | ->will($this->returnValue(true));
53 |
54 |
55 |
56 | /*
57 | * NOTE: The value of the at() index does not refer the value of ->method(), but to stack of all the different
58 | * method calls done by the mocked object (which sucks...).
59 | *
60 | */
61 | $injector_mock->expects($this->at(0))
62 | ->method('wp_enqueue_asset')
63 | ->with('annotator.css',
64 | $this->anything(),
65 | $this->anything(),
66 | $this->anything(),
67 | false,
68 | 'stylesheets'
69 | );
70 |
71 |
72 | $injector_mock->expects($this->at(2))
73 | ->method('wp_enqueue_asset')
74 | ->with('jquery.js',
75 | $this->anything(),
76 | $this->anything(),
77 | $this->anything(),
78 | false,
79 | 'javascripts'
80 | );
81 |
82 | $injector_mock->expects($this->at(3))
83 | ->method('wp_enqueue_asset')
84 | ->with('annotator-full.js',
85 | $this->anything(),
86 | $this->anything(),
87 | $this->anything(),
88 | false,
89 | 'javascripts'
90 | );
91 |
92 |
93 | $injector_mock->inject();
94 | }
95 |
96 | public function testShouldRespectTheUserDefinedPolicy(){
97 | list($settings_mock, $content_policy_mock) = $this->constructor_arguments;
98 | $mock_methods = array_merge($this->methods_to_mock, array('load_stylesheets','load_javascripts'));
99 |
100 | $injector_mock = $this->mockHelper('OkfnAnnotInjector',$mock_methods, $this->constructor_arguments);
101 |
102 |
103 | $content_policy_mock->expects($this->any())
104 | ->method('url_is_annotatable')
105 | ->will($this->returnValue(false));
106 |
107 | foreach($mock_methods as $method) {
108 | $injector_mock->expects($this->never())
109 | ->method($method);
110 | }
111 | }
112 |
113 | }
114 | ?>
115 |
--------------------------------------------------------------------------------
/lib/okfn-annot-settings.php:
--------------------------------------------------------------------------------
1 | 'okfn-annot',
10 | 'settings_page_title' => 'Annotator settings',
11 | 'menu_item_title' => 'Annotator',
12 | 'menu_item_identifier' => 'okfn-annotator',
13 | 'submit_parameter_name' => 'okfn-annotsettings-submit',
14 |
15 | //options exposed to the user and stored in wordpress settings
16 | 'default_options' => array(
17 | 'annotator_content' => '.entry-content',
18 | 'url_pattern' => '.*',
19 | )
20 | );
21 |
22 | function __construct(){
23 | parent::__construct();
24 | add_action('admin_menu', array($this,'register_menu'));
25 | }
26 |
27 | /*
28 | * Wrapper for calling wordpress internal 'get_option'. Its sole function is to
29 | * automatically add the plugin prefix defined in '$conf->forminput_prefix'.
30 | *
31 | * option - the name of the option to be retrieved
32 | * useprefix - whether or not the plugin prefix should be prepended; defaults to true (optional).
33 | *
34 | *
35 | * returns an option value
36 | */
37 |
38 | function get_option($option,$prefix=true) {
39 | if ($prefix) {
40 | $option_name = $this->conf->forminput_prefix . '-' . $option;
41 | }
42 |
43 | return get_option($option_name);
44 | }
45 |
46 | /*
47 | * Registers the Annotator plugin settings page and its menu item
48 | * using wordpress special function 'add_options_page'
49 | *
50 | * returns nothing
51 | *
52 | */
53 |
54 | function register_menu() {
55 | add_options_page(
56 | $this->conf->settings_page_title,
57 | $this->conf->menu_item_title,
58 | 'manage_options', //wordpress permission level
59 | $this->conf->menu_item_identifier,
60 | array($this,'process_request') //callback popoulating the settings page content
61 | );
62 | }
63 |
64 | /*
65 | * Checks for the presence of the form submit tocken
66 | *
67 | * returns a boolean
68 | */
69 |
70 | function form_is_submitted($params) {
71 | return isset($params[ $this->conf->submit_parameter_name ]);
72 | }
73 |
74 | /*
75 | * Sets the plugin options using the form input fields sent through the form
76 | *
77 | *
78 | * params - the request paramters
79 | *
80 | * returns nothing.
81 | *
82 | */
83 |
84 | function process_settings_form($params){
85 | $prefix = $this->conf->forminput_prefix;
86 | $options=OkfnUtils::filter_by_regexp("/^{$prefix}-/", $params);
87 |
88 | foreach($options as $option => $value) {
89 | print_r($option, $value);
90 | update_option($option, $value);
91 | }
92 |
93 | $this->render_settings_form($is_submit=true);
94 | }
95 |
96 | /*
97 | *
98 | * Renders the HTML for the Annotator settings options page.
99 | *
100 | * is_submit - boolean flag indicating whether data has been submitted
101 | *
102 | *
103 | * returns a rendered form template
104 | *
105 | */
106 |
107 | function render_settings_form($is_submit=false) {
108 |
109 | $prefix = $this->conf->forminput_prefix;
110 |
111 | $options = array(
112 | 'prefix' => $prefix,
113 | 'action' => $_SERVER['REQUEST_URI'],
114 | 'submit_name' => $this->conf->submit_parameter_name,
115 | 'submit_value' => __('Save settings'),
116 | 'form_submitted' => $is_submit,
117 | );
118 |
119 | foreach($this->conf->default_options as $optname => $value) {
120 | $stored_value = get_option("{$prefix}-{$optname}");
121 | $options[$optname] = empty($stored_value) ? $value : $stored_value ;
122 | }
123 |
124 | //unescape backslashes automatically added by php for string sanitation
125 | $options['url_pattern'] = stripslashes($options['url_pattern']);
126 |
127 | $mustache = new Mustache;
128 | $template = OkfnUtils::get_template('settings.html');
129 | print $mustache->render($template, $options);
130 | }
131 |
132 | /*
133 | * Public:
134 | *
135 | * Logical switch determining whether to render or to process the setting form.
136 | *
137 | *
138 | * params - the request parameters (defaults to $_POST if not provided).
139 | *
140 | *
141 | * returns nothing
142 | */
143 |
144 | function process_request($params=array()) {
145 |
146 | if (empty($params)) {
147 | $params = $_POST;
148 | }
149 |
150 |
151 | ( $this->form_is_submitted($params)) ?
152 | $this->process_settings_form($params) :
153 | $this->render_settings_form() ;
154 | }
155 |
156 | }
157 | ?>
158 |
--------------------------------------------------------------------------------
/lib/okfn-annot-injector.php:
--------------------------------------------------------------------------------
1 | '/vendor/',
14 |
15 | // In the sake of simplicity for now we only support the full minified version
16 | // of the annotator (A more fine grained control through the settings can be implemented later).
17 |
18 | 'javascripts' => array(
19 | array(
20 | 'file' => 'javascripts/jquery.min.js'
21 | ),
22 | array(
23 | 'file' => 'javascripts/annotator/annotator-full.min.js',
24 | 'dependencies' => array('json2','jquery'),
25 | ),
26 | ),
27 |
28 | 'stylesheets' => array(
29 | array(
30 | 'file' => 'stylesheets/annotator.min.css'
31 | )
32 | )
33 | );
34 | private $factory;
35 | private $content_policy;
36 |
37 | function __construct($factory,$content_policy){
38 | parent::__construct();
39 |
40 | $this->factory = $factory;
41 | $this->content_policy = $content_policy;
42 | }
43 |
44 | /*
45 | * Enqueues the Annotator's dependencies (i.e. JSON2, and jQuery).
46 | *
47 | * returns nothing
48 | *
49 | */
50 | function load_javascript_dependencies(){
51 | wp_enqueue_script('json2');
52 | //deregister the javascript version used by wordpress
53 | wp_deregister_script('jquery');
54 | }
55 |
56 | /*
57 | * Public:
58 | *
59 | * Enqueues the Annotator javascript files
60 | *
61 | * returns nothing
62 | *
63 | */
64 | public function load_javascripts() {
65 | $this->load_javascript_dependencies();
66 | $this->load_assets('javascripts');
67 | }
68 |
69 | /*
70 | * Public:
71 | *
72 | * Enqueues the Annotator stylesheet/s
73 | *
74 | * returns nothing
75 | *
76 | */
77 | function load_stylesheets() {
78 | $this->load_assets('stylesheets');
79 | }
80 |
81 |
82 | /*
83 | * Ensures that libraries are registered with the '.{js|css}' and not the '.min.{js|css}' prefix
84 | *
85 | * path - a relative path to an asset
86 | *
87 | * returns the asset id (stripped of the '.min' fragment ).
88 | *
89 | */
90 | function asset_id($path) {
91 | return preg_replace('/(\.min)\.(js|css)$/','.$2', basename($path) );
92 | }
93 |
94 |
95 | /*
96 | * Wrapper for wp_enqueue_{style|script}
97 | *
98 | * This has been implemented only for mocking/testing purposes.
99 | *
100 | * asset_id - The asset filename (without the path and the .min prefix).
101 | * asset_src - The asset url (relative to the plugin directory).
102 | * dependencies - Array of asset filenames specifying other assets that should be loaded first.
103 | * version_number - A version number to be passed to the library; defaults to Wordpress version number, (optional).
104 | * in_footer - Wether or not to load the library in the document footer rather than the header.
105 | * javascripts_or_stylesheets - Whether to load a javascript or a stylesheet asset
106 | *
107 | * returns nothing
108 | */
109 | function wp_enqueue_asset($asset_id, $asset_src, $dependencies, $version_number, $in_footer, $javascripts_or_stylesheets) {
110 |
111 | $loader_function = ($javascripts_or_stylesheets === 'javascripts') ?
112 | 'wp_enqueue_script' :
113 | 'wp_enqueue_style';
114 |
115 | call_user_func_array($loader_function, array($asset_id,
116 | plugins_url($asset_src , dirname(__FILE__)),
117 | $dependencies, $version_number, $in_footer
118 | ));
119 | }
120 |
121 | /*
122 | * Registers the annotator assets (as specified in $conf) and enqueues them using
123 | * the wordpress loading functions
124 | *
125 | * javascripts_or_stylesheets - Whether it should load javascripts or stylesheets.
126 | *
127 | * returns nothing
128 | */
129 |
130 | private function load_assets($javascripts_or_stylesheets) {
131 | foreach($this->conf->$javascripts_or_stylesheets as $asset) {
132 | $in_footer = isset($asset->in_footer) && $asset->in_footer;
133 | $this->wp_enqueue_asset(
134 | $this->asset_id($asset->file),
135 | "{$this->conf->assets_basepath}{$asset->file}",
136 | isset($asset->dependencies) ? $asset->dependencies : array(),
137 | false,
138 | $in_footer,
139 | $javascripts_or_stylesheets
140 | );
141 |
142 | }
143 | }
144 |
145 | /*
146 | *
147 | * Prints the instantiation snippet produced by the OkfnAnnotFactory object
148 | *
149 | * returns nothing
150 | */
151 | function print_snippet() {
152 | $snippet = $this->factory->create_snippet();
153 | if ($snippet) {
154 | print implode("\n",array(''));
155 | }
156 | }
157 |
158 | function inject() {
159 | if ($this->content_policy->url_is_annotatable()) {
160 | add_action('wp_print_styles', array($this,'load_stylesheets'));
161 | add_action('wp_print_scripts', array($this,'load_javascripts'));
162 | add_action('wp_footer', array($this,'print_snippet'));
163 | }
164 | }
165 | }
166 | ?>
167 |
--------------------------------------------------------------------------------
/vendor/Mustache.php:
--------------------------------------------------------------------------------
1 | false,
24 | MustacheException::UNCLOSED_SECTION => true,
25 | MustacheException::UNEXPECTED_CLOSE_SECTION => true,
26 | MustacheException::UNKNOWN_PARTIAL => false,
27 | MustacheException::UNKNOWN_PRAGMA => true,
28 | );
29 |
30 | // Override charset passed to htmlentities() and htmlspecialchars(). Defaults to UTF-8.
31 | protected $_charset = 'UTF-8';
32 |
33 | /**
34 | * Pragmas are macro-like directives that, when invoked, change the behavior or
35 | * syntax of Mustache.
36 | *
37 | * They should be considered extremely experimental. Most likely their implementation
38 | * will change in the future.
39 | */
40 |
41 | /**
42 | * The {{%DOT-NOTATION}} pragma allows context traversal via dots. Given the following context:
43 | *
44 | * $context = array('foo' => array('bar' => array('baz' => 'qux')));
45 | *
46 | * One could access nested properties using dot notation:
47 | *
48 | * {{%DOT-NOTATION}}{{foo.bar.baz}}
49 | *
50 | * Which would render as `qux`.
51 | */
52 | const PRAGMA_DOT_NOTATION = 'DOT-NOTATION';
53 |
54 | /**
55 | * The {{%IMPLICIT-ITERATOR}} pragma allows access to non-associative array data in an
56 | * iterable section:
57 | *
58 | * $context = array('items' => array('foo', 'bar', 'baz'));
59 | *
60 | * With this template:
61 | *
62 | * {{%IMPLICIT-ITERATOR}}{{#items}}{{.}}{{/items}}
63 | *
64 | * Would render as `foobarbaz`.
65 | *
66 | * {{%IMPLICIT-ITERATOR}} accepts an optional 'iterator' argument which allows implicit
67 | * iterator tags other than {{.}} ...
68 | *
69 | * {{%IMPLICIT-ITERATOR iterator=i}}{{#items}}{{i}}{{/items}}
70 | */
71 | const PRAGMA_IMPLICIT_ITERATOR = 'IMPLICIT-ITERATOR';
72 |
73 | /**
74 | * The {{%UNESCAPED}} pragma swaps the meaning of the {{normal}} and {{{unescaped}}}
75 | * Mustache tags. That is, once this pragma is activated the {{normal}} tag will not be
76 | * escaped while the {{{unescaped}}} tag will be escaped.
77 | *
78 | * Pragmas apply only to the current template. Partials, even those included after the
79 | * {{%UNESCAPED}} call, will need their own pragma declaration.
80 | *
81 | * This may be useful in non-HTML Mustache situations.
82 | */
83 | const PRAGMA_UNESCAPED = 'UNESCAPED';
84 |
85 | /**
86 | * Constants used for section and tag RegEx
87 | */
88 | const SECTION_TYPES = '\^#\/';
89 | const TAG_TYPES = '#\^\/=!<>\\{&';
90 |
91 | public $_otag = '{{';
92 | public $_ctag = '}}';
93 |
94 | protected $_tagRegEx;
95 |
96 | protected $_template = '';
97 | protected $_context = array();
98 | protected $_partials = array();
99 | protected $_pragmas = array();
100 |
101 | protected $_pragmasImplemented = array(
102 | self::PRAGMA_DOT_NOTATION,
103 | self::PRAGMA_IMPLICIT_ITERATOR,
104 | self::PRAGMA_UNESCAPED
105 | );
106 |
107 | protected $_localPragmas = array();
108 |
109 | /**
110 | * Mustache class constructor.
111 | *
112 | * This method accepts a $template string and a $view object. Optionally, pass an associative
113 | * array of partials as well.
114 | *
115 | * @access public
116 | * @param string $template (default: null)
117 | * @param mixed $view (default: null)
118 | * @param array $partials (default: null)
119 | * @return void
120 | */
121 | public function __construct($template = null, $view = null, $partials = null) {
122 | if ($template !== null) $this->_template = $template;
123 | if ($partials !== null) $this->_partials = $partials;
124 | if ($view !== null) $this->_context = array($view);
125 | }
126 |
127 | /**
128 | * Mustache class clone method.
129 | *
130 | * A cloned Mustache instance should have pragmas, delimeters and root context
131 | * reset to default values.
132 | *
133 | * @access public
134 | * @return void
135 | */
136 | public function __clone() {
137 | $this->_otag = '{{';
138 | $this->_ctag = '}}';
139 | $this->_localPragmas = array();
140 |
141 | if ($keys = array_keys($this->_context)) {
142 | $last = array_pop($keys);
143 | if ($this->_context[$last] instanceof Mustache) {
144 | $this->_context[$last] =& $this;
145 | }
146 | }
147 | }
148 |
149 | /**
150 | * Render the given template and view object.
151 | *
152 | * Defaults to the template and view passed to the class constructor unless a new one is provided.
153 | * Optionally, pass an associative array of partials as well.
154 | *
155 | * @access public
156 | * @param string $template (default: null)
157 | * @param mixed $view (default: null)
158 | * @param array $partials (default: null)
159 | * @return string Rendered Mustache template.
160 | */
161 | public function render($template = null, $view = null, $partials = null) {
162 | if ($template === null) $template = $this->_template;
163 | if ($partials !== null) $this->_partials = $partials;
164 |
165 | if ($view) {
166 | $this->_context = array($view);
167 | } else if (empty($this->_context)) {
168 | $this->_context = array($this);
169 | }
170 |
171 | $template = $this->_renderPragmas($template);
172 | return $this->_renderTemplate($template, $this->_context);
173 | }
174 |
175 | /**
176 | * Wrap the render() function for string conversion.
177 | *
178 | * @access public
179 | * @return string
180 | */
181 | public function __toString() {
182 | // PHP doesn't like exceptions in __toString.
183 | // catch any exceptions and convert them to strings.
184 | try {
185 | $result = $this->render();
186 | return $result;
187 | } catch (Exception $e) {
188 | return "Error rendering mustache: " . $e->getMessage();
189 | }
190 | }
191 |
192 | /**
193 | * Internal render function, used for recursive calls.
194 | *
195 | * @access protected
196 | * @param string $template
197 | * @return string Rendered Mustache template.
198 | */
199 | protected function _renderTemplate($template) {
200 | $template = $this->_renderSections($template);
201 | return $this->_renderTags($template);
202 | }
203 |
204 | /**
205 | * Render boolean, enumerable and inverted sections.
206 | *
207 | * @access protected
208 | * @param string $template
209 | * @return string
210 | */
211 | protected function _renderSections($template) {
212 | while ($section_data = $this->_findSection($template)) {
213 | list($section, $offset, $type, $tag_name, $content) = $section_data;
214 |
215 | $replace = '';
216 | $val = $this->_getVariable($tag_name);
217 | switch($type) {
218 | // inverted section
219 | case '^':
220 | if (empty($val)) {
221 | $replace .= $content;
222 | }
223 | break;
224 |
225 | // regular section
226 | case '#':
227 | if ($this->_varIsIterable($val)) {
228 | if ($this->_hasPragma(self::PRAGMA_IMPLICIT_ITERATOR)) {
229 | if ($opt = $this->_getPragmaOptions(self::PRAGMA_IMPLICIT_ITERATOR)) {
230 | $iterator = $opt['iterator'];
231 | } else {
232 | $iterator = '.';
233 | }
234 | } else {
235 | $iterator = false;
236 | }
237 |
238 | foreach ($val as $local_context) {
239 |
240 | if ($iterator) {
241 | $iterator_context = array($iterator => $local_context);
242 | $this->_pushContext($iterator_context);
243 | } else {
244 | $this->_pushContext($local_context);
245 | }
246 | $replace .= $this->_renderTemplate($content);
247 | $this->_popContext();
248 | }
249 | } else if ($val) {
250 | if (is_array($val) || is_object($val)) {
251 | $this->_pushContext($val);
252 | $replace .= $this->_renderTemplate($content);
253 | $this->_popContext();
254 | } else {
255 | $replace .= $content;
256 | }
257 | }
258 | break;
259 | }
260 |
261 | $template = substr_replace($template, $replace, $offset, strlen($section));
262 | }
263 |
264 | return $template;
265 | }
266 |
267 | /**
268 | * Prepare a section RegEx string for the given opening/closing tags.
269 | *
270 | * @access protected
271 | * @param string $otag
272 | * @param string $ctag
273 | * @return string
274 | */
275 | protected function _prepareSectionRegEx($otag, $ctag) {
276 | return sprintf(
277 | '/(?:(?<=\\n)[ \\t]*)?%s(?P=this[i+"Offset"]){k[i+"Container"]=m,k[i+"Offset"]=this[i+"Offset"]-h;break}h+=m.nodeValue.length}}return(new e.BrowserRange(k)).normalize(b)},b.prototype.serialize=function(a,b){return this.normalize(a).serialize(a,b)},b.prototype.toObject=function(){return{start:this.start,startOffset:this.startOffset,end:this.end,endOffset:this.endOffset}},b}(),m={uuid:function(){var a;return a=0,function(){return a++}}(),getGlobal:function(){return function(){return this}()},mousePosition:function(b,c){var d;return d=a(c).offset(),{top:b.pageY-d.top,left:b.pageX-d.left}},preventEventDefault:function(a){return a!=null?typeof a.preventDefault=="function"?a.preventDefault():void 0:void 0}},n=this.Annotator,b=function(b){function c(b,d){this.onDeleteAnnotation=x(this.onDeleteAnnotation,this),this.onEditAnnotation=x(this.onEditAnnotation,this),this.onAdderClick=x(this.onAdderClick,this),this.onAdderMousedown=x(this.onAdderMousedown,this),this.onHighlightMouseover=x(this.onHighlightMouseover,this),this.checkForEndSelection=x(this.checkForEndSelection,this),this.checkForStartSelection=x(this.checkForStartSelection,this),this.clearViewerHideTimer=x(this.clearViewerHideTimer,this),this.startViewerHideTimer=x(this.startViewerHideTimer,this),this.showViewer=x(this.showViewer,this),this.onEditorSubmit=x(this.onEditorSubmit,this),this.onEditorHide=x(this.onEditorHide,this),this.showEditor=x(this.showEditor,this);var e,f,g;c.__super__.constructor.apply(this,arguments),this.plugins={};if(!c.supported())return this;this.options.readOnly||this._setupDocumentEvents(),this._setupWrapper()._setupViewer()._setupEditor(),g=this.html;for(e in g)f=g[e],e!=="wrapper"&&(this[e]=a(f).appendTo(this.wrapper).hide())}return y(c,b),c.prototype.events={".annotator-adder button click":"onAdderClick",".annotator-adder button mousedown":"onAdderMousedown",".annotator-hl mouseover":"onHighlightMouseover",".annotator-hl mouseout":"startViewerHideTimer"},c.prototype.html={hl:'',adder:'
",wrapper:''},c.prototype.options={readOnly:!1},c.prototype.plugins={},c.prototype.editor=null,c.prototype.viewer=null,c.prototype.selectedRanges=null,c.prototype.mouseIsDown=!1,c.prototype.ignoreMouseup=!1,c.prototype.viewerHideTimer=null,c.prototype._setupWrapper=function(){return this.wrapper=a(this.html.wrapper),this.element.find("script").remove(),this.element.wrapInner(this.wrapper),this.wrapper=this.element.find(".annotator-wrapper"),this},c.prototype._setupViewer=function(){var b=this;return this.viewer=new c.Viewer({readOnly:this.options.readOnly}),this.viewer.hide().on("edit",this.onEditAnnotation).on("delete",this.onDeleteAnnotation).addField({load:function(c,d){return d.text?a(c).escape(d.text):a(c).html(""+u("No Comment")+""),b.publish("annotationViewerTextField",[c,d])}}).element.appendTo(this.wrapper).bind({mouseover:this.clearViewerHideTimer,mouseout:this.startViewerHideTimer}),this},c.prototype._setupEditor=function(){return this.editor=new c.Editor,this.editor.hide().on("hide",this.onEditorHide).on("save",this.onEditorSubmit).addField({type:"textarea",label:u("Comments")+"…",load:function(b,c){return a(b).find("textarea").val(c.text||"")},submit:function(b,c){return c.text=a(b).find("textarea").val()}}),this.editor.element.appendTo(this.wrapper),this},c.prototype._setupDocumentEvents=function(){return a(document).bind({mouseup:this.checkForEndSelection,mousedown:this.checkForStartSelection}),this},c.prototype.getSelectedRanges=function(){var b,c,d,f,g,h,i,j,k;i=m.getGlobal().getSelection(),g=[],h=[],i.isCollapsed||(g=function(){var a,g;g=[];for(c=0,a=i.rangeCount;0<=a?c