From 20d4bccc5703cb2648e65f1150c62b9010d5dd89 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Wed, 27 Nov 2019 09:42:22 -0500 Subject: [PATCH] Format whitespace in aff.html files Adds api options for use in GUI editor to autoformat html whitespace. Results in much nicer looking markup! --- ext/afform/core/CRM/Afform/ArrayHtml.php | 53 +++++++++++---- .../Civi/Api4/Utils/AfformFormatTrait.php | 23 ++++++- ext/afform/gui/ang/afGuiEditor.js | 8 +-- .../mock/tests/phpunit/api/v4/AfformTest.php | 68 ++++++++++++++++--- .../phpunit/api/v4/formatExamples/apple.php | 1 + .../phpunit/api/v4/formatExamples/banana.php | 20 +++++- .../phpunit/api/v4/formatExamples/cherry.php | 5 ++ .../phpunit/api/v4/formatExamples/empty.php | 1 + .../api/v4/formatExamples/self-closing.php | 7 ++ .../phpunit/api/v4/formatExamples/string.php | 1 + 10 files changed, 153 insertions(+), 34 deletions(-) diff --git a/ext/afform/core/CRM/Afform/ArrayHtml.php b/ext/afform/core/CRM/Afform/ArrayHtml.php index 4705ea2731..b0dae5fe7e 100644 --- a/ext/afform/core/CRM/Afform/ArrayHtml.php +++ b/ext/afform/core/CRM/Afform/ArrayHtml.php @@ -10,6 +10,8 @@ class CRM_Afform_ArrayHtml { const DEFAULT_TAG = 'div'; + private $indent = -1; + /** * This is a minimalist/temporary placeholder for a schema definition. * FIXME: It shouldn't be here or look like this. @@ -56,13 +58,19 @@ class CRM_Afform_ArrayHtml { * @var bool */ protected $deepCoding; + /** + * @var bool + */ + protected $formatWhitespace; /** * CRM_Afform_ArrayHtml constructor. * @param bool $deepCoding + * @param bool $formatWhitespace */ - public function __construct($deepCoding = TRUE) { + public function __construct($deepCoding = TRUE, $formatWhitespace = FALSE) { $this->deepCoding = $deepCoding; + $this->formatWhitespace = $formatWhitespace; } /** @@ -75,13 +83,15 @@ class CRM_Afform_ArrayHtml { if ($array === []) { return ''; } + $indent = $this->formatWhitespace ? str_repeat(' ', $this->indent) : ''; + $end = $this->formatWhitespace ? "\n" : ''; if (isset($array['#comment'])) { if (strpos($array['#comment'], '-->')) { Civi::log()->warning('Afform: Cannot store comment with text "-->". Munging.'); $array['#comment'] = str_replace('-->', '-- >', $array['#comment']); } - return sprintf('', $array['#comment']); + return $indent . sprintf('', $array['#comment']) . $end; } if (isset($array['#text'])) { @@ -93,7 +103,7 @@ class CRM_Afform_ArrayHtml { $children = empty($array['#children']) ? [] : $array['#children']; unset($array['#children']); - $buf = '<' . $tag; + $buf = $indent . '<' . $tag; foreach ($array as $attrName => $attrValue) { if ($attrName{0} === '#') { continue; @@ -115,23 +125,34 @@ class CRM_Afform_ArrayHtml { } } - if (isset($array['#markup'])) { - return $buf . '>' . $array['#markup'] . ''; + if (isset($array['#markup']) && !$this->formatWhitespace) { + $buf .= '>' . $array['#markup'] . ''; } - - if (empty($children) && $this->isSelfClosing($tag)) { + elseif (isset($array['#markup'])) { + $indent2 = str_repeat(' ', $this->indent + 1); + $buf .= '>' . $end . $indent2 . str_replace("\n<", "\n$indent2<", $array['#markup']) . $end . $indent . ''; + } + elseif (empty($children) && $this->isSelfClosing($tag)) { $buf .= ' />'; } else { - $buf .= '>'; - $buf .= $this->convertArraysToHtml($children); + $contents = $this->convertArraysToHtml($children); + // No indentation if contents are only text + if (!$this->formatWhitespace || strpos($contents, '<') === FALSE) { + $buf .= '>' . $contents; + } + else { + $buf .= '>' . $end; + $buf .= $contents . $indent; + } $buf .= ''; } - return $buf; + return $buf . $end; } public function convertArraysToHtml($children) { $buf = ''; + $this->indent++; foreach ($children as $child) { if (is_string($child)) { @@ -142,6 +163,7 @@ class CRM_Afform_ArrayHtml { } } + $this->indent--; return $buf; } @@ -183,7 +205,7 @@ class CRM_Afform_ArrayHtml { $arr[$attribute->name] = $this->decodeAttrValue($type, $txt); } if ($node->childNodes->length > 0) { - // In shallow mode, return "af-markup" blocks as-is + // In shallow mode, return "af-markup" containers as-is if (!$this->deepCoding && !empty($arr['class']) && strpos($arr['class'], 'af-markup') !== FALSE) { $arr['#markup'] = ''; foreach ($node->childNodes as $child) { @@ -215,7 +237,14 @@ class CRM_Afform_ArrayHtml { protected function convertNodesToArray($nodes) { $children = []; foreach ($nodes as $childNode) { - $children[] = $this->convertNodeToArray($childNode); + $childArray = $this->convertNodeToArray($childNode); + // Remove extra whitespace + if ($this->formatWhitespace && isset($childArray['#text'])) { + $childArray['#text'] = trim($childArray['#text']); + } + if (!isset($childArray['#text']) || strlen($childArray['#text'])) { + $children[] = $childArray; + } } return $children; } diff --git a/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php b/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php index 7d224a657f..4383a11066 100644 --- a/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php +++ b/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php @@ -7,15 +7,34 @@ namespace Civi\Api4\Utils; * * @method $this setLayoutFormat(string $layoutFormat) * @method string getLayoutFormat() + * @method $this setFormatWhitespace(string $layoutFormat) + * @method string getFormatWhitespace() */ trait AfformFormatTrait { /** + * Controls the return format of the "layout" property + * - html will return layout html as-is. + * - shallow will convert most html to an array, but leave tag attributes and af-markup containers alone. + * - deep will attempt to convert all html to an array, including tag attributes. + * * @var string * @options html,shallow,deep */ protected $layoutFormat = 'deep'; + /** + * Optionally manage whitespace for the "layout" property + * + * This option will strip whitepace from the returned layout array for "get" actions, + * and will auto-indent the aff.html for "save" actions. + * + * Note: currently this has no affect on "get" with "html" return format, which returns html as-is. + * + * @var bool + */ + protected $formatWhitespace = FALSE; + /** * @param string $html * @return mixed @@ -25,7 +44,7 @@ trait AfformFormatTrait { if ($this->layoutFormat === 'html') { return $html; } - $converter = new \CRM_Afform_ArrayHtml($this->layoutFormat !== 'shallow'); + $converter = new \CRM_Afform_ArrayHtml($this->layoutFormat !== 'shallow', $this->formatWhitespace); return $converter->convertHtmlToArray($html); } @@ -38,7 +57,7 @@ trait AfformFormatTrait { if ($this->layoutFormat === 'html') { return $mixed; } - $converter = new \CRM_Afform_ArrayHtml($this->layoutFormat !== 'shallow'); + $converter = new \CRM_Afform_ArrayHtml($this->layoutFormat !== 'shallow', $this->formatWhitespace); return $converter->convertArraysToHtml($mixed); } diff --git a/ext/afform/gui/ang/afGuiEditor.js b/ext/afform/gui/ang/afGuiEditor.js index 4428b72bb8..3a2f954ca3 100644 --- a/ext/afform/gui/ang/afGuiEditor.js +++ b/ext/afform/gui/ang/afGuiEditor.js @@ -43,7 +43,7 @@ }; if ($scope.afGuiEditor.name && $scope.afGuiEditor.name != '0') { // Todo - show error msg if form is not found - crmApi4('Afform', 'get', {where: [['name', '=', $scope.afGuiEditor.name]], layoutFormat: 'shallow'}, 0) + crmApi4('Afform', 'get', {where: [['name', '=', $scope.afGuiEditor.name]], layoutFormat: 'shallow', formatWhitespace: true}, 0) .then(initialize); } else { @@ -67,10 +67,6 @@ function initialize(afform) { $scope.afform = afform; $scope.changesSaved = 1; - // Remove empty text nodes, they just create clutter - removeRecursive($scope.afform.layout, function(item) { - return ('#text' in item) && _.trim(item['#text']).length === 0; - }); $scope.layout = findRecursive($scope.afform.layout, {'#tag': 'af-form'})[0]; evaluate($scope.layout['#children']); $scope.entities = findRecursive($scope.layout['#children'], {'#tag': 'af-entity'}, 'name'); @@ -153,7 +149,7 @@ $scope.save = function() { $scope.saving = $scope.changesSaved = true; - crmApi4('Afform', 'save', {records: [JSON.parse(angular.toJson($scope.afform))]}) + crmApi4('Afform', 'save', {formatWhitespace: true, records: [JSON.parse(angular.toJson($scope.afform))]}) .then(function (data) { $scope.saving = false; $scope.afform.name = data[0].name; diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php index 999d22a8a3..6a7fdd87e1 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php @@ -68,20 +68,19 @@ class api_v4_AfformTest extends api_v4_AfformTestCase { } public function getFormatExamples() { - $es = []; - - foreach (['empty', 'string', 'comments', 'self-closing', 'apple', 'banana', 'cherry'] as $exampleName) { - $exampleFile = '/formatExamples/' . $exampleName . '.php'; - $example = require __DIR__ . $exampleFile; - $formats = ['html', 'shallow', 'deep']; - foreach ($formats as $updateFormat) { - foreach ($formats as $readFormat) { - $es[] = ['mockBareFile', $updateFormat, $example[$updateFormat], $readFormat, $example[$readFormat], $exampleFile]; + $ex = []; + $formats = ['html', 'shallow', 'deep']; + foreach (glob(__DIR__ . '/formatExamples/*.php') as $exampleFile) { + $example = require $exampleFile; + if (isset($example['deep'])) { + foreach ($formats as $updateFormat) { + foreach ($formats as $readFormat) { + $ex[] = ['mockBareFile', $updateFormat, $example[$updateFormat], $readFormat, $example[$readFormat], $exampleFile]; + } } } } - - return $es; + return $ex; } /** @@ -123,6 +122,53 @@ class api_v4_AfformTest extends api_v4_AfformTestCase { Civi\Api4\Afform::revert()->addWhere('name', '=', $formName)->execute(); } + public function getWhitespaceExamples() { + $ex = []; + foreach (glob(__DIR__ . '/formatExamples/*.php') as $exampleFile) { + $example = require $exampleFile; + if (isset($example['pretty'])) { + $ex[] = ['mockBareFile', $example, $exampleFile]; + } + } + return $ex; + } + + /** + * This tests that a non-pretty html string will have its whitespace stripped & reformatted + * when using the "formatWhitespace" option. + * + * @dataProvider getWhitespaceExamples + */ + public function testWhitespaceFormat($directiveName, $example, $exampleName) { + Civi\Api4\Afform::save() + ->addRecord(['name' => $directiveName, 'layout' => $example['html']]) + ->setLayoutFormat('html') + ->execute(); + + $result = Civi\Api4\Afform::get() + ->addWhere('name', '=', $directiveName) + ->setLayoutFormat('shallow') + ->setFormatWhitespace(TRUE) + ->execute() + ->first(); + + $this->assertEquals($example['stripped'] ?? $example['shallow'], $result['layout']); + + Civi\Api4\Afform::save() + ->addRecord(['name' => $directiveName, 'layout' => $result['layout']]) + ->setLayoutFormat('shallow') + ->setFormatWhitespace(TRUE) + ->execute(); + + $result = Civi\Api4\Afform::get() + ->addWhere('name', '=', $directiveName) + ->setLayoutFormat('html') + ->execute() + ->first(); + + $this->assertEquals($example['pretty'], $result['layout']); + } + public function testAutoRequires() { $formName = 'mockPage'; $this->createLoggedInUser(); diff --git a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/apple.php b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/apple.php index 28d8ba4be2..d0674fc335 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/apple.php +++ b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/apple.php @@ -2,6 +2,7 @@ return [ 'html' => 'New text!', + 'pretty' => "New text!\n", 'shallow' => [ ['#tag' => 'strong', '#children' => [['#text' => 'New text!']]], ], diff --git a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/banana.php b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/banana.php index 7e0a134caa..4e2e63db3c 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/banana.php +++ b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/banana.php @@ -1,8 +1,13 @@ '
New text!
', - 'shallow' => [ + 'html' => '
New text!
', + 'pretty' => '
+ New text! + +
+', + 'stripped' => [ [ '#tag' => 'div', '#children' => [ @@ -11,11 +16,20 @@ return [ ], ], ], + 'shallow' => [ + [ + '#tag' => 'div', + '#children' => [ + ['#tag' => 'strong', '#children' => [['#text' => ' New text!']]], + ['#tag' => 'af-field', 'name' => 'do_not_sms', 'defn' => "{label: 'Do not do any of the emailing'}"], + ], + ], + ], 'deep' => [ [ '#tag' => 'div', '#children' => [ - ['#tag' => 'strong', '#children' => [['#text' => 'New text!']]], + ['#tag' => 'strong', '#children' => [['#text' => ' New text!']]], ['#tag' => 'af-field', 'name' => 'do_not_sms', 'defn' => ['label' => 'Do not do any of the emailing']], ], ], diff --git a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/cherry.php b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/cherry.php index f47efed0e4..44376d4cd9 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/cherry.php +++ b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/cherry.php @@ -2,11 +2,16 @@ return [ 'html' => 'First Second', + 'pretty' => "First\nSecond\n", 'shallow' => [ ['#tag' => 'span', '#children' => [['#text' => 'First']]], ['#text' => ' '], ['#tag' => 'span', '#children' => [['#text' => 'Second']]], ], + 'stripped' => [ + ['#tag' => 'span', '#children' => [['#text' => 'First']]], + ['#tag' => 'span', '#children' => [['#text' => 'Second']]], + ], 'deep' => [ ['#tag' => 'span', '#children' => [['#text' => 'First']]], ['#text' => ' '], diff --git a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/empty.php b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/empty.php index e08233a2d0..c8010a5b9b 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/empty.php +++ b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/empty.php @@ -2,6 +2,7 @@ return [ 'html' => '', + 'pretty' => '', 'shallow' => [], 'deep' => [], ]; diff --git a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/self-closing.php b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/self-closing.php index ab68d0238b..62063a8087 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/self-closing.php +++ b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/self-closing.php @@ -2,6 +2,13 @@ return [ 'html' => '


', + 'pretty' => ' + +
+
+
+
+', 'shallow' => [ ['#tag' => 'span', 'class' => 'one'], ['#tag' => 'img', 'class' => 'two'], diff --git a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/string.php b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/string.php index 221e1a36f0..28537729f9 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/formatExamples/string.php +++ b/ext/afform/mock/tests/phpunit/api/v4/formatExamples/string.php @@ -2,6 +2,7 @@ return [ 'html' => 'hello world', + 'pretty' => 'hello world', 'shallow' => [['#text' => 'hello world']], 'deep' => [['#text' => 'hello world']], ]; -- 2.25.1