Merge branch 'develop' into various_html_parsing_fixes

This commit is contained in:
troosan 2018-02-09 21:53:48 +01:00 committed by GitHub
commit 5b381bc0c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 495 additions and 39 deletions

View File

@ -9,6 +9,7 @@ v0.15.0 (?? ??? 2018)
- Parsing of "align" HTML attribute - @troosan #1231 - Parsing of "align" HTML attribute - @troosan #1231
- Parse formatting inside HTML lists - @troosan @samimussbach #1239 #945 #1215 #508 - Parse formatting inside HTML lists - @troosan @samimussbach #1239 #945 #1215 #508
- Parsing of CSS `direction` instruction, HTML `lang` attribute, formatting inside table cell - @troosan #1273 #1252 #1254 - Parsing of CSS `direction` instruction, HTML `lang` attribute, formatting inside table cell - @troosan #1273 #1252 #1254
- Add support for Track changes @Cip @troosan #354 #1262
### Fixed ### Fixed
- fix reading of docx default style - @troosan #1238 - fix reading of docx default style - @troosan #1238

View File

@ -31,7 +31,7 @@ column shows the containers while the rows lists the elements.
+-------+-----------------+-----------+----------+----------+---------+------------+------------+ +-------+-----------------+-----------+----------+----------+---------+------------+------------+
| 11 | Watermark | - | v | - | - | - | - | | 11 | Watermark | - | v | - | - | - | - |
+-------+-----------------+-----------+----------+----------+---------+------------+------------+ +-------+-----------------+-----------+----------+----------+---------+------------+------------+
| 12 | Object | v | v | v | v | v | v | | 12 | OLEObject | v | v | v | v | v | v |
+-------+-----------------+-----------+----------+----------+---------+------------+------------+ +-------+-----------------+-----------+----------+----------+---------+------------+------------+
| 13 | TOC | v | - | - | - | - | - | | 13 | TOC | v | - | - | - | - | - |
+-------+-----------------+-----------+----------+----------+---------+------------+------------+ +-------+-----------------+-----------+----------+----------+---------+------------+------------+
@ -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`. 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 Titles
~~~~~~ ~~~~~~
@ -276,11 +283,11 @@ Objects
------- -------
You can add OLE embeddings, such as Excel spreadsheets or PowerPoint You can add OLE embeddings, such as Excel spreadsheets or PowerPoint
presentations to the document by using ``addObject`` method. presentations to the document by using ``addOLEObject`` method.
.. code-block:: php .. code-block:: php
$section->addObject($src, [$style]); $section->addOLEObject($src, [$style]);
Table of contents Table of contents
----------------- -----------------
@ -309,7 +316,7 @@ Footnotes & endnotes
You can create footnotes with ``addFootnote`` and endnotes with You can create footnotes with ``addFootnote`` and endnotes with
``addEndnote`` in texts or textruns, but it's recommended to use textrun ``addEndnote`` in texts or textruns, but it's recommended to use textrun
to have better layout. You can use ``addText``, ``addLink``, to have better layout. You can use ``addText``, ``addLink``,
``addTextBreak``, ``addImage``, ``addObject`` on footnotes and endnotes. ``addTextBreak``, ``addImage``, ``addOLEObject`` on footnotes and endnotes.
On textrun: On textrun:
@ -466,3 +473,27 @@ The comment can contain formatted text. Once the comment has been added, it can
$text->setCommentStart($comment); $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. 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.
Track Changes
-------------
Track changes can be set on text elements. There are 2 ways to set the change information on an element.
Either by calling the `setChangeInfo()`, or by setting the `TrackChange` instance on the element with `setTrackChange()`.
.. code-block:: php
$phpWord = new \PhpOffice\PhpWord\PhpWord();
// New portrait section
$section = $phpWord->addSection();
$textRun = $section->addTextRun();
$text = $textRun->addText('Hello World! Time to ');
$text = $textRun->addText('wake ', array('bold' => true));
$text->setChangeInfo(TrackChange::INSERTED, 'Fred', time() - 1800);
$text = $textRun->addText('up');
$text->setTrackChange(new TrackChange(TrackChange::INSERTED, 'Fred'));
$text = $textRun->addText('go to sleep');
$text->setChangeInfo(TrackChange::DELETED, 'Barney', new \DateTime('@' . (time() - 3600)));

View File

@ -9,7 +9,7 @@ $phpWord = new \PhpOffice\PhpWord\PhpWord();
$section = $phpWord->addSection(); $section = $phpWord->addSection();
$section->addText('You can open this OLE object by double clicking on the icon:'); $section->addText('You can open this OLE object by double clicking on the icon:');
$section->addTextBreak(2); $section->addTextBreak(2);
$section->addObject('resources/_sheet.xls'); $section->addOLEObject('resources/_sheet.xls');
// Save file // Save file
echo write($phpWord, basename(__FILE__, '.php'), $writers); echo write($phpWord, basename(__FILE__, '.php'), $writers);

View File

@ -0,0 +1,29 @@
<?php
use PhpOffice\PhpWord\Element\TrackChange;
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();
$textRun = $section->addTextRun();
$text = $textRun->addText('Hello World! Time to ');
$text = $textRun->addText('wake ', array('bold' => true));
$text->setChangeInfo(TrackChange::INSERTED, 'Fred', time() - 1800);
$text = $textRun->addText('up');
$text->setTrackChange(new TrackChange(TrackChange::INSERTED, 'Fred'));
$text = $textRun->addText('go to sleep');
$text->setChangeInfo(TrackChange::DELETED, 'Barney', new \DateTime('@' . (time() - 3600)));
// Save file
echo write($phpWord, basename(__FILE__, '.php'), $writers);
if (!CLI) {
include_once 'Sample_Footer.php';
}

View File

@ -81,7 +81,7 @@ abstract class AbstractContainer extends AbstractElement
{ {
$elements = array( $elements = array(
'Text', 'TextRun', 'Bookmark', 'Link', 'PreserveText', 'TextBreak', 'Text', 'TextRun', 'Bookmark', 'Link', 'PreserveText', 'TextBreak',
'ListItem', 'ListItemRun', 'Table', 'Image', 'Object', 'ListItem', 'ListItemRun', 'Table', 'Image', 'Object', 'OLEObject',
'Footnote', 'Endnote', 'CheckBox', 'TextBox', 'Field', 'Footnote', 'Endnote', 'CheckBox', 'TextBox', 'Field',
'Line', 'Shape', 'Title', 'TOC', 'PageBreak', 'Line', 'Shape', 'Title', 'TOC', 'PageBreak',
'Chart', 'FormField', 'SDT', 'Comment', 'Chart', 'FormField', 'SDT', 'Comment',
@ -157,7 +157,7 @@ abstract class AbstractContainer extends AbstractElement
/** /**
* Get all elements * Get all elements
* *
* @return array * @return \PhpOffice\PhpWord\Element\AbstractElement[]
*/ */
public function getElements() public function getElements()
{ {

View File

@ -100,6 +100,13 @@ abstract class AbstractElement
*/ */
private $parent; private $parent;
/**
* changed element info
*
* @var TrackChange
*/
private $trackChange;
/** /**
* Parent container type * Parent container type
* *
@ -438,6 +445,38 @@ abstract class AbstractElement
return $style; return $style;
} }
/**
* Sets the trackChange information
*
* @param TrackChange $trackChange
*/
public function setTrackChange(TrackChange $trackChange)
{
$this->trackChange = $trackChange;
}
/**
* Gets the trackChange information
*
* @return TrackChange
*/
public function getTrackChange()
{
return $this->trackChange;
}
/**
* Set changed
*
* @param string $type INSERTED|DELETED
* @param string $author
* @param null|int|\DateTime $date allways in UTC
*/
public function setChangeInfo($type, $author, $date = null)
{
$this->trackChange = new TrackChange($type, $author, $date);
}
/** /**
* Set enum value * Set enum value
* *

View File

@ -55,12 +55,12 @@ class Comment extends TrackChange
* Create a new Comment Element * Create a new Comment Element
* *
* @param string $author * @param string $author
* @param \DateTime $date * @param null|\DateTime $date
* @param string $initials * @param string $initials
*/ */
public function __construct($author, $date = null, $initials = null) public function __construct($author, $date = null, $initials = null)
{ {
parent::__construct($author, $date); parent::__construct(null, $author, $date);
$this->initials = $initials; $this->initials = $initials;
} }

View File

@ -334,6 +334,10 @@ class Image extends AbstractElement
// Read image binary data and convert to hex/base64 string // Read image binary data and convert to hex/base64 string
if ($this->sourceType == self::SOURCE_GD) { if ($this->sourceType == self::SOURCE_GD) {
$imageResource = call_user_func($this->imageCreateFunc, $actualSource); $imageResource = call_user_func($this->imageCreateFunc, $actualSource);
if ($this->imageType === 'image/png') {
// PNG images need to preserve alpha channel information
imagesavealpha($imageResource, true);
}
ob_start(); ob_start();
call_user_func($this->imageFunc, $imageResource); call_user_func($this->imageFunc, $imageResource);
$imageBinary = ob_get_contents(); $imageBinary = ob_get_contents();

View File

@ -20,14 +20,25 @@ namespace PhpOffice\PhpWord\Element;
/** /**
* TrackChange element * TrackChange element
* @see http://datypic.com/sc/ooxml/t-w_CT_TrackChange.html * @see http://datypic.com/sc/ooxml/t-w_CT_TrackChange.html
* @see http://datypic.com/sc/ooxml/t-w_CT_RunTrackChange.html
*/ */
class TrackChange extends AbstractContainer class TrackChange extends AbstractContainer
{ {
const INSERTED = 'INSERTED';
const DELETED = 'DELETED';
/** /**
* @var string Container type * @var string Container type
*/ */
protected $container = 'TrackChange'; protected $container = 'TrackChange';
/**
* The type of change, (insert or delete), not applicable for PhpOffice\PhpWord\Element\Comment
*
* @var string
*/
private $changeType;
/** /**
* Author * Author
* *
@ -45,13 +56,17 @@ class TrackChange extends AbstractContainer
/** /**
* Create a new TrackChange Element * Create a new TrackChange Element
* *
* @param string $changeType
* @param string $author * @param string $author
* @param \DateTime $date * @param null|int|\DateTime $date
*/ */
public function __construct($author, \DateTime $date = null) public function __construct($changeType = null, $author = null, $date = null)
{ {
$this->changeType = $changeType;
$this->author = $author; $this->author = $author;
$this->date = $date; if ($date !== null) {
$this->date = ($date instanceof \DateTime) ? $date : new \DateTime('@' . $date);
}
} }
/** /**
@ -73,4 +88,14 @@ class TrackChange extends AbstractContainer
{ {
return $this->date; return $this->date;
} }
/**
* Get the Change type
*
* @return string
*/
public function getChangeType()
{
return $this->changeType;
}
} }

View File

@ -18,6 +18,7 @@
namespace PhpOffice\PhpWord\Reader\ODText; namespace PhpOffice\PhpWord\Reader\ODText;
use PhpOffice\Common\XMLReader; use PhpOffice\Common\XMLReader;
use PhpOffice\PhpWord\Element\TrackChange;
use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\PhpWord;
/** /**
@ -37,6 +38,8 @@ class Content extends AbstractPart
$xmlReader = new XMLReader(); $xmlReader = new XMLReader();
$xmlReader->getDomFromZip($this->docFile, $this->xmlFile); $xmlReader->getDomFromZip($this->docFile, $this->xmlFile);
$trackedChanges = array();
$nodes = $xmlReader->getElements('office:body/office:text/*'); $nodes = $xmlReader->getElements('office:body/office:text/*');
if ($nodes->length > 0) { if ($nodes->length > 0) {
$section = $phpWord->addSection(); $section = $phpWord->addSection();
@ -48,7 +51,37 @@ class Content extends AbstractPart
$section->addTitle($node->nodeValue, $depth); $section->addTitle($node->nodeValue, $depth);
break; break;
case 'text:p': // Paragraph 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) && is_array($changed)) {
$element->setTrackChange($changed['changed']);
if (isset($changed['textNodes'])) {
foreach ($changed['textNodes'] as $changedNode) {
$element = $section->addText($changedNode->nodeValue);
$element->setTrackChange($changed['changed']);
}
}
}
break; break;
case 'text:list': // List case 'text:list': // List
$listItems = $xmlReader->getElements('text:list-item/text:p', $node); $listItems = $xmlReader->getElements('text:list-item/text:p', $node);
@ -57,6 +90,21 @@ class Content extends AbstractPart
$section->addListItem($listItem->nodeValue, 0); $section->addListItem($listItem->nodeValue, 0);
} }
break; break;
case 'text:tracked-changes':
$changedRegions = $xmlReader->getElements('text:changed-region', $node);
foreach ($changedRegions as $changedRegion) {
$type = ($changedRegion->firstChild->nodeName == 'text:insertion') ? TrackChange::INSERTED : TrackChange::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 TrackChange($type, $author, $date);
$textNodes = $xmlReader->getElements('text:deletion/text:p', $changedRegion);
$trackedChanges[$changedRegion->getAttribute('text:id')] = array('changed' => $changed, 'textNodes'=> $textNodes);
}
break;
} }
} }
} }

View File

@ -18,6 +18,7 @@
namespace PhpOffice\PhpWord\Reader\Word2007; namespace PhpOffice\PhpWord\Reader\Word2007;
use PhpOffice\Common\XMLReader; use PhpOffice\Common\XMLReader;
use PhpOffice\PhpWord\Element\TrackChange;
use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\PhpWord;
/** /**
@ -156,8 +157,10 @@ abstract class AbstractPart
} else { } else {
// Text and TextRun // Text and TextRun
$runCount = $xmlReader->countElements('w:r', $domNode); $runCount = $xmlReader->countElements('w:r', $domNode);
$insCount = $xmlReader->countElements('w:ins', $domNode);
$delCount = $xmlReader->countElements('w:del', $domNode);
$linkCount = $xmlReader->countElements('w:hyperlink', $domNode); $linkCount = $xmlReader->countElements('w:hyperlink', $domNode);
$runLinkCount = $runCount + $linkCount; $runLinkCount = $runCount + $insCount + $delCount + $linkCount;
if (0 == $runLinkCount) { if (0 == $runLinkCount) {
$parent->addTextBreak(null, $paragraphStyle); $parent->addTextBreak(null, $paragraphStyle);
} else { } else {
@ -185,6 +188,13 @@ abstract class AbstractPart
*/ */
protected function readRun(XMLReader $xmlReader, \DOMElement $domNode, $parent, $docPart, $paragraphStyle = null) 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'))) { if (!in_array($domNode->nodeName, array('w:r', 'w:hyperlink'))) {
return; return;
} }
@ -228,8 +238,19 @@ abstract class AbstractPart
} }
} else { } else {
// TextRun // TextRun
$textContent = $xmlReader->getValue('w:t', $domNode); if ($domNode->parentNode->nodeName == 'w:del') {
$parent->addText($textContent, $fontStyle, $paragraphStyle); $textContent = $xmlReader->getValue('w:delText', $domNode);
} else {
$textContent = $xmlReader->getValue('w:t', $domNode);
}
/** @var AbstractElement $element */
$element = $parent->addText($textContent, $fontStyle, $paragraphStyle);
if (in_array($domNode->parentNode->nodeName, array('w:ins', 'w:del'))) {
$type = ($domNode->parentNode->nodeName == 'w:del') ? TrackChange::DELETED : TrackChange::INSERTED;
$author = $domNode->parentNode->getAttribute('w:author');
$date = \DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $domNode->parentNode->getAttribute('w:date'));
$element->setChangeInfo($type, $author, $date);
}
} }
} }
} }

View File

@ -56,7 +56,7 @@ class Styles extends AbstractPart
if ($paragraphDefaults !== null) { if ($paragraphDefaults !== null) {
$paragraphDefaultStyle = $this->readParagraphStyle($xmlReader, $paragraphDefaults); $paragraphDefaultStyle = $this->readParagraphStyle($xmlReader, $paragraphDefaults);
if ($paragraphDefaultStyle != null) { if ($paragraphDefaultStyle != null) {
$phpWord->setDefaultParagraphStyle(); $phpWord->setDefaultParagraphStyle($paragraphDefaultStyle);
} }
} }

View File

@ -352,6 +352,10 @@ abstract class AbstractWriter implements WriterInterface
// Retrive GD image content or get local media // Retrive GD image content or get local media
if (isset($element['isMemImage']) && $element['isMemImage']) { if (isset($element['isMemImage']) && $element['isMemImage']) {
$image = call_user_func($element['createFunction'], $element['source']); $image = call_user_func($element['createFunction'], $element['source']);
if ($element['imageType'] === 'image/png') {
// PNG images need to preserve alpha channel information
imagesavealpha($image, true);
}
ob_start(); ob_start();
call_user_func($element['imageFunction'], $image); call_user_func($element['imageFunction'], $image);
$imageContents = ob_get_contents(); $imageContents = ob_get_contents();

View File

@ -17,6 +17,7 @@
namespace PhpOffice\PhpWord\Writer\HTML\Element; namespace PhpOffice\PhpWord\Writer\HTML\Element;
use PhpOffice\PhpWord\Element\TrackChange;
use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Settings;
use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Font;
use PhpOffice\PhpWord\Style\Paragraph; use PhpOffice\PhpWord\Style\Paragraph;
@ -121,6 +122,9 @@ class Text extends AbstractElement
$content .= "<p{$style}>"; $content .= "<p{$style}>";
} }
//open track change tag
$content .= $this->writeTrackChangeOpening();
return $content; return $content;
} }
@ -132,6 +136,10 @@ class Text extends AbstractElement
protected function writeClosing() protected function writeClosing()
{ {
$content = ''; $content = '';
//close track change tag
$content .= $this->writeTrackChangeClosing();
if (!$this->withoutP) { if (!$this->withoutP) {
if (Settings::isOutputEscapingEnabled()) { if (Settings::isOutputEscapingEnabled()) {
$content .= $this->escaper->escapeHtml($this->closingText); $content .= $this->escaper->escapeHtml($this->closingText);
@ -145,6 +153,63 @@ class Text extends AbstractElement
return $content; return $content;
} }
/**
* writes the track change opening tag
*
* @return string the HTML, an empty string if no track change information
*/
private function writeTrackChangeOpening()
{
$changed = $this->element->getTrackChange();
if ($changed == null) {
return '';
}
$content = '';
if (($changed->getChangeType() == TrackChange::INSERTED)) {
$content .= '<ins data-phpword-prop=\'';
} elseif ($changed->getChangeType() == TrackChange::DELETED) {
$content .= '<del data-phpword-prop=\'';
}
$changedProp = array('changed' => array('author'=> $changed->getAuthor(), 'id' => $this->element->getElementId()));
if ($changed->getDate() != null) {
$changedProp['changed']['date'] = $changed->getDate()->format('Y-m-d\TH:i:s\Z');
}
$content .= json_encode($changedProp);
$content .= '\' ';
$content .= 'title="' . $changed->getAuthor();
if ($changed->getDate() != null) {
$dateUser = $changed->getDate()->format('Y-m-d H:i:s');
$content .= ' - ' . $dateUser;
}
$content .= '">';
return $content;
}
/**
* writes the track change closing tag
*
* @return string the HTML, an empty string if no track change information
*/
private function writeTrackChangeClosing()
{
$changed = $this->element->getTrackChange();
if ($changed == null) {
return '';
}
$content = '';
if (($changed->getChangeType() == TrackChange::INSERTED)) {
$content .= '</ins>';
} elseif ($changed->getChangeType() == TrackChange::DELETED) {
$content .= '</del>';
}
return $content;
}
/** /**
* Write paragraph style * Write paragraph style
* *

View File

@ -17,6 +17,7 @@
namespace PhpOffice\PhpWord\Writer\ODText\Element; namespace PhpOffice\PhpWord\Writer\ODText\Element;
use PhpOffice\PhpWord\Element\TrackChange;
use PhpOffice\PhpWord\Exception\Exception; use PhpOffice\PhpWord\Exception\Exception;
/** /**
@ -51,29 +52,50 @@ class Text extends AbstractElement
if (!$this->withoutP) { if (!$this->withoutP) {
$xmlWriter->startElement('text:p'); // text:p $xmlWriter->startElement('text:p'); // text:p
} }
if (empty($fontStyle)) { if ($element->getTrackChange() != null && $element->getTrackChange()->getChangeType() == TrackChange::DELETED) {
if (empty($paragraphStyle)) { $xmlWriter->startElement('text:change');
$xmlWriter->writeAttribute('text:style-name', 'P1'); $xmlWriter->writeAttribute('text:change-id', $element->getTrackChange()->getElementId());
} elseif (is_string($paragraphStyle)) {
$xmlWriter->writeAttribute('text:style-name', $paragraphStyle);
}
$this->writeText($element->getText());
} else {
if (empty($paragraphStyle)) {
$xmlWriter->writeAttribute('text:style-name', 'Standard');
} elseif (is_string($paragraphStyle)) {
$xmlWriter->writeAttribute('text:style-name', $paragraphStyle);
}
// text:span
$xmlWriter->startElement('text:span');
if (is_string($fontStyle)) {
$xmlWriter->writeAttribute('text:style-name', $fontStyle);
}
$this->writeText($element->getText());
$xmlWriter->endElement(); $xmlWriter->endElement();
} else {
if (empty($fontStyle)) {
if (empty($paragraphStyle)) {
$xmlWriter->writeAttribute('text:style-name', 'P1');
} elseif (is_string($paragraphStyle)) {
$xmlWriter->writeAttribute('text:style-name', $paragraphStyle);
}
$this->writeChangeInsertion(true, $element->getTrackChange());
$this->writeText($element->getText());
$this->writeChangeInsertion(false, $element->getTrackChange());
} else {
if (empty($paragraphStyle)) {
$xmlWriter->writeAttribute('text:style-name', 'Standard');
} elseif (is_string($paragraphStyle)) {
$xmlWriter->writeAttribute('text:style-name', $paragraphStyle);
}
// text:span
$xmlWriter->startElement('text:span');
if (is_string($fontStyle)) {
$xmlWriter->writeAttribute('text:style-name', $fontStyle);
}
$this->writeChangeInsertion(true, $element->getTrackChange());
$this->writeText($element->getText());
$this->writeChangeInsertion(false, $element->getTrackChange());
$xmlWriter->endElement();
}
} }
if (!$this->withoutP) { if (!$this->withoutP) {
$xmlWriter->endElement(); // text:p $xmlWriter->endElement(); // text:p
} }
} }
private function writeChangeInsertion($start = true, TrackChange $trackChange = null)
{
if ($trackChange == null || $trackChange->getChangeType() != TrackChange::INSERTED) {
return;
}
$xmlWriter = $this->getXmlWriter();
$xmlWriter->startElement('text:change-' . ($start ? 'start' : 'end'));
$xmlWriter->writeAttribute('text:change-id', $trackChange->getElementId());
$xmlWriter->endElement();
}
} }

View File

@ -18,10 +18,12 @@
namespace PhpOffice\PhpWord\Writer\ODText\Part; namespace PhpOffice\PhpWord\Writer\ODText\Part;
use PhpOffice\Common\XMLWriter; use PhpOffice\Common\XMLWriter;
use PhpOffice\PhpWord\Element\AbstractContainer;
use PhpOffice\PhpWord\Element\Image; use PhpOffice\PhpWord\Element\Image;
use PhpOffice\PhpWord\Element\Table; use PhpOffice\PhpWord\Element\Table;
use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\Text;
use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\TextRun;
use PhpOffice\PhpWord\Element\TrackChange;
use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style;
use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Font;
@ -74,6 +76,40 @@ class Content extends AbstractPart
$xmlWriter->startElement('office:body'); $xmlWriter->startElement('office:body');
$xmlWriter->startElement('office:text'); $xmlWriter->startElement('office:text');
// Tracked changes declarations
$trackedChanges = array();
$sections = $phpWord->getSections();
foreach ($sections as $section) {
$this->collectTrackedChanges($section, $trackedChanges);
}
$xmlWriter->startElement('text:tracked-changes');
foreach ($trackedChanges as $trackedElement) {
$trackedChange = $trackedElement->getTrackChange();
$xmlWriter->startElement('text:changed-region');
$trackedChange->setElementId();
$xmlWriter->writeAttribute('text:id', $trackedChange->getElementId());
if (($trackedChange->getChangeType() == TrackChange::INSERTED)) {
$xmlWriter->startElement('text:insertion');
} elseif ($trackedChange->getChangeType() == TrackChange::DELETED) {
$xmlWriter->startElement('text:deletion');
}
$xmlWriter->startElement('office:change-info');
$xmlWriter->writeElement('dc:creator', $trackedChange->getAuthor());
if ($trackedChange->getDate() != null) {
$xmlWriter->writeElement('dc:date', $trackedChange->getDate()->format('Y-m-d\TH:i:s\Z'));
}
$xmlWriter->endElement(); // office:change-info
if ($trackedChange->getChangeType() == TrackChange::DELETED) {
$xmlWriter->writeElement('text:p', $trackedElement->getText());
}
$xmlWriter->endElement(); // text:insertion|text:deletion
$xmlWriter->endElement(); // text:changed-region
}
$xmlWriter->endElement(); // text:tracked-changes
// Sequence declarations // Sequence declarations
$sequences = array('Illustration', 'Table', 'Text', 'Drawing'); $sequences = array('Illustration', 'Table', 'Text', 'Drawing');
$xmlWriter->startElement('text:sequence-decls'); $xmlWriter->startElement('text:sequence-decls');
@ -242,4 +278,23 @@ class Content extends AbstractPart
$element->setParagraphStyle("P{$paragraphStyleCount}"); $element->setParagraphStyle("P{$paragraphStyleCount}");
} }
} }
/**
* Finds all tracked changes
*
* @param AbstractContainer $container
* @param \PhpOffice\PhpWord\Element\AbstractElement[] $trackedChanges
*/
private function collectTrackedChanges(AbstractContainer $container, &$trackedChanges = array())
{
$elements = $container->getElements();
foreach ($elements as $element) {
if ($element->getTrackChange() != null) {
$trackedChanges[] = $element;
}
if (is_callable(array($element, 'getElements'))) {
$this->collectTrackedChanges($element, $trackedChanges);
}
}
}
} }

View File

@ -17,6 +17,8 @@
namespace PhpOffice\PhpWord\Writer\Word2007\Element; namespace PhpOffice\PhpWord\Writer\Word2007\Element;
use PhpOffice\PhpWord\Element\TrackChange;
/** /**
* Text element writer * Text element writer
* *
@ -37,16 +39,66 @@ class Text extends AbstractElement
$this->startElementP(); $this->startElementP();
$this->writeOpeningTrackChange();
$xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:r');
$this->writeFontStyle(); $this->writeFontStyle();
$xmlWriter->startElement('w:t'); $textElement = 'w:t';
//'w:delText' in case of deleted text
$changed = $element->getTrackChange();
if ($changed != null && $changed->getChangeType() == TrackChange::DELETED) {
$textElement = 'w:delText';
}
$xmlWriter->startElement($textElement);
$xmlWriter->writeAttribute('xml:space', 'preserve'); $xmlWriter->writeAttribute('xml:space', 'preserve');
$this->writeText($this->getText($element->getText())); $this->writeText($this->getText($element->getText()));
$xmlWriter->endElement(); $xmlWriter->endElement();
$xmlWriter->endElement(); // w:r $xmlWriter->endElement(); // w:r
$this->writeClosingTrackChange();
$this->endElementP(); // w:p $this->endElementP(); // w:p
} }
/**
* Write opening of changed element
*/
protected function writeOpeningTrackChange()
{
$changed = $this->getElement()->getTrackChange();
if ($changed == null) {
return;
}
$xmlWriter = $this->getXmlWriter();
if (($changed->getChangeType() == TrackChange::INSERTED)) {
$xmlWriter->startElement('w:ins');
} elseif ($changed->getChangeType() == TrackChange::DELETED) {
$xmlWriter->startElement('w:del');
}
$xmlWriter->writeAttribute('w:author', $changed->getAuthor());
if ($changed->getDate() != null) {
$xmlWriter->writeAttribute('w:date', $changed->getDate()->format('Y-m-d\TH:i:s\Z'));
}
$xmlWriter->writeAttribute('w:id', $this->getElement()->getElementId());
}
/**
* Write ending
*/
protected function writeClosingTrackChange()
{
$changed = $this->getElement()->getTrackChange();
if ($changed == null) {
return;
}
$xmlWriter = $this->getXmlWriter();
$xmlWriter->endElement(); // w:ins|w:del
}
} }

View File

@ -0,0 +1,44 @@
<?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-2017 PHPWord contributors
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
namespace PhpOffice\PhpWord\Element;
/**
* Test class for PhpOffice\PhpWord\Element\TrackChange
*
* @runTestsInSeparateProcesses
*/
class TrackChangeTest extends \PHPUnit\Framework\TestCase
{
/**
* New instance
*/
public function testConstructDefault()
{
$author = 'Test User';
$date = new \DateTime('2000-01-01');
$oTrackChange = new TrackChange(TrackChange::INSERTED, $author, $date);
$oText = new Text('dummy text');
$oText->setTrackChange($oTrackChange);
$this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\TrackChange', $oTrackChange);
$this->assertEquals($author, $oTrackChange->getAuthor());
$this->assertEquals($date, $oTrackChange->getDate());
$this->assertEquals(TrackChange::INSERTED, $oTrackChange->getChangeType());
}
}

View File

@ -19,8 +19,8 @@ namespace PhpOffice\PhpWord\Writer\Word2007;
use PhpOffice\Common\XMLWriter; use PhpOffice\Common\XMLWriter;
use PhpOffice\PhpWord\Element\Comment; use PhpOffice\PhpWord\Element\Comment;
use PhpOffice\PhpWord\Element\Text;
use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\TextRun;
use PhpOffice\PhpWord\Element\TrackChange;
use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\TestHelperDOCX; use PhpOffice\PhpWord\TestHelperDOCX;
@ -415,4 +415,20 @@ class ElementTest extends \PHPUnit\Framework\TestCase
$this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:commentRangeEnd')); $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:commentRangeEnd'));
$this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:commentReference')); $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:commentReference'));
} }
public function testTrackChange()
{
$phpWord = new PhpWord();
$section = $phpWord->addSection();
$text = $section->addText('my dummy text');
$text->setChangeInfo(TrackChange::INSERTED, 'author name');
$text2 = $section->addText('my other text');
$text2->setTrackChange(new TrackChange(TrackChange::DELETED, 'another author', new \DateTime()));
$doc = TestHelperDOCX::getDocument($phpWord);
$this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:ins/w:r'));
$this->assertEquals('author name', $doc->getElementAttribute('/w:document/w:body/w:p/w:ins', 'w:author'));
$this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:del/w:r/w:delText'));
}
} }