diff --git a/docs/elements.rst b/docs/elements.rst index c73ffa0645..c83bb4d042 100644 --- a/docs/elements.rst +++ b/docs/elements.rst @@ -77,6 +77,13 @@ italics, etc) or other elements, e.g. images or links. The syntaxes are as follo For available styling options see :ref:`font-style` and :ref:`paragraph-style`. +If you want to enable track changes on added text you can mark it as INSERTED or DELETED by a specific user at a given time: + +.. code-block:: php + + $text = $section->addText('Hello World!'); + $text->setChanged(\PhpOffice\PhpWord\Element\ChangedElement::TYPE_INSERTED, 'Fred', (new \DateTime())); + Titles ~~~~~~ diff --git a/samples/Sample_31_TrackChanges.php b/samples/Sample_31_TrackChanges.php new file mode 100644 index 0000000000..a94e57c0c7 --- /dev/null +++ b/samples/Sample_31_TrackChanges.php @@ -0,0 +1,23 @@ +addSection(); + +$text = $section->addText('Hello World!'); + +$text = $section->addText('Hello World!'); +$text->setChanged(\PhpOffice\PhpWord\Element\ChangedElement::TYPE_INSERTED, 'Fred', time() - 1800); + +$text = $section->addText('Hello World!'); +$text->setChanged(\PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED, 'Barney', time() - 3600); + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index 81e185289d..198893736e 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -93,6 +93,13 @@ abstract class AbstractElement */ private $nestedLevel = 0; + /** + * changed element info + * + * @var object + */ + public $changed; + /** * Parent container type * @@ -422,6 +429,28 @@ protected function setNewStyle($styleObject, $styleValue = null, $returnObject = return $style; } + /** + * Set changed + * + * @param int $type TYPE_INSERTED|TYPE_DELETED + * @param string $author + * @param timestamp $date allways in UTC + */ + public function setChanged($type, $author, $date) + { + $this->changed = new ChangedElement($type, $author, $date); + } + + /** + * Get changed + * + * @return object + */ + public function getChanged() + { + return $this->changed; + } + /** * Set enum value * diff --git a/src/PhpWord/Element/ChangedElement.php b/src/PhpWord/Element/ChangedElement.php new file mode 100644 index 0000000000..38dfa3cb27 --- /dev/null +++ b/src/PhpWord/Element/ChangedElement.php @@ -0,0 +1,57 @@ +changeType = $changeType; + } + + /** + * Get change type + * + * @return int + */ + public function getChangeType() + { + return $this->changeType; + } +} diff --git a/src/PhpWord/Reader/ODText/Content.php b/src/PhpWord/Reader/ODText/Content.php index 8843d8a276..1ffb0f1a81 100644 --- a/src/PhpWord/Reader/ODText/Content.php +++ b/src/PhpWord/Reader/ODText/Content.php @@ -37,6 +37,8 @@ public function read(PhpWord $phpWord) $xmlReader = new XMLReader(); $xmlReader->getDomFromZip($this->docFile, $this->xmlFile); + $trackedChanges = array(); + $nodes = $xmlReader->getElements('office:body/office:text/*'); if ($nodes->length > 0) { $section = $phpWord->addSection(); @@ -48,7 +50,36 @@ public function read(PhpWord $phpWord) $section->addTitle($node->nodeValue, $depth); break; case 'text:p': // Paragraph - $section->addText($node->nodeValue); + $children = $node->childNodes; + foreach ($children as $child) { + switch ($child->nodeName) { + case 'text:change-start': + $changeId = $child->getAttribute('text:change-id'); + if (isset($trackedChanges[$changeId])) { + $changed = $trackedChanges[$changeId]; + } + break; + case 'text:change-end': + unset($changed); + break; + case 'text:change': + $changeId = $child->getAttribute('text:change-id'); + if (isset($trackedChanges[$changeId])) { + $changed = $trackedChanges[$changeId]; + } + break; + } + } + $element = $section->addText($node->nodeValue); + if (isset($changed)) { + $element->changed = $changed['changed']; + if (isset($changed['textNodes'])) { + foreach ($changed['textNodes'] as $changedNode) { + $element = $section->addText($changedNode->nodeValue); + $element->changed = $changed['changed']; + } + } + } break; case 'text:list': // List $listItems = $xmlReader->getElements('text:list-item/text:p', $node); @@ -57,6 +88,22 @@ public function read(PhpWord $phpWord) $section->addListItem($listItem->nodeValue, 0); } break; + case 'text:tracked-changes': + $changedRegions = $xmlReader->getElements('text:changed-region', $node); + foreach ($changedRegions as $changedRegion) { + $type = ($changedRegion->firstChild->nodeName == 'text:insertion') ? \PhpOffice\PhpWord\Element\ChangedElement::TYPE_INSERTED : \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED; + $creatorNode = $xmlReader->getElements('office:change-info/dc:creator', $changedRegion->firstChild); + $author = $creatorNode[0]->nodeValue; + $dateNode = $xmlReader->getElements('office:change-info/dc:date', $changedRegion->firstChild); + $date = $dateNode[0]->nodeValue; + $date = preg_replace('/\.\d+$/', '', $date); + $date = \DateTime::createFromFormat('Y-m-d\TH:i:s', $date); + $changed = new \PhpOffice\PhpWord\Element\ChangedElement($type, $author, $date); + $textNodes = $xmlReader->getElements('text:deletion/text:p', $changedRegion); + $trackedChanges[$changedRegion->getAttribute('text:id')] = array('changed' => $changed, + 'textNodes'=> $textNodes, ); + } + break; } } } diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 6a48fd4681..f85d0dc605 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -156,8 +156,10 @@ protected function readParagraph(XMLReader $xmlReader, \DOMElement $domNode, $pa } else { // Text and TextRun $runCount = $xmlReader->countElements('w:r', $domNode); + $insCount = $xmlReader->countElements('w:ins', $domNode); + $delCount = $xmlReader->countElements('w:del', $domNode); $linkCount = $xmlReader->countElements('w:hyperlink', $domNode); - $runLinkCount = $runCount + $linkCount; + $runLinkCount = $runCount + $insCount + $delCount + $linkCount; if (0 == $runLinkCount) { $parent->addTextBreak(null, $paragraphStyle); } else { @@ -185,6 +187,13 @@ protected function readParagraph(XMLReader $xmlReader, \DOMElement $domNode, $pa */ protected function readRun(XMLReader $xmlReader, \DOMElement $domNode, $parent, $docPart, $paragraphStyle = null) { + if (in_array($domNode->nodeName, array('w:ins', 'w:del'))) { + $nodes = $xmlReader->getElements('*', $domNode); + foreach ($nodes as $node) { + return $this->readRun($xmlReader, $node, $parent, $docPart, $paragraphStyle); + } + } + if (!in_array($domNode->nodeName, array('w:r', 'w:hyperlink'))) { return; } @@ -228,8 +237,18 @@ protected function readRun(XMLReader $xmlReader, \DOMElement $domNode, $parent, } } else { // TextRun - $textContent = $xmlReader->getValue('w:t', $domNode); - $parent->addText($textContent, $fontStyle, $paragraphStyle); + if ($domNode->parentNode->nodeName == 'w:del') { + $textContent = $xmlReader->getValue('w:delText', $domNode); + } else { + $textContent = $xmlReader->getValue('w:t', $domNode); + } + $element = $parent->addText($textContent, $fontStyle, $paragraphStyle); + if (in_array($domNode->parentNode->nodeName, array('w:ins', 'w:del'))) { + $type = ($domNode->parentNode->nodeName == 'w:del') ? \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED : \PhpOffice\PhpWord\Element\ChangedElement::TYPE_INSERTED; + $author = $domNode->parentNode->getAttribute('w:author'); + $date = \DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $domNode->parentNode->getAttribute('w:date')); + $element->setChanged($type, $author, $date); + } } } } diff --git a/src/PhpWord/Writer/HTML/Element/Text.php b/src/PhpWord/Writer/HTML/Element/Text.php index 71cb75669a..cc5d1c59bc 100644 --- a/src/PhpWord/Writer/HTML/Element/Text.php +++ b/src/PhpWord/Writer/HTML/Element/Text.php @@ -121,6 +121,26 @@ protected function writeOpening() $content .= "
";
}
+ //open changed tag
+ $element = $this->element;
+ $changed = $element->getChanged();
+ if ($changed instanceof \PhpOffice\PhpWord\Element\ChangedElement) {
+ if (($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_INSERTED)) {
+ $content .= 'getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED) {
+ $content .= ' array('author'=> $changed->getAuthor(),
+ 'date' => $changed->getDate()->format('Y-m-d\TH:i:s\Z'),
+ 'id' => $element->getElementId(), ));
+ $content .= json_encode($changedProp);
+ $content .= '\' ';
+ $dateUser = $changed->getDate()->format('Y-m-d H:i:s');
+ $content .= 'title="' . $changed->getAuthor() . ' - ' . $dateUser . '" ';
+ $content .= '>';
+ }
+
return $content;
}
@@ -132,6 +152,18 @@ protected function writeOpening()
protected function writeClosing()
{
$content = '';
+
+ //close changed tag
+ $element = $this->element;
+ $changed = $element->getChanged();
+ if ($changed instanceof \PhpOffice\PhpWord\Element\ChangedElement) {
+ if (($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_INSERTED)) {
+ $content .= '';
+ } elseif ($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED) {
+ $content .= '';
+ }
+ }
+
if (!$this->withoutP) {
if (Settings::isOutputEscapingEnabled()) {
$content .= $this->escaper->escapeHtml($this->closingText);
diff --git a/src/PhpWord/Writer/Word2007/Element/Text.php b/src/PhpWord/Writer/Word2007/Element/Text.php
index e714943222..5884bd3e02 100644
--- a/src/PhpWord/Writer/Word2007/Element/Text.php
+++ b/src/PhpWord/Writer/Word2007/Element/Text.php
@@ -37,16 +37,66 @@ public function write()
$this->startElementP();
+ $changed = $element->getChanged();
+ if ($changed) {
+ $this->writeOpeningChanged();
+ }
+
$xmlWriter->startElement('w:r');
$this->writeFontStyle();
- $xmlWriter->startElement('w:t');
+ $textElement = 'w:t';
+ //'w:delText' in case of deleted text
+ if (($changed) && ($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED)) {
+ $textElement = 'w:delText';
+ }
+ $xmlWriter->startElement($textElement);
+
$xmlWriter->writeAttribute('xml:space', 'preserve');
$this->writeText($this->getText($element->getText()));
$xmlWriter->endElement();
$xmlWriter->endElement(); // w:r
+ $this->writeClosingChanged();
+
$this->endElementP(); // w:p
}
+
+ /**
+ * Write opening of changed element
+ */
+ protected function writeOpeningChanged()
+ {
+ $element = $this->getElement();
+ $changed = $element->getChanged();
+
+ $xmlWriter = $this->getXmlWriter();
+
+ if ($changed instanceof \PhpOffice\PhpWord\Element\ChangedElement) {
+ if (($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_INSERTED)) {
+ $xmlWriter->startElement('w:ins');
+ } elseif ($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED) {
+ $xmlWriter->startElement('w:del');
+ }
+ $xmlWriter->writeAttribute('w:author', $changed->getAuthor());
+ $xmlWriter->writeAttribute('w:date', $changed->getDate()->format('Y-m-d\TH:i:s\Z'));
+ $xmlWriter->writeAttribute('w:id', $element->getElementId());
+ }
+ }
+
+ /**
+ * Write ending
+ */
+ protected function writeClosingChanged()
+ {
+ $element = $this->getElement();
+ $changed = $element->getChanged();
+
+ $xmlWriter = $this->getXmlWriter();
+
+ if ($changed instanceof \PhpOffice\PhpWord\Element\ChangedElement) {
+ $xmlWriter->endElement(); // w:ins|w:del
+ }
+ }
}