Skip to content
7 changes: 7 additions & 0 deletions docs/elements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~

Expand Down
23 changes: 23 additions & 0 deletions samples/Sample_31_TrackChanges.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
include_once 'Sample_Header.php';

// New Word Document
echo date('H:i:s') , ' Create new PhpWord object' , EOL;
$phpWord = new \PhpOffice\PhpWord\PhpWord();

// New portrait section
$section = $phpWord->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';
}
29 changes: 29 additions & 0 deletions src/PhpWord/Element/AbstractElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ abstract class AbstractElement
*/
private $nestedLevel = 0;

/**
* changed element info
*
* @var object
*/
public $changed;

/**
* Parent container type
*
Expand Down Expand Up @@ -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
*
Expand Down
57 changes: 57 additions & 0 deletions src/PhpWord/Element/ChangedElement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
/**
* This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents.
*
* PHPWord is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation.
*
* For the full copyright and license information, please read the LICENSE
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
*
* @see https://github.com/PHPOffice/PHPWord
* @copyright 2010-2014 PHPWord contributors
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/

namespace PhpOffice\PhpWord\Element;

/**
* ChangedElement class
*/
class ChangedElement extends TrackChange
{
/**
* change type TYPE_INSERTED|TYPE_DELETED
*
* @var int
*/
private $changeType;

const TYPE_INSERTED = 1;
const TYPE_DELETED = 2;

/**
* Create a new Changed Element
*
* @param int $changeType
* @param string $author
* @param \DateTime $date
*/
public function __construct($changeType, $author, $date)
{
parent::__construct($author, $date);
$this->changeType = $changeType;
}

/**
* Get change type
*
* @return int
*/
public function getChangeType()
{
return $this->changeType;
}
}
49 changes: 48 additions & 1 deletion src/PhpWord/Reader/ODText/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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;
}
}
}
Expand Down
25 changes: 22 additions & 3 deletions src/PhpWord/Reader/Word2007/AbstractPart.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/PhpWord/Writer/HTML/Element/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,26 @@ protected function writeOpening()
$content .= "<p{$style}>";
}

//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 .= '<ins data-phpword-prop=\'';
} elseif ($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED) {
$content .= '<del data-phpword-prop=\'';
}

$changedProp = array('changed' => 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;
}

Expand All @@ -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 .= '</ins>';
} elseif ($changed->getChangeType() == \PhpOffice\PhpWord\Element\ChangedElement::TYPE_DELETED) {
$content .= '</del>';
}
}

if (!$this->withoutP) {
if (Settings::isOutputEscapingEnabled()) {
$content .= $this->escaper->escapeHtml($this->closingText);
Expand Down
52 changes: 51 additions & 1 deletion src/PhpWord/Writer/Word2007/Element/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}