diff --git a/docs/changes/1.x/1.5.0.md b/docs/changes/1.x/1.5.0.md index b96865bada..03da4ad70a 100644 --- a/docs/changes/1.x/1.5.0.md +++ b/docs/changes/1.x/1.5.0.md @@ -7,6 +7,7 @@ ### Bug fixes - Set writeAttribute return type by [@radarhere](https://github.com/radarhere) fixing [#2204](https://github.com/PHPOffice/PHPWord/issues/2204) in [#2776](https://github.com/PHPOffice/PHPWord/pull/2776) +- Writer RTF: Support listItem, including listTable by [@rasamassen](https://github.com/rasamassen) in [#2821](https://github.com/PHPOffice/PHPWord/pull/2821), fixing [#1106](https://github.com/PHPOffice/PHPWord/issues/1106) ### Miscellaneous @@ -16,4 +17,4 @@ ### BC Breaks -### Notes \ No newline at end of file +### Notes diff --git a/src/PhpWord/Style/ListItem.php b/src/PhpWord/Style/ListItem.php index e34aeb7c7c..fca6b1a924 100644 --- a/src/PhpWord/Style/ListItem.php +++ b/src/PhpWord/Style/ListItem.php @@ -19,6 +19,7 @@ namespace PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style; +use PhpOffice\PhpWord\Style\Numbering as NumberingStyle; /** * List item style. @@ -115,6 +116,21 @@ public function getNumStyle() return $this->numStyle; } + /** + * Get numbering style. + * + * @return ?NumberingStyle + */ + public function getNumberingStyle() + { + $numStyleObject = Style::getStyle($this->numStyle); + if ($numStyleObject instanceof NumberingStyle) { + return $numStyleObject; + } + + return null; + } + /** * Set numbering style name. * @@ -126,7 +142,7 @@ public function setNumStyle($value) { $this->numStyle = $value; $numStyleObject = Style::getStyle($this->numStyle); - if ($numStyleObject instanceof Numbering) { + if ($numStyleObject instanceof NumberingStyle) { $this->numId = $numStyleObject->getIndex(); $numStyleObject->setNumId($this->numId); } diff --git a/src/PhpWord/Writer/RTF.php b/src/PhpWord/Writer/RTF.php index 390311aa26..b694ee072d 100644 --- a/src/PhpWord/Writer/RTF.php +++ b/src/PhpWord/Writer/RTF.php @@ -101,6 +101,16 @@ public function getColorTable() return $this->getWriterPart('Header')->getColorTable(); } + /** + * Get list table. + * + * @return array + */ + public function getListTable() + { + return $this->getWriterPart('Header')->getListTable(); + } + /** * Get last paragraph style. * diff --git a/src/PhpWord/Writer/RTF/Element/ListItem.php b/src/PhpWord/Writer/RTF/Element/ListItem.php index 8ce7d0d063..5fd7c24a75 100644 --- a/src/PhpWord/Writer/RTF/Element/ListItem.php +++ b/src/PhpWord/Writer/RTF/Element/ListItem.php @@ -25,4 +25,48 @@ */ class ListItem extends Text { + /** + * Write list item element. + */ + public function write() + { + /** @var \PhpOffice\PhpWord\Element\Text $element Type hint */ + $element = $this->element; + if (!$element instanceof \PhpOffice\PhpWord\Element\ListItem) { + return ''; + } + + $this->getStyles(); + + $depth = (int) $element->getDepth(); + $style = $element->getStyle(); + $text = $element->getTextObject(); + + // Bullet List + $content = ''; + $content .= $this->writeOpening(); + if ($style instanceof \PhpOffice\PhpWord\Style\ListItem) { + $numStyle = $style->getNumberingStyle(); + $levels = $numStyle->getLevels(); + $content .= '\ilvl' . $element->getDepth(); + $content .= '\ls' . $style->getNumId(); + $content .= '\tx' . $levels[$depth]->getTabPos(); + $hanging = $levels[$depth]->getLeft() + $levels[$depth]->getHanging(); + $left = 0 - $levels[$depth]->getHanging(); + $content .= '\fi' . $left; + $content .= '\li' . $hanging; + $content .= '\lin' . $hanging; + } + $content .= $this->writeFontStyle(); // Doesn't work. Don't know why. Probalby something to do with \PphOffice\PhpWord\Element\ListItem storing styles in a textObject type \PphOffice\PhpWord\Element\Text rather than within the Element itself + $content .= PHP_EOL; + /* $content .= '{\listtext\f2 \\\'b7\tab }'; // Not sure if needed for listItemRun + $content .= PHP_EOL; */ + $content .= '{'; + $content .= $this->writeText($element->getText()); + $content .= '}'; + $content .= PHP_EOL; + $content .= $this->writeClosing(); + + return $content; + } } diff --git a/src/PhpWord/Writer/RTF/Part/Header.php b/src/PhpWord/Writer/RTF/Part/Header.php index 97644fe4ac..8e3ad034d3 100644 --- a/src/PhpWord/Writer/RTF/Part/Header.php +++ b/src/PhpWord/Writer/RTF/Part/Header.php @@ -22,6 +22,7 @@ use PhpOffice\PhpWord\Shared\Converter; use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Font; +use PhpOffice\PhpWord\Style\Numbering; use PhpOffice\PhpWord\Style\Table; /** @@ -53,6 +54,13 @@ class Header extends AbstractPart */ private $colorTable = []; + /** + * List table. + * + * @var array + */ + private $listTable = []; + /** * Get font table. * @@ -73,6 +81,16 @@ public function getColorTable() return $this->colorTable; } + /** + * Get list table. + * + * @return array + */ + public function getListTable() + { + return $this->listTable; + } + /** * Write part. * @@ -80,7 +98,7 @@ public function getColorTable() */ public function write() { - $this->registerFont(); + $this->registerHeader(); $content = ''; @@ -88,6 +106,7 @@ public function write() $content .= $this->writeDefaults(); $content .= $this->writeFontTable(); $content .= $this->writeColorTable(); + $content .= $this->writeListTable(); $content .= $this->writeGenerator(); $content .= PHP_EOL; @@ -166,6 +185,186 @@ private function writeColorTable() return $content; } + /** + * Write list table. + * + * @return string + */ + private function writeListTable() + { + $content = ''; + + $listType = [ + 'singleLevel' => '\listsimple1', + 'multilevel' => '\listsimple0', + 'hybridMultilevel' => '\listhybrid', + ]; + + $numberType = [ + \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL => '0', + \PhpOffice\PhpWord\SimpleType\NumberFormat::UPPER_ROMAN => '1', + \PhpOffice\PhpWord\SimpleType\NumberFormat::LOWER_ROMAN => '2', + \PhpOffice\PhpWord\SimpleType\NumberFormat::UPPER_LETTER => '3', + \PhpOffice\PhpWord\SimpleType\NumberFormat::LOWER_LETTER => '4', + \PhpOffice\PhpWord\SimpleType\NumberFormat::ORDINAL => '5', + \PhpOffice\PhpWord\SimpleType\NumberFormat::CARDINAL_TEXT => '6', + \PhpOffice\PhpWord\SimpleType\NumberFormat::ORDINAL_TEXT => '7', + /* \PhpOffice\PhpWord\SimpleType\NumberFormat::HEX => 'hex', + \PhpOffice\PhpWord\SimpleType\NumberFormat::CHICAGO => 'chicago', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IDEOGRAPH_DIGITAL => 'ideographDigital', + \PhpOffice\PhpWord\SimpleType\NumberFormat::JAPANESE_COUNTING => 'japaneseCounting', */ + \PhpOffice\PhpWord\SimpleType\NumberFormat::AIUEO => '12', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IROHA => '13', + /* \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_FULL_WIDTH => 'decimalFullWidth', + \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_HALF_WIDTH => 'decimalHalfWidth', + \PhpOffice\PhpWord\SimpleType\NumberFormat::JAPANESE_LEGAL => 'japaneseLegal', + \PhpOffice\PhpWord\SimpleType\NumberFormat::JAPANESE_DIGITAL_TEN_THOUSAND => 'japaneseDigitalTenThousand', + \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_ENCLOSED_CIRCLE => 'decimalEnclosedCircle', + \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_FULL_WIDTH2 => 'decimalFullWidth2', */ + \PhpOffice\PhpWord\SimpleType\NumberFormat::AIUEO_FULL_WIDTH => '20', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IROHA_FULL_WIDTH => '21', + // \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_ZERO => 'decimalZero', + \PhpOffice\PhpWord\SimpleType\NumberFormat::BULLET => '23', + /* \PhpOffice\PhpWord\SimpleType\NumberFormat::GANADA => 'ganada', + \PhpOffice\PhpWord\SimpleType\NumberFormat::CHOSUNG => 'chosung', + \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_ENCLOSED_FULL_STOP => 'decimalEnclosedFullstop', + \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_ENCLOSED_PAREN => 'decimalEnclosedParen', + \PhpOffice\PhpWord\SimpleType\NumberFormat::DECIMAL_ENCLOSED_CIRCLE_CHINESE => 'decimalEnclosedCircleChinese', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IDEOGRAPHENCLOSEDCIRCLE => 'ideographEnclosedCircle', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IDEOGRAPH_TRADITIONAL => 'ideographTraditional', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IDEOGRAPH_ZODIAC => 'ideographZodiac', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IDEOGRAPH_ZODIAC_TRADITIONAL => 'ideographZodiacTraditional', + \PhpOffice\PhpWord\SimpleType\NumberFormat::TAIWANESE_COUNTING => 'taiwaneseCounting', + \PhpOffice\PhpWord\SimpleType\NumberFormat::IDEOGRAPH_LEGAL_TRADITIONAL => 'ideographLegalTraditional', + \PhpOffice\PhpWord\SimpleType\NumberFormat::TAIWANESE_COUNTING_THOUSAND => 'taiwaneseCountingThousand', + \PhpOffice\PhpWord\SimpleType\NumberFormat::TAIWANESE_DIGITAL => 'taiwaneseDigital', + \PhpOffice\PhpWord\SimpleType\NumberFormat::CHINESE_COUNTING => 'chineseCounting', + \PhpOffice\PhpWord\SimpleType\NumberFormat::CHINESE_LEGAL_SIMPLIFIED => 'chineseLegalSimplified', + \PhpOffice\PhpWord\SimpleType\NumberFormat::CHINESE_COUNTING_THOUSAND => 'chineseCountingThousand', + \PhpOffice\PhpWord\SimpleType\NumberFormat::KOREAN_DIGITAL => 'koreanDigital', + \PhpOffice\PhpWord\SimpleType\NumberFormat::KOREAN_COUNTING => 'koreanCounting', + \PhpOffice\PhpWord\SimpleType\NumberFormat::KOREAN_LEGAL => 'koreanLegal', + \PhpOffice\PhpWord\SimpleType\NumberFormat::KOREAN_DIGITAL2 => 'koreanDigital2', */ + \PhpOffice\PhpWord\SimpleType\NumberFormat::VIETNAMESE_COUNTING => '56', + \PhpOffice\PhpWord\SimpleType\NumberFormat::RUSSIAN_LOWER => '58', + \PhpOffice\PhpWord\SimpleType\NumberFormat::RUSSIAN_UPPER => '59', + \PhpOffice\PhpWord\SimpleType\NumberFormat::NONE => '255', + /* \PhpOffice\PhpWord\SimpleType\NumberFormat::NUMBER_IN_DASH => 'numberInDash', + \PhpOffice\PhpWord\SimpleType\NumberFormat::HEBREW1 => 'hebrew1', + \PhpOffice\PhpWord\SimpleType\NumberFormat::HEBREW2 => 'hebrew2', + \PhpOffice\PhpWord\SimpleType\NumberFormat::ARABIC_ALPHA => 'arabicAlpha', */ + \PhpOffice\PhpWord\SimpleType\NumberFormat::ARABIC_ABJAD => '48', + \PhpOffice\PhpWord\SimpleType\NumberFormat::HINDI_VOWELS => '49', + \PhpOffice\PhpWord\SimpleType\NumberFormat::HINDI_CONSONANTS => '50', + \PhpOffice\PhpWord\SimpleType\NumberFormat::HINDI_NUMBERS => '51', + \PhpOffice\PhpWord\SimpleType\NumberFormat::HINDI_COUNTING => '52', + \PhpOffice\PhpWord\SimpleType\NumberFormat::THAI_LETTERS => '53', + \PhpOffice\PhpWord\SimpleType\NumberFormat::THAI_NUMBERS => '54', + \PhpOffice\PhpWord\SimpleType\NumberFormat::THAI_COUNTING => '55', + ]; + + $listAlignment = [ + 'left' => '0', + 'center' => '1', + 'right' => '2', + ]; + + $content .= '{'; + $content .= '\*\listtable' . PHP_EOL; + + foreach ($this->listTable as $list) { + $content .= '{'; + $content .= '\list\listtemplateid' . $list['numId']; + if (isset($listType[$list['type']])) { + $content .= $listType[$list['type']]; + } + $content .= PHP_EOL; + foreach ($list['listItems'] as $listItem) { + $content .= '{'; + $content .= '\listlevel'; + if (isset($numberType[$listItem['format']])) { + $content .= '\levelnfc' . $numberType[$listItem['format']]; + $content .= '\levelnfcn' . $numberType[$listItem['format']]; + } + if (isset($listAlignment[$listItem['alignment']])) { + $content .= '\leveljc' . $listAlignment[$listItem['alignment']]; + $content .= '\leveljcn' . $listAlignment[$listItem['alignment']]; + } + $content .= '\levelstartat' . $listItem['start']; + if (isset($listItem['restart'])) { + $content .= '\levelnorestart' . $listItem['restart']; + } + + // Level Text and Numbers + $positions = []; + $level = ''; + $strLength = ''; + $listText = (string) $listItem['text']; + if (strpos($listText, '%') !== false) { + $level = $this->lowerDigitsByOne(str_replace('%', '\\\'0', $listText)); + $levelNumbers = preg_replace('/\d/', 'X', str_replace('%', '', $listText)); + $offset = 0; + while (($pos = strpos($levelNumbers, 'X', $offset)) !== false) { + $positions[] = $pos; + $offset = $pos + 1; + } + $strLength = (string) sprintf('%02d', strlen($levelNumbers)); + } else { + $level = '\\\'' . (string) strtoupper(dechex(ord((string) iconv('UTF-8', 'UCS-2', $listText)))); + $strLength = '01'; + } + + $content .= '{'; + $content .= '\leveltext \\\'' . $strLength . $level; + $content .= ' ;}'; + $content .= '{'; + $content .= '\levelnumbers '; + foreach ($positions as $position) { + ++$position; + $content .= '\\\'0' . $position; + } + $content .= ';}'; + + // Font settings for Level Numbers + if (isset($listItem['font']) && !empty($listItem['font'])) { + $fontKey = array_search($listItem['font'], $this->fontTable); + if ($fontKey !== false) { + $content .= '\f' . $fontKey; + } + } + + // Tabs, Hanging, and First Line + $content .= '\levelfollow' . '0'; + $content .= '\jclisttab'; + $content .= '\tx' . $listItem['tabPos']; + $hanging = 0 - $listItem['hanging']; + $content .= '\fi' . $hanging; + $content .= '\li' . $listItem['left']; + $content .= '\lin' . $listItem['left']; + $content .= '}'; + $content .= PHP_EOL; + } + $content .= '\listid' . $list['numId'] . '}'; + $content .= PHP_EOL; + } + $content .= '}'; + $content .= PHP_EOL . PHP_EOL; + + $content .= '{'; + $content .= '\*\listoverridetable' . PHP_EOL; + foreach ($this->listTable as $list) { + $content .= '{'; + $content .= '\listoverride\listid' . $list['numId']; + $content .= '\listoverridecount0\ls' . $list['numId']; + $content .= '}'; + $content .= PHP_EOL; + } + $content .= '}'; + $content .= PHP_EOL . PHP_EOL; + + return $content; + } + /** * Write. * @@ -182,9 +381,9 @@ private function writeGenerator() } /** - * Register all fonts and colors in both named and inline styles to appropriate header table. + * Register all fonts, colors, and lists in both named and inline styles to appropriate header table. */ - private function registerFont(): void + private function registerHeader(): void { $phpWord = $this->getParentWriter()->getPhpWord(); $this->fontTable[] = Settings::getDefaultFontName(); @@ -192,7 +391,7 @@ private function registerFont(): void // Search named styles $styles = Style::getStyles(); foreach ($styles as $style) { - $this->registerFontItems($style); + $this->registerHeaderItems($style); } // Search inline styles @@ -203,7 +402,7 @@ private function registerFont(): void foreach ($elements as $element) { if (method_exists($element, 'getFontStyle')) { $style = $element->getFontStyle(); - $this->registerFontItems($style); + $this->registerHeaderItems($style); } } } @@ -225,11 +424,11 @@ private function registerBorderColor($style): void } /** - * Register fonts and colors. + * Register fonts, colors, and lists. * * @param Style\AbstractStyle $style */ - private function registerFontItems($style): void + private function registerHeaderItems($style): void { $defaultFont = Settings::getDefaultFontName(); $defaultColor = Settings::DEFAULT_FONT_COLOR; @@ -247,6 +446,9 @@ private function registerFontItems($style): void $this->registerTableItem($this->colorTable, $style->getBorderLeftColor(), $defaultColor); $this->registerTableItem($this->colorTable, $style->getBorderBottomColor(), $defaultColor); } + if ($style instanceof Numbering) { + $this->registerList($this->listTable, $style, $defaultFont); + } } /** @@ -262,4 +464,60 @@ private function registerTableItem(&$table, $value, $default = null): void $table[] = $value; } } + + /** + * Register lists and fonts within lists. + * + * @param array &$table + * @param Numbering $style + * @param string $defaultFont + */ + private function registerList(&$table, $style, $defaultFont = null): void + { + $listItems = []; + + $levels = $style->getLevels(); + foreach ($levels as $level) { + $this->registerTableItem($this->fontTable, $level->getFont(), $defaultFont); + + $listItem = [ + 'level' => $level->getLevel(), + 'start' => $level->getStart(), + 'restart' => $level->getRestart(), + 'format' => $level->getFormat(), + 'pStyle' => $level->getPStyle(), + 'suffix' => $level->getSuffix(), + 'text' => $level->getText(), + 'alignment' => $level->getAlignment(), + 'left' => $level->getLeft(), + 'hanging' => $level->getHanging(), + 'tabPos' => $level->getTabPos(), + 'font' => $level->getFont(), + 'hint' => $level->getHint(), + ]; + array_push($listItems, $listItem); + } + + $list = [ + 'numId' => $style->getNumId(), + 'type' => $style->getType(), + 'listItems' => $listItems, + ]; + $table[] = $list; + } + + /** + * NumberingLevel->getText() returns levels a step higher than expected in RTF \leveltext, (1-9) instead of (0-8). + * Thus all the digits need to be reduced by 1. + * + * @param string $string + */ + private function lowerDigitsByOne($string): string + { + return preg_replace_callback('/\d/', function ($matches) { + $digit = (int) $matches[0]; + + return ($digit > 0) ? (string) ($digit - 1) : (string) $digit; + }, $string); + } }