update readme and embed html2document as submodule

This commit is contained in:
Ventricule 2018-02-19 23:07:10 +01:00
commit 4e20e43e40
12 changed files with 1 additions and 1676 deletions

View file

@ -1,8 +0,0 @@
composer.phar
composer.lock
/vendor/
.idea
# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock

View file

@ -1,195 +0,0 @@
<?php
/**
* @link https://github.com/CatoTH/html2opendocument
* @author Tobias Hößl <tobias@hoessl.eu>
* @license https://opensource.org/licenses/MIT
*/
namespace CatoTH\HTML2OpenDocument;
abstract class Base
{
const NS_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0';
const NS_TEXT = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0';
const NS_FO = 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0';
const NS_STYLE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0';
const NS_TABLE = 'urn:oasis:names:tc:opendocument:xmlns:table:1.0';
const NS_CALCTEXT = 'urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0';
const NS_XLINK = 'http://www.w3.org/1999/xlink';
/** @var \DOMDocument */
protected $doc = null;
/** @var bool */
protected $DEBUG = false;
protected $trustHtml = false;
/** @var string */
protected $tmpPath = '/tmp/';
/** @var \ZipArchive */
private $zip;
/** @var @string */
private $tmpZipFile;
/**
* @param string $templateFile
* @param array $options
* @throws \Exception
*/
public function __construct($templateFile, $options = [])
{
$template = file_get_contents($templateFile);
if (isset($options['tmpPath']) && $options['tmpPath'] != '') {
$this->tmpPath = $options['tmpPath'];
}
if (isset($options['trustHtml'])) {
$this->trustHtml = ($options['trustHtml'] == true);
}
if(!file_exists($this->tmpPath)){
mkdir($this->tmpPath);
}
$this->tmpZipFile = $this->tmpPath . uniqid('zip-');
file_put_contents($this->tmpZipFile, $template);
$this->zip = new \ZipArchive();
if ($this->zip->open($this->tmpZipFile) !== true) {
throw new \Exception("cannot open <$this->tmpZipFile>\n");
}
$content = $this->zip->getFromName('content.xml');
$this->doc = new \DOMDocument();
$this->doc->loadXML($content);
}
/**
* @return string
*/
public function finishAndGetDocument()
{
$content = $this->create();
$this->zip->deleteName('content.xml');
$this->zip->addFromString('content.xml', $content);
$this->zip->close();
$content = file_get_contents($this->tmpZipFile);
unlink($this->tmpZipFile);
return $content;
}
/**
* @return string
*/
abstract function create();
/**
* @param string $html
* @param array $config
* @return string
*/
protected function purifyHTML($html, $config)
{
$configInstance = \HTMLPurifier_Config::create($config);
$configInstance->autoFinalize = false;
$purifier = \HTMLPurifier::instance($configInstance);
$purifier->config->set('Cache.SerializerPath', $this->tmpPath);
return $purifier->purify($html);
return $html;
}
/**
* @param string $html
* @return \DOMNode
*/
public function html2DOM($html)
{
if (!$this->trustHtml) {
$html = $this->purifyHTML(
$html,
[
'HTML.Doctype' => 'HTML 4.01 Transitional',
'HTML.Trusted' => true,
'CSS.Trusted' => true,
]
);
}
$src_doc = new \DOMDocument();
$src_doc->loadHTML('<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head><body>' . $html . "</body></html>");
$bodies = $src_doc->getElementsByTagName('body');
return $bodies->item(0);
}
/***
* @param bool $debug
*/
public function setDebug($debug)
{
$this->DEBUG = $debug;
}
/**
*/
public function debugOutput()
{
$this->doc->preserveWhiteSpace = false;
$this->doc->formatOutput = true;
echo htmlentities($this->doc->saveXML(), ENT_COMPAT, 'UTF-8');
die();
}
/**
* @param string $styleName
* @param string $family
* @param string $element
* @param string[] $attributes
*/
protected function appendStyleNode($styleName, $family, $element, $attributes)
{
$node = $this->doc->createElementNS(static::NS_STYLE, 'style');
$node->setAttribute('style:name', $styleName);
$node->setAttribute('style:family', $family);
$style = $this->doc->createElementNS(static::NS_STYLE, $element);
foreach ($attributes as $att_name => $att_val) {
$style->setAttribute($att_name, $att_val);
}
$node->appendChild($style);
foreach ($this->doc->getElementsByTagNameNS(static::NS_OFFICE, 'automatic-styles') as $element) {
/** @var \DOMElement $element */
$element->appendChild($node);
}
}
/**
* @param string $styleName
* @param array $attributes
*/
protected function appendTextStyleNode($styleName, $attributes)
{
$this->appendStyleNode($styleName, 'text', 'text-properties', $attributes);
}
/**
* @param string $styleName
* @param array $attributes
*/
protected function appendParagraphStyleNode($styleName, $attributes)
{
$this->appendStyleNode($styleName, 'paragraph', 'paragraph-properties', $attributes);
}
}

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Tobias Hößl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,94 +0,0 @@
This is a simple PHP-library to create create OpenDocument Text- and Spreadsheet-files (ODT / ODS) from HTML-formatted text.
It does not support formulae / calculations in spreadsheets. The focus lies on formatted text.
## Example Scripts
A demo script for the OpenDocument Text converter using the default template:
```php
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
$html = '<p>This is a demo for the converter.</p>
<p>The converter supports the following styles:</p>
<ul>
<li>Lists (UL / OL)</li>
<li><strong>STRONG</strong></li>
<li><u>U</u> (underlined)</li>
<li><s>S</s> (strike-through)</li>
<li><em>EM</em> (emphasis / italic)</li>
<li><ins>INS</ins> (Inserted text)</li>
<li><del>DEL</del> (Deleted text)</li>
<li>Line<br>breaks with BR</li>
</ul>
<blockquote>You can also use BLOCKQUOTE, though it lacks specific styling for now</blockquote>';
$html2 = '<p>You might be interested<br>in the fact that this converter<br>
also supports<br>line numbering<br>for selected paragraphs</p>
<p>Dummy Line<br>Dummy Line<br>Dummy Line<br>
Dummy Line<br>Dummy Line</p>';
$odt = new \CatoTH\HTML2OpenDocument\Text();
$odt->addHtmlTextBlock('<h1>Test Page</h1>');
$odt->addHtmlTextBlock($html, false);
$odt->addHtmlTextBlock('<h2>Line Numbering</h2>');
$odt->addHtmlTextBlock($html2, true);
$odt->finishAndOutputOdt('demo.odt');
```
A demo script for the OpenDocument Spreadsheet converter using the default template:
```php
use CatoTH\HTML2OpenDocument\Spreadsheet;
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
$ods = new \CatoTH\HTML2OpenDocument\Spreadsheet();
// Plain text
$ods->setCell(0, 0, Spreadsheet::TYPE_TEXT, 'Plain text with native formatting');
$ods->setCellStyle(0, 0, [], ['fo:font-weight' => 'bold']);
// Print a number as an actual number, just a little bit bigger
$ods->setCell(1, 0, Spreadsheet::TYPE_NUMBER, 23);
$ods->setCellStyle(1, 0, [], [
'fo:font-size' => '16pt',
'fo:font-weight' => 'bold',
]);
$ods->setMinRowHeight(1, 1.5);
// Print a number as text
$ods->setCell(2, 0, Spreadsheet::TYPE_TEXT, '42');
// Draw a border around two of the cells
$ods->drawBorder(1, 0, 2, 0, 1);
// Now we use HTML, and we need a bit more space for that
$html = '<p>The converter supports the following styles:</p>
<ul>
<li><strong>STRONG</strong></li>
<li><u>U</u> (underlined)</li>
<li><s>S</s> (strike-through)</li>
<li><em>EM</em> (emphasis / italic)</li>
<li><ins>Inserted text</ins></li>
<li><del>Deleted text</del></li>
<li>Line<br>breaks with BR</li>
<li>Lists (UL / OL) cannot be displayed as lists, but will be flattened to paragraphs</li>
</ul>
<blockquote>You can also use BLOCKQUOTE, though it lacks specific styling for now</blockquote>';
$ods->setMinRowHeight(3, 10);
$ods->setColumnWidth(1, 20);
$ods->setCell(3, 1, Spreadsheet::TYPE_HTML, $html);
$ods->finishAndOutputOds('demo.ods');
```
## License
This library is licensed under the [MIT license](http://opensource.org/licenses/MIT)

View file

@ -1,694 +0,0 @@
<?php
/**
* @link https://github.com/CatoTH/html2opendocument
* @author Tobias Hößl <tobias@hoessl.eu>
* @license https://opensource.org/licenses/MIT
*/
namespace CatoTH\HTML2OpenDocument;
class Spreadsheet extends Base
{
const TYPE_TEXT = 0;
const TYPE_NUMBER = 1;
const TYPE_HTML = 2;
const TYPE_LINK = 3;
const FORMAT_LINEBREAK = 0;
const FORMAT_BOLD = 1;
const FORMAT_ITALIC = 2;
const FORMAT_UNDERLINED = 3;
const FORMAT_STRIKE = 4;
const FORMAT_INS = 5;
const FORMAT_DEL = 6;
const FORMAT_LINK = 7;
const FORMAT_INDENTED = 8;
const FORMAT_SUP = 9;
const FORMAT_SUB = 10;
public static $FORMAT_NAMES = [
0 => 'linebreak',
1 => 'bold',
2 => 'italic',
3 => 'underlined',
4 => 'strike',
5 => 'ins',
6 => 'del',
7 => 'link',
8 => 'indented',
9 => 'sup',
10 => 'sub',
];
/** @var \DOMDocument */
protected $doc = null;
/** @var \DOMElement */
protected $domTable;
protected $matrix = [];
protected $matrixRows = 0;
protected $matrixCols = 0;
protected $matrixColWidths = [];
protected $matrixRowHeights = [];
protected $rowNodes = [];
protected $cellNodeMatrix = [];
protected $cellStylesMatrix = [];
protected $classCache = [];
/**
* @param array $options
* @throws \Exception
*/
public function __construct($options = [])
{
if (isset($options['templateFile']) && $options['templateFile'] != '') {
$templateFile = $options['templateFile'];
} else {
$templateFile = __DIR__ . DIRECTORY_SEPARATOR . 'default-template.ods';
}
parent::__construct($templateFile, $options);
}
/**
* @param string $filename
*/
public function finishAndOutputOds($filename = '')
{
header('Content-Type: application/vnd.oasis.opendocument.spreadsheet');
if ($filename != '') {
header('Content-disposition: attachment;filename="' . addslashes($filename) . '"');
}
echo $this->finishAndGetDocument();
die();
}
/**
* @param string $styleName
* @param array $cellAttributes
* @param array $textAttributes
*/
protected function appendCellStyleNode($styleName, $cellAttributes, $textAttributes)
{
$node = $this->doc->createElementNS(static::NS_STYLE, "style");
$node->setAttribute("style:name", $styleName);
$node->setAttribute("style:family", 'table-cell');
$node->setAttribute("style:parent-style-name", "Default");
if (count($cellAttributes) > 0) {
$style = $this->doc->createElementNS(static::NS_STYLE, 'table-cell-properties');
foreach ($cellAttributes as $att_name => $att_val) {
$style->setAttribute($att_name, $att_val);
}
$node->appendChild($style);
}
if (count($textAttributes) > 0) {
$style = $this->doc->createElementNS(static::NS_STYLE, 'text-properties');
foreach ($textAttributes as $att_name => $att_val) {
$style->setAttribute($att_name, $att_val);
}
$node->appendChild($style);
}
foreach ($this->doc->getElementsByTagNameNS(static::NS_OFFICE, 'automatic-styles') as $element) {
/** @var \DOMElement $element */
$element->appendChild($node);
}
}
/**
* @param string $styleName
* @param array $attributes
*/
protected function appendColStyleNode($styleName, $attributes)
{
$this->appendStyleNode($styleName, 'table-column', 'table-column-properties', $attributes);
}
/**
* @param string $styleName
* @param array $attributes
*/
protected function appendRowStyleNode($styleName, $attributes)
{
$this->appendStyleNode($styleName, 'table-row', 'table-row-properties', $attributes);
}
/**
* @param int $row
*/
protected function initRow($row)
{
if (!isset($this->matrix[$row])) {
$this->matrix[$row] = [];
}
if ($row > $this->matrixRows) {
$this->matrixRows = $row;
}
}
/**
* @param int $row
* @param string $col
* @param int $contentType
* @param string $content
* @param null|string $cssClass
* @param null|string $styles
*/
public function setCell($row, $col, $contentType, $content, $cssClass = null, $styles = null)
{
$this->initRow($row);
if ($col > $this->matrixCols) {
$this->matrixCols = $col;
}
$this->matrix[$row][$col] = [
'type' => $contentType,
'content' => $content,
'class' => $cssClass,
'styles' => $styles,
];
}
/**
* @param int $col
* @param float $widthInCm
*/
public function setColumnWidth($col, $widthInCm)
{
$this->matrixColWidths[$col] = $widthInCm;
}
/**
* @param int $row
* @param float $minHeightInCm
*/
public function setMinRowHeight($row, $minHeightInCm)
{
$this->initRow($row);
$rowHeight = (isset($this->matrixRowHeights[$row]) ? $this->matrixRowHeights[$row] : 1);
if ($minHeightInCm > $rowHeight) {
$rowHeight = $minHeightInCm;
}
$this->matrixRowHeights[$row] = $rowHeight;
}
/**
* @return \DOMElement
* @throws \Exception
*/
protected function getCleanDomTable()
{
$domTables = $this->doc->getElementsByTagNameNS(static::NS_TABLE, 'table');
if ($domTables->length != 1) {
throw new \Exception('Could not parse ODS template');
}
$this->domTable = $domTables->item(0);
$children = $this->domTable->childNodes;
for ($i = $children->length - 1; $i >= 0; $i--) {
$this->domTable->removeChild($children->item($i));
}
return $this->domTable;
}
/**
*/
protected function setColStyles()
{
for ($col = 0; $col <= $this->matrixCols; $col++) {
$element = $this->doc->createElementNS(static::NS_TABLE, 'table-column');
if (isset($this->matrixColWidths[$col])) {
$element->setAttribute('table:style-name', 'Antragsgruen_col_' . $col);
$this->appendColStyleNode('Antragsgruen_col_' . $col, [
'style:column-width' => $this->matrixColWidths[$col] . 'cm',
]);
}
$this->domTable->appendChild($element);
}
}
/**
*/
protected function setCellContent()
{
for ($row = 0; $row <= $this->matrixRows; $row++) {
$this->cellNodeMatrix[$row] = [];
$currentRow = $this->doc->createElementNS(static::NS_TABLE, 'table-row');
for ($col = 0; $col <= $this->matrixCols; $col++) {
$this->cellNodeMatrix[$row][$col] = [];
$currentCell = $this->doc->createElementNS(static::NS_TABLE, 'table-cell');
if (isset($this->matrix[$row][$col])) {
$cell = $this->matrix[$row][$col];
switch ($cell["type"]) {
case static::TYPE_TEXT:
$elementP = $this->doc->createElementNS(static::NS_TEXT, 'p');
$elementP->textContent = $cell['content'];
$currentCell->appendChild($elementP);
break;
case static::TYPE_NUMBER:
$elementP = $this->doc->createElementNS(static::NS_TEXT, 'p');
$elementP->textContent = $cell['content'];
$currentCell->appendChild($elementP);
$currentCell->setAttribute('calcext:value-type', 'float');
$currentCell->setAttribute('office:value-type', 'float');
$currentCell->setAttribute('office:value', (string)$cell['content']);
break;
case static::TYPE_LINK:
$elementP = $this->doc->createElementNS(static::NS_TEXT, 'p');
$elementA = $this->doc->createElementNS(static::NS_TEXT, 'a');
$elementA->setAttributeNS(static::NS_XLINK, 'xlink:href', $cell['content']['href']);
$textNode = $this->doc->createTextNode($cell['content']['text']);
$elementA->appendChild($textNode);
$elementP->appendChild($elementA);
$currentCell->appendChild($elementP);
break;
case static::TYPE_HTML:
$nodes = $this->html2OdsNodes($cell['content']);
foreach ($nodes as $node) {
$currentCell->appendChild($node);
}
//$this->setMinRowHeight($row, count($ps));
$styles = $cell['styles'];
if (isset($styles['fo:wrap-option']) && $styles['fo:wrap-option'] == 'no-wrap') {
$wrap = 'no-wrap';
$height = 1;
} else {
$wrap = 'wrap';
$width = (isset($this->matrixColWidths[$col]) ? $this->matrixColWidths[$col] : 2);
$height = (mb_strlen(strip_tags($this->matrix[$row][$col]['content'])) / ($width * 6));
}
$this->setCellStyle($row, $col, [
'fo:wrap-option' => $wrap,
], [
'fo:hyphenate' => 'true',
]);
$this->setMinRowHeight($row, $height);
break;
}
}
$currentRow->appendChild($currentCell);
$this->cellNodeMatrix[$row][$col] = $currentCell;
}
$this->domTable->appendChild($currentRow);
$this->rowNodes[$row] = $currentRow;
}
}
/**
* @param int $row
* @param int $col
* @param null|array $cellAttributes
* @param null|array $textAttributes
*/
public function setCellStyle($row, $col, $cellAttributes, $textAttributes)
{
if (!isset($this->cellStylesMatrix[$row])) {
$this->cellStylesMatrix[$row] = [];
}
if (!isset($this->cellStylesMatrix[$row][$col])) {
$this->cellStylesMatrix[$row][$col] = ['cell' => [], 'text' => []];
}
if (is_array($cellAttributes)) {
foreach ($cellAttributes as $key => $val) {
$this->cellStylesMatrix[$row][$col]['cell'][$key] = $val;
}
}
if (is_array($textAttributes)) {
foreach ($textAttributes as $key => $val) {
$this->cellStylesMatrix[$row][$col]['text'][$key] = $val;
}
}
}
/**
*/
public function setCellStyles()
{
for ($row = 0; $row <= $this->matrixRows; $row++) {
for ($col = 0; $col <= $this->matrixCols; $col++) {
if (isset($this->cellStylesMatrix[$row]) && isset($this->cellStylesMatrix[$row][$col])) {
$cell = $this->cellStylesMatrix[$row][$col];
} else {
$cell = ['cell' => [], 'text' => []];
}
$styleId = 'Antragsgruen_cell_' . $row . '_' . $col;
$cellStyles = array_merge([
'style:vertical-align' => 'top'
], $cell['cell']);
$this->appendCellStyleNode($styleId, $cellStyles, $cell['text']);
/** @var \DOMElement $currentCell */
$currentCell = $this->cellNodeMatrix[$row][$col];
$currentCell->setAttribute('table:style-name', $styleId);
}
}
/*
foreach ($this->cellStylesMatrix as $rowNr => $row) {
foreach ($row as $colNr => $cell) {
}
}
*/
}
/**
*/
public function setRowStyles()
{
foreach ($this->matrixRowHeights as $row => $height) {
$styleName = 'Antragsgruen_row_' . $row;
$this->appendRowStyleNode($styleName, [
'style:row-height' => ($height * 0.45) . 'cm',
]);
/** @var \DOMElement $node */
$node = $this->rowNodes[$row];
$node->setAttribute('table:style-name', $styleName);
}
}
/**
* @param int $fromRow
* @param int $fromCol
* @param int $toRow
* @param int $toCol
* @param float $width
*/
public function drawBorder($fromRow, $fromCol, $toRow, $toCol, $width)
{
for ($i = $fromRow; $i <= $toRow; $i++) {
$this->setCellStyle($i, $fromCol, [
'fo:border-left' => $width . 'pt solid #000000',
], []);
$this->setCellStyle($i, $toCol, [
'fo:border-right' => $width . 'pt solid #000000',
], []);
}
for ($i = $fromCol; $i <= $toCol; $i++) {
$this->setCellStyle($fromRow, $i, [
'fo:border-top' => $width . 'pt solid #000000',
], []);
$this->setCellStyle($toRow, $i, [
'fo:border-bottom' => $width . 'pt solid #000000',
], []);
}
}
/**
* @param \DOMElement $node
* @param array $currentFormats
* @return array
*/
protected function node2Formatting(\DOMElement $node, $currentFormats)
{
switch ($node->nodeName) {
case 'b':
case 'strong':
$currentFormats[] = static::FORMAT_BOLD;
break;
case 'i':
case 'em':
$currentFormats[] = static::FORMAT_ITALIC;
break;
case 's':
$currentFormats[] = static::FORMAT_STRIKE;
break;
case 'u':
$currentFormats[] = static::FORMAT_UNDERLINED;
break;
case 'sub':
$currentFormats[] = static::FORMAT_SUB;
break;
case 'sup':
$currentFormats[] = static::FORMAT_SUP;
break;
case 'br':
break;
case 'p':
case 'div':
case 'blockquote':
if ($node->hasAttribute('class')) {
$classes = explode(' ', $node->getAttribute('class'));
if (in_array('underline', $classes)) {
$currentFormats[] = static::FORMAT_UNDERLINED;
}
if (in_array('strike', $classes)) {
$currentFormats[] = static::FORMAT_STRIKE;
}
if (in_array('ins', $classes)) {
$currentFormats[] = static::FORMAT_INS;
}
if (in_array('inserted', $classes)) {
$currentFormats[] = static::FORMAT_INS;
}
if (in_array('del', $classes)) {
$currentFormats[] = static::FORMAT_DEL;
}
if (in_array('deleted', $classes)) {
$currentFormats[] = static::FORMAT_DEL;
}
}
break;
case 'ul':
case 'ol':
if ($node->hasAttribute('class')) {
$classes = explode(' ', $node->getAttribute('class'));
$currentFormats[] = static::FORMAT_INDENTED;
if (in_array('ins', $classes)) {
$currentFormats[] = static::FORMAT_INS;
}
if (in_array('inserted', $classes)) {
$currentFormats[] = static::FORMAT_INS;
}
if (in_array('del', $classes)) {
$currentFormats[] = static::FORMAT_DEL;
}
if (in_array('deleted', $classes)) {
$currentFormats[] = static::FORMAT_DEL;
}
}
break;
case 'li':
break;
case 'del':
$currentFormats[] = static::FORMAT_DEL;
break;
case 'ins':
$currentFormats[] = static::FORMAT_INS;
break;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
$currentFormats[] = static::FORMAT_BOLD;
break;
case 'a':
$currentFormats[] = static::FORMAT_LINK;
try {
$attr = $node->getAttribute('href');
if ($attr) {
$currentFormats['href'] = $attr;
}
} catch (\Exception $e) {
}
break;
case 'span':
default:
if ($node->hasAttribute('class')) {
$classes = explode(' ', $node->getAttribute('class'));
if (in_array('underline', $classes)) {
$currentFormats[] = static::FORMAT_UNDERLINED;
}
if (in_array('strike', $classes)) {
$currentFormats[] = static::FORMAT_STRIKE;
}
if (in_array('ins', $classes)) {
$currentFormats[] = static::FORMAT_INS;
}
if (in_array('inserted', $classes)) {
$currentFormats[] = static::FORMAT_INS;
}
if (in_array('del', $classes)) {
$currentFormats[] = static::FORMAT_DEL;
}
if (in_array('deleted', $classes)) {
$currentFormats[] = static::FORMAT_DEL;
}
if (in_array('superscript', $classes)) {
$currentFormats[] = static::FORMAT_SUP;
}
if (in_array('subscript', $classes)) {
$currentFormats[] = static::FORMAT_SUB;
}
}
break;
}
return $currentFormats;
}
/**
* @param \DOMNode $node
* @param array $currentFormats
* @return array
*/
protected function tokenizeFlattenHtml(\DOMNode $node, $currentFormats)
{
$return = [];
foreach ($node->childNodes as $child) {
switch ($child->nodeType) {
case XML_ELEMENT_NODE:
/** @var \DOMElement $child */
$formattings = $this->node2Formatting($child, $currentFormats);
$children = $this->tokenizeFlattenHtml($child, $formattings);
$return = array_merge($return, $children);
if (in_array($child->nodeName, ['br', 'div', 'p', 'li', 'blockquote'])) {
$return[] = [
'text' => '',
'formattings' => [static::FORMAT_LINEBREAK],
];
}
if (in_array($child->nodeName, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])) {
$return[] = [
'text' => '',
'formattings' => [static::FORMAT_LINEBREAK, static::FORMAT_BOLD],
];
}
break;
case XML_TEXT_NODE:
/** @var \DOMText $child */
$return[] = [
'text' => $child->data,
'formattings' => $currentFormats,
];
break;
default:
}
}
return $return;
}
/**
* @param array $formats
* @return string
*/
protected function getClassByFormats($formats)
{
sort($formats);
$key = implode('_', $formats);
if (!isset($this->classCache[$key])) {
$name = 'Antragsgruen';
$styles = [];
foreach ($formats as $format) {
if (!isset(static::$FORMAT_NAMES[$format])) {
continue;
}
$name .= '_' . static::$FORMAT_NAMES[$format];
switch ($format) {
case static::FORMAT_INS:
$styles['fo:color'] = '#00ff00';
$styles['style:text-underline-style'] = 'solid';
$styles['style:text-underline-width'] = 'auto';
$styles['style:text-underline-color'] = 'font-color';
break;
case static::FORMAT_DEL:
$styles['fo:color'] = '#ff0000';
$styles['style:text-line-through-type'] = 'single';
break;
case static::FORMAT_BOLD:
$styles['fo:font-weight'] = 'bold';
$styles['style:font-weight-asian'] = 'bold';
$styles['style:font-weight-complex'] = 'bold';
break;
case static::FORMAT_UNDERLINED:
$styles['style:text-underline-width'] = 'auto';
$styles['style:text-underline-color'] = 'font-color';
$styles['style:text-underline-style'] = 'solid';
break;
case static::FORMAT_STRIKE:
$styles['style:text-line-through-type'] = 'single';
break;
case static::FORMAT_ITALIC:
$styles['fo:font-style'] = 'italic';
$styles['style:font-style-asian'] = 'italic';
$styles['style:font-style-complex'] = 'italic';
break;
case static::FORMAT_SUP:
$styles['fo:font-size'] = '10pt';
$styles['style:text-position'] = '31%';
break;
case static::FORMAT_SUB:
$styles['fo:font-size'] = '10pt';
$styles['style:text-position'] = '-31%';
break;
}
}
$this->appendTextStyleNode($name, $styles);
$this->classCache[$key] = $name;
}
return $this->classCache[$key];
}
/**
* @param string $html
* @return array
*/
public function html2OdsNodes($html)
{
$body = $this->html2DOM($html);
$tokens = $this->tokenizeFlattenHtml($body, []);
$nodes = [];
$currentP = $this->doc->createElementNS(static::NS_TEXT, 'p');
foreach ($tokens as $token) {
if (trim($token['text']) != '') {
$node = $this->doc->createElement('text:span');
if (count($token['formattings']) > 0) {
$className = $this->getClassByFormats($token['formattings']);
$node->setAttribute('text:style-name', $className);
}
$textNode = $this->doc->createTextNode($token['text']);
$node->appendChild($textNode);
$currentP->appendChild($node);
}
if (in_array(static::FORMAT_LINEBREAK, $token['formattings'])) {
$nodes[] = $currentP;
$currentP = $this->doc->createElementNS(static::NS_TEXT, 'p');
}
}
$nodes[] = $currentP;
return $nodes;
}
/**
* @return string
* @throws \Exception
*/
public function create()
{
$this->getCleanDomTable();
$this->setColStyles();
$this->setCellContent();
$this->setRowStyles();
$this->setCellStyles();
$xml = $this->doc->saveXML();
$rows = explode("\n", $xml);
$rows[0] .= "\n";
return implode('', $rows) . "\n";
}
}

