From 67f3bd369cb42cc547d832035d25e3912447071e Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 31 Jan 2019 01:26:19 +0100 Subject: [PATCH 1/4] Add methods to replace macro with ComplexType --- .gitignore | 1 - samples/Sample_40_TemplateSetComplexValue.php | 45 ++++ .../Sample_40_TemplateSetComplexValue.docx | Bin 0 -> 14735 bytes src/PhpWord/TemplateProcessor.php | 212 ++++++++++++++++++ tests/PhpWord/TemplateProcessorTest.php | 128 +++++++++++ .../_includes/TestableTemplateProcesor.php | 28 +++ 6 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 samples/Sample_40_TemplateSetComplexValue.php create mode 100644 samples/resources/Sample_40_TemplateSetComplexValue.docx diff --git a/.gitignore b/.gitignore index b2ec7e2398..dd858ceac6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ composer.phar vendor /report /build -/samples/resources /samples/results /.settings phpword.ini diff --git a/samples/Sample_40_TemplateSetComplexValue.php b/samples/Sample_40_TemplateSetComplexValue.php new file mode 100644 index 0000000000..094823f784 --- /dev/null +++ b/samples/Sample_40_TemplateSetComplexValue.php @@ -0,0 +1,45 @@ +addText('This title has been set ', array('bold' => true, 'italic' => true, 'color' => 'blue')); +$title->addText('dynamically', array('bold' => true, 'italic' => true, 'color' => 'red', 'underline' => 'single')); +$templateProcessor->setComplexBlock('title', $title); + +$inline = new TextRun(); +$inline->addText('by a red italic text', array('italic' => true, 'color' => 'red')); +$templateProcessor->setComplexValue('inline', $inline); + +$table = new Table(array('borderSize' => 12, 'borderColor' => 'green', 'width' => 6000, 'unit' => TblWidth::TWIP)); +$table->addRow(); +$table->addCell(150)->addText('Cell A1'); +$table->addCell(150)->addText('Cell A2'); +$table->addCell(150)->addText('Cell A3'); +$table->addRow(); +$table->addCell(150)->addText('Cell B1'); +$table->addCell(150)->addText('Cell B2'); +$table->addCell(150)->addText('Cell B3'); +$templateProcessor->setComplexBlock('table', $table); + +$field = new Field('DATE', array('dateformat' => 'dddd d MMMM yyyy H:mm:ss'), array('PreserveFormat')); +$templateProcessor->setComplexValue('field', $field); + +// $link = new Link('https://github.com/PHPOffice/PHPWord'); +// $templateProcessor->setComplexValue('link', $link); + +echo date('H:i:s'), ' Saving the result document...', EOL; +$templateProcessor->saveAs('results/Sample_40_TemplateSetComplexValue.docx'); + +echo getEndingNotes(array('Word2007' => 'docx'), 'results/Sample_40_TemplateSetComplexValue.docx'); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/samples/resources/Sample_40_TemplateSetComplexValue.docx b/samples/resources/Sample_40_TemplateSetComplexValue.docx new file mode 100644 index 0000000000000000000000000000000000000000..7265908e8c5c54842b11079507f3fea33af4b8e1 GIT binary patch literal 14735 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 DlaFJ( literal 0 HcmV?d00001 diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index fbfdd9dc0f..498cf7016d 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord; use PhpOffice\Common\Text; +use PhpOffice\Common\XMLWriter; use PhpOffice\PhpWord\Escaper\RegExp; use PhpOffice\PhpWord\Escaper\Xml; use PhpOffice\PhpWord\Exception\CopyFileException; @@ -249,6 +250,46 @@ protected static function ensureUtf8Encoded($subject) return $subject; } + /** + * @param string $search + * @param \PhpOffice\PhpWord\Element\AbstractElement $complexType + */ + public function setComplexValue($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType) + { + $elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1); + $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; + + $xmlWriter = new XMLWriter(); + /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */ + $elementWriter = new $objectClass($xmlWriter, $complexType, true); + $elementWriter->write(); + + $where = $this->findContainingXmlBlockForMacro($search, 'w:r'); + $block = $this->getSlice($where['start'], $where['end']); + $textParts = $this->splitTextIntoTexts($block); + $this->replaceXmlBlock($search, $textParts, 'w:r'); + + $search = static::ensureMacroCompleted($search); + $this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:r'); + } + + /** + * @param string $search + * @param \PhpOffice\PhpWord\Element\AbstractElement $complexType + */ + public function setComplexBlock($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType) + { + $elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1); + $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; + + $xmlWriter = new XMLWriter(); + /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */ + $elementWriter = new $objectClass($xmlWriter, $complexType, false); + $elementWriter->write(); + + $this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:p'); + } + /** * @param mixed $search * @param mixed $replace @@ -685,6 +726,7 @@ public function cloneRowAndSetValues($search, $values) public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null) { $xmlBlock = null; + $matches = array(); preg_match( '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, @@ -724,6 +766,7 @@ public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVaria */ public function replaceBlock($blockname, $replacement) { + $matches = array(); preg_match( '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, @@ -865,6 +908,7 @@ protected function setValueForPart($search, $replace, $documentPartXML, $limit) */ protected function getVariablesForPart($documentPartXML) { + $matches = array(); preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches); return $matches[1]; @@ -893,6 +937,7 @@ protected function getMainPartName() $pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~'; + $matches = array(); preg_match($pattern, $contentTypes, $matches); return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml'; @@ -1031,4 +1076,171 @@ protected function replaceClonedVariables($variableReplacements, $xmlBlock) return $results; } + + /** + * Replace an XML block surrounding a macro with a new block + * + * @param string $macro Name of macro + * @param string $block New block content + * @param string $blockType XML tag type of block + * @return \PhpOffice\PhpWord\TemplateProcessor Fluent interface + */ + protected function replaceXmlBlock($macro, $block, $blockType = 'w:p') + { + $where = $this->findContainingXmlBlockForMacro($macro, $blockType); + if (false !== $where) { + $this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']); + } + + return $this; + } + + /** + * Find start and end of XML block containing the given macro + * e.g. ...${macro}... + * + * Note that only the first instance of the macro will be found + * + * @param string $macro Name of macro + * @param string $blockType XML tag for block + * @return bool|int[] FALSE if not found, otherwise array with start and end + */ + protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') + { + $macroPos = $this->findMacro($macro); + if (false === $macroPos) { + return false; + } + $start = $this->findXmlBlockStart($macroPos, $blockType); + if (0 > $start) { + return false; + } + $end = $this->findXmlBlockEnd($start, $blockType); + if (0 > $end) { + return false; + } + + return array('start' => $start, 'end' => $end); + } + + /** + * Find start and end of XML block containing the given block macro + * e.g. ...${macro}...${/macro}... + * + * Note that only the first instance of the macro will be found + * + * @param string $macro Name of macro + * @param string $blockType XML tag for block + * @return bool|int[] FALSE if not found, otherwise array with start and end + */ + protected function findContainingXmlBlockForBlockMacro($macro, $blockType = 'w:p') + { + $macroStartPos = $this->findMacro($macro); + if (0 > $macroStartPos) { + return false; + } + $macroEndPos = $this->findMacro('/' . $macro, $macroStartPos); + if (0 > $macroEndPos) { + return false; + } + $start = $this->findXmlBlockStart($macroStartPos, $blockType); + if (0 > $start) { + return false; + } + $end = $this->findXmlBlockEnd($macroEndPos, $blockType); + if (0 > $end) { + return false; + } + + return array('start' => $start, 'end' => $end); + } + + /** + * Find the position of (the start of) a macro + * + * Returns -1 if not found, otherwise position of opening $ + * + * Note that only the first instance of the macro will be found + * + * @param string $search Macro name + * @param string $offset Offset from which to start searching + * @return int -1 if macro not found + */ + protected function findMacro($search, $offset = 0) + { + $search = static::ensureMacroCompleted($search); + $pos = strpos($this->tempDocumentMainPart, $search, $offset); + + return ($pos === false) ? -1 : $pos; + } + + /** + * Find the start position of the nearest XML block start before $offset + * + * @param int $offset Search position + * @param string $blockType XML Block tag + * @return int -1 if block start not found + */ + protected function findXmlBlockStart($offset, $blockType) + { + // first try XML tag with attributes + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + // if not found, or if found but contains the XML tag without attribute + if (false === $blockStart || strrpos($this->getSlice($blockStart, $offset), '<' . $blockType . '>')) { + // also try XML tag without attributes + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + } + + return ($blockStart === false) ? -1 : $blockStart; + } + + /** + * Find the nearest block end position after $offset + * + * @param int $offset Search position + * @param string $blockType XML Block tag + * @return int -1 if block end not found + */ + protected function findXmlBlockEnd($offset, $blockType) + { + $blockEndStart = strpos($this->tempDocumentMainPart, '', $offset); + // return position of end of tag if found, otherwise -1 + + return ($blockEndStart === false) ? -1 : $blockEndStart + 3 + strlen($blockType); + } + + /** + * Splits a w:r/w:t into a list of w:r where each ${macro} is in a separate w:r + * + * @param string $text + * @return string + */ + protected function splitTextIntoTexts($text) + { + if (!$this->textNeedsSplitting($text)) { + return $text; + } + $matches = array(); + if (preg_match('/()/i', $text, $matches)) { + $extractedStyle = $matches[0]; + } else { + $extractedStyle = ''; + } + + $unformattedText = preg_replace('/>\s+<', $text); + $result = str_replace(array('${', '}'), array('' . $extractedStyle . '${', '}' . $extractedStyle . ''), $unformattedText); + + return str_replace(array('' . $extractedStyle . '', '', ''), array('', '', ''), $result); + } + + /** + * Returns true if string contains a macro that is not in it's own w:r + * + * @param string $text + * @return bool + */ + protected function textNeedsSplitting($text) + { + return preg_match('/[^>]\${|}[^<]/i', $text) == 1; + } } diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 286ffe97de..4c9f235801 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -17,6 +17,9 @@ namespace PhpOffice\PhpWord; +use PhpOffice\PhpWord\Element\Text; +use PhpOffice\PhpWord\Element\TextRun; + /** * @covers \PhpOffice\PhpWord\TemplateProcessor * @coversDefaultClass \PhpOffice\PhpWord\TemplateProcessor @@ -307,6 +310,59 @@ public function testSetValue() ); } + public function testSetComplexValue() + { + $title = new TextRun(); + $title->addText('This is my title'); + + $firstname = new Text('Donald'); + $lastname = new Text('Duck'); + + $mainPart = ' + + + Hello ${document-title} + + + + + Hello ${firstname} ${lastname} + + '; + + $result = ' + + + + + This is my title + + + + + Hello + + + + Donald + + + + + + + Duck + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setComplexBlock('document-title', $title); + $templateProcessor->setComplexValue('firstname', $firstname); + $templateProcessor->setComplexValue('lastname', $lastname); + + $this->assertEquals(preg_replace('/>\s+<', $result), preg_replace('/>\s+<', $templateProcessor->getMainPart())); + } + /** * @covers ::setValues * @test @@ -675,4 +731,76 @@ public function testGetVariables() $variables = $templateProcessor->getVariablesForPart('$15,000.00. ${variable_name}'); $this->assertEquals(array('variable_name'), $variables); } + + /** + * @covers ::textNeedsSplitting + */ + public function testTextNeedsSplitting() + { + $templateProcessor = new TestableTemplateProcesor(); + + $this->assertFalse($templateProcessor->textNeedsSplitting('${nothing-to-replace}')); + + $text = 'Hello ${firstname} ${lastname}'; + $this->assertTrue($templateProcessor->textNeedsSplitting($text)); + $splitText = $templateProcessor->splitTextIntoTexts($text); + $this->assertFalse($templateProcessor->textNeedsSplitting($splitText)); + } + + /** + * @covers ::splitTextIntoTexts + */ + public function testSplitTextIntoTexts() + { + $templateProcessor = new TestableTemplateProcesor(); + + $splitText = $templateProcessor->splitTextIntoTexts('${nothing-to-replace}'); + $this->assertEquals('${nothing-to-replace}', $splitText); + + $splitText = $templateProcessor->splitTextIntoTexts('Hello ${firstname} ${lastname}'); + $this->assertEquals('Hello ${firstname} ${lastname}', $splitText); + } + + public function testFindXmlBlockStart() + { + $toFind = ' + + + + + This whole paragraph will be replaced with my ${title} + '; + $mainPart = ' + + + + + + + ${value1} ${value2} + + + + + + + . + + + + + + + + + + ' . $toFind . ' + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $position = $templateProcessor->findContainingXmlBlockForMacro('${title}', 'w:r'); + + $this->assertEquals($toFind, $templateProcessor->getSlice($position['start'], $position['end'])); + } } diff --git a/tests/PhpWord/_includes/TestableTemplateProcesor.php b/tests/PhpWord/_includes/TestableTemplateProcesor.php index 3b6f5b56c1..44c0bb55d5 100644 --- a/tests/PhpWord/_includes/TestableTemplateProcesor.php +++ b/tests/PhpWord/_includes/TestableTemplateProcesor.php @@ -35,6 +35,16 @@ public function fixBrokenMacros($documentPart) return parent::fixBrokenMacros($documentPart); } + public function splitTextIntoTexts($text) + { + return parent::splitTextIntoTexts($text); + } + + public function textNeedsSplitting($text) + { + return parent::textNeedsSplitting($text); + } + public function getVariablesForPart($documentPartXML) { $documentPartXML = parent::fixBrokenMacros($documentPartXML); @@ -42,6 +52,24 @@ public function getVariablesForPart($documentPartXML) return parent::getVariablesForPart($documentPartXML); } + public function findXmlBlockStart($offset, $blockType) + { + return parent::findXmlBlockStart($offset, $blockType); + } + + public function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') + { + return parent::findContainingXmlBlockForMacro($macro, $blockType); + } + + public function getSlice($startPosition, $endPosition = 0) + { + return parent::getSlice($startPosition, $endPosition); + } + + /** + * @return string + */ public function getMainPart() { return $this->tempDocumentMainPart; From d862b1f267ca86635593b3af3c637889248602ed Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 31 Jan 2019 01:32:00 +0100 Subject: [PATCH 2/4] update documentation --- docs/templates-processing.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index 325de8de74..5b32aa18e0 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -215,3 +215,32 @@ Applies the XSL stylesheet passed to header part, footer part and main part $xslDomDocument = new \DOMDocument(); $xslDomDocument->load('/path/to/my/stylesheet.xsl'); $templateProcessor->applyXslStyleSheet($xslDomDocument); + +setComplexValue +""""""""""""""" +Raplaces a ${macro} with the ComplexType passed. +See ``Sample_40_TemplateSetComplexValue.php`` for examples. + +.. code-block:: php + + $inline = new TextRun(); + $inline->addText('by a red italic text', array('italic' => true, 'color' => 'red')); + $templateProcessor->setComplexValue('inline', $inline); + +setComplexBlock +""""""""""""""" +Raplaces a ${macro} with the ComplexType passed. +See ``Sample_40_TemplateSetComplexValue.php`` for examples. + +.. code-block:: php + + $table = new Table(array('borderSize' => 12, 'borderColor' => 'green', 'width' => 6000, 'unit' => TblWidth::TWIP)); + $table->addRow(); + $table->addCell(150)->addText('Cell A1'); + $table->addCell(150)->addText('Cell A2'); + $table->addCell(150)->addText('Cell A3'); + $table->addRow(); + $table->addCell(150)->addText('Cell B1'); + $table->addCell(150)->addText('Cell B2'); + $table->addCell(150)->addText('Cell B3'); + $templateProcessor->setComplexBlock('table', $table); From bc448aed6c7df7782d9655d5df99e2789090daa6 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 4 Feb 2019 21:53:19 +0100 Subject: [PATCH 3/4] improve code coverage --- src/PhpWord/TemplateProcessor.php | 42 ++++--------------------- tests/PhpWord/TemplateProcessorTest.php | 27 ++++++++++++++++ 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 498cf7016d..9e12028d41 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -1108,7 +1108,7 @@ protected function replaceXmlBlock($macro, $block, $blockType = 'w:p') protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') { $macroPos = $this->findMacro($macro); - if (false === $macroPos) { + if (0 > $macroPos) { return false; } $start = $this->findXmlBlockStart($macroPos, $blockType); @@ -1116,39 +1116,8 @@ protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') return false; } $end = $this->findXmlBlockEnd($start, $blockType); - if (0 > $end) { - return false; - } - - return array('start' => $start, 'end' => $end); - } - - /** - * Find start and end of XML block containing the given block macro - * e.g. ...${macro}...${/macro}... - * - * Note that only the first instance of the macro will be found - * - * @param string $macro Name of macro - * @param string $blockType XML tag for block - * @return bool|int[] FALSE if not found, otherwise array with start and end - */ - protected function findContainingXmlBlockForBlockMacro($macro, $blockType = 'w:p') - { - $macroStartPos = $this->findMacro($macro); - if (0 > $macroStartPos) { - return false; - } - $macroEndPos = $this->findMacro('/' . $macro, $macroStartPos); - if (0 > $macroEndPos) { - return false; - } - $start = $this->findXmlBlockStart($macroStartPos, $blockType); - if (0 > $start) { - return false; - } - $end = $this->findXmlBlockEnd($macroEndPos, $blockType); - if (0 > $end) { + //if not found or if resulting string does not contain the macro we are searching for + if (0 > $end || strstr($this->getSlice($start, $end), $macro) === false) { return false; } @@ -1183,12 +1152,13 @@ protected function findMacro($search, $offset = 0) */ protected function findXmlBlockStart($offset, $blockType) { + $reverseOffset = (strlen($this->tempDocumentMainPart) - $offset) * -1; // first try XML tag with attributes - $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', $reverseOffset); // if not found, or if found but contains the XML tag without attribute if (false === $blockStart || strrpos($this->getSlice($blockStart, $offset), '<' . $blockType . '>')) { // also try XML tag without attributes - $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', $reverseOffset); } return ($blockStart === false) ? -1 : $blockStart; diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 4c9f235801..043ad1ff6f 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -803,4 +803,31 @@ public function testFindXmlBlockStart() $this->assertEquals($toFind, $templateProcessor->getSlice($position['start'], $position['end'])); } + + public function testShouldReturnFalseIfXmlBlockNotFound() + { + $mainPart = ' + + + + + + this is my text containing a ${macro} + + + '; + $templateProcessor = new TestableTemplateProcesor($mainPart); + + //non-existing macro + $result = $templateProcessor->findContainingXmlBlockForMacro('${fake-macro}', 'w:p'); + $this->assertFalse($result); + + //existing macro but not inside node looked for + $result = $templateProcessor->findContainingXmlBlockForMacro('${macro}', 'w:fake-node'); + $this->assertFalse($result); + + //existing macro but end tag not found after macro + $result = $templateProcessor->findContainingXmlBlockForMacro('${macro}', 'w:rPr'); + $this->assertFalse($result); + } } From d2b0b317e025d6e42bd8389258b3e6ca9d4dfed1 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 4 Feb 2019 22:57:33 +0100 Subject: [PATCH 4/4] fix scrutinizer warnings --- src/PhpWord/TemplateProcessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 9e12028d41..0a36661756 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -1088,7 +1088,7 @@ protected function replaceClonedVariables($variableReplacements, $xmlBlock) protected function replaceXmlBlock($macro, $block, $blockType = 'w:p') { $where = $this->findContainingXmlBlockForMacro($macro, $blockType); - if (false !== $where) { + if (is_array($where)) { $this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']); } @@ -1132,7 +1132,7 @@ protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') * Note that only the first instance of the macro will be found * * @param string $search Macro name - * @param string $offset Offset from which to start searching + * @param int $offset Offset from which to start searching * @return int -1 if macro not found */ protected function findMacro($search, $offset = 0)