From 2d2759585d11627fcd2475be0ee39b24cd4837eb Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Jan 2025 05:46:32 +0900 Subject: [PATCH] Feature: Ruby (phonetic guide) text - Word2007 Read/Write, HTML Read/Write, RTF write, ODT basic write (#2727) * Allow reading ruby text in from Word2007 docs * Rename ruby language to language Id * Add basic ruby Word2007 writer * Update changelog * Add ruby element docs * Run PHP CS Fixer * Fix PHPStan errors * Run PHP CS Fixer again * Fix ruby in titles when reading * Add/tweak tests for reading/writing Title with ruby * Use elementExists to try and fix PHP7.1 * Review: add missing type hints; refine types * Add setters to Ruby class * Add basic sample for ruby text * Add base constructor to RubyProperties * Add basic ruby tests * Run PHP CS Fixer * Update sample to handle properties better * Add Ruby HTML output including tests * Update changelog * Update PhpStand baseline * Forgot to run php-cs-fixer again * Update 1.4.0.md * Fix TextRun::getText not working with Ruby element * Add test for TextRun ruby element * Add RTF ruby output * Add ruby HTML reading * Tweak param calls in testRubyWriting * Update changelog * Update sample 46 to be less confusing with ruby output * Move ODText/Element/Text::replaceTabs method Moved to parent AbstractElement so other classes could make use of it * Add very basic ODT Ruby output * Run PHP CS Fixer * Update 1.4.0.md * Try to fix unexpected EOF in HtmlTest * Add missing tests for RubyProperties --- docs/changes/1.x/1.4.0.md | 1 + docs/usage/elements/ruby.md | 57 ++++++ mkdocs.yml | 1 + phpstan-baseline.neon | 20 +- samples/Sample_46_RubyPhoneticGuide.php | 70 +++++++ src/PhpWord/ComplexType/RubyProperties.php | 188 ++++++++++++++++++ src/PhpWord/Element/AbstractContainer.php | 3 +- src/PhpWord/Element/Ruby.php | 114 +++++++++++ src/PhpWord/Element/TextRun.php | 3 + src/PhpWord/Reader/Word2007/AbstractPart.php | 44 +++- src/PhpWord/Shared/Html.php | 83 +++++++- src/PhpWord/Writer/HTML/Element/Ruby.php | 130 ++++++++++++ .../Writer/ODText/Element/AbstractElement.php | 28 +++ src/PhpWord/Writer/ODText/Element/Ruby.php | 64 ++++++ src/PhpWord/Writer/ODText/Element/Text.php | 29 --- src/PhpWord/Writer/RTF/Element/Ruby.php | 59 ++++++ src/PhpWord/Writer/RTF/Element/Title.php | 9 +- src/PhpWord/Writer/Word2007/Element/Ruby.php | 81 ++++++++ .../ComplexType/RubyPropertiesTest.php | 141 +++++++++++++ tests/PhpWordTests/Element/RubyTest.php | 104 ++++++++++ tests/PhpWordTests/Element/TextRunTest.php | 21 ++ .../Reader/Word2007/ElementTest.php | 132 ++++++++++++ tests/PhpWordTests/Shared/HtmlTest.php | 48 +++++ .../Writer/HTML/Element/RubyTest.php | 99 +++++++++ tests/PhpWordTests/Writer/HTML/Helper.php | 32 ++- .../Writer/ODText/ElementTest.php | 36 +++- tests/PhpWordTests/Writer/RTF/ElementTest.php | 41 ++++ .../Writer/Word2007/ElementTest.php | 130 ++++++++++++ 28 files changed, 1725 insertions(+), 43 deletions(-) create mode 100644 docs/usage/elements/ruby.md create mode 100644 samples/Sample_46_RubyPhoneticGuide.php create mode 100644 src/PhpWord/ComplexType/RubyProperties.php create mode 100644 src/PhpWord/Element/Ruby.php create mode 100644 src/PhpWord/Writer/HTML/Element/Ruby.php create mode 100644 src/PhpWord/Writer/ODText/Element/Ruby.php create mode 100644 src/PhpWord/Writer/RTF/Element/Ruby.php create mode 100644 src/PhpWord/Writer/Word2007/Element/Ruby.php create mode 100644 tests/PhpWordTests/ComplexType/RubyPropertiesTest.php create mode 100644 tests/PhpWordTests/Element/RubyTest.php create mode 100644 tests/PhpWordTests/Writer/HTML/Element/RubyTest.php diff --git a/docs/changes/1.x/1.4.0.md b/docs/changes/1.x/1.4.0.md index e733a5b2e2..c8a019115d 100644 --- a/docs/changes/1.x/1.4.0.md +++ b/docs/changes/1.x/1.4.0.md @@ -13,6 +13,7 @@ - Autoload : Allow to use PHPWord without Composer fixing [#2543](https://github.com/PHPOffice/PHPWord/issues/2543), [#2552](https://github.com/PHPOffice/PHPWord/issues/2552), [#2716](https://github.com/PHPOffice/PHPWord/issues/2716), [#2717](https://github.com/PHPOffice/PHPWord/issues/2717) in [#2722](https://github.com/PHPOffice/PHPWord/pull/2722) - Add Default font color for Word by [@Collie-IT](https://github.com/Collie-IT) in [#2700](https://github.com/PHPOffice/PHPWord/pull/2700) - Writer HTML: Support Default font color by [@MichaelPFrey](https://github.com/MichaelPFrey) +- Add basic ruby text (phonetic guide) support for Word2007 and HTML Reader/Writer, RTF Writer, basic support for ODT writing by [@Deadpikle](https://github.com/Deadpikle) in [#2727](https://github.com/PHPOffice/PHPWord/pull/2727) ### Bug fixes diff --git a/docs/usage/elements/ruby.md b/docs/usage/elements/ruby.md new file mode 100644 index 0000000000..508b97cd84 --- /dev/null +++ b/docs/usage/elements/ruby.md @@ -0,0 +1,57 @@ +# Ruby + +Ruby (phonetic guide) text can be added by using the ``addRuby`` method. Ruby elements require a ``RubyProperties`` object, a ``TextRun`` for the base text, and a ``TextRun`` for the actual ruby (phonetic guide) text. + +Here is one example for a complete ruby element setup: + +``` php +addSection(); +$properties = new RubyProperties(); +$properties->setAlignment(RubyProperties::ALIGNMENT_RIGHT_VERTICAL); +$properties->setFontFaceSize(10); +$properties->setFontPointsAboveBaseText(4); +$properties->setFontSizeForBaseText(18); +$properties->setLanguageId('ja-JP'); + +$baseTextRun = new TextRun(null); +$baseTextRun->addText('私'); +$rubyTextRun = new TextRun(null); +$rubyTextRun->addText('わたし'); + +$section->addRuby($baseTextRun, $rubyTextRun, $properties); +``` + +- ``$baseTextRun``. ``TextRun`` to be used for the base text. +- ``$rubyTextRun``. ``TextRun`` to be used for the ruby text. +- ``$properties``. ``RubyProperties`` properties object for the ruby text. + +A title with a phonetic guide is a little more complex, but still possible. Make sure you add the appropraite title style to your document. + +```php +$phpWord = new PhpWord(); +$fontStyle = new Font(); +$fontStyle->setAllCaps(true); +$fontStyle->setBold(true); +$fontStyle->setSize(24); +$phpWord->addTitleStyle(1, ['name' => 'Arial', 'size' => 24, 'bold' => true, 'color' => '990000']); + +$section = $phpWord->addSection(); +$properties = new RubyProperties(); +$properties->setAlignment(RubyProperties::ALIGNMENT_RIGHT_VERTICAL); +$properties->setFontFaceSize(10); +$properties->setFontPointsAboveBaseText(4); +$properties->setFontSizeForBaseText(18); +$properties->setLanguageId('ja-JP'); + +$baseTextRun = new TextRun(null); +$baseTextRun->addText('私'); +$rubyTextRun = new TextRun(null); +$rubyTextRun->addText('わたし'); + +$textRun = new TextRun(); +$textRun->addRuby($baseTextRun, $rubyTextRun, $properties); +$section->addTitle($textRun, 1); +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 6b2c1b7fa6..0462c9c8a4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -63,6 +63,7 @@ nav: - OLE Object: 'usage/elements/oleobject.md' - Page Break: 'usage/elements/pagebreak.md' - Preserve Text: 'usage/elements/preservetext.md' + - Ruby: 'usage/elements/ruby.md' - Text: 'usage/elements/text.md' - TextBox: 'usage/elements/textbox.md' - Text Break: 'usage/elements/textbreak.md' diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 901888024d..86a542769a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -430,6 +430,11 @@ parameters: count: 1 path: src/PhpWord/Shared/Html.php + - + message: "#^Method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseRuby\\(\\) has no return type specified\\.$#" + count: 1 + path: src/PhpWord/Shared/Html.php + - message: "#^Method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseStyleDeclarations\\(\\) has no return type specified\\.$#" count: 1 @@ -442,7 +447,7 @@ parameters: - message: "#^Parameter \\#1 \\$attribute of static method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseStyle\\(\\) expects DOMAttr, DOMNode given\\.$#" - count: 1 + count: 3 path: src/PhpWord/Shared/Html.php - @@ -1051,14 +1056,14 @@ parameters: path: src/PhpWord/Writer/ODText/Element/Table.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:replacetabs\\(\\) has parameter \\$text with no type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\AbstractElement\\:\\:replaceTabs\\(\\) has parameter \\$text with no type specified\\.$#" count: 1 - path: src/PhpWord/Writer/ODText/Element/Text.php + path: src/PhpWord/Writer/ODText/Element/AbstractElement.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:replacetabs\\(\\) has parameter \\$xmlWriter with no type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\AbstractElement\\:\\:replaceTabs\\(\\) has parameter \\$xmlWriter with no type specified\\.$#" count: 1 - path: src/PhpWord/Writer/ODText/Element/Text.php + path: src/PhpWord/Writer/ODText/Element/AbstractElement.php - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:writeChangeInsertion\\(\\) has parameter \\$start with no type specified\\.$#" @@ -1689,6 +1694,11 @@ parameters: message: "#^Cannot access property \\$length on DOMNodeList\\\\|false\\.$#" count: 9 path: tests/PhpWordTests/Writer/HTML/ElementTest.php + + - + message: "#^Cannot access property \\$length on DOMNodeList\\\\|false\\.$#" + count: 2 + path: tests/PhpWordTests/Writer/HTML/Element/RubyTest.php - message: "#^Cannot call method item\\(\\) on DOMNodeList\\\\|false\\.$#" diff --git a/samples/Sample_46_RubyPhoneticGuide.php b/samples/Sample_46_RubyPhoneticGuide.php new file mode 100644 index 0000000000..0d991de756 --- /dev/null +++ b/samples/Sample_46_RubyPhoneticGuide.php @@ -0,0 +1,70 @@ +addSection(); + +$section->addText('Here is some normal text with no ruby, also known as "phonetic guide", text.'); + +$properties = new RubyProperties(); +$properties->setAlignment(RubyProperties::ALIGNMENT_CENTER); +$properties->setFontFaceSize(10); +$properties->setFontPointsAboveBaseText(20); +$properties->setFontSizeForBaseText(18); +$properties->setLanguageId('en-US'); + +$textRun = $section->addTextRun(); +$textRun->addText('Here is a demonstration of ruby text for '); +$baseTextRun = new TextRun(null); +$baseTextRun->addText('this'); +$rubyTextRun = new TextRun(null); +$rubyTextRun->addText('ruby-text'); +$textRun->addRuby($baseTextRun, $rubyTextRun, $properties); +$textRun->addText(' word.'); + +$textRun = $section->addTextRun(); +$properties = new RubyProperties(); +$properties->setAlignment(RubyProperties::ALIGNMENT_CENTER); +$properties->setFontFaceSize(10); +$properties->setFontPointsAboveBaseText(20); +$properties->setFontSizeForBaseText(18); +$properties->setLanguageId('ja-JP'); +$textRun->addText('Here is a demonstration of ruby text for Japanese text: '); +$baseTextRun = new TextRun(null); +$baseTextRun->addText('私'); +$rubyTextRun = new TextRun(null); +$rubyTextRun->addText('わたし'); +$textRun->addRuby($baseTextRun, $rubyTextRun, $properties); + +$section->addText('You can also have ruby text for titles:'); + +$phpWord->addTitleStyle(1, ['name' => 'Arial', 'size' => 24, 'bold' => true, 'color' => '000099']); + +$properties = new RubyProperties(); +$properties->setAlignment(RubyProperties::ALIGNMENT_CENTER); +$properties->setFontFaceSize(10); +$properties->setFontPointsAboveBaseText(50); +$properties->setFontSizeForBaseText(18); +$properties->setLanguageId('ja-JP'); + +$baseTextRun = new TextRun(null); +$baseTextRun->addText('私'); +$rubyTextRun = new TextRun(null); +$rubyTextRun->addText('わたし'); +$textRun = new TextRun(); +$textRun->addRuby($baseTextRun, $rubyTextRun, $properties); +$section->addTitle($textRun, 1); + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/src/PhpWord/ComplexType/RubyProperties.php b/src/PhpWord/ComplexType/RubyProperties.php new file mode 100644 index 0000000000..2409151644 --- /dev/null +++ b/src/PhpWord/ComplexType/RubyProperties.php @@ -0,0 +1,188 @@ +alignment = self::ALIGNMENT_DISTRIBUTE_SPACE; + $this->fontFaceSize = 12; + $this->fontPointsAboveText = 22; + $this->languageId = 'ja-JP'; + $this->baseTextFontSize = 24; + } + + /** + * Get the ruby alignment. + */ + public function getAlignment(): string + { + return $this->alignment; + } + + /** + * Set the Ruby Alignment (center, distributeLetter, distributeSpace, left, right, rightVertical). + */ + public function setAlignment(string $alignment): self + { + $alignmentTypes = [ + self::ALIGNMENT_CENTER, + self::ALIGNMENT_DISTRIBUTE_LETTER, + self::ALIGNMENT_DISTRIBUTE_SPACE, + self::ALIGNMENT_LEFT, + self::ALIGNMENT_RIGHT, + self::ALIGNMENT_RIGHT_VERTICAL, + ]; + + if (in_array($alignment, $alignmentTypes)) { + $this->alignment = $alignment; + } else { + throw new InvalidArgumentException('Invalid value, alignments of ' . implode(', ', $alignmentTypes) . ' possible'); + } + + return $this; + } + + /** + * Get the ruby font face size. + */ + public function getFontFaceSize(): float + { + return $this->fontFaceSize; + } + + /** + * Set the ruby font face size. + */ + public function setFontFaceSize(float $size): self + { + $this->fontFaceSize = $size; + + return $this; + } + + /** + * Get the ruby font points above base text. + */ + public function getFontPointsAboveBaseText(): float + { + return $this->fontPointsAboveText; + } + + /** + * Set the ruby font points above base text. + */ + public function setFontPointsAboveBaseText(float $size): self + { + $this->fontPointsAboveText = $size; + + return $this; + } + + /** + * Get the ruby font size for base text. + */ + public function getFontSizeForBaseText(): float + { + return $this->baseTextFontSize; + } + + /** + * Set the ruby font size for base text. + */ + public function setFontSizeForBaseText(float $size): self + { + $this->baseTextFontSize = $size; + + return $this; + } + + /** + * Get the ruby language id. + */ + public function getLanguageId(): string + { + return $this->languageId; + } + + /** + * Set the ruby language id. + */ + public function setLanguageId(string $langId): self + { + $this->languageId = $langId; + + return $this; + } +} diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index f311d68206..37140b4582 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -50,6 +50,7 @@ * @method FormField addFormField(string $type, mixed $fStyle = null, mixed $pStyle = null) * @method SDT addSDT(string $type) * @method Formula addFormula(Math $math) + * @method Ruby addRuby(TextRun $baseText, TextRun $rubyText, \PhpOffice\PhpWord\ComplexType\RubyProperties $properties) * @method \PhpOffice\PhpWord\Element\OLEObject addObject(string $source, mixed $style = null) deprecated, use addOLEObject instead * * @since 0.10.0 @@ -91,7 +92,7 @@ public function __call($function, $args) 'Footnote', 'Endnote', 'CheckBox', 'TextBox', 'Field', 'Line', 'Shape', 'Title', 'TOC', 'PageBreak', 'Chart', 'FormField', 'SDT', 'Comment', - 'Formula', + 'Formula', 'Ruby', ]; $functions = []; foreach ($elements as $element) { diff --git a/src/PhpWord/Element/Ruby.php b/src/PhpWord/Element/Ruby.php new file mode 100644 index 0000000000..4b032a220d --- /dev/null +++ b/src/PhpWord/Element/Ruby.php @@ -0,0 +1,114 @@ +baseTextRun = $baseTextRun; + $this->rubyTextRun = $rubyTextRun; + $this->properties = $properties; + } + + /** + * Get base text run. + */ + public function getBaseTextRun(): TextRun + { + return $this->baseTextRun; + } + + /** + * Set the base text run. + */ + public function setBaseTextRun(TextRun $textRun): self + { + $this->baseTextRun = $textRun; + + return $this; + } + + /** + * Get ruby text run. + */ + public function getRubyTextRun(): TextRun + { + return $this->rubyTextRun; + } + + /** + * Set the ruby text run. + */ + public function setRubyTextRun(TextRun $textRun): self + { + $this->rubyTextRun = $textRun; + + return $this; + } + + /** + * Get ruby properties. + */ + public function getProperties(): RubyProperties + { + return $this->properties; + } + + /** + * Set the ruby properties. + */ + public function setProperties(RubyProperties $properties): self + { + $this->properties = $properties; + + return $this; + } +} diff --git a/src/PhpWord/Element/TextRun.php b/src/PhpWord/Element/TextRun.php index 5dc8ef53c7..0c9a2322a7 100644 --- a/src/PhpWord/Element/TextRun.php +++ b/src/PhpWord/Element/TextRun.php @@ -86,6 +86,9 @@ public function getText(): string foreach ($this->getElements() as $element) { if ($element instanceof Text) { $outstr .= $element->getText(); + } elseif ($element instanceof Ruby) { + $outstr .= $element->getBaseTextRun()->getText() . + ' (' . $element->getRubyTextRun()->getText() . ')'; } } diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index d14ca603e7..96d4a46f8a 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -22,10 +22,13 @@ use DOMElement; use InvalidArgumentException; use PhpOffice\Math\Reader\OfficeMathML; +use PhpOffice\PhpWord\ComplexType\RubyProperties; use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType; use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\AbstractElement; use PhpOffice\PhpWord\Element\FormField; +use PhpOffice\PhpWord\Element\Ruby; +use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\PhpWord; @@ -297,7 +300,8 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par if ($headingDepth !== null) { $textContent = null; $nodes = $xmlReader->getElements('w:r|w:hyperlink', $domNode); - if ($nodes->length === 1) { + $hasRubyElement = $xmlReader->elementExists('w:r/w:ruby', $domNode); + if ($nodes->length === 1 && !$hasRubyElement) { $textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8'); } else { $textContent = new TextRun($paragraphStyle); @@ -585,9 +589,47 @@ protected function readRunChild(XMLReader $xmlReader, DOMElement $node, Abstract } } elseif ($node->nodeName == 'w:softHyphen') { $element = $parent->addText("\u{200c}", $fontStyle, $paragraphStyle); + } elseif ($node->nodeName == 'w:ruby') { + $rubyPropertiesNode = $xmlReader->getElement('w:rubyPr', $node); + $properties = $this->readRubyProperties($xmlReader, $rubyPropertiesNode); + // read base text node + $baseText = new TextRun($paragraphStyle); + $baseTextNode = $xmlReader->getElement('w:rubyBase/w:r', $node); + $this->readRun($xmlReader, $baseTextNode, $baseText, $docPart, $paragraphStyle); + // read the actual ruby text (e.g. furigana in Japanese) + $rubyText = new TextRun($paragraphStyle); + $rubyTextNode = $xmlReader->getElement('w:rt/w:r', $node); + $this->readRun($xmlReader, $rubyTextNode, $rubyText, $docPart, $paragraphStyle); + // add element to parent + $parent->addRuby($baseText, $rubyText, $properties); } } + /** + * Read w:rubyPr element. + * + * @param XMLReader $xmlReader reader for XML + * @param DOMElement $domNode w:RubyPr element + * + * @return RubyProperties ruby properties from element + */ + protected function readRubyProperties(XMLReader $xmlReader, DOMElement $domNode): RubyProperties + { + $rubyAlignment = $xmlReader->getElement('w:rubyAlign', $domNode)->getAttribute('w:val'); + $rubyHps = $xmlReader->getElement('w:hps', $domNode)->getAttribute('w:val'); // font face + $rubyHpsRaise = $xmlReader->getElement('w:hpsRaise', $domNode)->getAttribute('w:val'); // pts above base text + $rubyHpsBaseText = $xmlReader->getElement('w:hpsBaseText', $domNode)->getAttribute('w:val'); // base text size + $rubyLid = $xmlReader->getElement('w:lid', $domNode)->getAttribute('w:val'); // type of ruby + $properties = new RubyProperties(); + $properties->setAlignment($rubyAlignment); + $properties->setFontFaceSize((float) $rubyHps); + $properties->setFontPointsAboveBaseText((float) $rubyHpsRaise); + $properties->setFontSizeForBaseText((float) $rubyHpsBaseText); + $properties->setLanguageId($rubyLid); + + return $properties; + } + /** * Read w:tbl. * diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index d94c13fdf7..4573daf2a9 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -23,9 +23,11 @@ use DOMNode; use DOMXPath; use Exception; +use PhpOffice\PhpWord\ComplexType\RubyProperties; use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\Row; use PhpOffice\PhpWord\Element\Table; +use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\NumberFormat; @@ -239,6 +241,7 @@ protected static function parseNode($node, $element, $styles = [], $data = []): 'a' => ['Link', $node, $element, $styles, null, null, null], 'input' => ['Input', $node, $element, $styles, null, null, null], 'hr' => ['HorizRule', $node, $element, $styles, null, null, null], + 'ruby' => ['Ruby', $node, $element, $styles, null, null, null], ]; $newElement = null; @@ -302,7 +305,7 @@ protected static function parseChildNodes($node, $element, $styles, $data): void * @param AbstractContainer $element * @param array &$styles * - * @return \PhpOffice\PhpWord\Element\PageBreak|\PhpOffice\PhpWord\Element\TextRun + * @return \PhpOffice\PhpWord\Element\PageBreak|TextRun */ protected static function parseParagraph($node, $element, &$styles) { @@ -346,7 +349,7 @@ protected static function parseInput($node, $element, &$styles): void * @param array &$styles * @param string $argument1 Name of heading style * - * @return \PhpOffice\PhpWord\Element\TextRun + * @return TextRun * * @todo Think of a clever way of defining header styles, now it is only based on the assumption, that * Heading1 - Heading6 are already defined somewhere @@ -464,7 +467,7 @@ protected static function parseRow($node, $element, &$styles) * @param Table $element * @param array &$styles * - * @return \PhpOffice\PhpWord\Element\Cell|\PhpOffice\PhpWord\Element\TextRun $element + * @return \PhpOffice\PhpWord\Element\Cell|TextRun $element */ protected static function parseCell($node, $element, &$styles) { @@ -705,6 +708,10 @@ protected static function parseStyleDeclarations(array $selectors, array $styles case 'text-align': $styles['alignment'] = self::mapAlign($value, $bidi); + break; + case 'ruby-align': + $styles['rubyAlignment'] = self::mapRubyAlign($value); + break; case 'display': $styles['hidden'] = $value === 'none' || $value === 'hidden'; @@ -1103,6 +1110,23 @@ protected static function mapAlign($cssAlignment, $bidi) } } + /** + * Transforms a HTML/CSS ruby alignment into a \PhpOffice\PhpWord\SimpleType\Jc. + */ + protected static function mapRubyAlign(string $cssRubyAlignment): string + { + switch ($cssRubyAlignment) { + case 'center': + return RubyProperties::ALIGNMENT_CENTER; + case 'start': + return RubyProperties::ALIGNMENT_LEFT; + case 'space-between': + return RubyProperties::ALIGNMENT_DISTRIBUTE_SPACE; + default: + return ''; + } + } + /** * Transforms a HTML/CSS vertical alignment. * @@ -1231,6 +1255,59 @@ protected static function parseHorizRule($node, $element): void // - repeated text, e.g. underline "_", because of unpredictable line wrapping } + /** + * Parse ruby node. + * + * @param DOMNode $node + * @param AbstractContainer $element + * @param array $styles + */ + protected static function parseRuby($node, $element, &$styles) + { + $rubyProperties = new RubyProperties(); + $baseTextRun = new TextRun($styles['paragraph']); + $rubyTextRun = new TextRun(null); + if ($node->hasAttributes()) { + $langAttr = $node->attributes->getNamedItem('lang'); + if ($langAttr !== null) { + $rubyProperties->setLanguageId($langAttr->textContent); + } + $styleAttr = $node->attributes->getNamedItem('style'); + if ($styleAttr !== null) { + $styles = self::parseStyle($styleAttr, $styles['paragraph']); + if (isset($styles['rubyAlignment']) && $styles['rubyAlignment'] !== '') { + $rubyProperties->setAlignment($styles['rubyAlignment']); + } + if (isset($styles['size']) && $styles['size'] !== '') { + $rubyProperties->setFontSizeForBaseText($styles['size']); + } + $baseTextRun->setParagraphStyle($styles); + } + } + foreach ($node->childNodes as $child) { + if ($child->nodeName === '#text') { + $content = trim($child->textContent); + if ($content !== '') { + $baseTextRun->addText($content); + } + } elseif ($child->nodeName === 'rt') { + $rubyTextRun->addText(trim($child->textContent)); + if ($child->hasAttributes()) { + $styleAttr = $child->attributes->getNamedItem('style'); + if ($styleAttr !== null) { + $styles = self::parseStyle($styleAttr, []); + if (isset($styles['size']) && $styles['size'] !== '') { + $rubyProperties->setFontFaceSize($styles['size']); + } + $rubyTextRun->setParagraphStyle($styles); + } + } + } + } + + return $element->addRuby($baseTextRun, $rubyTextRun, $rubyProperties); + } + private static function convertRgb(string $rgb): string { if (preg_match(self::RGB_REGEXP, $rgb, $matches) === 1) { diff --git a/src/PhpWord/Writer/HTML/Element/Ruby.php b/src/PhpWord/Writer/HTML/Element/Ruby.php new file mode 100644 index 0000000000..b3ab13c35e --- /dev/null +++ b/src/PhpWord/Writer/HTML/Element/Ruby.php @@ -0,0 +1,130 @@ +processFontStyle(); + + /** @var \PhpOffice\PhpWord\Element\Ruby $element Type hint */ + $element = $this->element; + + $baseText = $this->parentWriter->escapeHTML($element->getBaseTextRun()->getText()); + $rubyText = $this->parentWriter->escapeHTML($element->getRubyTextRun()->getText()); + + $rubyTagPropertyCSS = $this->getPropertyCssForRubyTag($element->getProperties()); + $lang = $element->getProperties()->getLanguageId(); + $content = "getParagraphStyleForTextRun($element->getBaseTextRun(), $rubyTagPropertyCSS)} lang=\"{$lang}\">"; + $content .= $baseText; + $content .= ' ('; + $rtTagPropertyCSS = $this->getPropertyCssForRtTag($element->getProperties()); + $content .= "getParagraphStyleForTextRun($element->getRubyTextRun(), $rtTagPropertyCSS)}>"; + $content .= $rubyText; + $content .= ''; + $content .= ')'; + $content .= ''; + + return $content; + } + + /** + * Get property CSS for the tag. + */ + private function getPropertyCssForRubyTag(RubyProperties $properties): string + { + // alignment CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-align + $alignment = 'space-between'; + switch ($properties->getAlignment()) { + case RubyProperties::ALIGNMENT_CENTER: + $alignment = 'center'; + + break; + case RubyProperties::ALIGNMENT_LEFT: + $alignment = 'start'; + + break; + default: + $alignment = 'space-between'; + + break; + } + + return + 'font-size:' . $properties->getFontSizeForBaseText() . 'pt' . ';' . + 'ruby-align:' . $alignment . ';'; + } + + /** + * Get property CSS for the tag. + */ + private function getPropertyCssForRtTag(RubyProperties $properties): string + { + // alignment CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-align + return 'font-size:' . $properties->getFontFaceSize() . 'pt' . ';'; + } + + /** + * Write paragraph style for a given TextRun. + */ + private function getParagraphStyleForTextRun(TextRun $textRun, string $extraCSS): string + { + $style = ''; + if (!method_exists($textRun, 'getParagraphStyle')) { + return $style; + } + + $paragraphStyle = $textRun->getParagraphStyle(); + $pStyleIsObject = ($paragraphStyle instanceof Paragraph); + if ($pStyleIsObject) { + $styleWriter = new ParagraphStyleWriter($paragraphStyle); + $styleWriter->setParentWriter($this->parentWriter); + $style = $styleWriter->write(); + } elseif (is_string($paragraphStyle)) { + $style = $paragraphStyle; + } + if ($style !== null && $style !== '') { + if ($pStyleIsObject) { + // CSS pairs (style="...") + $style = " style=\"{$style}{$extraCSS}\""; + } else { + // class name; need to append extra styles manually + $style = " class=\"{$style}\" style=\"{$extraCSS}\""; + } + } elseif ($extraCSS !== '') { + $style = " style=\"{$extraCSS}\""; + } + + return $style; + } +} diff --git a/src/PhpWord/Writer/ODText/Element/AbstractElement.php b/src/PhpWord/Writer/ODText/Element/AbstractElement.php index ff3a6569de..97d1875cd1 100644 --- a/src/PhpWord/Writer/ODText/Element/AbstractElement.php +++ b/src/PhpWord/Writer/ODText/Element/AbstractElement.php @@ -27,4 +27,32 @@ */ abstract class AbstractElement extends Word2007AbstractElement { + protected function replaceTabs($text, $xmlWriter): void + { + if (preg_match('/^ +/', $text, $matches)) { + $num = strlen($matches[0]); + $xmlWriter->startElement('text:s'); + $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); + $xmlWriter->endElement(); + $text = preg_replace('/^ +/', '', $text); + } + preg_match_all('/([\\s\\S]*?)(\\t| +| ?$)/', $text, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $this->writeText($match[1]); + if ($match[2] === '') { + break; + } elseif ($match[2] === "\t") { + $xmlWriter->writeElement('text:tab'); + } elseif ($match[2] === ' ') { + $xmlWriter->writeElement('text:s'); + + break; + } else { + $num = strlen($match[2]); + $xmlWriter->startElement('text:s'); + $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); + $xmlWriter->endElement(); + } + } + } } diff --git a/src/PhpWord/Writer/ODText/Element/Ruby.php b/src/PhpWord/Writer/ODText/Element/Ruby.php new file mode 100644 index 0000000000..41a86776d4 --- /dev/null +++ b/src/PhpWord/Writer/ODText/Element/Ruby.php @@ -0,0 +1,64 @@ +getXmlWriter(); + $element = $this->getElement(); + if (!$element instanceof \PhpOffice\PhpWord\Element\Ruby) { + return; + } + $paragraphStyle = $element->getBaseTextRun()->getParagraphStyle(); + + if (!$this->withoutP) { + $xmlWriter->startElement('text:p'); // text:p + } + if (empty($paragraphStyle)) { + if (!$this->withoutP) { + $xmlWriter->writeAttribute('text:style-name', 'Normal'); + } + } elseif (is_string($paragraphStyle)) { + if (!$this->withoutP) { + $xmlWriter->writeAttribute('text:style-name', $paragraphStyle); + } + } + + $this->replaceTabs($element->getBaseTextRun()->getText(), $xmlWriter); + $this->writeText(' ('); + $this->replaceTabs($element->getRubyTextRun()->getText(), $xmlWriter); + $this->writeText(')'); + + if (!$this->withoutP) { + $xmlWriter->endElement(); // text:p + } + } +} diff --git a/src/PhpWord/Writer/ODText/Element/Text.php b/src/PhpWord/Writer/ODText/Element/Text.php index 65909fce0c..3996972387 100644 --- a/src/PhpWord/Writer/ODText/Element/Text.php +++ b/src/PhpWord/Writer/ODText/Element/Text.php @@ -89,35 +89,6 @@ public function write(): void } } - private function replacetabs($text, $xmlWriter): void - { - if (preg_match('/^ +/', $text, $matches)) { - $num = strlen($matches[0]); - $xmlWriter->startElement('text:s'); - $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); - $xmlWriter->endElement(); - $text = preg_replace('/^ +/', '', $text); - } - preg_match_all('/([\\s\\S]*?)(\\t| +| ?$)/', $text, $matches, PREG_SET_ORDER); - foreach ($matches as $match) { - $this->writeText($match[1]); - if ($match[2] === '') { - break; - } elseif ($match[2] === "\t") { - $xmlWriter->writeElement('text:tab'); - } elseif ($match[2] === ' ') { - $xmlWriter->writeElement('text:s'); - - break; - } else { - $num = strlen($match[2]); - $xmlWriter->startElement('text:s'); - $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); - $xmlWriter->endElement(); - } - } - } - private function writeChangeInsertion($start = true, ?TrackChange $trackChange = null): void { if ($trackChange == null || $trackChange->getChangeType() != TrackChange::INSERTED) { diff --git a/src/PhpWord/Writer/RTF/Element/Ruby.php b/src/PhpWord/Writer/RTF/Element/Ruby.php new file mode 100644 index 0000000000..e2b617c556 --- /dev/null +++ b/src/PhpWord/Writer/RTF/Element/Ruby.php @@ -0,0 +1,59 @@ +element; + $elementClass = str_replace('\\Writer\\RTF', '', static::class); + if (!$element instanceof $elementClass || !is_string($element->getBaseTextRun()->getText())) { + return ''; + } + + $this->getStyles(); + + $content = ''; + $content .= $this->writeOpening(); + $content .= '{'; + $content .= $this->writeFontStyle(); + $content .= $this->writeText($element->getBaseTextRun()->getText()); + $rubyText = $element->getRubyTextRun()->getText(); + if ($rubyText !== '') { + $content .= ' ('; + $content .= $this->writeText($rubyText); + $content .= ')'; + } + $content .= '}'; + $content .= $this->writeClosing(); + + return $content; + } +} diff --git a/src/PhpWord/Writer/RTF/Element/Title.php b/src/PhpWord/Writer/RTF/Element/Title.php index 1022e59f8f..06266897b3 100644 --- a/src/PhpWord/Writer/RTF/Element/Title.php +++ b/src/PhpWord/Writer/RTF/Element/Title.php @@ -59,10 +59,15 @@ public function write() /** @var \PhpOffice\PhpWord\Element\Title $element Type hint */ $element = $this->element; $elementClass = str_replace('\\Writer\\RTF', '', static::class); - if (!$element instanceof $elementClass || !is_string($element->getText())) { + if (!$element instanceof $elementClass) { return ''; } + $textToWrite = $element->getText(); + if ($textToWrite instanceof \PhpOffice\PhpWord\Element\TextRun) { + $textToWrite = $textToWrite->getText(); // gets text from TextRun + } + $this->getStyles(); $content = ''; @@ -83,7 +88,7 @@ public function write() $content .= '{'; $content .= $this->writeFontStyle(); - $content .= $this->writeText($element->getText()); + $content .= $this->writeText($textToWrite); $content .= '}'; $content .= $this->writeClosing(); $content .= $endout; diff --git a/src/PhpWord/Writer/Word2007/Element/Ruby.php b/src/PhpWord/Writer/Word2007/Element/Ruby.php new file mode 100644 index 0000000000..f30a5f7e84 --- /dev/null +++ b/src/PhpWord/Writer/Word2007/Element/Ruby.php @@ -0,0 +1,81 @@ +getXmlWriter(); + $element = $this->getElement(); + if (!$element instanceof \PhpOffice\PhpWord\Element\Ruby) { + return; + } + /** @var \PhpOffice\PhpWord\Element\Ruby $element */ + $this->startElementP(); + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:ruby'); + + // write properties + $xmlWriter->startElement('w:rubyPr'); + $properties = $element->getProperties(); + $xmlWriter->startElement('w:rubyAlign'); + $xmlWriter->writeAttribute('w:val', $properties->getAlignment()); + $xmlWriter->endElement(); // w:rubyAlign + $xmlWriter->startElement('w:hps'); + $xmlWriter->writeAttribute('w:val', $properties->getFontFaceSize()); + $xmlWriter->endElement(); // w:hps + $xmlWriter->startElement('w:hpsRaise'); + $xmlWriter->writeAttribute('w:val', $properties->getFontPointsAboveBaseText()); + $xmlWriter->endElement(); // w:hpsRaise + $xmlWriter->startElement('w:hpsBaseText'); + $xmlWriter->writeAttribute('w:val', $properties->getFontSizeForBaseText()); + $xmlWriter->endElement(); // w:hpsBaseText + $xmlWriter->startElement('w:lid'); + $xmlWriter->writeAttribute('w:val', $properties->getLanguageId()); + $xmlWriter->endElement(); // w:lid + + $xmlWriter->endElement(); // w:rubyPr + + // write ruby text + $xmlWriter->startElement('w:rt'); + $rubyTextRun = $element->getRubyTextRun(); + $textRunWriter = new TextRun($xmlWriter, $rubyTextRun, true); + $textRunWriter->write(); + $xmlWriter->endElement(); // w:rt + // write base text + $xmlWriter->startElement('w:rubyBase'); + $baseTextRun = $element->getBaseTextRun(); + $textRunWriter = new TextRun($xmlWriter, $baseTextRun, true); + $textRunWriter->write(); + $xmlWriter->endElement(); // w:rubyBase + + $xmlWriter->endElement(); // w:ruby + $xmlWriter->endElement(); // w:r + + $this->endElementP(); + } +} diff --git a/tests/PhpWordTests/ComplexType/RubyPropertiesTest.php b/tests/PhpWordTests/ComplexType/RubyPropertiesTest.php new file mode 100644 index 0000000000..6d16ebb21d --- /dev/null +++ b/tests/PhpWordTests/ComplexType/RubyPropertiesTest.php @@ -0,0 +1,141 @@ +getAlignment()); + self::assertTrue($properties->getAlignment() !== '' && $properties->getAlignment() !== null); + self::assertIsFloat($properties->getFontFaceSize()); + self::assertIsFloat($properties->getFontPointsAboveBaseText()); + self::assertIsFloat($properties->getFontSizeForBaseText()); + self::assertIsString($properties->getLanguageId()); + self::assertTrue($properties->getLanguageId() !== '' && $properties->getLanguageId() !== null); + } + + /** + * Get/set alignment. + */ + public function testAlignment(): void + { + $properties = new RubyProperties(); + self::assertIsString($properties->getAlignment()); + self::assertTrue($properties->getAlignment() !== '' && $properties->getAlignment() !== null); + $properties->setAlignment(RubyProperties::ALIGNMENT_RIGHT_VERTICAL); + self::assertEquals(RubyProperties::ALIGNMENT_RIGHT_VERTICAL, $properties->getAlignment()); + } + + /** + * Set valid alignments. Make sure we can set all valid types - should not throw exception. + */ + public function testValidAlignments(): void + { + $properties = new RubyProperties(); + $types = [ + RubyProperties::ALIGNMENT_CENTER, + RubyProperties::ALIGNMENT_DISTRIBUTE_LETTER, + RubyProperties::ALIGNMENT_DISTRIBUTE_SPACE, + RubyProperties::ALIGNMENT_LEFT, + RubyProperties::ALIGNMENT_RIGHT, + RubyProperties::ALIGNMENT_RIGHT_VERTICAL, + ]; + foreach ($types as $type) { + $properties->setAlignment($type); + self::assertEquals($type, $properties->getAlignment()); + } + } + + /** + * Test throws exception on invalid alignment type. + */ + public function testInvalidAlignment(): void + { + $this->expectException(InvalidArgumentException::class); + $properties = new RubyProperties(); + $properties->setAlignment('invalid alignment type'); + } + + /** + * Get/set font face size. + */ + public function testFontFaceSize(): void + { + $properties = new RubyProperties(); + + self::assertTrue($properties->getFontFaceSize() > 0); + $properties->setFontFaceSize(42.42); + self::assertEqualsWithDelta(42.42, $properties->getFontFaceSize(), 0.00001); // use delta as it is a float compare + self::assertIsFloat($properties->getFontFaceSize()); + } + + /** + * Get/set font points above base text. + */ + public function testFontPointsAboveBaseText(): void + { + $properties = new RubyProperties(); + + self::assertTrue($properties->getFontPointsAboveBaseText() > 0); + $properties->setFontPointsAboveBaseText(43.42); + self::assertEqualsWithDelta(43.42, $properties->getFontPointsAboveBaseText(), 0.00001); // use delta as it is a float compare + self::assertIsFloat($properties->getFontPointsAboveBaseText()); + } + + /** + * Get/set font size for base text. + */ + public function testFontSizeForBaseText(): void + { + $properties = new RubyProperties(); + + self::assertTrue($properties->getFontSizeForBaseText() > 0); + $properties->setFontSizeForBaseText(45.42); + self::assertEqualsWithDelta(45.42, $properties->getFontSizeForBaseText(), 0.00001); // use delta as it is a float compare + self::assertIsFloat($properties->getFontSizeForBaseText()); + } + + /** + * Get/set language id. + */ + public function testLanguageId(): void + { + $properties = new RubyProperties(); + + self::assertTrue($properties->getLanguageId() !== '' && $properties->getLanguageId() !== null); + $properties->setLanguageId('en-US'); + self::assertIsString($properties->getLanguageId()); + self::assertEquals('en-US', $properties->getLanguageId()); + } +} diff --git a/tests/PhpWordTests/Element/RubyTest.php b/tests/PhpWordTests/Element/RubyTest.php new file mode 100644 index 0000000000..0e14994fd2 --- /dev/null +++ b/tests/PhpWordTests/Element/RubyTest.php @@ -0,0 +1,104 @@ +getBaseTextRun()->getText()); + self::assertInstanceOf('PhpOffice\\PhpWord\\Element\\TextRun', $ruby->getBaseTextRun()); + self::assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $ruby->getBaseTextRun()->getParagraphStyle()); + self::assertEquals('', $ruby->getRubyTextRun()->getText()); + self::assertInstanceOf('PhpOffice\\PhpWord\\Element\\TextRun', $ruby->getRubyTextRun()); + self::assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $ruby->getRubyTextRun()->getParagraphStyle()); + self::assertInstanceOf('PhpOffice\\PhpWord\\ComplexType\\RubyProperties', $ruby->getProperties()); + self::assertEquals(RubyProperties::ALIGNMENT_DISTRIBUTE_SPACE, $ruby->getProperties()->getAlignment()); + } + + /** + * Get/set base text. + */ + public function testBaseText(): void + { + $ruby = new Ruby(new TextRun(), new TextRun(), new RubyProperties()); + + self::assertEquals('', $ruby->getBaseTextRun()->getText()); + $tr = new TextRun(); + $tr->addText('Hello, world'); + $ruby->setBaseTextRun($tr); + self::assertInstanceOf('PhpOffice\\PhpWord\\Element\\TextRun', $ruby->getBaseTextRun()); + self::assertEquals('Hello, world', $ruby->getBaseTextRun()->getText()); + } + + /** + * Get/set ruby text. + */ + public function testRubyText(): void + { + $ruby = new Ruby(new TextRun(), new TextRun(), new RubyProperties()); + + self::assertEquals('', $ruby->getRubyTextRun()->getText()); + $tr = new TextRun(); + $tr->addText('Hello, ruby'); + $ruby->setRubyTextRun($tr); + self::assertInstanceOf('PhpOffice\\PhpWord\\Element\\TextRun', $ruby->getRubyTextRun()); + self::assertEquals('Hello, ruby', $ruby->getRubyTextRun()->getText()); + } + + /** + * Get/set ruby properties. + */ + public function testRubyProperties(): void + { + $ruby = new Ruby(new TextRun(), new TextRun(), new RubyProperties()); + + self::assertEquals(RubyProperties::ALIGNMENT_DISTRIBUTE_SPACE, $ruby->getProperties()->getAlignment()); + + $properties = new RubyProperties(); + $properties->setAlignment(RubyProperties::ALIGNMENT_RIGHT_VERTICAL); + $properties->setFontFaceSize(1); + $properties->setFontPointsAboveBaseText(2); + $properties->setFontSizeForBaseText(3); + $properties->setLanguageId('en-US'); + $ruby->setProperties($properties); + + self::assertInstanceOf('PhpOffice\\PhpWord\\ComplexType\\RubyProperties', $ruby->getProperties()); + self::assertEquals(RubyProperties::ALIGNMENT_RIGHT_VERTICAL, $ruby->getProperties()->getAlignment()); + self::assertEquals(1, $ruby->getProperties()->getFontFaceSize()); + self::assertEquals(2, $ruby->getProperties()->getFontPointsAboveBaseText()); + self::assertEquals(3, $ruby->getProperties()->getFontSizeForBaseText()); + self::assertEquals('en-US', $ruby->getProperties()->getLanguageId()); + } +} diff --git a/tests/PhpWordTests/Element/TextRunTest.php b/tests/PhpWordTests/Element/TextRunTest.php index 8d250676f4..e3a2826f0c 100644 --- a/tests/PhpWordTests/Element/TextRunTest.php +++ b/tests/PhpWordTests/Element/TextRunTest.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWordTests\Element; +use PhpOffice\PhpWord\ComplexType\RubyProperties; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\SimpleType\Jc; @@ -183,4 +184,24 @@ public function testParagraph(): void $oText->setParagraphStyle(['alignment' => Jc::CENTER, 'spaceAfter' => 100]); self::assertInstanceOf('PhpOffice\\PhpWord\\Style\\Paragraph', $oText->getParagraphStyle()); } + + /** + * Add ruby element and get raw text. + */ + public function testRubyElementGetText(): void + { + $oTextRun = new TextRun(); + $oTextRun->setPhpWord(new PhpWord()); + + $properties = new RubyProperties(); + $baseTextRun = new TextRun(null); + $baseTextRun->addText('私'); + $rubyTextRun = new TextRun(null); + $rubyTextRun->addText('わたし'); + $element = $oTextRun->addRuby($baseTextRun, $rubyTextRun, $properties); + + self::assertInstanceOf('PhpOffice\\PhpWord\\Element\\Ruby', $element); + self::assertCount(1, $oTextRun->getElements()); + self::assertEquals('私 (わたし)', $oTextRun->getText()); + } } diff --git a/tests/PhpWordTests/Reader/Word2007/ElementTest.php b/tests/PhpWordTests/Reader/Word2007/ElementTest.php index b27fc9df1d..892e59adf7 100644 --- a/tests/PhpWordTests/Reader/Word2007/ElementTest.php +++ b/tests/PhpWordTests/Reader/Word2007/ElementTest.php @@ -18,6 +18,8 @@ namespace PhpOffice\PhpWordTests\Reader\Word2007; +use PhpOffice\PhpWord\ComplexType\RubyProperties; +use PhpOffice\PhpWord\Element\Ruby; use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\Style\Font; @@ -544,4 +546,134 @@ public function testReadFormFieldCheckbox(): void self::assertEquals('checkbox', $subElements[0]->getType()); self::assertEquals('SomeCheckbox', $subElements[0]->getName()); } + + /** + * Test reading of ruby. + */ + public function testReadRuby(): void + { + $documentXml = ' + + + + + + + + + + + + + + + + わたし + + + + + + + + + + + + + '; + + $phpWord = $this->getDocumentFromString(['document' => $documentXml]); + $elements = $phpWord->getSection(0)->getElements(); + self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]); + $subElements = $elements[0]->getElements(); // + self::assertInstanceOf('PhpOffice\PhpWord\Element\Ruby', $subElements[0]); + /** @var RubyProperties $rubyProperties */ + $rubyProperties = $subElements[0]->getProperties(); + self::assertEquals(RubyProperties::ALIGNMENT_DISTRIBUTE_SPACE, $rubyProperties->getAlignment()); + self::assertEquals(12, $rubyProperties->getFontFaceSize()); + self::assertEquals(22, $rubyProperties->getFontPointsAboveBaseText()); + self::assertEquals(24, $rubyProperties->getFontSizeForBaseText()); + self::assertEquals('ja-JP', $rubyProperties->getLanguageId()); + /** @var \PhpOffice\PhpWord\Element\TextRun $textRun */ + $textRun = $subElements[0]->getBaseTextRun(); + self::assertEquals('私', $textRun->getText()); + $textRun = $subElements[0]->getRubyTextRun(); + self::assertEquals('わたし', $textRun->getText()); + } + + /** + * Test reading of ruby title. + */ + public function testReadRubyTitle(): void + { + $documentXml = ' + + + + + + + + + + + + + + + + + + + かみ + + + + + + + + + + + + + '; + + $phpWord = $this->getDocumentFromString(['document' => $documentXml]); + $elements = $phpWord->getSection(0)->getElements(); + self::assertInstanceOf('PhpOffice\PhpWord\Element\Title', $elements[0]); + /** @var \PhpOffice\PhpWord\Element\Title $title */ + $title = $elements[0]; + /** @var \PhpOffice\PhpWord\Element\TextRun $textRun */ + $textRun = $title->getText(); + $subElements = $textRun->getElements(); // + self::assertInstanceOf('PhpOffice\PhpWord\Element\Ruby', $subElements[0]); + /** @var Ruby $ruby */ + $ruby = $subElements[0]; + /** @var RubyProperties $rubyProperties */ + $rubyProperties = $ruby->getProperties(); + self::assertEquals(RubyProperties::ALIGNMENT_DISTRIBUTE_SPACE, $rubyProperties->getAlignment()); + self::assertEquals(20, $rubyProperties->getFontFaceSize()); + self::assertEquals(38, $rubyProperties->getFontPointsAboveBaseText()); + self::assertEquals(40, $rubyProperties->getFontSizeForBaseText()); + self::assertEquals('ja-JP', $rubyProperties->getLanguageId()); + /** @var \PhpOffice\PhpWord\Element\TextRun $textRun */ + $textRun = $ruby->getBaseTextRun(); + self::assertEquals('神', $textRun->getText()); + $textRun = $ruby->getRubyTextRun(); + self::assertEquals('かみ', $textRun->getText()); + } } diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php index 4edc5432b1..7551f51cb8 100644 --- a/tests/PhpWordTests/Shared/HtmlTest.php +++ b/tests/PhpWordTests/Shared/HtmlTest.php @@ -19,6 +19,7 @@ namespace PhpOffice\PhpWordTests\Shared; use Exception; +use PhpOffice\PhpWord\ComplexType\RubyProperties; use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\Element\Table; use PhpOffice\PhpWord\PhpWord; @@ -1317,4 +1318,51 @@ public static function providerParseWidth(): array ['400', 6000, TblWidth::TWIP], ]; } + + /** + * Test ruby. + */ + public function testParseRubyHtml(): void + { + $html = << + base text + ( + ruby text + ) + +HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby')); + self::assertEquals('ruby text', $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rt/w:r/w:t')->textContent); + self::assertEquals( + 'base text', + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyBase/w:r/w:t')->textContent + ); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:rubyAlign')); + self::assertEquals( + RubyProperties::ALIGNMENT_CENTER, + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:rubyAlign', 'w:val') + ); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hps')); + self::assertEquals( + 10, + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hps', 'w:val') + ); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hpsBaseText')); + self::assertEquals( + 20, + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hpsBaseText', 'w:val') + ); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:lid')); + self::assertEquals( + 'en-US', + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:lid', 'w:val') + ); + } } diff --git a/tests/PhpWordTests/Writer/HTML/Element/RubyTest.php b/tests/PhpWordTests/Writer/HTML/Element/RubyTest.php new file mode 100644 index 0000000000..2ca556bc51 --- /dev/null +++ b/tests/PhpWordTests/Writer/HTML/Element/RubyTest.php @@ -0,0 +1,99 @@ +addSection(); + $properties = new RubyProperties(); + $properties->setAlignment(RubyProperties::ALIGNMENT_CENTER); + $properties->setFontFaceSize(10); + $properties->setFontPointsAboveBaseText(4); + $properties->setFontSizeForBaseText(20); + $properties->setLanguageId('ja-JP'); + + $baseTextRun = new TextRun(null); + $baseTextRun->addText('私'); + $rubyTextRun = new TextRun(null); + $rubyTextRun->addText('わたし'); + $section->addRuby($baseTextRun, $rubyTextRun, $properties); + + $dom = Helper::getAsHTML($phpWord, '', '', ['ruby', 'rt', 'rp']); + $xpath = new DOMXPath($dom); + self::assertEquals(1, $xpath->query('/html/body/div/ruby')->length); + // ensure text is right + $rubyElement = $dom->getElementsByTagName('ruby')->item(0); + $rtElement = $dom->getElementsByTagName('rt')->item(0); + self::assertNotNull($rubyElement); + self::assertNotNull($rtElement); + self::assertEquals($baseTextRun->getText() . ' (' . $rubyTextRun->getText() . ')', $rubyElement->textContent); + self::assertEquals($rubyTextRun->getText(), $rtElement->textContent); + // check style + self::assertEquals('font-size:20pt;ruby-align:center;', $rubyElement->attributes->getNamedItem('style')->textContent); + self::assertEquals('font-size:10pt;', $rtElement->attributes->getNamedItem('style')->textContent); + } + + /** + * Tests writing ruby HTML. + */ + public function testWriteRubyHtmlParagraphStyle(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $properties = new RubyProperties(); + $properties->setAlignment(RubyProperties::ALIGNMENT_CENTER); + $properties->setFontFaceSize(10); + $properties->setFontPointsAboveBaseText(4); + $properties->setFontSizeForBaseText(20); + $properties->setLanguageId('ja-JP'); + + $baseTextRun = new TextRun(['lineHeight' => '8']); + $baseTextRun->addText('私'); + $rubyTextRun = new TextRun(['lineHeight' => '4']); + $rubyTextRun->addText('わたし'); + $section->addRuby($baseTextRun, $rubyTextRun, $properties); + + $dom = Helper::getAsHTML($phpWord, '', '', ['ruby', 'rt', 'rp']); + $xpath = new DOMXPath($dom); + self::assertEquals(1, $xpath->query('/html/body/div/ruby')->length); + // ensure text is right + $rubyElement = $dom->getElementsByTagName('ruby')->item(0); + $rtElement = $dom->getElementsByTagName('rt')->item(0); + self::assertNotNull($rubyElement); + self::assertNotNull($rtElement); + self::assertEquals($baseTextRun->getText() . ' (' . $rubyTextRun->getText() . ')', $rubyElement->textContent); + self::assertEquals($rubyTextRun->getText(), $rtElement->textContent); + // check style + self::assertEquals('line-height: 8;font-size:20pt;ruby-align:center;', $rubyElement->attributes->getNamedItem('style')->textContent); + self::assertEquals('line-height: 4;font-size:10pt;', $rtElement->attributes->getNamedItem('style')->textContent); + } +} diff --git a/tests/PhpWordTests/Writer/HTML/Helper.php b/tests/PhpWordTests/Writer/HTML/Helper.php index 0fe390fd0d..37f640d28a 100644 --- a/tests/PhpWordTests/Writer/HTML/Helper.php +++ b/tests/PhpWordTests/Writer/HTML/Helper.php @@ -20,6 +20,8 @@ use DOMDocument; use DOMXPath; +use Exception; +use LibXMLError; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Writer\HTML; @@ -85,13 +87,41 @@ public static function getLength(DOMXPath $xpath, string $query): int return $returnVal; } - public static function getAsHTML(PhpWord $phpWord, string $defaultWhiteSpace = '', string $defaultGenericFont = ''): DOMDocument + public static function getAsHTML(PhpWord $phpWord, string $defaultWhiteSpace = '', string $defaultGenericFont = '', array $validTags = []): DOMDocument { $htmlWriter = new HTML($phpWord); $htmlWriter->setDefaultWhiteSpace($defaultWhiteSpace); $htmlWriter->setDefaultGenericFont($defaultGenericFont); $dom = new DOMDocument(); + // DOMDocument does not always accept HTML5 tags like + // So, we can manually filter out those errors for testing purposes ONLY. + $original = libxml_use_internal_errors(true); $dom->loadHTML($htmlWriter->getContent()); + $errors = libxml_get_errors(); + $errorsToReport = []; + foreach ($errors as $error) { + /** @var LibXMLError $error */ + if ($error->code === 801) { + $didFindValidTag = false; + foreach ($validTags as $tag) { + if (trim($error->message) === ('Tag ' . $tag . ' invalid')) { + $didFindValidTag = true; + + break; + } + } + if (!$didFindValidTag) { + $errorsToReport[] = $error; + } + } else { + $errorsToReport[] = $error; + } + } + libxml_clear_errors(); + libxml_use_internal_errors($original); + if (count($errorsToReport) > 0) { + throw new Exception('Errors when loading DOMDocument: ' . print_r($errors, true)); + } return $dom; } diff --git a/tests/PhpWordTests/Writer/ODText/ElementTest.php b/tests/PhpWordTests/Writer/ODText/ElementTest.php index 56bbf025ed..4df140aaa4 100644 --- a/tests/PhpWordTests/Writer/ODText/ElementTest.php +++ b/tests/PhpWordTests/Writer/ODText/ElementTest.php @@ -19,6 +19,8 @@ namespace PhpOffice\PhpWordTests\Writer\ODText; use DateTime; +use PhpOffice\PhpWord\ComplexType\RubyProperties; +use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Shared\XMLWriter; @@ -201,7 +203,7 @@ public function testTextRunTitle(): void $section = $phpWord->addSection(); $section->addTitle('Text Title', 1); $section->addText('Text following Text Title'); - $textRun = new \PhpOffice\PhpWord\Element\TextRun(); + $textRun = new TextRun(); $textRun->addText('Text Run'); $textRun->addText(' Title'); $section->addTitle($textRun, 1); @@ -328,4 +330,36 @@ public function testTrackedChanges(): void $element = "$p2t/text:change"; self::AssertEquals($tc3id, $doc->getElementAttribute($element, 'text:change-id')); } + + /** + * Test ruby output. + * Note that this test will need to be updated when ODT Ruby output supports + * ODT's native ruby functionality. + */ + public function testRubyText(): void + { + $esc = \PhpOffice\PhpWord\Settings::isOutputEscapingEnabled(); + \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true); + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $properties = new RubyProperties(); + $properties->setAlignment(RubyProperties::ALIGNMENT_RIGHT_VERTICAL); + $properties->setFontFaceSize(10); + $properties->setFontPointsAboveBaseText(4); + $properties->setFontSizeForBaseText(18); + $properties->setLanguageId('ja-JP'); + + $baseTextRun = new TextRun(null); + $baseTextRun->addText('私'); + $rubyTextRun = new TextRun(null); + $rubyTextRun->addText('わたし'); + $section->addRuby($baseTextRun, $rubyTextRun, $properties); + + $doc = TestHelperDOCX::getDocument($phpWord, 'ODText'); + \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled($esc); + $p2t = '/office:document-content/office:body/office:text/text:section'; + $element = "$p2t/text:p[2]"; + self::assertTrue($doc->elementExists($element)); + self::assertEquals('私 (わたし)', $doc->getElement($element)->nodeValue); + } } diff --git a/tests/PhpWordTests/Writer/RTF/ElementTest.php b/tests/PhpWordTests/Writer/RTF/ElementTest.php index dbd937bc0d..36504a18f8 100644 --- a/tests/PhpWordTests/Writer/RTF/ElementTest.php +++ b/tests/PhpWordTests/Writer/RTF/ElementTest.php @@ -175,4 +175,45 @@ public function testTitle(): void $expect = "\\pard\\nowidctlpar \\sb0\\sa0{\\outlinelevel0{\\cf0\\f0 First Heading}\\par\n}"; self::assertEquals($expect, $this->removeCr($elwrite)); } + + public function testRuby(): void + { + $parentWriter = new RTF(); + $properties = new \PhpOffice\PhpWord\ComplexType\RubyProperties(); + $baseTextRun = new \PhpOffice\PhpWord\Element\TextRun(null); + $baseTextRun->addText('base text'); + $rubyTextRun = new \PhpOffice\PhpWord\Element\TextRun(null); + $rubyTextRun->addText('ruby'); + $element = new \PhpOffice\PhpWord\Element\TextRun(); + $element->addRuby($baseTextRun, $rubyTextRun, $properties); + + $textrun = new RTF\Element\TextRun($parentWriter, $element); + $expect = "\\pard\\nowidctlpar {{base text (ruby)}}\\par\n"; + self::assertEquals($expect, $this->removeCr($textrun)); + } + + public function testRubyTitle(): void + { + $parentWriter = new RTF(); + $properties = new \PhpOffice\PhpWord\ComplexType\RubyProperties(); + $baseTextRun = new \PhpOffice\PhpWord\Element\TextRun(null); + $baseTextRun->addText('base text'); + $rubyTextRun = new \PhpOffice\PhpWord\Element\TextRun(null); + $rubyTextRun->addText('ruby'); + $textRun = new \PhpOffice\PhpWord\Element\TextRun(); + $textRun->addRuby($baseTextRun, $rubyTextRun, $properties); + + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $phpWord->addTitleStyle( + 1, + ['size' => 24, 'bold' => true, 'color' => '000099'], + ['spaceBefore' => 0, 'spaceAfter' => 2] + ); + $section = $phpWord->addSection(); + $element = $section->addTitle($textRun, 1); + $elwrite = new RTF\Element\Title($parentWriter, $element); + + $expect = "\\pard\\nowidctlpar \\sb0\\sa2{\\outlinelevel0{\\cf0\\f0\\fs48\\b base text (ruby)}\\par\n}"; + self::assertEquals($expect, $this->removeCr($elwrite)); + } } diff --git a/tests/PhpWordTests/Writer/Word2007/ElementTest.php b/tests/PhpWordTests/Writer/Word2007/ElementTest.php index 999e188e82..c22994607b 100644 --- a/tests/PhpWordTests/Writer/Word2007/ElementTest.php +++ b/tests/PhpWordTests/Writer/Word2007/ElementTest.php @@ -19,7 +19,9 @@ namespace PhpOffice\PhpWordTests\Writer\Word2007; use DateTime; +use PhpOffice\PhpWord\ComplexType\RubyProperties; use PhpOffice\PhpWord\Element\Comment; +use PhpOffice\PhpWord\Element\Ruby; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\PhpWord; @@ -534,4 +536,132 @@ public function testListItemRunStyleWriting(): void self::assertEquals(' in bold', $doc->getElement('/w:document/w:body/w:p/w:r[2]/w:t')->nodeValue); self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r[2]/w:rPr/w:b')); } + + /** + * Test Ruby writing. + */ + public function testRubyWriting(): void + { + $phpWord = new PhpWord(); + + $section = $phpWord->addSection(); + $properties = new RubyProperties(); + $properties->setAlignment(RubyProperties::ALIGNMENT_RIGHT_VERTICAL); + $properties->setFontFaceSize(10); + $properties->setFontPointsAboveBaseText(4); + $properties->setFontSizeForBaseText(18); + $properties->setLanguageId('ja-JP'); + + $baseTextRun = new TextRun(null); + $baseTextRun->addText('私'); + $rubyTextRun = new TextRun(null); + $rubyTextRun->addText('わたし'); + $section->addRuby($baseTextRun, $rubyTextRun, $properties); + + $doc = TestHelperDOCX::getDocument($phpWord); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby')); + // check props + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr')); + self::assertEquals( + RubyProperties::ALIGNMENT_RIGHT_VERTICAL, + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:rubyAlign', 'w:val') + ); + self::assertEquals( + 10, + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hps', 'w:val') + ); + self::assertEquals( + 4, + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hpsRaise', 'w:val') + ); + self::assertEquals( + 18, + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hpsBaseText', 'w:val') + ); + self::assertEquals( + 'ja-JP', + $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:lid', 'w:val') + ); + // check ruby text + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rt/w:r/w:t')); + self::assertEquals( + 'わたし', + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rt/w:r/w:t')->nodeValue + ); + // check base text + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyBase/w:r/w:t')); + self::assertEquals( + '私', + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyBase/w:r/w:t')->nodeValue + ); + } + + /** + * Test Ruby title writing. + */ + public function testRubyTitleWriting(): void + { + $phpWord = new PhpWord(); + $phpWord->addTitleStyle(1, ['name' => 'Arial', 'size' => 24, 'bold' => true, 'color' => '990000']); + + $section = $phpWord->addSection(); + $properties = new RubyProperties(); + $properties->setAlignment(RubyProperties::ALIGNMENT_RIGHT_VERTICAL); + $properties->setFontFaceSize(10); + $properties->setFontPointsAboveBaseText(4); + $properties->setFontSizeForBaseText(18); + $properties->setLanguageId('ja-JP'); + + $baseTextRun = new TextRun(null); + $baseTextRun->addText('私'); + $rubyTextRun = new TextRun(null); + $rubyTextRun->addText('わたし'); + + $textRun = new TextRun(); + $textRun->addRuby($baseTextRun, $rubyTextRun, $properties); + $section->addTitle($textRun, 1); + + $doc = TestHelperDOCX::getDocument($phpWord); + // make sure style made it in + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:pPr/w:pStyle')); + self::assertEquals( + 'Heading1', + $doc->getElement('/w:document/w:body/w:p/w:pPr/w:pStyle')->getAttribute('w:val') + ); + // check ruby props + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr')); + self::assertEquals( + RubyProperties::ALIGNMENT_RIGHT_VERTICAL, + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:rubyAlign')->getAttribute('w:val') + ); + self::assertEquals( + 10, + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hps')->getAttribute('w:val') + ); + self::assertEquals( + 4, + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hpsRaise')->getAttribute('w:val') + ); + self::assertEquals( + 18, + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:hpsBaseText')->getAttribute('w:val') + ); + self::assertEquals( + 'ja-JP', + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyPr/w:lid')->getAttribute('w:val') + ); + // check ruby text + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rt/w:r/w:t')); + self::assertEquals( + 'わたし', + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rt/w:r/w:t')->nodeValue + ); + // check base text + self::assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:ruby/w:rubyBase/w:r/w:t')); + self::assertEquals( + '私', + $doc->getElement('/w:document/w:body/w:p/w:r/w:ruby/w:rubyBase/w:r/w:t')->nodeValue + ); + } }