From 98e9fb3371b348c93c5d3388f0dea7802a218534 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 15 May 2017 23:43:57 -0700 Subject: [PATCH] CRM-20600 - Civi\Angular\ChangeSet - Implement a utility for modifying HTML snippets via phpQuery --- Civi/Angular/ChangeSet.php | 191 +++++++++++++++++++ Civi/Angular/ChangeSetInterface.php | 38 ++++ tests/phpunit/Civi/Angular/ChangeSetTest.php | 102 ++++++++++ 3 files changed, 331 insertions(+) create mode 100644 Civi/Angular/ChangeSet.php create mode 100644 Civi/Angular/ChangeSetInterface.php create mode 100644 tests/phpunit/Civi/Angular/ChangeSetTest.php diff --git a/Civi/Angular/ChangeSet.php b/Civi/Angular/ChangeSet.php new file mode 100644 index 0000000000..f3cb2db35b --- /dev/null +++ b/Civi/Angular/ChangeSet.php @@ -0,0 +1,191 @@ +resFilters as $filter) { + if ($filter['resourceType'] === $resourceType) { + $resources = call_user_func($filter['callback'], $resources); + } + } + } + return $resources; + } + + /** + * Update a set of HTML snippets. + * + * @param array $changeSets + * Array(ChangeSet). + * @param array $strings + * Array(string $path => string $html). + * @return array + * Updated list of $strings. + * @throws \CRM_Core_Exception + */ + private static function applyHtmlFilters($changeSets, $strings) { + $coder = new Coder(); + + foreach ($strings as $path => $html) { + /** @var \phpQueryObject $doc */ + $doc = NULL; + + // Most docs don't need phpQueryObject. Initialize phpQuery on first match. + + foreach ($changeSets as $changeSet) { + /** @var ChangeSet $changeSet */ + foreach ($changeSet->htmlFilters as $filter) { + if (preg_match($filter['regex'], $path)) { + if ($doc === NULL) { + $doc = \phpQuery::newDocument($html, 'text/html'); + if (\CRM_Core_Config::singleton()->debug && !$coder->checkConsistentHtml($html)) { + throw new \CRM_Core_Exception("Cannot process $path: inconsistent markup. Use check-angular.php to investigate."); + } + } + call_user_func($filter['callback'], $doc, $path); + } + } + } + + if ($doc !== NULL) { + $strings[$path] = $coder->encode($doc); + } + } + return $strings; + } + + /** + * @var string + */ + protected $name; + + /** + * @var array + * Each item is an array with keys: + * - resourceType: string + * - callback: function + */ + protected $resFilters = array(); + + /** + * @var array + * Each item is an array with keys: + * - regex: string + * - callback: function + */ + protected $htmlFilters = array(); + + /** + * @param string $name + * Symbolic name for this changeset. + * @return \Civi\Angular\ChangeSetInterface + */ + public static function create($name) { + $changeSet = new ChangeSet(); + $changeSet->name = $name; + return $changeSet; + } + + /** + * Declare that $module requires additional dependencies. + * + * @param string $module + * @param string|array $dependencies + * @return ChangeSet + */ + public function requires($module, $dependencies) { + $dependencies = (array) $dependencies; + return $this->alterResource('requires', + function ($values) use ($module, $dependencies) { + if (!isset($values[$module])) { + $values[$module] = array(); + } + $values[$module] = array_unique(array_merge($values[$module], $dependencies)); + return $values; + }); + } + + /** + * Declare a change to a resource. + * + * @param string $resourceType + * @param callable $callback + * @return ChangeSet + */ + public function alterResource($resourceType, $callback) { + $this->resFilters[] = array( + 'resourceType' => $resourceType, + 'callback' => $callback, + ); + return $this; + } + + /** + * Declare a change to HTML. + * + * @param string $file + * A file name, wildcard, or regex. + * Ex: '~/crmHello/intro.html' (filename) + * Ex: '~/crmHello/*.html' (wildcard) + * Ex: ';(Edit|List)Ctrl\.html$;' (regex) + * @param callable $callback + * Function which accepts up to two parameters: + * - phpQueryObject $doc + * - string $path + * @return ChangeSet + */ + public function alterHtml($file, $callback) { + $this->htmlFilters[] = array( + 'regex' => ($file{0} === ';') ? $file : $this->createRegex($file), + 'callback' => $callback, + ); + return $this; + } + + /** + * Convert a string with a wildcard (*) to a regex. + * + * @param string $filterExpr + * Ex: "/foo/*.bar" + * @return string + * Ex: ";^/foo/[^/]*\.bar$;" + */ + protected function createRegex($filterExpr) { + $regex = preg_quote($filterExpr, ';'); + $regex = str_replace('\\*', '[^/]*', $regex); + $regex = ";^$regex$;"; + return $regex; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) { + $this->name = $name; + } + +} diff --git a/Civi/Angular/ChangeSetInterface.php b/Civi/Angular/ChangeSetInterface.php new file mode 100644 index 0000000000..9f30447e37 --- /dev/null +++ b/Civi/Angular/ChangeSetInterface.php @@ -0,0 +1,38 @@ + 0); + + $changeSet->alterHtml('~/foo.html', function (\phpQueryObject $doc, $file) use (&$counts) { + $counts[$file]++; + $doc->find('.foo')->after('

world

'); + }); + $changeSet->alterHtml('~/f*.html', function (\phpQueryObject $doc, $file) use (&$counts) { + $counts[$file]++; + $doc->find('.bar')->after('

cruel world

'); + }); + $changeSet->alterHtml('/path/does/not/exist.html', function(\phpQueryObject $doc) { + throw new \Exception("This should not be called. The file does not exist!"); + }); + + $results = ChangeSet::applyResourceFilters(array($changeSet), 'partials', array( + '~/foo.html' => '

Hello

Goodbye

', + )); + + $this->assertHtmlEquals( + '

Hello

world

Goodbye

cruel world

', + $results['~/foo.html'] + ); + $this->assertEquals(2, $counts['~/foo.html']); + } + + /** + * Insert content using append() and prepend(). + */ + public function testAppendPrepend() { + $changeSet = ChangeSet::create(__FUNCTION__); + $counts = array('~/foo.html' => 0); + + $changeSet->alterHtml('~/foo.html', function (\phpQueryObject $doc, $file) use (&$counts) { + $counts[$file]++; + $doc->find('.foo')->append('

world

'); + }); + $changeSet->alterHtml('~/*.html', function (\phpQueryObject $doc, $file) use (&$counts) { + $counts[$file]++; + $doc->find('.bar')->prepend('Cruel world,'); + }); + $changeSet->alterHtml('/path/does/not/exist.html', function(\phpQueryObject $doc) { + throw new \Exception("This should not be called. The file does not exist!"); + }); + + $originals = array( + '~/foo.html' => '

Hello

Goodbye

', + ); + $results = ChangeSet::applyResourceFilters(array($changeSet), 'partials', $originals); + + $this->assertHtmlEquals( + '

Hello

world

Cruel world,Goodbye

', + $results['~/foo.html'] + ); + $this->assertEquals(2, $counts['~/foo.html']); + } + + protected function assertHtmlEquals($expected, $actual, $message = '') { + $expected = preg_replace(';>[ \r\n\t]+;', '>', $expected); + $actual = preg_replace(';>[ \r\n\t]+;', '>', $actual); + $this->assertEquals($expected, $actual, $message); + } + +} -- 2.25.1