From 69a32a3f0e6ac0e6a3cd0cd21a985bb148f5c404 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 11 Sep 2019 17:16:24 -0400 Subject: [PATCH] WIP ArrayHtml --- ext/afform/core/CRM/Afform/ArrayHtml.php | 152 +++++++++++++++--- .../Civi/Api4/Utils/AfformFormatTrait.php | 8 +- .../mock/tests/phpunit/api/v4/AfformTest.php | 34 +++- 3 files changed, 161 insertions(+), 33 deletions(-) diff --git a/ext/afform/core/CRM/Afform/ArrayHtml.php b/ext/afform/core/CRM/Afform/ArrayHtml.php index af5c4a4c41..66757c3ba9 100644 --- a/ext/afform/core/CRM/Afform/ArrayHtml.php +++ b/ext/afform/core/CRM/Afform/ArrayHtml.php @@ -10,21 +10,58 @@ class CRM_Afform_ArrayHtml { const DEFAULT_TAG = 'div'; + /** + * This is a minimalist/temporary placeholder for a schema definition. + * FIXME: It shouldn't be here or look like this. + * + * @var array + * Ex: $protoSchema['my-tag']['my-attr'] = 'text'; + */ + private $protoSchema = [ + '*' => [ + '*' => 'text', + ], + 'af-model-prop' => [ + 'af-name' => 'text', + 'af-entity' => 'text', + 'af-data' => 'js', + ], + 'af-field' => [ + 'field-name' => 'text', + 'field-defn' => 'js', + ], + 'af-model' => [ + 'af-name' => 'text', + ], + ]; + + /** + * @var bool + */ + protected $deepCoding; + + /** + * CRM_Afform_ArrayHtml constructor. + * @param bool $deepCoding + */ + public function __construct($deepCoding = TRUE) { + $this->deepCoding = $deepCoding; + } + /** * @param array $array * Ex: ['#tag' => 'div', 'class' => 'greeting', '#children' => ['Hello world']] - * @param string $format * @return string * Ex: '
Hello world
' */ - public function convertArrayToHtml(array $array, $format = 'shallow') { + public function convertArrayToHtml(array $array) { if ($array === []) { return ''; } $tag = empty($array['#tag']) ? self::DEFAULT_TAG : $array['#tag']; unset($array['#tag']); - $children = empty($array['#children']) ? self::DEFAULT_TAG : $array['#children']; + $children = empty($array['#children']) ? [] : $array['#children']; unset($array['#children']); $buf = '<' . $tag; @@ -35,13 +72,12 @@ class CRM_Afform_ArrayHtml { if (!preg_match('/^[a-zA-Z0-9\-]+$/', $attrName)) { throw new \RuntimeException("Malformed HTML attribute"); } - if (is_string($attrValue)) { - // FIXME attribute encoding - $buf .= sprintf(' %s="%s"', $attrName, htmlentities($attrValue)); - } - elseif (is_array($attrValue) && $this->allowStructuredAttribute($tag, $attrName)) { - // FIXME attribute encoding - $buf .= sprintf(' %s="%s"', $attrName, htmlentities(json_encode($attrValue))); + + $type = $this->pickAttrType($tag, $attrName); + $encodedValue = $this->encodeAttrValue($type, $attrValue); + if ($encodedValue !== NULL) { + // ENT_COMPAT: Will convert double-quotes and leave single-quotes alone. + $buf .= sprintf(" %s=\"%s\"", $attrName, htmlentities($encodedValue, ENT_COMPAT | ENT_XHTML)); } else { Civi::log()->warning('Afform: Cannot serialize attribute {attrName}', [ @@ -67,11 +103,10 @@ class CRM_Afform_ArrayHtml { /** * @param string $html * Ex: '
Hello world
' - * @param string $format * @return array * Ex: ['#tag' => 'div', 'class' => 'greeting', '#children' => ['Hello world']] */ - public function convertHtmlToArray($html, $format = 'shallow') { + public function convertHtmlToArray($html) { if ($html === '') { return []; } @@ -99,12 +134,9 @@ class CRM_Afform_ArrayHtml { $arr = ['#tag' => $node->tagName]; foreach ($node->attributes as $attribute) { $txt = $attribute->textContent; - if ($txt && $txt{0} === '{' && $txt{1} !== '{' && $this->allowStructuredAttribute($node->tagName, $attribute->name)) { - $arr[$attribute->name] = sprintf('PARSE-ME(%s)', $txt); - } - else { - $arr[$attribute->name] = $txt; - } + + $type = $this->pickAttrType($node->tagName, $attribute->name); + $arr[$attribute->name] = $this->decodeAttrValue($type, $txt); } foreach ($node->childNodes as $childNode) { $arr['#children'][] = $this->convertNodeToArray($childNode); @@ -122,9 +154,87 @@ class CRM_Afform_ArrayHtml { } } - public function allowStructuredAttribute($tag, $attr) { - // FIXME: use whitelist of allowed angular directives - return FALSE; + /** + * Determine the type of data that is stored in an attribute. + * + * @param string $tag + * Ex: 'af-model-prop' + * @param string $attrName + * Ex: 'af-name' + * @return string + * Ex: 'text' or 'js' + */ + protected function pickAttrType($tag, $attrName) { + if (!$this->deepCoding) { + return 'text'; + } + + if (isset($this->protoSchema[$tag][$attrName])) { + return $this->protoSchema[$tag][$attrName]; + } + + if (isset($this->protoSchema['*'][$attrName])) { + return $this->protoSchema['*'][$attrName]; + } + + return $this->protoSchema['*']['*']; + } + + /** + * Given an array (deep) representation of an attribute, determine the + * attribute's content. + * + * @param string $type + * Ex A: 'text' + * Ex B: 'js' + * @param mixed $mixedAttrValue + * The mixed (string/array/int) representation of the value in deep format + * Ex A: 'hello' + * Ex B: ['hello' => 123] + * @return string + * An equivalent HTML attribute content (text). + * Ex A: 'hello' + * Ex B: '{hello: 123}' + */ + protected function encodeAttrValue($type, $mixedAttrValue) { + switch ($type) { + case 'text': + return $mixedAttrValue; + + case 'js': + $v = CRM_Utils_JS::writeObject($mixedAttrValue, TRUE); + return $v; + + default: + return NULL; + } + } + + /** + * Given a string representation of an attribute value, determine the + * equivalent array (deep) representation. + * + * @param string $type + * Ex A: 'text' + * Ex B: 'js' + * @param string $txtAttrValue + * The textual representation of the value from HTML notation. + * Ex A: 'hello' + * Ex B: '{hello: 123}' + * @return mixed + * The mixed (string/array/int) rerepresentation of the value in deep format + * Ex A: 'hello' + * Ex B: ['hello' => 123] + */ + protected function decodeAttrValue($type, $txtAttrValue) { + if ($type == 'js') { + $attrValue = CRM_Utils_JS::decode($txtAttrValue); + return $attrValue; + } + else { + $attrValue = $txtAttrValue; + return $attrValue; + } } } diff --git a/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php b/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php index 4f3af74281..dd3e7cd6e3 100644 --- a/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php +++ b/ext/afform/core/Civi/Api4/Utils/AfformFormatTrait.php @@ -25,8 +25,8 @@ trait AfformFormatTrait { if ($this->layoutFormat === 'html') { return $html; } - $converter = new \CRM_Afform_ArrayHtml(); - return $converter->convertHtmlToArray($html, $this->layoutFormat); + $converter = new \CRM_Afform_ArrayHtml($this->layoutFormat === 'deep'); + return $converter->convertHtmlToArray($html); } /** @@ -38,8 +38,8 @@ trait AfformFormatTrait { if ($this->layoutFormat === 'html') { return $mixed; } - $converter = new \CRM_Afform_ArrayHtml(); - return $converter->convertArrayToHtml($mixed, $this->layoutFormat); + $converter = new \CRM_Afform_ArrayHtml($this->layoutFormat === 'deep'); + return $converter->convertArrayToHtml($mixed); } } diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php index ad00a70e15..05cf0a3b73 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php @@ -1,8 +1,5 @@ New text!'; - $asArray = ['#tag' => 'strong', '#children' => ['New text!']]; + $asHtml = '
New text!
'; + $asShallow = [ + '#tag' => 'div', + '#children' => [ + ['#tag' => 'strong', '#children' => ['New text!']], + ['#tag' => 'af-field', 'field-name' => 'do_not_sms', 'field-defn' => "{label: 'Do not do any of the emailing'}"], + ], + ]; + $asDeep = [ + '#tag' => 'div', + '#children' => [ + ['#tag' => 'strong', '#children' => ['New text!']], + ['#tag' => 'af-field', 'field-name' => 'do_not_sms', 'field-defn' => ['label' => 'Do not do any of the emailing']], + ], + ]; - $es[] = ['fakelibBareFile', 'html', $asHtml, 'array', $asArray]; - $es[] = ['fakelibBareFile', 'array', $asArray, 'html', $asHtml]; $es[] = ['fakelibBareFile', 'html', $asHtml, 'html', $asHtml]; - $es[] = ['fakelibBareFile', 'array', $asArray, 'array', $asArray]; + + $es[] = ['fakelibBareFile', 'html', $asHtml, 'shallow', $asShallow]; + $es[] = ['fakelibBareFile', 'shallow', $asShallow, 'html', $asHtml]; + $es[] = ['fakelibBareFile', 'shallow', $asShallow, 'shallow', $asShallow]; + + $es[] = ['fakelibBareFile', 'html', $asHtml, 'deep', $asDeep]; + $es[] = ['fakelibBareFile', 'deep', $asDeep, 'html', $asHtml]; + $es[] = ['fakelibBareFile', 'deep', $asDeep, 'deep', $asDeep]; + + $es[] = ['fakelibBareFile', 'shallow', $asShallow, 'deep', $asDeep]; + $es[] = ['fakelibBareFile', 'deep', $asDeep, 'shallow', $asShallow]; return $es; } -- 2.25.1