diff --git a/.gitignore b/.gitignore index 98f65dbf..66e64406 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ Thumbs.db Desktop.ini .idea _build +/build phpunit.xml composer.lock composer.phar diff --git a/composer.json b/composer.json index b06ca3c0..0cfa7d15 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "require-dev": { "phpunit/phpunit": "3.7.*", "phpdocumentor/phpdocumentor":"2.*", + "twig/twig":"1.27", "squizlabs/php_codesniffer": "1.*", "phpmd/phpmd": "2.*", "phploc/phploc": "2.*", diff --git a/docs/elements.rst b/docs/elements.rst index 5b671936..c89903f1 100644 --- a/docs/elements.rst +++ b/docs/elements.rst @@ -377,7 +377,35 @@ To be completed Fields ------ -To be completed +Currently the following fields are supported: + +- PAGE +- NUMPAGES +- DATE +- XE +- INDEX + +.. code-block:: php + + $section->addField($fieldType, [$properties], [$options], [$fieldText]) + +See ``\PhpOffice\PhpWord\Element\Field`` for list of properties and options available for each field type. +Options which are not specifically defined can be added. Those must start with a ``\``. + +For instance for the INDEX field, you can do the following (See `Index Field for list of available options `_ ): + +.. code-block:: php + + //the $fieldText can be either a simple string + $fieldText = 'The index value'; + + //or a 'TextRun', to be able to format the text you want in the index + $fieldText = new TextRun(); + $fieldText->addText('My '); + $fieldText->addText('bold index', ['bold' => true]); + $fieldText->addText(' entry'); + + $section->addField('INDEX', array(), array('\\e " " \\h "A" \\c "3"'), $fieldText); Line ------ @@ -398,4 +426,27 @@ Available line style attributes: - ``endArrow``. End type of arrow: block, open, classic, diamond, oval. - ``width``. Line-object width in pt. - ``height``. Line-object height in pt. -- ``flip``. Flip the line element: true, false. \ No newline at end of file +- ``flip``. Flip the line element: true, false. + +Comments +--------- + +Comments can be added to a document by using ``addComment``. +The comment can contain formatted text. Once the comment has been added, it can be linked to any to any element. + +.. code-block:: php + + // first create a comment + $comment= new \PhpOffice\PhpWord\Element\Comment('Authors name', new \DateTime(), 'my_initials'); + $comment->addText('Test', array('bold' => true)); + + // add it to the document + $phpWord->addComment($comment); + + $textrun = $section->addTextRun(); + $textrun->addText('This '); + $text = $textrun->addText('is'); + // link the comment to the text you just created + $text->setCommentStart($comment); + +If no end is set for a comment using the ``setCommentEnd``, the comment will be ended automatically at the end of the element it is started on. \ No newline at end of file diff --git a/docs/intro.rst b/docs/intro.rst index d1c791cf..d88cd626 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -49,6 +49,7 @@ Features - Insert drawing shapes (arc, curve, line, polyline, rect, oval) - Insert charts (pie, doughnut, bar, line, area, scatter, radar) - Insert form fields (textinput, checkbox, and dropdown) +- Insert comments - Create document from templates - Use XSL 1.0 style sheets to transform headers, main document part, and footers of an OOXML template - ... and many more features on progress @@ -102,6 +103,8 @@ Writers +---------------------------+----------------------+--------+-------+-------+--------+-------+ | | Endnote | ✓ | | | ✓ | | +---------------------------+----------------------+--------+-------+-------+--------+-------+ +| | Comments | ✓ | | | | | ++---------------------------+----------------------+--------+-------+-------+--------+-------+ | **Graphs** | 2D basic graphs | ✓ | | | | | +---------------------------+----------------------+--------+-------+-------+--------+-------+ | | 2D advanced graphs | | | | | | @@ -161,6 +164,8 @@ Readers +---------------------------+----------------------+--------+-------+-------+-------+-------+ | | Endnote | ✓ | | | | | +---------------------------+----------------------+--------+-------+-------+-------+-------+ +| | Comments | | | | | | ++---------------------------+----------------------+--------+-------+-------+-------+-------+ | **Graphs** | 2D basic graphs | | | | | | +---------------------------+----------------------+--------+-------+-------+-------+-------+ | | 2D advanced graphs | | | | | | diff --git a/samples/Sample_27_Field.php b/samples/Sample_27_Field.php index 57747895..b5be12ca 100644 --- a/samples/Sample_27_Field.php +++ b/samples/Sample_27_Field.php @@ -1,4 +1,6 @@ addText('Date field:'); $section->addField('DATE', array('dateformat' => 'dddd d MMMM yyyy H:mm:ss'), array('PreserveFormat')); $section->addText('Page field:'); -$section->addField('PAGE', array('format' => 'ArabicDash')); +$section->addField('PAGE', array('format' => 'Arabic')); $section->addText('Number of pages field:'); -$section->addField('NUMPAGES', array('format' => 'Arabic', 'numformat' => '0,00'), array('PreserveFormat')); +$section->addField('NUMPAGES', array('numformat' => '0,00', 'format' => 'Arabic'), array('PreserveFormat')); + +$textrun = $section->addTextRun(); +$textrun->addText('An index field is '); +$textrun->addField('XE', array(), array('Italic'), 'My first index'); +$textrun->addText('here:'); + +$indexEntryText = new TextRun(); +$indexEntryText->addText('My '); +$indexEntryText->addText('bold index', ['bold' => true]); +$indexEntryText->addText(' entry'); + +$textrun = $section->addTextRun(); +$textrun->addText('A complex index field is '); +$textrun->addField('XE', array(), array('Bold'), $indexEntryText); +$textrun->addText('here:'); + +$section->addText('The actual index:'); +$section->addField('INDEX', array(), array('\\e " "'), 'right click to update the index'); $textrun = $section->addTextRun(array('alignment' => \PhpOffice\PhpWord\SimpleType\Jc::CENTER)); $textrun->addText('This is the date of lunar calendar '); diff --git a/samples/Sample_37_Comments.php b/samples/Sample_37_Comments.php new file mode 100644 index 00000000..670e914b --- /dev/null +++ b/samples/Sample_37_Comments.php @@ -0,0 +1,63 @@ +addText('Test', array('bold' => true)); +$phpWord->addComment($comment); + +$section = $phpWord->addSection(); + +$textrun = $section->addTextRun(); +$textrun->addText('This '); +$text = $textrun->addText('is'); +$text->setCommentRangeStart($comment); +$textrun->addText(' a test'); + +$section->addTextBreak(2); + +// Let's create a comment that we will link to a start element and an end element +$commentWithStartAndEnd = new \PhpOffice\PhpWord\Element\Comment('Foo Bar', new \DateTime(), ''); +$commentWithStartAndEnd->addText('A comment with a start and an end'); +$phpWord->addComment($commentWithStartAndEnd); + +$textrunWithEnd = $section->addTextRun(); +$textrunWithEnd->addText('This '); +$textToStartOn = $textrunWithEnd->addText('is', array('bold' => true)); +$textToStartOn->setCommentRangeStart($commentWithStartAndEnd); +$textrunWithEnd->addText(' another', array('italic' => true)); +$textToEndOn = $textrunWithEnd->addText(' test'); +$textToEndOn->setCommentRangeEnd($commentWithStartAndEnd); + +$section->addTextBreak(2); + +// Let's add a comment on an image +$commentOnImage = new \PhpOffice\PhpWord\Element\Comment('Mr Smart', new \DateTime(), ''); +$imageComment = $commentOnImage->addTextRun(); +$imageComment->addText('Hey, Mars does look '); +$imageComment->addText('red', array('color' => 'FF0000')); +$phpWord->addComment($commentOnImage); +$image = $section->addImage('resources/_mars.jpg'); +$image->setCommentRangeStart($commentOnImage); + +$section->addTextBreak(2); + +// We can also do things the other way round, link the comment to the element +$anotherText = $section->addText("another text"); + +$comment1 = new \PhpOffice\PhpWord\Element\Comment('Authors name', new \DateTime(), 'my_initials'); +$comment1->addText('Test', array('bold' => true)); +$comment1->setStartElement($anotherText); +$comment1->setEndElement($anotherText); +$phpWord->addComment($comment1); + + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/src/PhpWord/Collection/Comments.php b/src/PhpWord/Collection/Comments.php new file mode 100644 index 00000000..e0383814 --- /dev/null +++ b/src/PhpWord/Collection/Comments.php @@ -0,0 +1,27 @@ + $generalContainers, 'FormField' => $generalContainers, 'SDT' => $generalContainers, - 'TextRun' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox'), + 'TrackChange' => $generalContainers, + 'TextRun' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox', 'TrackChange'), 'ListItem' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox'), 'ListItemRun' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox'), 'Table' => array('Section', 'Header', 'Footer', 'Cell', 'TextBox'), diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index b0ed8ae2..8ff64194 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -108,12 +108,26 @@ abstract class AbstractElement protected $mediaRelation = false; /** - * Is part of collection; true for Title, Footnote, Endnote, and Chart + * Is part of collection; true for Title, Footnote, Endnote, Chart, and Comment * * @var bool */ protected $collectionRelation = false; + /** + * The start position for the linked comment + * + * @var Comment + */ + protected $commentRangeStart; + + /** + * The end position for the linked comment + * + * @var Comment + */ + protected $commentRangeEnd; + /** * Get PhpWord * @@ -265,6 +279,55 @@ abstract class AbstractElement return $this->nestedLevel; } + /** + * Get comment start + * + * @return Comment + */ + public function getCommentRangeStart() + { + return $this->commentRangeStart; + } + + /** + * Set comment start + * + * @param Comment $value + */ + public function setCommentRangeStart(Comment $value) + { + if ($this instanceof Comment) { + throw new \InvalidArgumentException("Cannot set a Comment on a Comment"); + } + $this->commentRangeStart= $value; + $this->commentRangeStart->setStartElement($this); + } + + /** + * Get comment end + * + * @return Comment + */ + public function getCommentRangeEnd() + { + return $this->commentRangeEnd; + } + + /** + * Set comment end + * + * @param Comment $value + * @return void + */ + public function setCommentRangeEnd(Comment $value) + { + if ($this instanceof Comment) { + throw new \InvalidArgumentException("Cannot set a Comment on a Comment"); + } + $this->commentRangeEnd= $value; + $this->commentRangeEnd->setEndElement($this); + } + /** * Set parent container * diff --git a/src/PhpWord/Element/Comment.php b/src/PhpWord/Element/Comment.php new file mode 100644 index 00000000..def9d5a8 --- /dev/null +++ b/src/PhpWord/Element/Comment.php @@ -0,0 +1,122 @@ +initials = $initials; + return $this; + } + + /** + * Get Initials + * + * @return string + */ + public function getInitials() + { + return $this->initials; + } + + /** + * Sets the element where this comment starts + * + * @param \PhpOffice\PhpWord\Element\AbstractElement $value + */ + public function setStartElement(AbstractElement $value) + { + $this->startElement = $value; + if ($value->getCommentRangeStart() == null) { + $value->setCommentRangeStart($this); + } + } + + /** + * Get the element where this comment starts + * + * @return \PhpOffice\PhpWord\Element\AbstractElement + */ + public function getStartElement() + { + return $this->startElement; + } + + /** + * Sets the element where this comment ends + * + * @param \PhpOffice\PhpWord\Element\AbstractElement $value + */ + public function setEndElement(AbstractElement $value) + { + $this->endElement = $value; + if ($value->getCommentRangeEnd() == null) { + $value->setCommentRangeEnd($this); + } + } + + /** + * Get the element where this comment ends + * + * @return \PhpOffice\PhpWord\Element\AbstractElement + */ + public function getEndElement() + { + return $this->endElement; + } +} diff --git a/src/PhpWord/Element/Field.php b/src/PhpWord/Element/Field.php index 48dc1d2e..380d7a92 100644 --- a/src/PhpWord/Element/Field.php +++ b/src/PhpWord/Element/Field.php @@ -40,7 +40,8 @@ class Field extends AbstractElement ), 'NUMPAGES'=>array( 'properties'=>array( - 'format' => array('Arabic', 'ArabicDash', 'alphabetic', 'ALPHABETIC', 'roman', 'ROMAN'), + 'format' => array('Arabic', 'ArabicDash', 'CardText', 'DollarText', 'Ordinal', 'OrdText', + 'alphabetic', 'ALPHABETIC', 'roman', 'ROMAN', 'Caps', 'FirstCap', 'Lower', 'Upper'), 'numformat' => array('0', '0,00', '#.##0', '#.##0,00', '€ #.##0,00(€ #.##0,00)', '0%', '0,00%') ), 'options'=>array('PreserveFormat') @@ -52,6 +53,14 @@ class Field extends AbstractElement 'h:mm am/pm', 'h:mm:ss am/pm', 'HH:mm', 'HH:mm:ss') ), 'options'=>array('PreserveFormat', 'LunarCalendar', 'SakaEraCalendar', 'LastUsedFormat') + ), + 'XE'=>array( + 'properties' => array(), + 'options' => array('Bold', 'Italic') + ), + 'INDEX'=>array( + 'properties' => array(), + 'options' => array('PreserveFormat') ) ); @@ -62,6 +71,13 @@ class Field extends AbstractElement */ protected $type; + /** + * Field text + * + * @var TextRun | string + */ + protected $text; + /** * Field properties * @@ -82,12 +98,14 @@ class Field extends AbstractElement * @param string $type * @param array $properties * @param array $options + * @param TextRun | string $text */ - public function __construct($type = null, $properties = array(), $options = array()) + public function __construct($type = null, $properties = array(), $options = array(), $text = null) { $this->setType($type); $this->setProperties($properties); $this->setOptions($options); + $this->setText($text); } /** @@ -105,7 +123,7 @@ class Field extends AbstractElement if (isset($this->fieldsArray[$type])) { $this->type = $type; } else { - throw new \InvalidArgumentException("Invalid type"); + throw new \InvalidArgumentException("Invalid type '$type'"); } } return $this->type; @@ -135,7 +153,7 @@ class Field extends AbstractElement if (is_array($properties)) { foreach (array_keys($properties) as $propkey) { if (!(isset($this->fieldsArray[$this->type]['properties'][$propkey]))) { - throw new \InvalidArgumentException("Invalid property"); + throw new \InvalidArgumentException("Invalid property '$propkey'"); } } $this->properties = array_merge($this->properties, $properties); @@ -166,8 +184,8 @@ class Field extends AbstractElement { if (is_array($options)) { foreach (array_keys($options) as $optionkey) { - if (!(isset($this->fieldsArray[$this->type]['options'][$optionkey]))) { - throw new \InvalidArgumentException("Invalid option"); + if (!(isset($this->fieldsArray[$this->type]['options'][$optionkey])) && substr($optionkey, 0, 1) !== '\\') { + throw new \InvalidArgumentException("Invalid option '$optionkey', possible values are " . implode(', ', $this->fieldsArray[$this->type]['options'])); } } $this->options = array_merge($this->options, $options); @@ -184,4 +202,35 @@ class Field extends AbstractElement { return $this->options; } + + /** + * Set Field text + * + * @param string | TextRun $text + * + * @return string | TextRun + * + * @throws \InvalidArgumentException + */ + public function setText($text) + { + if (isset($text)) { + if (is_string($text) || $text instanceof TextRun) { + $this->text = $text; + } else { + throw new \InvalidArgumentException("Invalid text"); + } + } + return $this->text; + } + + /** + * Get Field text + * + * @return string | TextRun + */ + public function getText() + { + return $this->text; + } } diff --git a/src/PhpWord/Element/TrackChange.php b/src/PhpWord/Element/TrackChange.php new file mode 100644 index 00000000..782e6f35 --- /dev/null +++ b/src/PhpWord/Element/TrackChange.php @@ -0,0 +1,77 @@ +author = $author; + $this->date = $date; + return $this; + } + + /** + * Get TrackChange Author + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * Get TrackChange Date + * + * @return \DateTime + */ + public function getDate() + { + return $this->date; + } +} diff --git a/src/PhpWord/PhpWord.php b/src/PhpWord/PhpWord.php index bb5b4956..1571537e 100644 --- a/src/PhpWord/PhpWord.php +++ b/src/PhpWord/PhpWord.php @@ -27,11 +27,13 @@ use PhpOffice\PhpWord\Exception\Exception; * @method Collection\Footnotes getFootnotes() * @method Collection\Endnotes getEndnotes() * @method Collection\Charts getCharts() + * @method Collection\Comments getComments() * @method int addBookmark(Element\Bookmark $bookmark) * @method int addTitle(Element\Title $title) * @method int addFootnote(Element\Footnote $footnote) * @method int addEndnote(Element\Endnote $endnote) * @method int addChart(Element\Chart $chart) + * @method int addComment(Element\Comment $comment) * * @method Style\Paragraph addParagraphStyle(string $styleName, array $styles) * @method Style\Font addFontStyle(string $styleName, mixed $fontStyle, mixed $paragraphStyle = null) @@ -84,7 +86,7 @@ class PhpWord public function __construct() { // Collection - $collections = array('Bookmarks', 'Titles', 'Footnotes', 'Endnotes', 'Charts'); + $collections = array('Bookmarks', 'Titles', 'Footnotes', 'Endnotes', 'Charts', 'Comments'); foreach ($collections as $collection) { $class = 'PhpOffice\\PhpWord\\Collection\\' . $collection; $this->collections[$collection] = new $class(); @@ -118,7 +120,7 @@ class PhpWord $addCollection = array(); $addStyle = array(); - $collections = array('Bookmark', 'Title', 'Footnote', 'Endnote', 'Chart'); + $collections = array('Bookmark', 'Title', 'Footnote', 'Endnote', 'Chart', 'Comment'); foreach ($collections as $collection) { $getCollection[] = strtolower("get{$collection}s"); $addCollection[] = strtolower("add{$collection}"); diff --git a/src/PhpWord/Style/Font.php b/src/PhpWord/Style/Font.php index b625e3b8..9056844a 100644 --- a/src/PhpWord/Style/Font.php +++ b/src/PhpWord/Style/Font.php @@ -725,7 +725,7 @@ class Font extends AbstractStyle } /** - * Set shading + * Set Paragraph * * @param mixed $value * @return self diff --git a/src/PhpWord/Writer/Word2007.php b/src/PhpWord/Writer/Word2007.php index 8e10f5f6..bb7b521f 100644 --- a/src/PhpWord/Writer/Word2007.php +++ b/src/PhpWord/Writer/Word2007.php @@ -60,6 +60,7 @@ class Word2007 extends AbstractWriter implements WriterInterface 'DocPropsCustom' => 'docProps/custom.xml', 'RelsDocument' => 'word/_rels/document.xml.rels', 'Document' => 'word/document.xml', + 'Comments' => 'word/comments.xml', 'Styles' => 'word/styles.xml', 'Numbering' => 'word/numbering.xml', 'Settings' => 'word/settings.xml', @@ -129,6 +130,7 @@ class Word2007 extends AbstractWriter implements WriterInterface $this->addNotes($zip, $rId, 'footnote'); $this->addNotes($zip, $rId, 'endnote'); + $this->addComments($zip, $rId); $this->addChart($zip, $rId); // Write parts @@ -255,6 +257,30 @@ class Word2007 extends AbstractWriter implements WriterInterface } } + /** + * Add comments + * + * @param \PhpOffice\PhpWord\Shared\ZipArchive $zip + * @param integer &$rId + * @return void + */ + private function addComments(ZipArchive $zip, &$rId) + { + $phpWord = $this->getPhpWord(); + $collection = $phpWord->getComments(); + $partName = "comments"; + + // Add comment relations and contents + /** @var \PhpOffice\PhpWord\Collection\AbstractCollection $collection Type hint */ + if ($collection->countItems() > 0) { + $this->relationships[] = array('target' => "{$partName}.xml", 'type' => $partName, 'rID' => ++$rId); + + // Write content file, e.g. word/comments.xml + $writerPart = $this->getWriterPart($partName)->setElements($collection->getItems()); + $zip->addFromString("word/{$partName}.xml", $writerPart->write()); + } + } + /** * Add chart. * diff --git a/src/PhpWord/Writer/Word2007/Element/AbstractElement.php b/src/PhpWord/Writer/Word2007/Element/AbstractElement.php index f5a454d2..79877b10 100644 --- a/src/PhpWord/Writer/Word2007/Element/AbstractElement.php +++ b/src/PhpWord/Writer/Word2007/Element/AbstractElement.php @@ -103,6 +103,7 @@ abstract class AbstractElement $this->writeParagraphStyle(); } } + $this->writeCommentRangeStart(); } /** @@ -112,11 +113,63 @@ abstract class AbstractElement */ protected function endElementP() { + $this->writeCommentRangeEnd(); if (!$this->withoutP) { $this->xmlWriter->endElement(); // w:p } } + /** + * Writes the w:commentRangeStart DOM element + * + * @return void + */ + protected function writeCommentRangeStart() + { + if ($this->element->getCommentRangeStart() != null) { + $comment = $this->element->getCommentRangeStart(); + //only set the ID if it is not yet set, otherwise it will overwrite it + if ($comment->getElementId() == null) { + $comment->setElementId(); + } + + $this->xmlWriter->writeElementBlock('w:commentRangeStart', array('w:id' => $comment->getElementId())); + + } + } + + /** + * Writes the w:commentRangeEnd DOM element + * + * @return void + */ + protected function writeCommentRangeEnd() + { + if ($this->element->getCommentRangeEnd() != null) { + $comment = $this->element->getCommentRangeEnd(); + //only set the ID if it is not yet set, otherwise it will overwrite it + if ($comment->getElementId() == null) { + $comment->setElementId(); + } + + $this->xmlWriter->writeElementBlock('w:commentRangeEnd', array('w:id' => $comment->getElementId())); + $this->xmlWriter->startElement('w:r'); + $this->xmlWriter->writeElementBlock('w:commentReference', array('w:id' => $comment->getElementId())); + $this->xmlWriter->endElement(); + } elseif ($this->element->getCommentRangeStart() != null && $this->element->getCommentRangeStart()->getEndElement() == null) { + $comment = $this->element->getCommentRangeStart(); + //only set the ID if it is not yet set, otherwise it will overwrite it + if ($comment->getElementId() == null) { + $comment->setElementId(); + } + + $this->xmlWriter->writeElementBlock('w:commentRangeEnd', array('w:id' => $comment->getElementId())); + $this->xmlWriter->startElement('w:r'); + $this->xmlWriter->writeElementBlock('w:commentReference', array('w:id' => $comment->getElementId())); + $this->xmlWriter->endElement(); + } + } + /** * Write ending. * diff --git a/src/PhpWord/Writer/Word2007/Element/Chart.php b/src/PhpWord/Writer/Word2007/Element/Chart.php index 12602532..ecdde362 100644 --- a/src/PhpWord/Writer/Word2007/Element/Chart.php +++ b/src/PhpWord/Writer/Word2007/Element/Chart.php @@ -45,6 +45,7 @@ class Chart extends AbstractElement if (!$this->withoutP) { $xmlWriter->startElement('w:p'); } + $this->writeCommentRangeStart(); $xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:drawing'); diff --git a/src/PhpWord/Writer/Word2007/Element/Field.php b/src/PhpWord/Writer/Word2007/Element/Field.php index ae4c66ba..9fc45b21 100644 --- a/src/PhpWord/Writer/Word2007/Element/Field.php +++ b/src/PhpWord/Writer/Word2007/Element/Field.php @@ -37,16 +37,90 @@ class Field extends Text return; } + $this->startElementP(); + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:fldChar'); + $xmlWriter->writeAttribute('w:fldCharType', 'begin'); + $xmlWriter->endElement(); // w:fldChar + $xmlWriter->endElement(); // w:r + $instruction = ' ' . $element->getType() . ' '; + if ($element->getText() != null) { + if (is_string($element->getText())) { + $instruction .= '"' . $element->getText() . '" '; + $instruction .= $this->buildPropertiesAndOptions($element); + } else { + $instruction .= '"'; + } + } else { + $instruction .= $this->buildPropertiesAndOptions($element); + } + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:instrText'); + $xmlWriter->writeAttribute('xml:space', 'preserve'); + $xmlWriter->text($instruction); + $xmlWriter->endElement(); // w:instrText + $xmlWriter->endElement(); // w:r + + if ($element->getText() != null) { + if ($element->getText() instanceof \PhpOffice\PhpWord\Element\TextRun) { + + $containerWriter = new Container($xmlWriter, $element->getText(), true); + $containerWriter->write(); + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:instrText'); + $xmlWriter->text('"' . $this->buildPropertiesAndOptions($element)); + $xmlWriter->endElement(); // w:instrText + $xmlWriter->endElement(); // w:r + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:instrText'); + $xmlWriter->writeAttribute('xml:space', 'preserve'); + $xmlWriter->text(' '); + $xmlWriter->endElement(); // w:instrText + $xmlWriter->endElement(); // w:r + } + } + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:fldChar'); + $xmlWriter->writeAttribute('w:fldCharType', 'separate'); + $xmlWriter->endElement(); // w:fldChar + $xmlWriter->endElement(); // w:r + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:rPr'); + $xmlWriter->startElement('w:noProof'); + $xmlWriter->endElement(); // w:noProof + $xmlWriter->endElement(); // w:rPr + $xmlWriter->writeElement('w:t', $element->getText() != null && is_string($element->getText()) ? $element->getText() : '1'); + $xmlWriter->endElement(); // w:r + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:fldChar'); + $xmlWriter->writeAttribute('w:fldCharType', 'end'); + $xmlWriter->endElement(); // w:fldChar + $xmlWriter->endElement(); // w:r + + $this->endElementP(); // w:p + } + + private function buildPropertiesAndOptions(\PhpOffice\PhpWord\Element\Field $element) + { + $propertiesAndOptions = ''; $properties = $element->getProperties(); foreach ($properties as $propkey => $propval) { switch ($propkey) { case 'format': + $propertiesAndOptions.= '\* ' . $propval . ' '; + break; case 'numformat': - $instruction .= '\* ' . $propval . ' '; + $propertiesAndOptions.= '\# ' . $propval . ' '; break; case 'dateformat': - $instruction .= '\@ "' . $propval . '" '; + $propertiesAndOptions.= '\@ "' . $propval . '" '; break; } } @@ -55,34 +129,27 @@ class Field extends Text foreach ($options as $option) { switch ($option) { case 'PreserveFormat': - $instruction .= '\* MERGEFORMAT '; + $propertiesAndOptions.= '\* MERGEFORMAT '; break; case 'LunarCalendar': - $instruction .= '\h '; + $propertiesAndOptions.= '\h '; break; case 'SakaEraCalendar': - $instruction .= '\s '; + $propertiesAndOptions.= '\s '; break; case 'LastUsedFormat': - $instruction .= '\l '; + $propertiesAndOptions.= '\l '; break; + case 'Bold': + $propertiesAndOptions.= '\b '; + break; + case 'Italic': + $propertiesAndOptions.= '\i '; + break; + default: + $propertiesAndOptions.= $option .' '; } } - - $this->startElementP(); - - $xmlWriter->startElement('w:fldSimple'); - $xmlWriter->writeAttribute('w:instr', $instruction); - $xmlWriter->startElement('w:r'); - $xmlWriter->startElement('w:rPr'); - $xmlWriter->startElement('w:noProof'); - $xmlWriter->endElement(); // w:noProof - $xmlWriter->endElement(); // w:rPr - - $xmlWriter->writeElement('w:t', '1'); - $xmlWriter->endElement(); // w:r - $xmlWriter->endElement(); // w:fldSimple - - $this->endElementP(); // w:p + return $propertiesAndOptions; } } diff --git a/src/PhpWord/Writer/Word2007/Element/Image.php b/src/PhpWord/Writer/Word2007/Element/Image.php index 914c78ea..edf32739 100644 --- a/src/PhpWord/Writer/Word2007/Element/Image.php +++ b/src/PhpWord/Writer/Word2007/Element/Image.php @@ -63,6 +63,7 @@ class Image extends AbstractElement $xmlWriter->startElement('w:p'); $styleWriter->writeAlignment(); } + $this->writeCommentRangeStart(); $xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:pict'); diff --git a/src/PhpWord/Writer/Word2007/Element/Line.php b/src/PhpWord/Writer/Word2007/Element/Line.php index ade91fb8..ebc5d395 100644 --- a/src/PhpWord/Writer/Word2007/Element/Line.php +++ b/src/PhpWord/Writer/Word2007/Element/Line.php @@ -48,6 +48,7 @@ class Line extends AbstractElement $xmlWriter->startElement('w:p'); $styleWriter->writeAlignment(); } + $this->writeCommentRangeStart(); $xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:pict'); diff --git a/src/PhpWord/Writer/Word2007/Element/Object.php b/src/PhpWord/Writer/Word2007/Element/Object.php index 4fdf6fed..fc0532cd 100644 --- a/src/PhpWord/Writer/Word2007/Element/Object.php +++ b/src/PhpWord/Writer/Word2007/Element/Object.php @@ -51,6 +51,7 @@ class Object extends AbstractElement $xmlWriter->startElement('w:p'); $styleWriter->writeAlignment(); } + $this->writeCommentRangeStart(); $xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:object'); diff --git a/src/PhpWord/Writer/Word2007/Element/Shape.php b/src/PhpWord/Writer/Word2007/Element/Shape.php index f282c4a5..a589af6c 100644 --- a/src/PhpWord/Writer/Word2007/Element/Shape.php +++ b/src/PhpWord/Writer/Word2007/Element/Shape.php @@ -55,6 +55,7 @@ class Shape extends AbstractElement if (!$this->withoutP) { $xmlWriter->startElement('w:p'); } + $this->writeCommentRangeStart(); $xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:pict'); diff --git a/src/PhpWord/Writer/Word2007/Element/TextBox.php b/src/PhpWord/Writer/Word2007/Element/TextBox.php index 3c4f48c2..e83fe0c9 100644 --- a/src/PhpWord/Writer/Word2007/Element/TextBox.php +++ b/src/PhpWord/Writer/Word2007/Element/TextBox.php @@ -45,6 +45,7 @@ class TextBox extends Image $xmlWriter->startElement('w:p'); $styleWriter->writeAlignment(); } + $this->writeCommentRangeStart(); $xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:pict'); diff --git a/src/PhpWord/Writer/Word2007/Part/Comments.php b/src/PhpWord/Writer/Word2007/Part/Comments.php new file mode 100644 index 00000000..73314785 --- /dev/null +++ b/src/PhpWord/Writer/Word2007/Part/Comments.php @@ -0,0 +1,103 @@ +getXmlWriter(); + + $xmlWriter->startDocument('1.0', 'UTF-8', 'yes'); + $xmlWriter->startElement('w:comments'); + $xmlWriter->writeAttribute('xmlns:ve', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); + $xmlWriter->writeAttribute('xmlns:o', 'urn:schemas-microsoft-com:office:office'); + $xmlWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $xmlWriter->writeAttribute('xmlns:m', 'http://schemas.openxmlformats.org/officeDocument/2006/math'); + $xmlWriter->writeAttribute('xmlns:v', 'urn:schemas-microsoft-com:vml'); + $xmlWriter->writeAttribute('xmlns:wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'); + $xmlWriter->writeAttribute('xmlns:w10', 'urn:schemas-microsoft-com:office:word'); + $xmlWriter->writeAttribute('xmlns:w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'); + $xmlWriter->writeAttribute('xmlns:wne', 'http://schemas.microsoft.com/office/word/2006/wordml'); + + if ($this->elements !== null) { + foreach ($this->elements as $element) { + if ($element instanceof Comment) { + $this->writeComment($xmlWriter, $element); + } + } + } + + $xmlWriter->endElement(); // w:comments + + return $xmlWriter->getData(); + } + + /** + * Write comment item. + * + * @param \PhpOffice\Common\XMLWriter $xmlWriter + * @param \PhpOffice\PhpWord\Element\Comment $comment + * @return void + */ + protected function writeComment(XMLWriter $xmlWriter, Comment $comment) + { + $xmlWriter->startElement('w:comment'); + $xmlWriter->writeAttribute('w:id', $comment->getElementId()); + $xmlWriter->writeAttribute('w:author', $comment->getAuthor()); + $xmlWriter->writeAttribute('w:date', $comment->getDate()->format($this->dateFormat)); + $xmlWriter->writeAttribute('w:initials', $comment->getInitials()); + + $containerWriter = new Container($xmlWriter, $comment); + $containerWriter->write(); + + $xmlWriter->endElement(); // w:comment + } + + /** + * Set element + * + * @param \PhpOffice\PhpWord\Collection\Comments $elements + * @return self + */ + public function setElements($elements) + { + $this->elements = $elements; + + return $this; + } +} diff --git a/src/PhpWord/Writer/Word2007/Part/ContentTypes.php b/src/PhpWord/Writer/Word2007/Part/ContentTypes.php index 1c81f343..7a03243e 100644 --- a/src/PhpWord/Writer/Word2007/Part/ContentTypes.php +++ b/src/PhpWord/Writer/Word2007/Part/ContentTypes.php @@ -49,6 +49,7 @@ class ContentTypes extends AbstractPart '/word/theme/theme1.xml' => $openXMLPrefix . 'officedocument.theme+xml', '/word/webSettings.xml' => $wordMLPrefix . 'webSettings+xml', '/word/fontTable.xml' => $wordMLPrefix . 'fontTable+xml', + '/word/comments.xml' => $wordMLPrefix . 'comments+xml', ); $defaults = $contentTypes['default']; diff --git a/src/PhpWord/Writer/Word2007/Part/Styles.php b/src/PhpWord/Writer/Word2007/Part/Styles.php index 7bcb8d92..01b84c08 100644 --- a/src/PhpWord/Writer/Word2007/Part/Styles.php +++ b/src/PhpWord/Writer/Word2007/Part/Styles.php @@ -170,6 +170,9 @@ class Styles extends AbstractPart $xmlWriter->startElement('w:link'); $xmlWriter->writeAttribute('w:val', $styleLink); $xmlWriter->endElement(); + } else if (!is_null($paragraphStyle)) { + // if type is 'paragraph' it should have a styleId + $xmlWriter->writeAttribute('w:styleId', $styleName); } // Style name @@ -178,7 +181,13 @@ class Styles extends AbstractPart $xmlWriter->endElement(); // Parent style - $xmlWriter->writeElementIf(!is_null($paragraphStyle), 'w:basedOn', 'w:val', 'Normal'); + if (!is_null($paragraphStyle)) { + if ($paragraphStyle->getStyleName() != null) { + $xmlWriter->writeElementBlock('w:basedOn', 'w:val', $paragraphStyle->getStyleName()); + } elseif ($paragraphStyle->getBasedOn() != null) { + $xmlWriter->writeElementBlock('w:basedOn', 'w:val', $paragraphStyle->getBasedOn()); + } + } // w:pPr if (!is_null($paragraphStyle)) { diff --git a/tests/PhpWord/Element/CommentTest.php b/tests/PhpWord/Element/CommentTest.php new file mode 100644 index 00000000..db9ec902 --- /dev/null +++ b/tests/PhpWord/Element/CommentTest.php @@ -0,0 +1,83 @@ +setStartElement($oText); + $oComment->setEndElement($oText); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Comment', $oComment); + $this->assertEquals($author, $oComment->getAuthor()); + $this->assertEquals($date, $oComment->getDate()); + $this->assertEquals($initials, $oComment->getInitials()); + $this->assertEquals($oText, $oComment->getStartElement()); + $this->assertEquals($oText, $oComment->getEndElement()); + } + + /** + * Add text + */ + public function testAddText() + { + $oComment = new Comment('Test User', new \DateTime(), 'my_initials'); + $element = $oComment->addText('text'); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Text', $element); + $this->assertCount(1, $oComment->getElements()); + $this->assertEquals('text', $element->getText()); + } + + /** + * Get elements + */ + public function testGetElements() + { + $oComment = new Comment('Test User', new \DateTime(), 'my_initials'); + + $this->assertInternalType('array', $oComment->getElements()); + } + + /** + * Set/get relation Id + */ + public function testRelationId() + { + $oComment = new Comment('Test User', new \DateTime(), 'my_initials'); + + $iVal = rand(1, 1000); + $oComment->setRelationId($iVal); + $this->assertEquals($iVal, $oComment->getRelationId()); + } +} diff --git a/tests/PhpWord/Element/FieldTest.php b/tests/PhpWord/Element/FieldTest.php index b9afad1f..6f5ebbbf 100644 --- a/tests/PhpWord/Element/FieldTest.php +++ b/tests/PhpWord/Element/FieldTest.php @@ -70,6 +70,47 @@ class FieldTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array('SakaEraCalendar', 'PreserveFormat'), $oField->getOptions()); } + /** + * New instance with type and properties and options and text + */ + public function testConstructWithTypePropertiesOptionsText() + { + $oField = new Field('XE', array(), array('Bold', 'Italic'), 'FieldValue'); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Field', $oField); + $this->assertEquals('XE', $oField->getType()); + $this->assertEquals(array(), $oField->getProperties()); + $this->assertEquals(array('Bold', 'Italic'), $oField->getOptions()); + $this->assertEquals('FieldValue', $oField->getText()); + } + + /** + * New instance with type and properties and options and text as TextRun + */ + public function testConstructWithTypePropertiesOptionsTextAsTextRun() + { + $textRun = new TextRun(); + $textRun->addText('test string'); + + $oField = new Field('XE', array(), array('Bold', 'Italic'), $textRun); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Field', $oField); + $this->assertEquals('XE', $oField->getType()); + $this->assertEquals(array(), $oField->getProperties()); + $this->assertEquals(array('Bold', 'Italic'), $oField->getOptions()); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\TextRun', $oField->getText()); + } + + public function testConstructWithOptionValue() + { + $oField = new Field('INDEX', array(), array('\\c "3" \\h "A"')); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Field', $oField); + $this->assertEquals('INDEX', $oField->getType()); + $this->assertEquals(array(), $oField->getProperties()); + $this->assertEquals(array('\\c "3" \\h "A"'), $oField->getOptions()); + } + /** * Test setType exception * @@ -105,4 +146,16 @@ class FieldTest extends \PHPUnit_Framework_TestCase $object = new Field('PAGE'); $object->setOptions(array('foo' => 'bar')); } + + /** + * Test setText exception + * + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid text + */ + public function testSetTextException() + { + $object = new Field('XE'); + $object->setText(array()); + } } diff --git a/tests/PhpWord/Writer/Word2007/ElementTest.php b/tests/PhpWord/Writer/Word2007/ElementTest.php index 027ba86a..b3c7b197 100644 --- a/tests/PhpWord/Writer/Word2007/ElementTest.php +++ b/tests/PhpWord/Writer/Word2007/ElementTest.php @@ -19,6 +19,7 @@ namespace PhpOffice\PhpWord\Writer\Word2007; use PhpOffice\Common\XMLWriter; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\TestHelperDOCX; +use PhpOffice\PhpWord\Element\TextRun; /** * Test class for PhpOffice\PhpWord\Writer\Word2007\Element subnamespace @@ -192,6 +193,51 @@ class ElementTest extends \PHPUnit_Framework_TestCase } } + public function testFieldElement() + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + + $section->addField('INDEX', array(), array('\\c "3"')); + $section->addField('XE', array(), array('Bold', 'Italic'), 'Index Entry'); + $section->addField('DATE', array('dateformat' => 'd-M-yyyy'), array('PreserveFormat', 'LastUsedFormat')); + $section->addField('DATE', array(), array('LunarCalendar')); + $section->addField('DATE', array(), array('SakaEraCalendar')); + $section->addField('NUMPAGES', array('format' => 'roman', 'numformat' => '0,00'), array('SakaEraCalendar')); + $doc = TestHelperDOCX::getDocument($phpWord); + + $element = '/w:document/w:body/w:p/w:r/w:instrText'; + $this->assertTrue($doc->elementExists($element)); + $this->assertEquals(' INDEX \\c "3" ', $doc->getElement($element)->textContent); + } + + public function testFieldElementWithComplexText() + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + + $text = new TextRun(); + $text->addText('test string', array('bold' => true)); + + $section->addField('XE', array(), array('Bold', 'Italic'), $text); + $doc = TestHelperDOCX::getDocument($phpWord); + + $element = '/w:document/w:body/w:p/w:r[2]/w:instrText'; + $this->assertTrue($doc->elementExists($element)); + $this->assertEquals(' XE "', $doc->getElement($element)->textContent); + + $element = '/w:document/w:body/w:p/w:r[3]/w:rPr/w:b'; + $this->assertTrue($doc->elementExists($element)); + + $element = '/w:document/w:body/w:p/w:r[3]/w:t'; + $this->assertTrue($doc->elementExists($element)); + $this->assertEquals('test string', $doc->getElement($element)->textContent); + + $element = '/w:document/w:body/w:p/w:r[4]/w:instrText'; + $this->assertTrue($doc->elementExists($element)); + $this->assertEquals('"\\b \\i ', $doc->getElement($element)->textContent); + } + /** * Test form fields */ diff --git a/tests/PhpWord/Writer/Word2007/Part/CommentsTest.php b/tests/PhpWord/Writer/Word2007/Part/CommentsTest.php new file mode 100644 index 00000000..aac4b15b --- /dev/null +++ b/tests/PhpWord/Writer/Word2007/Part/CommentsTest.php @@ -0,0 +1,61 @@ +addText('Test'); + + $phpWord = new PhpWord(); + $phpWord->addComment($comment); + $doc = TestHelperDOCX::getDocument($phpWord); + + $path = '/w:comments/w:comment'; + $file = 'word/comments.xml'; + + $this->assertTrue($doc->elementExists($path, $file)); + + $element = $doc->getElement($path, $file); + $this->assertNotNull($element->getAttribute('w:id')); + $this->assertEquals("Authors name", $element->getAttribute('w:author')); + $this->assertEquals("my_initials", $element->getAttribute('w:initials')); + } +} diff --git a/tests/PhpWord/Writer/Word2007/Part/StylesTest.php b/tests/PhpWord/Writer/Word2007/Part/StylesTest.php index f40387a1..9153fe65 100644 --- a/tests/PhpWord/Writer/Word2007/Part/StylesTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/StylesTest.php @@ -18,7 +18,10 @@ namespace PhpOffice\PhpWord\Writer\Word2007\Part; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\SimpleType\Jc; +use PhpOffice\PhpWord\Style\Font; +use PhpOffice\PhpWord\Style\Paragraph; use PhpOffice\PhpWord\TestHelperDOCX; +use PhpOffice\PhpWord\Writer\Word2007; /** * Test class for PhpOffice\PhpWord\Writer\Word2007\Part\Styles @@ -74,4 +77,69 @@ class StylesTest extends \PHPUnit_Framework_TestCase $element = $doc->getElement($path, $file); $this->assertEquals('Normal', $element->getAttribute('w:val')); } + + public function testFontStyleBasedOn() + { + $phpWord = new PhpWord(); + + $baseParagraphStyle = new Paragraph(); + $baseParagraphStyle->setAlignment(Jc::CENTER); + $baseParagraphStyle = $phpWord->addParagraphStyle('BaseStyle', $baseParagraphStyle); + + $childFont = new Font(); + $childFont->setParagraph($baseParagraphStyle); + $childFont->setSize(16); + $childFont = $phpWord->addFontStyle('ChildFontStyle', $childFont); + + $otherFont = new Font(); + $otherFont->setSize(20); + $otherFont = $phpWord->addFontStyle('OtherFontStyle', $otherFont); + + $doc = TestHelperDOCX::getDocument($phpWord); + + $file = 'word/styles.xml'; + + // Normal style generated? + $path = '/w:styles/w:style[@w:styleId="BaseStyle"]/w:name'; + $element = $doc->getElement($path, $file); + $this->assertEquals('BaseStyle', $element->getAttribute('w:val')); + + // Font style with paragraph should have it's base style set to that paragraphs style name + $path = '/w:styles/w:style[w:name/@w:val="ChildFontStyle"]/w:basedOn'; + $element = $doc->getElement($path, $file); + $this->assertEquals('BaseStyle', $element->getAttribute('w:val')); + + // Font style without paragraph should not have a base style set + $path = '/w:styles/w:style[w:name/@w:val="OtherFontStyle"]/w:basedOn'; + $element = $doc->getElement($path, $file); + $this->assertNull($element); + } + + function testFontStyleBasedOnOtherFontStyle() { + $phpWord = new PhpWord(); + + $styleGenerationP = new Paragraph(); + $styleGenerationP->setAlignment(Jc::BOTH); + + $styleGeneration = new Font(); + $styleGeneration->setParagraph($styleGenerationP); + $styleGeneration->setSize(9.5); + $phpWord->addFontStyle('Generation', $styleGeneration); + + $styleGenerationEteinteP = new Paragraph(); + $styleGenerationEteinteP->setBasedOn('Generation'); + + $styleGenerationEteinte = new Font(); + $styleGenerationEteinte->setParagraph($styleGenerationEteinteP); + $styleGenerationEteinte->setSize(8.5); + $phpWord->addFontStyle('GeneratEteinte', $styleGenerationEteinte); + + $doc = TestHelperDOCX::getDocument($phpWord); + + $file = 'word/styles.xml'; + + $path = '/w:styles/w:style[@w:styleId="GeneratEteinte"]/w:basedOn'; + $element = $doc->getElement($path, $file); + $this->assertEquals('Generation', $element->getAttribute('w:val')); + } }