View file

@ -1,583 +0,0 @@
<?php
/**
* @link https://github.com/CatoTH/html2opendocument
* @author Tobias Hößl <tobias@hoessl.eu>
* @license https://opensource.org/licenses/MIT
*/
namespace CatoTH\HTML2OpenDocument;
class Text extends Base
{
/** @var null|\DOMElement */
private $nodeText = null;
/** @var bool */
private $node_template_1_used = false;
/** @var string[] */
private $replaces = [];
/** @var array */
private $textBlocks = [];
const STYLE_INS = 'ins';
const STYLE_DEL = 'del';
/**
* @param array $options
*
* @throws \Exception
*/
public function __construct($options = [])
{
if (isset($options['templateFile']) && $options['templateFile'] != '') {
$templateFile = $options['templateFile'];
} else {
$templateFile = __DIR__ . DIRECTORY_SEPARATOR . 'default-template.odt';
}
parent::__construct($templateFile, $options);
}
/**
* @param string $filename
*/
public function finishAndOutputOdt($filename = '')
{
header('Content-Type: application/vnd.oasis.opendocument.text');
if ($filename != '') {
header('Content-disposition: attachment;filename="' . addslashes($filename) . '"');
}
echo $this->finishAndGetDocument();
die();
}
/**
* @param string $search
* @param string $replace
*/
public function addReplace($search, $replace)
{
$this->replaces[$search] = $replace;
}
/**
* @param string $html
* @param bool $lineNumbered
*/
public function addHtmlTextBlock($html, $lineNumbered = false)
{
$this->textBlocks[] = ['text' => $html, 'lineNumbered' => $lineNumbered];
}
/**
* @param \DOMElement $element
* @return string[]
*/
protected static function getCSSClasses(\DOMElement $element)
{
if ($element->hasAttribute('class')) {
return explode(' ', $element->getAttribute('class'));
} else {
return [];
}
}
/**
* @param \DOMElement $element
* @param string[] $parentStyles
* @return string[]
*/
protected static function getChildStyles(\DOMElement $element, $parentStyles = [])
{
$classes = static::getCSSClasses($element);
$childStyles = $parentStyles;
if (in_array('ins', $classes)) {
$childStyles[] = static::STYLE_INS;
}
if (in_array('inserted', $classes)) {
$childStyles[] = static::STYLE_INS;
}
if (in_array('del', $classes)) {
$childStyles[] = static::STYLE_DEL;
}
if (in_array('deleted', $classes)) {
$childStyles[] = static::STYLE_DEL;
}
return array_unique($childStyles);
}
/**
* @param string[] $classes
*
* @return null|string
*/
protected static function cssClassesToInternalClass($classes)
{
if (in_array('underline', $classes)) {
return 'AntragsgruenUnderlined';
}
if (in_array('strike', $classes)) {
return 'AntragsgruenStrike';
}
if (in_array('ins', $classes)) {
return 'AntragsgruenIns';
}
if (in_array('inserted', $classes)) {
return 'AntragsgruenIns';
}
if (in_array('del', $classes)) {
return 'AntragsgruenDel';
}
if (in_array('deleted', $classes)) {
return 'AntragsgruenDel';
}
if (in_array('superscript', $classes)) {
return 'AntragsgruenSup';
}
if (in_array('subscript', $classes)) {
return 'AntragsgruenSub';
}
return null;
}
/**
* Wraps all child nodes with text:p nodes, if necessary
* (it's not necessary for child nodes that are p's themselves or lists)
*
* @param \DOMElement $parentEl
* @param boolean $lineNumbered
*
* @return \DOMElement
*/
protected function wrapChildrenWithP(\DOMElement $parentEl, $lineNumbered)
{
$childNodes = [];
while ($parentEl->childNodes->length > 0) {
$el = $parentEl->firstChild;
$parentEl->removeChild($el);
$childNodes[] = $el;
}
$appendNode = null;
foreach ($childNodes as $childNode) {
if (in_array(strtolower($childNode->nodeName), ['p', 'list'])) {
if ($appendNode) {
$parentEl->appendChild($appendNode);
$appendNode = null;
}
$parentEl->appendChild($childNode);
} else {
if (!$appendNode) {
$appendNode = $this->getNextNodeTemplate($lineNumbered);
}
$appendNode->appendChild($childNode);
}
}
if ($appendNode) {
$parentEl->appendChild($appendNode);
}
return $parentEl;
}
/**
* @param \DOMNode $srcNode
* @param bool $lineNumbered
* @param bool $inP
* @param string[]   $parentStyles
*
* @return \DOMNode[]
* @throws \Exception
*/
protected function html2ooNodeInt($srcNode, $lineNumbered, $inP, $parentStyles = [])
{
switch ($srcNode->nodeType) {
case XML_ELEMENT_NODE:
/** @var \DOMElement $srcNode */
if ($this->DEBUG) {
echo "Element - " . $srcNode->nodeName . " / Children: " . $srcNode->childNodes->length . "<br>";
}
$needsIntermediateP = false;
$childStyles = static::getChildStyles($srcNode, $parentStyles);
switch ($srcNode->nodeName) {
case 'b':
case 'strong':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenBold');
break;
case 'i':
case 'em':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenItalic');
break;
case 's':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenStrike');
break;
case 'u':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenUnderlined');
break;
case 'sub':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenSub');
break;
case 'sup':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenSup');
break;
case 'br':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'line-break');
break;
case 'del':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenDel');
break;
case 'ins':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$dstEl->setAttribute('text:style-name', 'AntragsgruenIns');
break;
case 'a':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'a');
try {
$attr = $srcNode->getAttribute('href');
if ($attr) {
$dstEl->setAttribute('xlink:href', $attr);
}
} catch (\Exception $e) {
}
break;
case 'p':
if ($inP) {
$dstEl = $this->createNodeWithBaseStyle('span', $lineNumbered);
} else {
$dstEl = $this->createNodeWithBaseStyle('p', $lineNumbered);
}
$intClass = static::cssClassesToInternalClass(static::getCSSClasses($srcNode));
if ($intClass) {
$dstEl->setAttribute('text:style-name', $intClass);
}
$inP = true;
break;
case 'div':
// We're basically ignoring DIVs here, as there is no corresponding element in OpenDocument
// Therefore no support for styles and classes set on DIVs yet.
$dstEl = null;
break;
case 'blockquote':
$dstEl = $this->createNodeWithBaseStyle('p', $lineNumbered);
$class = ($lineNumbered ? 'Blockquote_Linenumbered' : 'Blockquote');
$dstEl->setAttribute('text:style-name', 'Antragsgrün_20_' . $class);
if ($srcNode->childNodes->length == 1) {
foreach ($srcNode->childNodes as $child) {
if ($child->nodeName == 'p') {
$srcNode = $child;
}
}
}
$inP = true;
break;
case 'ul':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'list');
break;
case 'ol':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'list');
break;
case 'li':
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'list-item');
$needsIntermediateP = true;
$inP = true;
break;
case 'h1':
$dstEl = $this->createNodeWithBaseStyle('p', $lineNumbered);
$dstEl->setAttribute('text:style-name', 'Antragsgrün_20_H1');
$inP = true;
break;
case 'h2':
$dstEl = $this->createNodeWithBaseStyle('p', $lineNumbered);
$dstEl->setAttribute('text:style-name', 'Antragsgrün_20_H2');
$inP = true;
break;
case 'h3':
$dstEl = $this->createNodeWithBaseStyle('p', $lineNumbered);
$dstEl->setAttribute('text:style-name', 'Antragsgrün_20_H3');
$inP = true;
break;
case 'h4':
case 'h5':
case 'h6':
$dstEl = $this->createNodeWithBaseStyle('p', $lineNumbered);
$dstEl->setAttribute('text:style-name', 'Antragsgrün_20_H4');
$inP = true;
break;
case 'span':
default:
$dstEl = $this->doc->createElementNS(static::NS_TEXT, 'span');
$intClass = static::cssClassesToInternalClass(static::getCSSClasses($srcNode));
if ($intClass) {
$dstEl->setAttribute('text:style-name', $intClass);
}
break;
}
if ($dstEl === null) {
$ret = [];
foreach ($srcNode->childNodes as $child) {
/** @var \DOMNode $child */
if ($this->DEBUG) {
echo "CHILD<br>" . $child->nodeType . "<br>";
}
$dstNodes = $this->html2ooNodeInt($child, $lineNumbered, $inP, $childStyles);
foreach ($dstNodes as $dstNode) {
$ret[] = $dstNode;
}
}
return $ret;
}
foreach ($srcNode->childNodes as $child) {
/** @var \DOMNode $child */
if ($this->DEBUG) {
echo "CHILD<br>" . $child->nodeType . "<br>";
}
$dstNodes = $this->html2ooNodeInt($child, $lineNumbered, $inP, $childStyles);
foreach ($dstNodes as $dstNode) {
$dstEl->appendChild($dstNode);
}
}
if ($needsIntermediateP && $dstEl->childNodes->length > 0) {
$dstEl = static::wrapChildrenWithP($dstEl, $lineNumbered);
}
return [$dstEl];
case XML_TEXT_NODE:
/** @var \DOMText $srcNode */
$textnode = new \DOMText();
$textnode->data = $srcNode->data;
if ($this->DEBUG) {
echo 'Text<br>';
}
if (in_array(static::STYLE_DEL, $parentStyles)) {
$dstEl = $this->createNodeWithBaseStyle('span', $lineNumbered);
$dstEl->setAttribute('text:style-name', 'AntragsgruenDel');
$dstEl->appendChild($textnode);
$textnode = $dstEl;
}
if (in_array(static::STYLE_INS, $parentStyles)) {
$dstEl = $this->createNodeWithBaseStyle('span', $lineNumbered);
$dstEl->setAttribute('text:style-name', 'AntragsgruenIns');
$dstEl->appendChild($textnode);
$textnode = $dstEl;
}
return [$textnode];
break;
case XML_DOCUMENT_TYPE_NODE:
if ($this->DEBUG) {
echo 'Type Node<br>';
}
return [];
default:
if ($this->DEBUG) {
echo 'Unknown Node: ' . $srcNode->nodeType . '<br>';
}
return [];
}
}
/**
* @param string $html
* @param bool $lineNumbered
*
* @return \DOMNode[]
* @throws \Exception
*/
protected function html2ooNodes($html, $lineNumbered)
{
if (!is_string($html)) {
echo print_r($html, true);
echo print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true);
die();
}
$body = $this->html2DOM($html);
$retNodes = [];
for ($i = 0; $i < $body->childNodes->length; $i++) {
$child = $body->childNodes->item($i);
/** @var \DOMNode $child */
if ($child->nodeName == 'ul') {
// Alle anderen Nodes dieses Aufrufs werden ignoriert
if ($this->DEBUG) {
echo 'LIST<br>';
}
$recNewNodes = $this->html2ooNodeInt($child, $lineNumbered, false);
} else {
if ($child->nodeType == XML_TEXT_NODE) {
$new_node = $this->getNextNodeTemplate($lineNumbered);
/** @var \DOMText $child */
if ($this->DEBUG) {
echo $child->nodeName . ' - ' . htmlentities($child->data, ENT_COMPAT, 'UTF-8') . '<br>';
}
$text = new \DOMText();
$text->data = $child->data;
$new_node->appendChild($text);
$recNewNodes = [$new_node];
} else {
if ($this->DEBUG) {
echo $child->nodeName . '!!!!!!!!!!!!<br>';
}
$recNewNodes = $this->html2ooNodeInt($child, $lineNumbered, false);
}
}
foreach ($recNewNodes as $recNewNode) {
$retNodes[] = $recNewNode;
}
}
return $retNodes;
}
/**
* @return string
* @throws \Exception
*/
public function create()
{
$this->appendTextStyleNode('AntragsgruenBold', [
'fo:font-weight' => 'bold',
'style:font-weight-asian' => 'bold',
'style:font-weight-complex' => 'bold',
]);
$this->appendTextStyleNode('AntragsgruenItalic', [
'fo:font-style' => 'italic',
'style:font-style-asian' => 'italic',
'style:font-style-complex' => 'italic',
]);
$this->appendTextStyleNode('AntragsgruenUnderlined', [
'style:text-underline-width' => 'auto',
'style:text-underline-color' => 'font-color',
'style:text-underline-style' => 'solid',
]);
$this->appendTextStyleNode('AntragsgruenStrike', [
'style:text-line-through-style' => 'solid',
'style:text-line-through-type' => 'single',
]);
$this->appendTextStyleNode('AntragsgruenIns', [
'fo:color' => '#008800',
'style:text-underline-style' => 'solid',
'style:text-underline-width' => 'auto',
'style:text-underline-color' => 'font-color',
'fo:font-weight' => 'bold',
'style:font-weight-asian' => 'bold',
'style:font-weight-complex' => 'bold',
]);
$this->appendTextStyleNode('AntragsgruenDel', [
'fo:color' => '#880000',
'style:text-line-through-style' => 'solid',
'style:text-line-through-type' => 'single',
'fo:font-style' => 'italic',
'style:font-style-asian' => 'italic',
'style:font-style-complex' => 'italic',
]);
$this->appendTextStyleNode('AntragsgruenSub', [
'style:text-position' => 'sub 58%',
]);
$this->appendTextStyleNode('AntragsgruenSup', [
'style:text-position' => 'super 58%',
]);
/** @var \DOMNode[] $nodes */
$nodes = [];
foreach ($this->doc->getElementsByTagNameNS(static::NS_TEXT, 'span') as $element) {
$nodes[] = $element;
}
foreach ($this->doc->getElementsByTagNameNS(static::NS_TEXT, 'p') as $element) {
$nodes[] = $element;
}
$searchFor = array_keys($this->replaces);
$replaceWith = array_values($this->replaces);
foreach ($nodes as $node) {
$children = $node->childNodes;
foreach ($children as $child) {
if ($child->nodeType == XML_TEXT_NODE) {
/** @var \DOMText $child */
$child->data = preg_replace($searchFor, $replaceWith, $child->data);
if (preg_match("/\{\{ANTRAGSGRUEN:DUMMY\}\}/siu", $child->data)) {
$node->parentNode->removeChild($node);
}
if (preg_match("/\{\{ANTRAGSGRUEN:TEXT\}\}/siu", $child->data)) {
$this->nodeText = $node;
}
}
}
}
foreach ($this->textBlocks as $textBlock) {
$newNodes = $this->html2ooNodes($textBlock['text'], $textBlock['lineNumbered']);
foreach ($newNodes as $newNode) {
$this->nodeText->parentNode->insertBefore($newNode, $this->nodeText);
}
}
$this->nodeText->parentNode->removeChild($this->nodeText);
return $this->doc->saveXML();
}
/**
* @param bool $lineNumbers
*
* @return \DOMNode
*/
protected function getNextNodeTemplate($lineNumbers)
{
$node = $this->nodeText->cloneNode();
/** @var \DOMElement $node */
if ($lineNumbers) {
if ($this->node_template_1_used) {
$node->setAttribute('text:style-name', 'Antragsgrün_20_LineNumbered_20_Standard');
} else {
$this->node_template_1_used = true;
$node->setAttribute('text:style-name', 'Antragsgrün_20_LineNumbered_20_First');
}
} else {
$node->setAttribute('text:style-name', 'Antragsgrün_20_Standard');
}
return $node;
}
/**
* @param string $nodeType
* @param bool $lineNumbers
*
* @return \DOMElement|\DOMNode
*/
protected function createNodeWithBaseStyle($nodeType, $lineNumbers)
{
$node = $this->doc->createElementNS(static::NS_TEXT, $nodeType);
if ($lineNumbers) {
if ($this->node_template_1_used) {
$node->setAttribute('text:style-name', 'Antragsgrün_20_LineNumbered_20_Standard');
} else {
$this->node_template_1_used = true;
$node->setAttribute('text:style-name', 'Antragsgrün_20_LineNumbered_20_First');
}
} else {
$node->setAttribute('text:style-name', 'Antragsgrün_20_Standard');
}
return $node;
}
}

