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