diff --git a/app/Http/Requests/Task/StoreTaskRequest.php b/app/Http/Requests/Task/StoreTaskRequest.php index 456b350fb526..d9ddff121d4e 100644 --- a/app/Http/Requests/Task/StoreTaskRequest.php +++ b/app/Http/Requests/Task/StoreTaskRequest.php @@ -55,8 +55,11 @@ class StoreTaskRequest extends Request } $rules['time_log'] = ['bail',function ($attribute, $values, $fail) { + + if(is_string($values)) + $values = json_decode($values, true); - if(!is_array(json_decode($values, true))) { + if(!is_array($values)) { $fail('The '.$attribute.' must be a valid array.'); return; } diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index ee0f6711b6d6..85b8254dd75a 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -62,7 +62,11 @@ class UpdateTaskRequest extends Request $rules['time_log'] = ['bail', function ($attribute, $values, $fail) { - if(!is_array(json_decode($values, true))) { + if(is_string($values)) { + $values = json_decode($values, true); + } + + if(!is_array($values)) { $fail('The '.$attribute.' must be a valid array.'); return; } diff --git a/app/Services/Pdf/PdfService.php b/app/Services/Pdf/PdfService.php index 7a999ae98262..a1b7823ed7b6 100644 --- a/app/Services/Pdf/PdfService.php +++ b/app/Services/Pdf/PdfService.php @@ -73,7 +73,6 @@ class PdfService } public function boot(): self - { { $this->init(); diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 1b44ffbd5f9c..4e2f42d03bd8 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -13,15 +13,15 @@ namespace App\Services\Template; use App\Models\Client; use App\Models\Company; +use App\Models\Credit; use App\Models\Design; use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; use App\Models\PurchaseOrder; -use App\Transformers\ProjectTransformer; -use App\Transformers\PurchaseOrderTransformer; -use App\Transformers\QuoteTransformer; -use App\Transformers\TaskTransformer; +use App\Models\Quote; +use App\Models\RecurringInvoice; +use App\Models\Vendor; use App\Utils\HostedPDF\NinjaPdf; use App\Utils\HtmlEngine; use App\Utils\Number; @@ -29,20 +29,13 @@ use App\Utils\PaymentHtmlEngine; use App\Utils\Traits\MakesDates; use App\Utils\Traits\Pdf\PdfMaker; use App\Utils\VendorHtmlEngine; -use League\Fractal\Manager; -use League\Fractal\Serializer\ArraySerializer; -use Twig\Environment; +use League\CommonMark\CommonMarkConverter; use Twig\Error\Error; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -use Twig\Extension\DebugExtension; -use Twig\Extension\StringLoaderExtension; use Twig\Extra\Intl\IntlExtension; -use Twig\Loader\FilesystemLoader; use Twig\Sandbox\SecurityError; -use Twig\TwigFilter; -use Twig\TwigFunction; class TemplateService { @@ -51,7 +44,7 @@ class TemplateService private \DomDocument $document; - public Environment $twig; + public \Twig\Environment $twig; private string $compiled_html = ''; @@ -91,22 +84,22 @@ class TemplateService $this->document = new \DOMDocument(); $this->document->validateOnParse = true; - $loader = new FilesystemLoader(storage_path()); - $this->twig = new Environment($loader, [ + + $loader = new \Twig\Loader\FilesystemLoader(storage_path()); + $this->twig = new \Twig\Environment($loader, [ 'debug' => true, ]); - - $string_extension = new StringLoaderExtension(); + $string_extension = new \Twig\Extension\StringLoaderExtension(); $this->twig->addExtension($string_extension); $this->twig->addExtension(new IntlExtension()); - $this->twig->addExtension(new DebugExtension()); - - $function = new TwigFunction('img', function ($string, $style = '') { - return ''; + $this->twig->addExtension(new \Twig\Extension\DebugExtension()); + + $function = new \Twig\TwigFunction('img', function ($string, $style = '') { + return ''; }); $this->twig->addFunction($function); - $filter = new TwigFilter('sum', function (array $array, string $column) { + $filter = new \Twig\TwigFilter('sum', function (array $array, string $column) { return array_sum(array_column($array, $column)); }); @@ -240,19 +233,19 @@ class TemplateService $template = $this->twig->createTemplate(html_entity_decode($template)); } catch(SyntaxError $e) { nlog($e->getMessage()); - continue; + throw ($e); } catch(Error $e) { - nlog("error = " .$e->getMessage()); - continue; + nlog("error = " . $e->getMessage()); + throw ($e); } catch(RuntimeError $e) { - nlog("runtime = " .$e->getMessage()); - continue; + nlog("runtime = " . $e->getMessage()); + throw ($e); } catch(LoaderError $e) { nlog("loader = " . $e->getMessage()); - continue; + throw ($e); } catch(SecurityError $e) { nlog("security = " . $e->getMessage()); - continue; + throw ($e); } $template = $template->render($this->data); @@ -285,7 +278,6 @@ class TemplateService $html = $this->getHtml(); foreach($this->variables as $key => $variable) { - if(isset($variable['labels']) && isset($variable['values'])) { $html = strtr($html, $variable['labels']); $html = strtr($html, $variable['values']); @@ -367,7 +359,7 @@ class TemplateService $processed = []; - if(in_array($key, ['tasks','projects','aging']) || !$value->first()) { + if(in_array($key, ['tasks', 'projects', 'aging']) || !$value->first()) { return $processed; } @@ -432,7 +424,8 @@ class TemplateService ->map(function ($invoice) { $payments = []; - + $this->entity = $invoice; + if($invoice->payments ?? false) { $payments = $invoice->payments->map(function ($payment) { return $this->transformPayment($payment); @@ -442,6 +435,8 @@ class TemplateService return [ 'amount' => Number::formatMoney($invoice->amount, $invoice->client), 'balance' => Number::formatMoney($invoice->balance, $invoice->client), + 'status_id' => $invoice->status_id, + 'status' => Invoice::stringStatus($invoice->status_id), 'balance_raw' => $invoice->balance, 'number' => $invoice->number ?: '', 'discount' => $invoice->discount, @@ -479,7 +474,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $invoice->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $invoice->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $invoice->custom_surcharge_tax4, - 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client): (array) [], + 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client) : (array) [], 'reminder1_sent' => $this->translateDate($invoice->reminder1_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder2_sent' => $this->translateDate($invoice->reminder2_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder3_sent' => $this->translateDate($invoice->reminder3_sent, $invoice->client->date_format(), $invoice->client->locale()), @@ -512,7 +507,7 @@ class TemplateService */ public function padLineItems(array $items, Vendor | Client $client_or_vendor): array { - return collect($items)->map(function ($item) use ($client) { + return collect($items)->map(function ($item) use ($client_or_vendor) { $item->cost_raw = $item->cost ?? 0; $item->discount_raw = $item->discount ?? 0; @@ -521,16 +516,16 @@ class TemplateService $item->tax_amount_raw = $item->tax_amount ?? 0; $item->product_cost_raw = $item->product_cost ?? 0; - $item->cost = Number::formatMoney($item->cost_raw, $client); - + $item->cost = Number::formatMoney($item->cost_raw, $client_or_vendor); + if($item->is_amount_discount) { - $item->discount = Number::formatMoney($item->discount_raw, $client); + $item->discount = Number::formatMoney($item->discount_raw, $client_or_vendor); } - - $item->line_total = Number::formatMoney($item->line_total_raw, $client); - $item->gross_line_total = Number::formatMoney($item->gross_line_total_raw, $client); - $item->tax_amount = Number::formatMoney($item->tax_amount_raw, $client); - $item->product_cost = Number::formatMoney($item->product_cost_raw, $client); + + $item->line_total = Number::formatMoney($item->line_total_raw, $client_or_vendor); + $item->gross_line_total = Number::formatMoney($item->gross_line_total_raw, $client_or_vendor); + $item->tax_amount = Number::formatMoney($item->tax_amount_raw, $client_or_vendor); + $item->product_cost = Number::formatMoney($item->product_cost_raw, $client_or_vendor); return $item; @@ -597,7 +592,7 @@ class TemplateService 'balance_raw' => ($payment->amount - $payment->refunded - $payment->applied), 'date' => $this->translateDate($payment->date, $payment->client->date_format(), $payment->client->locale()), 'method' => $payment->translatedType(), - 'currency' => $payment->currency->code ?? $payment->company->currency()->code, + 'currency' => $payment->currency->code, 'exchange_rate' => $payment->exchange_rate, 'transaction_reference' => $payment->transaction_reference, 'is_manual' => $payment->is_manual, @@ -670,7 +665,7 @@ class TemplateService } /** - * + * * * @param array | \Illuminate\Support\Collection $quotes * @return array @@ -678,7 +673,7 @@ class TemplateService public function processQuotes($quotes): array { - return collect($quotes)->map(function ($quote){ + return collect($quotes)->map(function ($quote) { return [ 'amount' => Number::formatMoney($quote->amount, $quote->client), @@ -753,6 +748,8 @@ class TemplateService $credits = collect($credits) ->map(function ($credit) { + $this->entity = $credit; + return [ 'amount' => Number::formatMoney($credit->amount, $credit->client), 'balance' => Number::formatMoney($credit->balance, $credit->client), @@ -793,7 +790,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $credit->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $credit->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $credit->custom_surcharge_tax4, - 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client): (array) [], + 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client) : (array) [], 'reminder1_sent' => $this->translateDate($credit->reminder1_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder2_sent' => $this->translateDate($credit->reminder2_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder3_sent' => $this->translateDate($credit->reminder3_sent, $credit->client->date_format(), $credit->client->locale()), @@ -843,16 +840,16 @@ class TemplateService public function processTasks($tasks, bool $nested = false): array { - return collect($tasks)->map(function ($task) use ($nested){ + return collect($tasks)->map(function ($task) use ($nested) { return [ 'number' => (string) $task->number ?: '', 'description' => (string) $task->description ?: '', 'duration' => $task->duration ?: 0, - 'rate' => Number::formatMoney($task->rate ?? 0, $task->client ?? $task->company), + 'rate' => Number::formatMoney($task->rate ?? 0, $task->client ?? $task->company), 'created_at' => $this->translateDate($task->created_at, $task->client ? $task->client->date_format() : $task->company->date_format(), $task->client ? $task->client->locale() : $task->company->locale()), 'updated_at' => $this->translateDate($task->updated_at, $task->client ? $task->client->date_format() : $task->company->date_format(), $task->client ? $task->client->locale() : $task->company->locale()), - 'date' => $task->calculated_start_date ? $this->translateDate($task->calculated_start_date , $task->client ? $task->client->date_format() : $task->company->date_format(), $task->client ? $task->client->locale() : $task->company->locale()) : '', + 'date' => $task->calculated_start_date ? $this->translateDate($task->calculated_start_date, $task->client ? $task->client->date_format() : $task->company->date_format(), $task->client ? $task->client->locale() : $task->company->locale()) : '', // 'invoice_id' => $this->encodePrimaryKey($task->invoice_id) ?: '', 'project' => ($task->project && !$nested) ? $this->transformProject($task->project, true) : [], 'time_log' => $task->processLogs(), @@ -884,7 +881,7 @@ class TemplateService { return - collect($projects)->map(function ($project){ + collect($projects)->map(function ($project) { return $this->transformProject($project); @@ -924,14 +921,14 @@ class TemplateService } /** - * + * * @param array | \Illuminate\Support\Collection $purchase_orders * @return array */ public function processPurchaseOrders($purchase_orders): array { - return collect($purchase_orders)->map(function ($purchase_order){ + return collect($purchase_orders)->map(function ($purchase_order) { return [ 'vendor' => $purchase_order->vendor ? [ @@ -1034,4 +1031,421 @@ class TemplateService return $this; } + /** + * Parses and finds any field stacks to inject into the DOM Document + * + * @return self + */ + public function parseGlobalStacks(): self + { + $stacks = [ + 'entity-details', + 'client-details', + 'vendor-details', + 'company-details', + 'company-address', + 'shipping-details', + ]; + + collect($stacks)->filter(function ($stack) { + return $this->document->getElementById($stack) ?? false; + }) + ->map(function ($stack) { + $node = $this->document->getElementById($stack); + return ['stack' => $stack, 'labels' => $node->getAttribute('labels')]; + }) + ->each(function ($stack) { + $this->parseStack($stack); + }); + + return $this; + + } + + /** + * Injects field stacks into Template + * + * @param array $stack + * @return self + */ + private function parseStack(array $stack): self + { + + match($stack['stack']) { + 'entity-details' => $this->entityDetails(), + 'client-details' => $this->clientDetails($stack['labels'] == 'true'), + 'vendor-details' => $this->vendorDetails($stack['labels'] == 'true'), + 'company-details' => $this->companyDetails($stack['labels'] == 'true'), + 'company-address' => $this->companyAddress($stack['labels'] == 'true'), + 'shipping-details' => $this->shippingDetails($stack['labels'] == 'true'), + }; + + $this->save(); + + return $this; + } + + /** + * Inject the Company Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function companyDetails(bool $include_labels): self + { + $var_set = $this->getVarSet(); + + $company_details = + collect($this->company->settings->pdf_variables->company_details) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + }) + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]]; + }); + })->toArray(); + + // nlog($company_details); + + $company_details = $include_labels ? $this->labelledFieldStack($company_details, 'company_details-') : $company_details; + + // nlog($company_details); + + $this->updateElementProperties('company-details', $company_details); + + return $this; + } + + private function companyAddress(bool $include_labels = false): self + { + + $var_set = $this->getVarSet(); + + $company_address = + collect($this->company->settings->pdf_variables->company_address) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + }) + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_address-' . substr($variable, 1)]]; + }); + })->toArray(); + + $company_address = $include_labels ? $this->labelledFieldStack($company_address, 'company_address-') : $company_address; + + $this->updateElementProperties('company-address', $company_address); + + return $this; + } + + /** + * Injects the Shipping Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function shippingDetails(bool $include_labels = false): self + { + if(!$this->entity->client) { + return $this; + } + + $this->client = $this->entity->client; + + $shipping_address = [ + ['element' => 'p', 'content' => ctrans('texts.shipping_address'), 'properties' => ['data-ref' => 'shipping_address-label', 'style' => 'font-weight: bold; text-transform: uppercase']], + ['element' => 'p', 'content' => $this->client->name, 'show_empty' => false, 'properties' => ['data-ref' => 'shipping_address-client.name']], + ['element' => 'p', 'content' => $this->client->shipping_address1, 'show_empty' => false, 'properties' => ['data-ref' => 'shipping_address-client.shipping_address1']], + ['element' => 'p', 'content' => $this->client->shipping_address2, 'show_empty' => false, 'properties' => ['data-ref' => 'shipping_address-client.shipping_address2']], + ['element' => 'p', 'show_empty' => false, 'elements' => [ + ['element' => 'span', 'content' => "{$this->client->shipping_city} ", 'properties' => ['ref' => 'shipping_address-client.shipping_city']], + ['element' => 'span', 'content' => "{$this->client->shipping_state} ", 'properties' => ['ref' => 'shipping_address-client.shipping_state']], + ['element' => 'span', 'content' => "{$this->client->shipping_postal_code} ", 'properties' => ['ref' => 'shipping_address-client.shipping_postal_code']], + ]], + ['element' => 'p', 'content' => optional($this->client->shipping_country)->name, 'show_empty' => false], + ]; + + $shipping_address = + collect($shipping_address)->filter(function ($address) { + return isset($address['content']) && !empty($address['content']); + })->toArray(); + + $this->updateElementProperties('shipping-details', $shipping_address); + + return $this; + } + + /** + * Injects the Client Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function clientDetails(bool $include_labels = false): self + { + $var_set = $this->getVarSet(); + + $client_details = + collect($this->company->settings->pdf_variables->client_details) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + }) + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'client_details-' . substr($variable, 1)]]; + }); + })->toArray(); + + $client_details = $include_labels ? $this->labelledFieldStack($client_details, 'client_details-') : $client_details; + + $this->updateElementProperties('client-details', $client_details); + + return $this; + } + + /** + * Resolves the entity. + * + * Only required for resolving the entity-details stack + * + * @return string + */ + private function resolveEntity(): string + { + $entity_string = ''; + + match($this->entity) { + ($this->entity instanceof Invoice) => $entity_string = 'invoice', + ($this->entity instanceof Quote) => $entity_string = 'quote', + ($this->entity instanceof Credit) => $entity_string = 'credit', + ($this->entity instanceof RecurringInvoice) => $entity_string = 'invoice', + ($this->entity instanceof PurchaseOrder) => $entity_string = 'purchase_order', + default => $entity_string = 'invoice', + }; + + return $entity_string; + + } + + /** + * Returns the variable array by first key, if it exists + * + * @return array + */ + private function getVarSet(): array + { + return array_key_exists(array_key_first($this->variables), $this->variables) ? $this->variables[array_key_first($this->variables)] : $this->variables; + } + + /** + * Injects the entity details to the DOM document + * + * @return self + */ + private function entityDetails(): self + { + $entity_string = $this->resolveEntity(); + $entity_string_prop = "{$entity_string}_details"; + $var_set = $this->getVarSet(); + + $entity_details = + collect($this->company->settings->pdf_variables->{$entity_string_prop}) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + })->toArray(); + + $this->updateElementProperties("entity-details", $this->labelledFieldStack($entity_details, 'entity_details-')); + + return $this; + } + + /** + * Generates the field stacks with labels + * + * @param array $variables + * @return array + */ + private function labelledFieldStack(array $variables, string $data_ref): array + { + + $elements = []; + + foreach ($variables as $variable) { + $_variable = explode('.', $variable)[1]; + $_customs = ['custom1', 'custom2', 'custom3', 'custom4']; + + $var = str_replace("custom", "custom_value", $_variable); + + $hidden_prop = ($data_ref == 'entity_details-') ? $this->entityVariableCheck($variable) : false; + + if (in_array($_variable, $_customs) && !empty($this->entity->{$var})) { + $elements[] = ['element' => 'tr', 'elements' => [ + ['element' => 'th', 'content' => $variable . '_label', 'properties' => ['data-ref' => $data_ref . substr($variable, 1) . '_label']], + ['element' => 'th', 'content' => $variable, 'properties' => ['data-ref' => $data_ref . substr($variable, 1)]], + ]]; + } else { + $elements[] = ['element' => 'tr', 'properties' => ['hidden' => $hidden_prop], 'elements' => [ + ['element' => 'th', 'content' => $variable . '_label', 'properties' => ['data-ref' => $data_ref . substr($variable, 1) . '_label']], + ['element' => 'th', 'content' => $variable, 'properties' => ['data-ref' => $data_ref . substr($variable, 1)]], + ]]; + } + } + + return $elements; + + } + + /** + * Inject Vendor Details into DOM Document + * + * @param bool $include_labels + * @return self + */ + private function vendorDetails(bool $include_labels = false): self + { + + $var_set = $this->getVarSet(); + + $vendor_details = + collect($this->company->settings->pdf_variables->vendor_details) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + })->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]]; + }); + })->toArray(); + + $vendor_details = $include_labels ? $this->labelledFieldStack($vendor_details, 'vendor_details-') : $vendor_details; + + $this->updateElementProperties('vendor-details', $vendor_details); + + return $this; + } + + + /** + * Performs a variable check to ensure + * the variable exists + * + * @param string $variable + * @return bool + * + */ + public function entityVariableCheck(string $variable): bool + { + // When it comes to invoice balance, we'll always show it. + if ($variable == '$invoice.total') { + return false; + } + + // Some variables don't map 1:1 to table columns. This gives us support for such cases. + $aliases = [ + '$quote.balance_due' => 'partial', + ]; + + try { + $_variable = explode('.', $variable)[1]; + } catch (\Exception $e) { + throw new \Exception('Company settings seems to be broken. Missing $this->service->config->entity.variable type.'); + } + + if (\in_array($variable, \array_keys($aliases))) { + $_variable = $aliases[$variable]; + } + + if (is_null($this->entity->{$_variable}) || empty($this->entity->{$_variable})) { + return true; + } + + return false; + } + + //////////////////////////////////////// + // Dom Traversal + /////////////////////////////////////// + + public function updateElementProperties(string $element_id, array $elements): self + { + $node = $this->document->getElementById($element_id); + + $this->createElementContent($node, $elements); + + return $this; + } + + public function updateElementProperty($element, string $attribute, ?string $value) + { + + if ($attribute == 'hidden' && ($value == false || $value == 'false')) { + return $element; + } + + $element->setAttribute($attribute, $value); + + if ($element->getAttribute($attribute) === $value) { + return $element; + } + + return $element; + + } + + public function createElementContent($element, $children): self + { + + foreach ($children as $child) { + $contains_html = false; + + //06-11-2023 for some reason this parses content as HTML + // if ($child['element'] !== 'script') { + // if ($this->company->markdown_enabled && array_key_exists('content', $child)) { + // $child['content'] = str_replace('
', "\r", $child['content']); + // $child['content'] = $this->commonmark->convert($child['content'] ?? ''); + // } + // } + + if (isset($child['content'])) { + if (isset($child['is_empty']) && $child['is_empty'] === true) { + continue; + } + + $contains_html = preg_match('#(?<=<)\w+(?=[^<]*?>)#', $child['content'], $m) != 0; + } + + if ($contains_html) { + // If the element contains the HTML, we gonna display it as is. Backend is going to + // encode it for us, preventing any errors on the processing stage. + // Later, we decode this using Javascript so it looks like it's normal HTML being injected. + // To get all elements that need frontend decoding, we use 'data-state' property. + + $_child = $this->document->createElement($child['element'], ''); + $_child->setAttribute('data-state', 'encoded-html'); + $_child->nodeValue = htmlspecialchars($child['content']); + } else { + // .. in case string doesn't contain any HTML, we'll just return + // raw $content. + + $_child = $this->document->createElement($child['element'], isset($child['content']) ? htmlspecialchars($child['content']) : ''); + } + + $element->appendChild($_child); + + if (isset($child['properties'])) { + foreach ($child['properties'] as $property => $value) { + $this->updateElementProperty($_child, $property, $value); + } + } + + if (isset($child['elements'])) { + $this->createElementContent($_child, $child['elements']); + } + + } + + return $this; + } + }