View file

@ -1,30 +0,0 @@
{
"name": "catoth/html2opendocument",
"description": "Converting simple HTML to Opendocument Text (ODT) or Spreadsheets (ODS)",
"type": "library",
"minimum-stability": "stable",
"license": "MIT",
"keywords": [
"opendocument",
"html",
"odt",
"ods"
],
"authors": [
{
"name": "Tobias Hößl",
"email": "tobias@hoessl.eu"
}
],
"require": {
"php": ">=5.5.0",
"ext-zip": "*",
"ext-dom": "*",
"ezyang/htmlpurifier": "*"
},
"autoload": {
"psr-4": {
"CatoTH\\HTML2OpenDocument\\": ""
}
}
}

View file

@ -1,50 +0,0 @@
<?php
use CatoTH\HTML2OpenDocument\Spreadsheet;
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor/autoload.php');
$ods = new \CatoTH\HTML2OpenDocument\Spreadsheet();
// Plain text
$ods->setCell(0, 0, Spreadsheet::TYPE_TEXT, 'Plain text with native formatting');
$ods->setCellStyle(0, 0, [], ['fo:font-weight' => 'bold']);
// Print a number as an actual number, just a little bit bigger
$ods->setCell(1, 0, Spreadsheet::TYPE_NUMBER, 23);
$ods->setCellStyle(1, 0, [], [
'fo:font-size' => '16pt',
'fo:font-weight' => 'bold',
]);
$ods->setMinRowHeight(1, 1.5);
// Print a number as text
$ods->setCell(2, 0, Spreadsheet::TYPE_TEXT, '42');
// Draw a border around two of the cells
$ods->drawBorder(1, 0, 2, 0, 1);
// Now we use HTML, and we need a bit more space for that
$html = '<p>The converter supports the following styles:</p>
<ul>
<li><strong>STRONG</strong></li>
<li><u>U</u> (underlined)</li>
<li><s>S</s> (strike-through)</li>
<li><em>EM</em> (emphasis / italic)</li>
<li><ins>Inserted text</ins></li>
<li><del>Deleted text</del></li>
<li>Line<br>breaks with BR</li>
<li>Lists (UL / OL) cannot be displayed as lists, but will be flattened to paragraphs</li>
</ul>
<blockquote>You can also use BLOCKQUOTE, though it lacks specific styling for now</blockquote>';
$ods->setMinRowHeight(3, 10);
$ods->setColumnWidth(1, 20);
$ods->setCell(3, 1, Spreadsheet::TYPE_HTML, $html);
$ods->finishAndOutputOds('demo.ods');
?>

View file

@ -1,4 +1,4 @@
#Libreto
# Libreto
![Libreto](http://libreto.net/assets/images/libretonet.png)