diff options
author | Lars Knoll <lars.knoll@nokia.com> | 2009-03-23 09:18:55 (GMT) |
---|---|---|
committer | Simon Hausmann <simon.hausmann@nokia.com> | 2009-03-23 09:18:55 (GMT) |
commit | e5fcad302d86d316390c6b0f62759a067313e8a9 (patch) | |
tree | c2afbf6f1066b6ce261f14341cf6d310e5595bc1 /src/gui/text/qtextdocumentfragment.cpp | |
download | Qt-e5fcad302d86d316390c6b0f62759a067313e8a9.zip Qt-e5fcad302d86d316390c6b0f62759a067313e8a9.tar.gz Qt-e5fcad302d86d316390c6b0f62759a067313e8a9.tar.bz2 |
Long live Qt 4.5!
Diffstat (limited to 'src/gui/text/qtextdocumentfragment.cpp')
-rw-r--r-- | src/gui/text/qtextdocumentfragment.cpp | 1217 |
1 files changed, 1217 insertions, 0 deletions
diff --git a/src/gui/text/qtextdocumentfragment.cpp b/src/gui/text/qtextdocumentfragment.cpp new file mode 100644 index 0000000..21958a6 --- /dev/null +++ b/src/gui/text/qtextdocumentfragment.cpp @@ -0,0 +1,1217 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtextdocumentfragment.h" +#include "qtextdocumentfragment_p.h" +#include "qtextcursor_p.h" +#include "qtextlist.h" +#include "private/qunicodetables_p.h" + +#include <qdebug.h> +#include <qtextcodec.h> +#include <qbytearray.h> +#include <qdatastream.h> +#include <qdatetime.h> + +QT_BEGIN_NAMESPACE + +QTextCopyHelper::QTextCopyHelper(const QTextCursor &_source, const QTextCursor &_destination, bool forceCharFormat, const QTextCharFormat &fmt) + : formatCollection(*_destination.d->priv->formatCollection()), originalText(_source.d->priv->buffer()) +{ + src = _source.d->priv; + dst = _destination.d->priv; + insertPos = _destination.position(); + this->forceCharFormat = forceCharFormat; + primaryCharFormatIndex = convertFormatIndex(fmt); + cursor = _source; +} + +int QTextCopyHelper::convertFormatIndex(const QTextFormat &oldFormat, int objectIndexToSet) +{ + QTextFormat fmt = oldFormat; + if (objectIndexToSet != -1) { + fmt.setObjectIndex(objectIndexToSet); + } else if (fmt.objectIndex() != -1) { + int newObjectIndex = objectIndexMap.value(fmt.objectIndex(), -1); + if (newObjectIndex == -1) { + QTextFormat objFormat = src->formatCollection()->objectFormat(fmt.objectIndex()); + Q_ASSERT(objFormat.objectIndex() == -1); + newObjectIndex = formatCollection.createObjectIndex(objFormat); + objectIndexMap.insert(fmt.objectIndex(), newObjectIndex); + } + fmt.setObjectIndex(newObjectIndex); + } + int idx = formatCollection.indexForFormat(fmt); + Q_ASSERT(formatCollection.format(idx).type() == oldFormat.type()); + return idx; +} + +int QTextCopyHelper::appendFragment(int pos, int endPos, int objectIndex) +{ + QTextDocumentPrivate::FragmentIterator fragIt = src->find(pos); + const QTextFragmentData * const frag = fragIt.value(); + + Q_ASSERT(objectIndex == -1 + || (frag->size_array[0] == 1 && src->formatCollection()->format(frag->format).objectIndex() != -1)); + + int charFormatIndex; + if (forceCharFormat) + charFormatIndex = primaryCharFormatIndex; + else + charFormatIndex = convertFormatIndex(frag->format, objectIndex); + + const int inFragmentOffset = qMax(0, pos - fragIt.position()); + int charsToCopy = qMin(int(frag->size_array[0] - inFragmentOffset), endPos - pos); + + QTextBlock nextBlock = src->blocksFind(pos + 1); + + int blockIdx = -2; + if (nextBlock.position() == pos + 1) { + blockIdx = convertFormatIndex(nextBlock.blockFormat()); + } else if (pos == 0 && insertPos == 0) { + dst->setBlockFormat(dst->blocksBegin(), dst->blocksBegin(), convertFormat(src->blocksBegin().blockFormat()).toBlockFormat()); + dst->setCharFormat(-1, 1, convertFormat(src->blocksBegin().charFormat()).toCharFormat()); + } + + QString txtToInsert(originalText.constData() + frag->stringPosition + inFragmentOffset, charsToCopy); + if (txtToInsert.length() == 1 + && (txtToInsert.at(0) == QChar::ParagraphSeparator + || txtToInsert.at(0) == QTextBeginningOfFrame + || txtToInsert.at(0) == QTextEndOfFrame + ) + ) { + dst->insertBlock(txtToInsert.at(0), insertPos, blockIdx, charFormatIndex); + ++insertPos; + } else { + if (nextBlock.textList()) { + QTextBlock dstBlock = dst->blocksFind(insertPos); + if (!dstBlock.textList()) { + // insert a new text block with the block and char format from the + // source block to make sure that the following text fragments + // end up in a list as they should + int listBlockFormatIndex = convertFormatIndex(nextBlock.blockFormat()); + int listCharFormatIndex = convertFormatIndex(nextBlock.charFormat()); + dst->insertBlock(insertPos, listBlockFormatIndex, listCharFormatIndex); + ++insertPos; + } + } + dst->insert(insertPos, txtToInsert, charFormatIndex); + const int userState = nextBlock.userState(); + if (userState != -1) + dst->blocksFind(insertPos).setUserState(userState); + insertPos += txtToInsert.length(); + } + + return charsToCopy; +} + +void QTextCopyHelper::appendFragments(int pos, int endPos) +{ + Q_ASSERT(pos < endPos); + + while (pos < endPos) + pos += appendFragment(pos, endPos); +} + +void QTextCopyHelper::copy() +{ + if (cursor.hasComplexSelection()) { + QTextTable *table = cursor.currentTable(); + int row_start, col_start, num_rows, num_cols; + cursor.selectedTableCells(&row_start, &num_rows, &col_start, &num_cols); + + QTextTableFormat tableFormat = table->format(); + tableFormat.setColumns(num_cols); + tableFormat.clearColumnWidthConstraints(); + const int objectIndex = dst->formatCollection()->createObjectIndex(tableFormat); + + Q_ASSERT(row_start != -1); + for (int r = row_start; r < row_start + num_rows; ++r) { + for (int c = col_start; c < col_start + num_cols; ++c) { + QTextTableCell cell = table->cellAt(r, c); + const int rspan = cell.rowSpan(); + const int cspan = cell.columnSpan(); + if (rspan != 1) { + int cr = cell.row(); + if (cr != r) + continue; + } + if (cspan != 1) { + int cc = cell.column(); + if (cc != c) + continue; + } + + // add the QTextBeginningOfFrame + QTextCharFormat cellFormat = cell.format(); + if (r + rspan >= row_start + num_rows) { + cellFormat.setTableCellRowSpan(row_start + num_rows - r); + } + if (c + cspan >= col_start + num_cols) { + cellFormat.setTableCellColumnSpan(col_start + num_cols - c); + } + const int charFormatIndex = convertFormatIndex(cellFormat, objectIndex); + + int blockIdx = -2; + const int cellPos = cell.firstPosition(); + QTextBlock block = src->blocksFind(cellPos); + if (block.position() == cellPos) { + blockIdx = convertFormatIndex(block.blockFormat()); + } + + dst->insertBlock(QTextBeginningOfFrame, insertPos, blockIdx, charFormatIndex); + ++insertPos; + + // nothing to add for empty cells + if (cell.lastPosition() > cellPos) { + // add the contents + appendFragments(cellPos, cell.lastPosition()); + } + } + } + + // add end of table + int end = table->lastPosition(); + appendFragment(end, end+1, objectIndex); + } else { + appendFragments(cursor.selectionStart(), cursor.selectionEnd()); + } +} + +QTextDocumentFragmentPrivate::QTextDocumentFragmentPrivate(const QTextCursor &_cursor) + : ref(1), doc(new QTextDocument), importedFromPlainText(false) +{ + doc->setUndoRedoEnabled(false); + + if (!_cursor.hasSelection()) + return; + + doc->docHandle()->beginEditBlock(); + QTextCursor destCursor(doc); + QTextCopyHelper(_cursor, destCursor).copy(); + doc->docHandle()->endEditBlock(); + + if (_cursor.d) + doc->docHandle()->mergeCachedResources(_cursor.d->priv); +} + +void QTextDocumentFragmentPrivate::insert(QTextCursor &_cursor) const +{ + if (_cursor.isNull()) + return; + + QTextDocumentPrivate *destPieceTable = _cursor.d->priv; + destPieceTable->beginEditBlock(); + + QTextCursor sourceCursor(doc); + sourceCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + QTextCopyHelper(sourceCursor, _cursor, importedFromPlainText, _cursor.charFormat()).copy(); + + destPieceTable->endEditBlock(); +} + +/*! + \class QTextDocumentFragment + \reentrant + + \brief The QTextDocumentFragment class represents a piece of formatted text + from a QTextDocument. + + \ingroup text + \ingroup shared + + A QTextDocumentFragment is a fragment of rich text, that can be inserted into + a QTextDocument. A document fragment can be created from a + QTextDocument, from a QTextCursor's selection, or from another + document fragment. Document fragments can also be created by the + static functions, fromPlainText() and fromHtml(). + + The contents of a document fragment can be obtained as plain text + by using the toPlainText() function, or it can be obtained as HTML + with toHtml(). +*/ + + +/*! + Constructs an empty QTextDocumentFragment. + + \sa isEmpty() +*/ +QTextDocumentFragment::QTextDocumentFragment() + : d(0) +{ +} + +/*! + Converts the given \a document into a QTextDocumentFragment. + Note that the QTextDocumentFragment only stores the document contents, not meta information + like the document's title. +*/ +QTextDocumentFragment::QTextDocumentFragment(const QTextDocument *document) + : d(0) +{ + if (!document) + return; + + QTextCursor cursor(const_cast<QTextDocument *>(document)); + cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + d = new QTextDocumentFragmentPrivate(cursor); +} + +/*! + Creates a QTextDocumentFragment from the \a{cursor}'s selection. + If the cursor doesn't have a selection, the created fragment is empty. + + \sa isEmpty() QTextCursor::selection() +*/ +QTextDocumentFragment::QTextDocumentFragment(const QTextCursor &cursor) + : d(0) +{ + if (!cursor.hasSelection()) + return; + + d = new QTextDocumentFragmentPrivate(cursor); +} + +/*! + \fn QTextDocumentFragment::QTextDocumentFragment(const QTextDocumentFragment &other) + + Copy constructor. Creates a copy of the \a other fragment. +*/ +QTextDocumentFragment::QTextDocumentFragment(const QTextDocumentFragment &rhs) + : d(rhs.d) +{ + if (d) + d->ref.ref(); +} + +/*! + \fn QTextDocumentFragment &QTextDocumentFragment::operator=(const QTextDocumentFragment &other) + + Assigns the \a other fragment to this fragment. +*/ +QTextDocumentFragment &QTextDocumentFragment::operator=(const QTextDocumentFragment &rhs) +{ + if (rhs.d) + rhs.d->ref.ref(); + if (d && !d->ref.deref()) + delete d; + d = rhs.d; + return *this; +} + +/*! + Destroys the document fragment. +*/ +QTextDocumentFragment::~QTextDocumentFragment() +{ + if (d && !d->ref.deref()) + delete d; +} + +/*! + Returns true if the fragment is empty; otherwise returns false. +*/ +bool QTextDocumentFragment::isEmpty() const +{ + return !d || !d->doc || d->doc->docHandle()->length() <= 1; +} + +/*! + Returns the document fragment's text as plain text (i.e. with no + formatting information). + + \sa toHtml() +*/ +QString QTextDocumentFragment::toPlainText() const +{ + if (!d) + return QString(); + + return d->doc->toPlainText(); +} + +// #### Qt 5: merge with other overload +/*! + \overload +*/ + +#ifndef QT_NO_TEXTHTMLPARSER + +QString QTextDocumentFragment::toHtml() const +{ + return toHtml(QByteArray()); +} + +/*! + \since 4.2 + + Returns the contents of the document fragment as HTML, + using the specified \a encoding (e.g., "UTF-8", "ISO 8859-1"). + + \sa toPlainText(), QTextDocument::toHtml(), QTextCodec +*/ +QString QTextDocumentFragment::toHtml(const QByteArray &encoding) const +{ + if (!d) + return QString(); + + return QTextHtmlExporter(d->doc).toHtml(encoding, QTextHtmlExporter::ExportFragment); +} + +#endif // QT_NO_TEXTHTMLPARSER + +/*! + Returns a document fragment that contains the given \a plainText. + + When inserting such a fragment into a QTextDocument the current char format of + the QTextCursor used for insertion is used as format for the text. +*/ +QTextDocumentFragment QTextDocumentFragment::fromPlainText(const QString &plainText) +{ + QTextDocumentFragment res; + + res.d = new QTextDocumentFragmentPrivate; + res.d->importedFromPlainText = true; + QTextCursor cursor(res.d->doc); + cursor.insertText(plainText); + return res; +} + +static QTextListFormat::Style nextListStyle(QTextListFormat::Style style) +{ + if (style == QTextListFormat::ListDisc) + return QTextListFormat::ListCircle; + else if (style == QTextListFormat::ListCircle) + return QTextListFormat::ListSquare; + return style; +} + +#ifndef QT_NO_TEXTHTMLPARSER + +QTextHtmlImporter::QTextHtmlImporter(QTextDocument *_doc, const QString &_html, ImportMode mode, const QTextDocument *resourceProvider) + : indent(0), compressNextWhitespace(PreserveWhiteSpace), doc(_doc), importMode(mode) +{ + cursor = QTextCursor(doc); + wsm = QTextHtmlParserNode::WhiteSpaceNormal; + + QString html = _html; + const int startFragmentPos = html.indexOf(QLatin1String("<!--StartFragment-->")); + if (startFragmentPos != -1) { + QString qt3RichTextHeader(QLatin1String("<meta name=\"qrichtext\" content=\"1\" />")); + + // Hack for Qt3 + const bool hasQtRichtextMetaTag = html.contains(qt3RichTextHeader); + + const int endFragmentPos = html.indexOf(QLatin1String("<!--EndFragment-->")); + if (startFragmentPos < endFragmentPos) + html = html.mid(startFragmentPos, endFragmentPos - startFragmentPos); + else + html = html.mid(startFragmentPos); + + if (hasQtRichtextMetaTag) + html.prepend(qt3RichTextHeader); + } + + parse(html, resourceProvider ? resourceProvider : doc); +// dumpHtml(); +} + +void QTextHtmlImporter::import() +{ + cursor.beginEditBlock(); + hasBlock = true; + forceBlockMerging = false; + compressNextWhitespace = RemoveWhiteSpace; + blockTagClosed = false; + for (currentNodeIdx = 0; currentNodeIdx < count(); ++currentNodeIdx) { + currentNode = &at(currentNodeIdx); + wsm = textEditMode ? QTextHtmlParserNode::WhiteSpacePreWrap : currentNode->wsm; + + /* + * process each node in three stages: + * 1) check if the hierarchy changed and we therefore passed the + * equivalent of a closing tag -> we may need to finish off + * some structures like tables + * + * 2) check if the current node is a special node like a + * <table>, <ul> or <img> tag that requires special processing + * + * 3) if the node should result in a QTextBlock create one and + * finally insert text that may be attached to the node + */ + + /* emit 'closing' table blocks or adjust current indent level + * if we + * 1) are beyond the first node + * 2) the current node not being a child of the previous node + * means there was a tag closing in the input html + */ + if (currentNodeIdx > 0 && (currentNode->parent != currentNodeIdx - 1)) { + blockTagClosed = closeTag(); + // visually collapse subsequent block tags, but if the element after the closed block tag + // is for example an inline element (!isBlock) we have to make sure we start a new paragraph by setting + // hasBlock to false. + if (blockTagClosed + && !currentNode->isBlock() + && currentNode->id != Html_unknown) + { + hasBlock = false; + } else if (hasBlock) { + // when collapsing subsequent block tags we need to clear the block format + QTextBlockFormat blockFormat = currentNode->blockFormat; + blockFormat.setIndent(indent); + + QTextBlockFormat oldFormat = cursor.blockFormat(); + if (oldFormat.hasProperty(QTextFormat::PageBreakPolicy)) { + QTextFormat::PageBreakFlags pageBreak = oldFormat.pageBreakPolicy(); + if (pageBreak == QTextFormat::PageBreak_AlwaysAfter) + /* We remove an empty paragrah that requested a page break after. + moving that request to the next paragraph means we also need to make + that a pagebreak before to keep the same visual appearance. + */ + pageBreak = QTextFormat::PageBreak_AlwaysBefore; + blockFormat.setPageBreakPolicy(pageBreak); + } + + cursor.setBlockFormat(blockFormat); + } + } + + if (currentNode->displayMode == QTextHtmlElement::DisplayNone) { + if (currentNode->id == Html_title) + doc->setMetaInformation(QTextDocument::DocumentTitle, currentNode->text); + // ignore explicitly 'invisible' elements + continue; + } + + if (processSpecialNodes() == ContinueWithNextNode) + continue; + + // make sure there's a block for 'Blah' after <ul><li>foo</ul>Blah + if (blockTagClosed + && !hasBlock + && !currentNode->isBlock() + && !currentNode->text.isEmpty() && !currentNode->hasOnlyWhitespace() + && currentNode->displayMode == QTextHtmlElement::DisplayInline) { + + QTextBlockFormat block = currentNode->blockFormat; + block.setIndent(indent); + + appendBlock(block, currentNode->charFormat); + + hasBlock = true; + } + + if (currentNode->isBlock()) { + if (processBlockNode() == ContinueWithNextNode) + continue; + } + + if (currentNode->charFormat.isAnchor() && !currentNode->charFormat.anchorName().isEmpty()) { + namedAnchors.append(currentNode->charFormat.anchorName()); + } + + if (appendNodeText()) + hasBlock = false; // if we actually appended text then we don't + // have an empty block anymore + } + + cursor.endEditBlock(); +} + +bool QTextHtmlImporter::appendNodeText() +{ + const int initialCursorPosition = cursor.position(); + QTextCharFormat format = currentNode->charFormat; + + if(wsm == QTextHtmlParserNode::WhiteSpacePre || wsm == QTextHtmlParserNode::WhiteSpacePreWrap) + compressNextWhitespace = PreserveWhiteSpace; + + QString text = currentNode->text; + + QString textToInsert; + textToInsert.reserve(text.size()); + + for (int i = 0; i < text.length(); ++i) { + QChar ch = text.at(i); + + if (ch.isSpace() + && ch != QChar::Nbsp + && ch != QChar::ParagraphSeparator) { + + if (compressNextWhitespace == CollapseWhiteSpace) + compressNextWhitespace = RemoveWhiteSpace; // allow this one, and remove the ones coming next. + else if(compressNextWhitespace == RemoveWhiteSpace) + continue; + + if (wsm == QTextHtmlParserNode::WhiteSpacePre + || textEditMode + ) { + if (ch == QLatin1Char('\n')) { + if (textEditMode) + continue; + } else if (ch == QLatin1Char('\r')) { + continue; + } + } else if (wsm != QTextHtmlParserNode::WhiteSpacePreWrap) { + compressNextWhitespace = RemoveWhiteSpace; + if (wsm == QTextHtmlParserNode::WhiteSpaceNoWrap) + ch = QChar::Nbsp; + else + ch = QLatin1Char(' '); + } + } else { + compressNextWhitespace = PreserveWhiteSpace; + } + + if (ch == QLatin1Char('\n') + || ch == QChar::ParagraphSeparator) { + + if (!textToInsert.isEmpty()) { + cursor.insertText(textToInsert, format); + textToInsert.clear(); + } + + QTextBlockFormat fmt = cursor.blockFormat(); + + if (fmt.hasProperty(QTextFormat::BlockBottomMargin)) { + QTextBlockFormat tmp = fmt; + tmp.clearProperty(QTextFormat::BlockBottomMargin); + cursor.setBlockFormat(tmp); + } + + fmt.clearProperty(QTextFormat::BlockTopMargin); + appendBlock(fmt, cursor.charFormat()); + } else { + if (!namedAnchors.isEmpty()) { + if (!textToInsert.isEmpty()) { + cursor.insertText(textToInsert, format); + textToInsert.clear(); + } + + format.setAnchor(true); + format.setAnchorNames(namedAnchors); + cursor.insertText(ch, format); + namedAnchors.clear(); + format.clearProperty(QTextFormat::IsAnchor); + format.clearProperty(QTextFormat::AnchorName); + } else { + textToInsert += ch; + } + } + } + + if (!textToInsert.isEmpty()) { + cursor.insertText(textToInsert, format); + } + + return cursor.position() != initialCursorPosition; +} + +QTextHtmlImporter::ProcessNodeResult QTextHtmlImporter::processSpecialNodes() +{ + switch (currentNode->id) { + case Html_body: + if (currentNode->charFormat.background().style() != Qt::NoBrush) { + QTextFrameFormat fmt = doc->rootFrame()->frameFormat(); + fmt.setBackground(currentNode->charFormat.background()); + doc->rootFrame()->setFrameFormat(fmt); + const_cast<QTextHtmlParserNode *>(currentNode)->charFormat.clearProperty(QTextFormat::BackgroundBrush); + } + compressNextWhitespace = RemoveWhiteSpace; + break; + + case Html_ol: + case Html_ul: { + QTextListFormat::Style style = currentNode->listStyle; + + if (currentNode->id == Html_ul && !currentNode->hasOwnListStyle && currentNode->parent) { + const QTextHtmlParserNode *n = &at(currentNode->parent); + while (n) { + if (n->id == Html_ul) { + style = nextListStyle(currentNode->listStyle); + } + if (n->parent) + n = &at(n->parent); + else + n = 0; + } + } + + QTextListFormat listFmt; + listFmt.setStyle(style); + + ++indent; + if (currentNode->hasCssListIndent) + listFmt.setIndent(currentNode->cssListIndent); + else + listFmt.setIndent(indent); + + List l; + l.format = listFmt; + l.listNode = currentNodeIdx; + lists.append(l); + compressNextWhitespace = RemoveWhiteSpace; + + // broken html: <ul>Text here<li>Foo + const QString simpl = currentNode->text.simplified(); + if (simpl.isEmpty() || simpl.at(0).isSpace()) + return ContinueWithNextNode; + break; + } + + case Html_table: { + Table t = scanTable(currentNodeIdx); + tables.append(t); + hasBlock = false; + compressNextWhitespace = RemoveWhiteSpace; + return ContinueWithNextNode; + } + + case Html_tr: + return ContinueWithNextNode; + + case Html_img: { + QTextImageFormat fmt; + fmt.setName(currentNode->imageName); + + fmt.merge(currentNode->charFormat); + + if (currentNode->imageWidth != -1) + fmt.setWidth(currentNode->imageWidth); + if (currentNode->imageHeight != -1) + fmt.setHeight(currentNode->imageHeight); + + cursor.insertImage(fmt, QTextFrameFormat::Position(currentNode->cssFloat)); + + cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor); + cursor.mergeCharFormat(currentNode->charFormat); + cursor.movePosition(QTextCursor::Right); + compressNextWhitespace = CollapseWhiteSpace; + + hasBlock = false; + return ContinueWithNextNode; + } + + case Html_hr: { + QTextBlockFormat blockFormat = currentNode->blockFormat; + blockFormat.setTopMargin(topMargin(currentNodeIdx)); + blockFormat.setBottomMargin(bottomMargin(currentNodeIdx)); + blockFormat.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, currentNode->width); + if (hasBlock && importMode == ImportToDocument) + cursor.mergeBlockFormat(blockFormat); + else + appendBlock(blockFormat); + hasBlock = false; + compressNextWhitespace = RemoveWhiteSpace; + return ContinueWithNextNode; + } + + default: break; + } + return ContinueWithCurrentNode; +} + +// returns true if a block tag was closed +bool QTextHtmlImporter::closeTag() +{ + const QTextHtmlParserNode *closedNode = &at(currentNodeIdx - 1); + const int endDepth = depth(currentNodeIdx) - 1; + int depth = this->depth(currentNodeIdx - 1); + bool blockTagClosed = false; + + while (depth > endDepth) { + Table *t = 0; + if (!tables.isEmpty()) + t = &tables.last(); + + switch (closedNode->id) { + case Html_tr: + if (t && !t->isTextFrame) { + ++t->currentRow; + + // for broken html with rowspans but missing tr tags + while (!t->currentCell.atEnd() && t->currentCell.row < t->currentRow) + ++t->currentCell; + } + + blockTagClosed = true; + break; + + case Html_table: + if (!t) + break; + indent = t->lastIndent; + + tables.resize(tables.size() - 1); + t = 0; + + if (tables.isEmpty()) { + cursor = doc->rootFrame()->lastCursorPosition(); + } else { + t = &tables.last(); + if (t->isTextFrame) + cursor = t->frame->lastCursorPosition(); + else if (!t->currentCell.atEnd()) + cursor = t->currentCell.cell().lastCursorPosition(); + } + + // we don't need an extra block after tables, so we don't + // claim to have closed one for the creation of a new one + // in import() + blockTagClosed = false; + compressNextWhitespace = RemoveWhiteSpace; + break; + + case Html_th: + case Html_td: + if (t && !t->isTextFrame) + ++t->currentCell; + blockTagClosed = true; + compressNextWhitespace = RemoveWhiteSpace; + break; + + case Html_ol: + case Html_ul: + if (lists.isEmpty()) + break; + lists.resize(lists.size() - 1); + --indent; + blockTagClosed = true; + break; + + case Html_br: + compressNextWhitespace = RemoveWhiteSpace; + break; + + case Html_div: + if (closedNode->children.isEmpty()) + break; + // fall through + default: + if (closedNode->isBlock()) + blockTagClosed = true; + break; + } + + closedNode = &at(closedNode->parent); + --depth; + } + + return blockTagClosed; +} + +QTextHtmlImporter::Table QTextHtmlImporter::scanTable(int tableNodeIdx) +{ + Table table; + table.columns = 0; + + QVector<QTextLength> columnWidths; + + int tableHeaderRowCount = 0; + QVector<int> rowNodes; + rowNodes.reserve(at(tableNodeIdx).children.count()); + foreach (int row, at(tableNodeIdx).children) + switch (at(row).id) { + case Html_tr: + rowNodes += row; + break; + case Html_thead: + case Html_tbody: + case Html_tfoot: + foreach (int potentialRow, at(row).children) + if (at(potentialRow).id == Html_tr) { + rowNodes += potentialRow; + if (at(row).id == Html_thead) + ++tableHeaderRowCount; + } + break; + default: break; + } + + QVector<RowColSpanInfo> rowColSpans; + QVector<RowColSpanInfo> rowColSpanForColumn; + + int effectiveRow = 0; + foreach (int row, rowNodes) { + int colsInRow = 0; + + foreach (int cell, at(row).children) + if (at(cell).isTableCell()) { + // skip all columns with spans from previous rows + while (colsInRow < rowColSpanForColumn.size()) { + const RowColSpanInfo &spanInfo = rowColSpanForColumn[colsInRow]; + + if (spanInfo.row + spanInfo.rowSpan > effectiveRow) { + Q_ASSERT(spanInfo.col == colsInRow); + colsInRow += spanInfo.colSpan; + } else + break; + } + + const QTextHtmlParserNode &c = at(cell); + const int currentColumn = colsInRow; + colsInRow += c.tableCellColSpan; + + RowColSpanInfo spanInfo; + spanInfo.row = effectiveRow; + spanInfo.col = currentColumn; + spanInfo.colSpan = c.tableCellColSpan; + spanInfo.rowSpan = c.tableCellRowSpan; + if (spanInfo.colSpan > 1 || spanInfo.rowSpan > 1) + rowColSpans.append(spanInfo); + + columnWidths.resize(qMax(columnWidths.count(), colsInRow)); + rowColSpanForColumn.resize(columnWidths.size()); + for (int i = currentColumn; i < currentColumn + c.tableCellColSpan; ++i) { + if (columnWidths.at(i).type() == QTextLength::VariableLength) { + QTextLength w = c.width; + if (c.tableCellColSpan > 1 && w.type() != QTextLength::VariableLength) + w = QTextLength(w.type(), w.value(100.) / c.tableCellColSpan); + columnWidths[i] = w; + } + rowColSpanForColumn[i] = spanInfo; + } + } + + table.columns = qMax(table.columns, colsInRow); + + ++effectiveRow; + } + table.rows = effectiveRow; + + table.lastIndent = indent; + indent = 0; + + if (table.rows == 0 || table.columns == 0) + return table; + + QTextFrameFormat fmt; + const QTextHtmlParserNode &node = at(tableNodeIdx); + + if (!node.isTextFrame) { + QTextTableFormat tableFmt; + tableFmt.setCellSpacing(node.tableCellSpacing); + tableFmt.setCellPadding(node.tableCellPadding); + if (node.blockFormat.hasProperty(QTextFormat::BlockAlignment)) + tableFmt.setAlignment(node.blockFormat.alignment()); + tableFmt.setColumns(table.columns); + tableFmt.setColumnWidthConstraints(columnWidths); + tableFmt.setHeaderRowCount(tableHeaderRowCount); + fmt = tableFmt; + } + + fmt.setTopMargin(topMargin(tableNodeIdx)); + fmt.setBottomMargin(bottomMargin(tableNodeIdx)); + fmt.setLeftMargin(leftMargin(tableNodeIdx) + + table.lastIndent * 40 // ##### not a good emulation + ); + fmt.setRightMargin(rightMargin(tableNodeIdx)); + + // compatibility + if (qFuzzyCompare(fmt.leftMargin(), fmt.rightMargin()) + && qFuzzyCompare(fmt.leftMargin(), fmt.topMargin()) + && qFuzzyCompare(fmt.leftMargin(), fmt.bottomMargin())) + fmt.setProperty(QTextFormat::FrameMargin, fmt.leftMargin()); + + fmt.setBorderStyle(node.borderStyle); + fmt.setBorderBrush(node.borderBrush); + fmt.setBorder(node.tableBorder); + fmt.setWidth(node.width); + fmt.setHeight(node.height); + if (node.blockFormat.hasProperty(QTextFormat::PageBreakPolicy)) + fmt.setPageBreakPolicy(node.blockFormat.pageBreakPolicy()); + + if (node.blockFormat.hasProperty(QTextFormat::LayoutDirection)) + fmt.setLayoutDirection(node.blockFormat.layoutDirection()); + if (node.charFormat.background().style() != Qt::NoBrush) + fmt.setBackground(node.charFormat.background()); + fmt.setPosition(QTextFrameFormat::Position(node.cssFloat)); + + if (node.isTextFrame) { + if (node.isRootFrame) { + table.frame = cursor.currentFrame(); + table.frame->setFrameFormat(fmt); + } else + table.frame = cursor.insertFrame(fmt); + + table.isTextFrame = true; + } else { + const int oldPos = cursor.position(); + QTextTable *textTable = cursor.insertTable(table.rows, table.columns, fmt.toTableFormat()); + table.frame = textTable; + + for (int i = 0; i < rowColSpans.count(); ++i) { + const RowColSpanInfo &nfo = rowColSpans.at(i); + textTable->mergeCells(nfo.row, nfo.col, nfo.rowSpan, nfo.colSpan); + } + + table.currentCell = TableCellIterator(textTable); + cursor.setPosition(oldPos); // restore for caption support which needs to be inserted right before the table + } + return table; +} + +QTextHtmlImporter::ProcessNodeResult QTextHtmlImporter::processBlockNode() +{ + QTextBlockFormat block; + QTextCharFormat charFmt; + bool modifiedBlockFormat = true; + bool modifiedCharFormat = true; + + if (currentNode->isTableCell() && !tables.isEmpty()) { + Table &t = tables.last(); + if (!t.isTextFrame && !t.currentCell.atEnd()) { + QTextTableCell cell = t.currentCell.cell(); + if (cell.isValid()) { + QTextTableCellFormat fmt = cell.format().toTableCellFormat(); + if (topPadding(currentNodeIdx) >= 0) + fmt.setTopPadding(topPadding(currentNodeIdx)); + if (bottomPadding(currentNodeIdx) >= 0) + fmt.setBottomPadding(bottomPadding(currentNodeIdx)); + if (leftPadding(currentNodeIdx) >= 0) + fmt.setLeftPadding(leftPadding(currentNodeIdx)); + if (rightPadding(currentNodeIdx) >= 0) + fmt.setRightPadding(rightPadding(currentNodeIdx)); + cell.setFormat(fmt); + + cursor.setPosition(cell.firstPosition()); + } + } + hasBlock = true; + compressNextWhitespace = RemoveWhiteSpace; + + if (currentNode->charFormat.background().style() != Qt::NoBrush) { + charFmt.setBackground(currentNode->charFormat.background()); + cursor.mergeBlockCharFormat(charFmt); + } + } + + if (hasBlock) { + block = cursor.blockFormat(); + charFmt = cursor.blockCharFormat(); + modifiedBlockFormat = false; + modifiedCharFormat = false; + } + + // collapse + { + qreal tm = qreal(topMargin(currentNodeIdx)); + if (tm > block.topMargin()) { + block.setTopMargin(tm); + modifiedBlockFormat = true; + } + } + + int bottomMargin = this->bottomMargin(currentNodeIdx); + + // for list items we may want to collapse with the bottom margin of the + // list. + const QTextHtmlParserNode *parentNode = currentNode->parent ? &at(currentNode->parent) : 0; + if ((currentNode->id == Html_li || currentNode->id == Html_dt || currentNode->id == Html_dd) + && parentNode + && (parentNode->isListStart() || parentNode->id == Html_dl) + && (parentNode->children.last() == currentNodeIdx)) { + bottomMargin = qMax(bottomMargin, this->bottomMargin(currentNode->parent)); + } + + if (block.bottomMargin() != bottomMargin) { + block.setBottomMargin(bottomMargin); + modifiedBlockFormat = true; + } + + { + const qreal lm = leftMargin(currentNodeIdx); + const qreal rm = rightMargin(currentNodeIdx); + + if (block.leftMargin() != lm) { + block.setLeftMargin(lm); + modifiedBlockFormat = true; + } + if (block.rightMargin() != rm) { + block.setRightMargin(rm); + modifiedBlockFormat = true; + } + } + + if (currentNode->id != Html_li + && indent != 0 + && (lists.isEmpty() + || !hasBlock + || !lists.last().list + || lists.last().list->itemNumber(cursor.block()) == -1 + ) + ) { + block.setIndent(indent); + modifiedBlockFormat = true; + } + + if (currentNode->blockFormat.propertyCount() > 0) { + modifiedBlockFormat = true; + block.merge(currentNode->blockFormat); + } + + if (currentNode->charFormat.propertyCount() > 0) { + modifiedCharFormat = true; + charFmt.merge(currentNode->charFormat); + } + + // #################### + // block.setFloatPosition(node->cssFloat); + + if (wsm == QTextHtmlParserNode::WhiteSpacePre) { + block.setNonBreakableLines(true); + modifiedBlockFormat = true; + } + + if (currentNode->charFormat.background().style() != Qt::NoBrush && !currentNode->isTableCell()) { + block.setBackground(currentNode->charFormat.background()); + modifiedBlockFormat = true; + } + + if (hasBlock && (!currentNode->isEmptyParagraph || forceBlockMerging)) { + if (modifiedBlockFormat) + cursor.setBlockFormat(block); + if (modifiedCharFormat) + cursor.setBlockCharFormat(charFmt); + } else { + if (currentNodeIdx == 1 && cursor.position() == 0 && currentNode->isEmptyParagraph) { + cursor.setBlockFormat(block); + cursor.setBlockCharFormat(charFmt); + } else { + appendBlock(block, charFmt); + } + } + + if (currentNode->userState != -1) + cursor.block().setUserState(currentNode->userState); + + if (currentNode->id == Html_li && !lists.isEmpty()) { + List &l = lists.last(); + if (l.list) { + l.list->add(cursor.block()); + } else { + l.list = cursor.createList(l.format); + const qreal listTopMargin = topMargin(l.listNode); + if (listTopMargin > block.topMargin()) { + block.setTopMargin(listTopMargin); + cursor.mergeBlockFormat(block); + } + } + if (hasBlock) { + QTextBlockFormat fmt; + fmt.setIndent(0); + cursor.mergeBlockFormat(fmt); + } + } + + forceBlockMerging = false; + if (currentNode->id == Html_body || currentNode->id == Html_html) + forceBlockMerging = true; + + if (currentNode->isEmptyParagraph) { + hasBlock = false; + return ContinueWithNextNode; + } + + hasBlock = true; + blockTagClosed = false; + return ContinueWithCurrentNode; +} + +void QTextHtmlImporter::appendBlock(const QTextBlockFormat &format, QTextCharFormat charFmt) +{ + if (!namedAnchors.isEmpty()) { + charFmt.setAnchor(true); + charFmt.setAnchorNames(namedAnchors); + namedAnchors.clear(); + } + + cursor.insertBlock(format, charFmt); + + if (wsm != QTextHtmlParserNode::WhiteSpacePre && wsm != QTextHtmlParserNode::WhiteSpacePreWrap) + compressNextWhitespace = RemoveWhiteSpace; +} + +#endif // QT_NO_TEXTHTMLPARSER + +/*! + \fn QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &text) + + Returns a QTextDocumentFragment based on the arbitrary piece of + HTML in the given \a text. The formatting is preserved as much as + possible; for example, "<b>bold</b>" will become a document + fragment with the text "bold" with a bold character format. +*/ + +#ifndef QT_NO_TEXTHTMLPARSER + +QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &html) +{ + return fromHtml(html, 0); +} + +/*! + \fn QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &text, const QTextDocument *resourceProvider) + \since 4.2 + + Returns a QTextDocumentFragment based on the arbitrary piece of + HTML in the given \a text. The formatting is preserved as much as + possible; for example, "<b>bold</b>" will become a document + fragment with the text "bold" with a bold character format. + + If the provided HTML contains references to external resources such as imported style sheets, then + they will be loaded through the \a resourceProvider. +*/ + +QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &html, const QTextDocument *resourceProvider) +{ + QTextDocumentFragment res; + res.d = new QTextDocumentFragmentPrivate; + + QTextHtmlImporter importer(res.d->doc, html, QTextHtmlImporter::ImportToFragment, resourceProvider); + importer.import(); + return res; +} + +QT_END_NAMESPACE +#endif // QT_NO_TEXTHTMLPARSER |