Files
firefox/dom/serializers/nsPlainTextSerializer.cpp

1923 lines
63 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* nsIContentSerializer implementation that can be used with an
* nsIDocumentEncoder to convert a DOM into plaintext in a nice way
* (eg for copy/paste as plaintext).
*/
#include "nsPlainTextSerializer.h"
#include "mozilla/Casting.h"
#include "mozilla/Preferences.h"
#include "mozilla/Span.h"
#include "mozilla/StaticPrefs_converter.h"
#include "mozilla/TextEditor.h"
#include "mozilla/Utf16.h"
#include "mozilla/dom/AbstractRange.h"
#include "mozilla/dom/CharacterData.h"
#include "mozilla/dom/CharacterDataBuffer.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/Text.h"
#include "mozilla/intl/Segmenter.h"
#include "mozilla/intl/UnicodeProperties.h"
#include "nsCRT.h"
#include "nsComputedDOMStyle.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsGkAtoms.h"
#include "nsIDocumentEncoder.h"
#include "nsPrintfCString.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsUnicodeProperties.h"
namespace mozilla {
class Encoding;
}
using namespace mozilla;
using namespace mozilla::dom;
#define PREF_STRUCTS "converter.html2txt.structs"
#define PREF_HEADER_STRATEGY "converter.html2txt.header_strategy"
static const int32_t kTabSize = 4;
static const int32_t kIndentSizeHeaders =
2; /* Indention of h1, if
mHeaderStrategy = kIndentIncreasedWithHeaderLevel
or = kNumberHeadingsAndIndentSlightly. Indention of
other headers is derived from that. */
static const int32_t kIndentIncrementHeaders =
2; /* If mHeaderStrategy = kIndentIncreasedWithHeaderLevel,
indent h(x+1) this many
columns more than h(x) */
static const int32_t kIndentSizeList = kTabSize;
// Indention of non-first lines of ul and ol
static const int32_t kIndentSizeDD = kTabSize; // Indention of <dd>
static const char16_t kNBSP = 160;
static const char16_t kSPACE = ' ';
static int32_t HeaderLevel(const nsAtom* aTag);
static int32_t GetUnicharWidth(char32_t ucs);
static int32_t GetUnicharStringWidth(Span<const char16_t> aString);
// Someday may want to make this non-const:
static const uint32_t TagStackSize = 500;
NS_IMPL_CYCLE_COLLECTING_ADDREF(nsPlainTextSerializer)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsPlainTextSerializer)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsPlainTextSerializer)
NS_INTERFACE_MAP_ENTRY(nsIContentSerializer)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION(nsPlainTextSerializer)
nsresult NS_NewPlainTextSerializer(nsIContentSerializer** aSerializer) {
RefPtr<nsPlainTextSerializer> it = new nsPlainTextSerializer();
it.forget(aSerializer);
return NS_OK;
}
// @param aFlags As defined in nsIDocumentEncoder.idl.
static void DetermineLineBreak(const int32_t aFlags, nsAString& aLineBreak) {
// Set the line break character:
if ((aFlags & nsIDocumentEncoder::OutputCRLineBreak) &&
(aFlags & nsIDocumentEncoder::OutputLFLineBreak)) {
// Windows
aLineBreak.AssignLiteral(u"\r\n");
} else if (aFlags & nsIDocumentEncoder::OutputCRLineBreak) {
// Mac
aLineBreak.AssignLiteral(u"\r");
} else if (aFlags & nsIDocumentEncoder::OutputLFLineBreak) {
// Unix/DOM
aLineBreak.AssignLiteral(u"\n");
} else {
// Platform/default
aLineBreak.AssignLiteral(NS_ULINEBREAK);
}
}
void nsPlainTextSerializer::CurrentLine::MaybeReplaceNbspsInContent(
const int32_t aFlags) {
if (!(aFlags & nsIDocumentEncoder::OutputPersistNBSP)) {
// First, replace all nbsp characters with spaces,
// which the unicode encoder won't do for us.
mContent.ReplaceChar(kNBSP, kSPACE);
}
}
void nsPlainTextSerializer::CurrentLine::ResetContentAndIndentationHeader() {
mContent.Truncate();
mIndentation.mHeader.Truncate();
}
int32_t nsPlainTextSerializer::CurrentLine::FindWrapIndexForContent(
const uint32_t aWrapColumn, bool aUseLineBreaker) const {
MOZ_ASSERT(!mContent.IsEmpty());
const uint32_t prefixwidth = DeterminePrefixWidth();
int32_t goodSpace = 0;
if (aUseLineBreaker) {
// We advance one line break point at a time from the beginning of the
// mContent until we find a width less than or equal to wrap column.
uint32_t width = 0;
intl::LineBreakIteratorUtf16 lineBreakIter(mContent);
while (Maybe<uint32_t> nextGoodSpace = lineBreakIter.Next()) {
// Trim space at the tail. UAX#14 doesn't have break opportunity for
// ASCII space at the tail.
const Maybe<uint32_t> originalNextGoodSpace = nextGoodSpace;
while (*nextGoodSpace > 0 &&
mContent.CharAt(*nextGoodSpace - 1) == u' ') {
*nextGoodSpace -= 1;
}
if (*nextGoodSpace == 0) {
// Restore the original nextGoodSpace.
nextGoodSpace = originalNextGoodSpace;
}
width += GetUnicharStringWidth(Span<const char16_t>(
mContent.get() + goodSpace, *nextGoodSpace - goodSpace));
if (prefixwidth + width > aWrapColumn) {
// The next break point makes the width exceeding the wrap column, or
// there was a newline, so goodSpace is what we want.
break;
}
goodSpace = AssertedCast<int32_t>(*nextGoodSpace);
if (mContent.CharAt(*nextGoodSpace) == u'\n') {
// If we hit a newline, also stop looking now, but account for it.
goodSpace += 1;
break;
}
}
return goodSpace;
}
// In this case we don't want strings, especially CJK-ones, to be split. See
// bug 333064 for more information. We break only at ASCII spaces.
if (aWrapColumn >= prefixwidth) {
// Search backward from the adjusted wrap column or from the text end.
goodSpace =
std::min<int32_t>(aWrapColumn - prefixwidth, mContent.Length() - 1);
while (goodSpace >= 0) {
if (nsCRT::IsAsciiSpace(mContent.CharAt(goodSpace))) {
return goodSpace;
}
goodSpace--;
}
}
// Search forward from the adjusted wrap column.
goodSpace = (prefixwidth > aWrapColumn) ? 1 : aWrapColumn - prefixwidth;
const int32_t contentLength = mContent.Length();
while (goodSpace < contentLength &&
!nsCRT::IsAsciiSpace(mContent.CharAt(goodSpace))) {
goodSpace++;
}
return goodSpace;
}
nsPlainTextSerializer::OutputManager::OutputManager(const int32_t aFlags,
nsAString& aOutput)
: mFlags{aFlags}, mOutput{aOutput}, mAtFirstColumn{true} {
MOZ_ASSERT(aOutput.IsEmpty());
DetermineLineBreak(mFlags, mLineBreak);
}
void nsPlainTextSerializer::OutputManager::Append(
const CurrentLine& aLine,
const StripTrailingWhitespaces aStripTrailingWhitespaces) {
if (IsAtFirstColumn()) {
nsAutoString quotesAndIndent;
aLine.CreateQuotesAndIndent(quotesAndIndent);
if ((aStripTrailingWhitespaces == StripTrailingWhitespaces::kMaybe)) {
const bool stripTrailingSpaces = aLine.mContent.IsEmpty();
if (stripTrailingSpaces) {
quotesAndIndent.Trim(" ", false, true, false);
}
}
Append(quotesAndIndent);
}
Append(aLine.mContent);
}
void nsPlainTextSerializer::OutputManager::Append(const nsAString& aString) {
if (!aString.IsEmpty()) {
mOutput.Append(aString);
mAtFirstColumn = false;
}
}
void nsPlainTextSerializer::OutputManager::AppendLineBreak(bool aForceCRLF) {
mOutput.Append(aForceCRLF ? u"\r\n"_ns : mLineBreak);
mAtFirstColumn = true;
}
uint32_t nsPlainTextSerializer::OutputManager::GetOutputLength() const {
return mOutput.Length();
}
nsPlainTextSerializer::nsPlainTextSerializer()
: mFloatingLines(-1),
kSpace(u" "_ns) // Init of "constant"
{
mSpanLevel = 0;
for (int32_t i = 0; i <= 6; i++) {
mHeaderCounter[i] = 0;
}
// Flow
mEmptyLines = 1; // The start of the document is an "empty line" in itself,
mInWhitespace = false;
mPreFormattedMail = false;
mPreformattedBlockBoundary = false;
// initialize the tag stack to zero:
// The stack only ever contains pointers to static atoms, so they don't
// need refcounting.
mTagStack = new const nsAtom*[TagStackSize];
mTagStackIndex = 0;
mIgnoreAboveIndex = (uint32_t)kNotFound;
mULCount = 0;
}
nsPlainTextSerializer::~nsPlainTextSerializer() {
delete[] mTagStack;
NS_WARNING_ASSERTION(mHeadLevel == 0, "Wrong head level!");
}
nsPlainTextSerializer::Settings::HeaderStrategy
nsPlainTextSerializer::Settings::Convert(const int32_t aPrefHeaderStrategy) {
HeaderStrategy result{HeaderStrategy::kIndentIncreasedWithHeaderLevel};
switch (aPrefHeaderStrategy) {
case 0: {
result = HeaderStrategy::kNoIndentation;
break;
}
case 1: {
result = HeaderStrategy::kIndentIncreasedWithHeaderLevel;
break;
}
case 2: {
result = HeaderStrategy::kNumberHeadingsAndIndentSlightly;
break;
}
default: {
NS_WARNING(
nsPrintfCString("Header strategy pref contains undefined value: %i",
aPrefHeaderStrategy)
.get());
}
}
return result;
}
const int32_t kDefaultHeaderStrategy = 1;
void nsPlainTextSerializer::Settings::Init(const int32_t aFlags,
const uint32_t aWrapColumn) {
mFlags = aFlags;
if (mFlags & nsIDocumentEncoder::OutputFormatted) {
// Get some prefs that controls how we do formatted output
mStructs = Preferences::GetBool(PREF_STRUCTS, mStructs);
int32_t headerStrategy =
Preferences::GetInt(PREF_HEADER_STRATEGY, kDefaultHeaderStrategy);
mHeaderStrategy = Convert(headerStrategy);
}
mWithRubyAnnotation = StaticPrefs::converter_html2txt_always_include_ruby() ||
(mFlags & nsIDocumentEncoder::OutputRubyAnnotation);
// XXX We should let the caller decide whether to do this or not
mFlags &= ~nsIDocumentEncoder::OutputNoFramesContent;
mWrapColumn = aWrapColumn;
}
void nsPlainTextSerializer::HardWrapString(nsAString& aString,
uint32_t aWrapColumn,
int32_t aFlags) {
MOZ_ASSERT(aFlags & nsIDocumentEncoder::OutputWrap, "Why?");
MOZ_ASSERT(aWrapColumn, "Why?");
Settings settings;
settings.Init(aFlags, aWrapColumn);
// Line breaker will do the right thing, no need to split manually.
nsAutoString output;
{
OutputManager manager(aFlags, output);
CurrentLine line;
bool first = true;
for (const auto& content : aString.Split(u'\n')) {
if (first) {
first = false;
} else {
manager.Flush(line);
manager.AppendLineBreak();
}
line.mContent.Assign(content);
PerformWrapAndOutputCompleteLines(settings, line, manager,
/* aUseLineBreaker = */ true,
/* aAllowBonusWidth = */ false,
nullptr);
}
manager.Flush(line);
}
aString.Assign(std::move(output));
}
NS_IMETHODIMP
nsPlainTextSerializer::Init(const uint32_t aFlags, uint32_t aWrapColumn,
const Encoding* aEncoding, bool aIsCopying,
bool aIsWholeDocument,
bool* aNeedsPreformatScanning, nsAString& aOutput) {
#ifdef DEBUG
// Check if the major control flags are set correctly.
if (aFlags & nsIDocumentEncoder::OutputFormatFlowed) {
// One of OutputFormatted or OutputWrap must be set, but not both.
NS_ASSERTION((aFlags & nsIDocumentEncoder::OutputFormatted) !=
(aFlags & nsIDocumentEncoder::OutputWrap),
"If you want format=flowed, you must combine it "
"with either nsIDocumentEncoder::OutputFormatted "
"or nsIDocumentEncoder::OutputWrap");
}
if (aFlags & nsIDocumentEncoder::OutputFormatted) {
NS_ASSERTION(
!(aFlags & nsIDocumentEncoder::OutputPreformatted),
"Can't do formatted and preformatted output at the same time!");
}
#endif
MOZ_ASSERT(!(aFlags & nsIDocumentEncoder::OutputFormatDelSp) ||
(aFlags & nsIDocumentEncoder::OutputFormatFlowed));
*aNeedsPreformatScanning = true;
mSettings.Init(aFlags, aWrapColumn);
mOutputManager.emplace(mSettings.GetFlags(), aOutput);
mUseLineBreaker = mSettings.MayWrap() && mSettings.MayBreakLines();
mLineBreakDue = false;
mFloatingLines = -1;
mPreformattedBlockBoundary = false;
MOZ_ASSERT(mOLStack.IsEmpty());
return NS_OK;
}
bool nsPlainTextSerializer::GetLastBool(const nsTArray<bool>& aStack) {
uint32_t size = aStack.Length();
if (size == 0) {
return false;
}
return aStack.ElementAt(size - 1);
}
void nsPlainTextSerializer::SetLastBool(nsTArray<bool>& aStack, bool aValue) {
uint32_t size = aStack.Length();
if (size > 0) {
aStack.ElementAt(size - 1) = aValue;
} else {
NS_ERROR("There is no \"Last\" value");
}
}
void nsPlainTextSerializer::PushBool(nsTArray<bool>& aStack, bool aValue) {
aStack.AppendElement(bool(aValue));
}
bool nsPlainTextSerializer::PopBool(nsTArray<bool>& aStack) {
return aStack.Length() ? aStack.PopLastElement() : false;
}
bool nsPlainTextSerializer::IsIgnorableRubyAnnotation(
const nsAtom* aTag) const {
if (mSettings.GetWithRubyAnnotation()) {
return false;
}
return aTag == nsGkAtoms::rp || aTag == nsGkAtoms::rt ||
aTag == nsGkAtoms::rtc;
}
// Return true if aElement has 'display:none' or if we just don't know.
static bool IsDisplayNone(Element* aElement) {
RefPtr<const ComputedStyle> computedStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(aElement);
return !computedStyle ||
computedStyle->StyleDisplay()->mDisplay == StyleDisplay::None;
}
static bool IsIgnorableScriptOrStyle(Element* aElement) {
return aElement->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style) &&
IsDisplayNone(aElement);
}
NS_IMETHODIMP
nsPlainTextSerializer::AppendText(Text* aText, int32_t aStartOffset,
int32_t aEndOffset) {
if (mIgnoreAboveIndex != (uint32_t)kNotFound) {
return NS_OK;
}
NS_ASSERTION(aStartOffset >= 0, "Negative start offset for text fragment!");
if (aStartOffset < 0) return NS_ERROR_INVALID_ARG;
NS_ENSURE_ARG(aText);
nsresult rv = NS_OK;
const CharacterDataBuffer* characterDataBuffer = nullptr;
if (!(characterDataBuffer = aText->GetCharacterDataBuffer())) {
return NS_ERROR_FAILURE;
}
int32_t fragLength = characterDataBuffer->GetLength();
int32_t endoffset =
(aEndOffset == -1) ? fragLength : std::min(aEndOffset, fragLength);
NS_ASSERTION(aStartOffset <= endoffset,
"A start offset is beyond the end of the text fragment!");
int32_t length = endoffset - aStartOffset;
if (length <= 0) {
return NS_OK;
}
// If we don't want any output, just return.
if (!DoOutput()) {
return NS_OK;
}
if (mLineBreakDue) {
EnsureVerticalSpace(mFloatingLines);
}
// Check whether this text node is under an element that doesnt need to be
// serialized. If so, we can return early here.
if (MustSuppressLeaf()) {
return NS_OK;
}
nsAutoString textstr;
if (characterDataBuffer->Is2b()) {
textstr.Assign(characterDataBuffer->Get2b() + aStartOffset, length);
} else {
// AssignASCII is for 7-bit character only, so don't use it
const char* data = characterDataBuffer->Get1b();
CopyASCIItoUTF16(Substring(data + aStartOffset, data + endoffset), textstr);
}
// Mask the text if the text node is in a password field.
if (aText->HasFlag(NS_MAYBE_MASKED)) {
TextEditor::MaskString(textstr, *aText, 0, aStartOffset);
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputForPlainTextClipboardCopy)) {
// XXX it would be nice if we could just use the Write() to handle the line
// breaks for all cases (bug 1993406).
Write(textstr);
return rv;
}
// We have to split the string across newlines
// to match parser behavior
int32_t start = 0;
int32_t offset = textstr.FindCharInSet(u"\n\r");
while (offset != kNotFound) {
if (offset > start) {
// Pass in the line
DoAddText(Substring(textstr, start, offset - start));
}
// Pass in a newline
DoAddLineBreak();
start = offset + 1;
offset = textstr.FindCharInSet(u"\n\r", start);
}
// Consume the last bit of the string if there's any left
if (start < length) {
if (start) {
DoAddText(Substring(textstr, start, length - start));
} else {
DoAddText(textstr);
}
}
return rv;
}
NS_IMETHODIMP
nsPlainTextSerializer::AppendCDATASection(Text* aCDATASection,
int32_t aStartOffset,
int32_t aEndOffset) {
MOZ_ASSERT(!aCDATASection ||
aCDATASection->NodeType() == nsINode::CDATA_SECTION_NODE);
return AppendText(aCDATASection, aStartOffset, aEndOffset);
}
NS_IMETHODIMP
nsPlainTextSerializer::ScanElementForPreformat(Element* aElement) {
mPreformatStack.push(IsElementPreformatted(aElement));
return NS_OK;
}
NS_IMETHODIMP
nsPlainTextSerializer::ForgetElementForPreformat(Element* aElement) {
MOZ_RELEASE_ASSERT(!mPreformatStack.empty(),
"Tried to pop without previous push.");
mPreformatStack.pop();
return NS_OK;
}
NS_IMETHODIMP
nsPlainTextSerializer::AppendElementStart(Element* aElement,
Element* aOriginalElement) {
NS_ENSURE_ARG(aElement);
nsresult rv = NS_OK;
nsAtom* id = GetIdForContent(aElement);
if (!FragmentOrElement::IsHTMLVoid(id)) {
rv = DoOpenContainer(aElement, id);
} else {
rv = DoAddLeaf(aElement, id);
}
if (id == nsGkAtoms::head) {
++mHeadLevel;
}
return rv;
}
NS_IMETHODIMP
nsPlainTextSerializer::AppendElementEnd(Element* aElement,
Element* aOriginalElement) {
NS_ENSURE_ARG(aElement);
nsresult rv = NS_OK;
nsAtom* id = GetIdForContent(aElement);
if (!FragmentOrElement::IsHTMLVoid(id)) {
rv = DoCloseContainer(aElement, id);
}
if (id == nsGkAtoms::head) {
NS_ASSERTION(mHeadLevel != 0, "mHeadLevel being decremented below 0");
--mHeadLevel;
}
return rv;
}
NS_IMETHODIMP
nsPlainTextSerializer::FlushAndFinish() {
MOZ_ASSERT(mOutputManager);
mOutputManager->Flush(mCurrentLine);
return Finish();
}
NS_IMETHODIMP
nsPlainTextSerializer::Finish() {
mOutputManager.reset();
return NS_OK;
}
NS_IMETHODIMP
nsPlainTextSerializer::GetOutputLength(uint32_t& aLength) const {
MOZ_ASSERT(mOutputManager);
aLength = mOutputManager->GetOutputLength();
return NS_OK;
}
NS_IMETHODIMP
nsPlainTextSerializer::AppendDocumentStart(Document* aDocument) {
return NS_OK;
}
constexpr int32_t kOlStackDummyValue = 0;
nsresult nsPlainTextSerializer::DoOpenContainer(Element* aElement,
const nsAtom* aTag) {
MOZ_ASSERT(aElement);
MOZ_ASSERT(GetIdForContent(aElement) == aTag);
MOZ_ASSERT(!FragmentOrElement::IsHTMLVoid(aTag));
if (IsIgnorableRubyAnnotation(aTag)) {
// Ignorable ruby annotation shouldn't be replaced by a placeholder
// character, neither any of its descendants.
mIgnoredChildNodeLevel++;
return NS_OK;
}
if (IsIgnorableScriptOrStyle(aElement)) {
mIgnoredChildNodeLevel++;
return NS_OK;
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputForPlainTextClipboardCopy)) {
if (mPreformattedBlockBoundary && DoOutput()) {
// Should always end a line, but get no more whitespace
if (mFloatingLines < 0) mFloatingLines = 0;
mLineBreakDue = true;
}
mPreformattedBlockBoundary = false;
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputRaw)) {
// Raw means raw. Don't even think about doing anything fancy
// here like indenting, adding line breaks or any other
// characters such as list item bullets, quote characters
// around <q>, etc.
return NS_OK;
}
if (mTagStackIndex < TagStackSize) {
mTagStack[mTagStackIndex++] = aTag;
}
if (mIgnoreAboveIndex != (uint32_t)kNotFound) {
return NS_OK;
}
// Reset this so that <blockquote type=cite> doesn't affect the whitespace
// above random <pre>s below it.
mHasWrittenCiteBlockquote =
mHasWrittenCiteBlockquote && aTag == nsGkAtoms::pre;
bool isInCiteBlockquote = false;
// XXX special-case <blockquote type=cite> so that we don't add additional
// newlines before the text.
if (aTag == nsGkAtoms::blockquote) {
nsAutoString value;
nsresult rv = GetAttributeValue(aElement, nsGkAtoms::type, value);
isInCiteBlockquote = NS_SUCCEEDED(rv) && value.EqualsIgnoreCase("cite");
}
if (mLineBreakDue && !isInCiteBlockquote) EnsureVerticalSpace(mFloatingLines);
// Check if this tag's content that should not be output
if ((aTag == nsGkAtoms::noscript &&
!mSettings.HasFlag(nsIDocumentEncoder::OutputNoScriptContent)) ||
((aTag == nsGkAtoms::iframe || aTag == nsGkAtoms::noframes) &&
!mSettings.HasFlag(nsIDocumentEncoder::OutputNoFramesContent))) {
// Ignore everything that follows the current tag in
// question until a matching end tag is encountered.
mIgnoreAboveIndex = mTagStackIndex - 1;
return NS_OK;
}
if (aTag == nsGkAtoms::body) {
// Try to figure out here whether we have a
// preformatted style attribute set by Thunderbird.
//
// Trigger on the presence of a "pre-wrap" in the
// style attribute. That's a very simplistic way to do
// it, but better than nothing.
nsAutoString style;
int32_t whitespace;
if (NS_SUCCEEDED(GetAttributeValue(aElement, nsGkAtoms::style, style)) &&
(kNotFound != (whitespace = style.Find(u"white-space:")))) {
if (kNotFound != style.LowerCaseFindASCII("pre-wrap", whitespace)) {
#ifdef DEBUG_preformatted
printf("Set mPreFormattedMail based on style pre-wrap\n");
#endif
mPreFormattedMail = true;
} else if (kNotFound != style.LowerCaseFindASCII("pre", whitespace)) {
#ifdef DEBUG_preformatted
printf("Set mPreFormattedMail based on style pre\n");
#endif
mPreFormattedMail = true;
}
} else {
/* See comment at end of function. */
mInWhitespace = true;
mPreFormattedMail = false;
}
return NS_OK;
}
// Keep this in sync with DoCloseContainer!
if (!DoOutput()) {
return NS_OK;
}
if (aTag == nsGkAtoms::p)
EnsureVerticalSpace(1);
else if (aTag == nsGkAtoms::pre) {
if (GetLastBool(mIsInCiteBlockquote))
EnsureVerticalSpace(0);
else if (mHasWrittenCiteBlockquote) {
EnsureVerticalSpace(0);
mHasWrittenCiteBlockquote = false;
} else
EnsureVerticalSpace(1);
} else if (aTag == nsGkAtoms::tr) {
PushBool(mHasWrittenCellsForRow, false);
} else if (aTag == nsGkAtoms::td || aTag == nsGkAtoms::th) {
// We must make sure that the content of two table cells get a
// space between them.
// To make the separation between cells most obvious and
// importable, we use a TAB.
if (mHasWrittenCellsForRow.IsEmpty()) {
// We don't always see a <tr> (nor a <table>) before the <td> if we're
// copying part of a table
PushBool(mHasWrittenCellsForRow, true); // will never be popped
} else if (GetLastBool(mHasWrittenCellsForRow)) {
// Bypass |Write| so that the TAB isn't compressed away.
AddToLine(u"\t", 1);
mInWhitespace = true;
} else {
SetLastBool(mHasWrittenCellsForRow, true);
}
} else if (aTag == nsGkAtoms::ul) {
// Indent here to support nested lists, which aren't included in li :-(
EnsureVerticalSpace(IsInOlOrUl() ? 0 : 1);
// Must end the current line before we change indention
mCurrentLine.mIndentation.mLength += kIndentSizeList;
mULCount++;
} else if (aTag == nsGkAtoms::ol) {
EnsureVerticalSpace(IsInOlOrUl() ? 0 : 1);
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
// Must end the current line before we change indention
nsAutoString startAttr;
int32_t startVal = 1;
if (NS_SUCCEEDED(
GetAttributeValue(aElement, nsGkAtoms::start, startAttr))) {
nsresult rv = NS_OK;
startVal = startAttr.ToInteger(&rv);
if (NS_FAILED(rv)) {
startVal = 1;
}
}
mOLStack.AppendElement(startVal);
} else {
mOLStack.AppendElement(kOlStackDummyValue);
}
mCurrentLine.mIndentation.mLength += kIndentSizeList; // see ul
} else if (aTag == nsGkAtoms::li &&
mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
if (mTagStackIndex > 1 && IsInOL()) {
if (!mOLStack.IsEmpty()) {
nsAutoString valueAttr;
if (NS_SUCCEEDED(
GetAttributeValue(aElement, nsGkAtoms::value, valueAttr))) {
nsresult rv = NS_OK;
int32_t valueAttrVal = valueAttr.ToInteger(&rv);
if (NS_SUCCEEDED(rv)) {
mOLStack.LastElement() = valueAttrVal;
}
}
// This is what nsBulletFrame does for OLs:
mCurrentLine.mIndentation.mHeader.AppendInt(mOLStack.LastElement(), 10);
mOLStack.LastElement()++;
} else {
mCurrentLine.mIndentation.mHeader.Append(char16_t('#'));
}
mCurrentLine.mIndentation.mHeader.Append(char16_t('.'));
} else {
static const char bulletCharArray[] = "*o+#";
uint32_t index = mULCount > 0 ? (mULCount - 1) : 3;
char bulletChar = bulletCharArray[index % 4];
mCurrentLine.mIndentation.mHeader.Append(char16_t(bulletChar));
}
mCurrentLine.mIndentation.mHeader.Append(char16_t(' '));
} else if (aTag == nsGkAtoms::dl) {
EnsureVerticalSpace(1);
} else if (aTag == nsGkAtoms::dt) {
EnsureVerticalSpace(0);
} else if (aTag == nsGkAtoms::dd) {
EnsureVerticalSpace(0);
mCurrentLine.mIndentation.mLength += kIndentSizeDD;
} else if (aTag == nsGkAtoms::span) {
++mSpanLevel;
} else if (aTag == nsGkAtoms::blockquote) {
// Push
PushBool(mIsInCiteBlockquote, isInCiteBlockquote);
if (isInCiteBlockquote) {
EnsureVerticalSpace(0);
mCurrentLine.mCiteQuoteLevel++;
} else {
EnsureVerticalSpace(1);
mCurrentLine.mIndentation.mLength +=
kTabSize; // Check for some maximum value?
}
} else if (aTag == nsGkAtoms::q) {
Write(u"\""_ns);
}
// Else make sure we'll separate block level tags,
// even if we're about to leave, before doing any other formatting.
else if (IsCssBlockLevelElement(aElement)) {
EnsureVerticalSpace(0);
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
OpenContainerForOutputFormatted(aElement, aTag);
}
return NS_OK;
}
void nsPlainTextSerializer::OpenContainerForOutputFormatted(
Element* aElement, const nsAtom* aTag) {
MOZ_ASSERT(aElement);
MOZ_ASSERT(GetIdForContent(aElement) == aTag);
MOZ_ASSERT(!FragmentOrElement::IsHTMLVoid(aTag));
const bool currentNodeIsConverted = IsCurrentNodeConverted(aElement);
if (aTag == nsGkAtoms::h1 || aTag == nsGkAtoms::h2 || aTag == nsGkAtoms::h3 ||
aTag == nsGkAtoms::h4 || aTag == nsGkAtoms::h5 || aTag == nsGkAtoms::h6) {
EnsureVerticalSpace(2);
if (mSettings.GetHeaderStrategy() ==
Settings::HeaderStrategy::kNumberHeadingsAndIndentSlightly) {
mCurrentLine.mIndentation.mLength += kIndentSizeHeaders;
// Caching
int32_t level = HeaderLevel(aTag);
// Increase counter for current level
mHeaderCounter[level]++;
// Reset all lower levels
int32_t i;
for (i = level + 1; i <= 6; i++) {
mHeaderCounter[i] = 0;
}
// Construct numbers
nsAutoString leadup;
for (i = 1; i <= level; i++) {
leadup.AppendInt(mHeaderCounter[i]);
leadup.Append(char16_t('.'));
}
leadup.Append(char16_t(' '));
Write(leadup);
} else if (mSettings.GetHeaderStrategy() ==
Settings::HeaderStrategy::kIndentIncreasedWithHeaderLevel) {
mCurrentLine.mIndentation.mLength += kIndentSizeHeaders;
for (int32_t i = HeaderLevel(aTag); i > 1; i--) {
// for h(x), run x-1 times
mCurrentLine.mIndentation.mLength += kIndentIncrementHeaders;
}
}
} else if (aTag == nsGkAtoms::sup && mSettings.GetStructs() &&
!currentNodeIsConverted) {
Write(u"^"_ns);
} else if (aTag == nsGkAtoms::sub && mSettings.GetStructs() &&
!currentNodeIsConverted) {
Write(u"_"_ns);
} else if (aTag == nsGkAtoms::code && mSettings.GetStructs() &&
!currentNodeIsConverted) {
Write(u"|"_ns);
} else if ((aTag == nsGkAtoms::strong || aTag == nsGkAtoms::b) &&
mSettings.GetStructs() && !currentNodeIsConverted) {
Write(u"*"_ns);
} else if ((aTag == nsGkAtoms::em || aTag == nsGkAtoms::i) &&
mSettings.GetStructs() && !currentNodeIsConverted) {
Write(u"/"_ns);
} else if (aTag == nsGkAtoms::u && mSettings.GetStructs() &&
!currentNodeIsConverted) {
Write(u"_"_ns);
}
/* Container elements are always block elements, so we shouldn't
output any whitespace immediately after the container tag even if
there's extra whitespace there because the HTML is pretty-printed
or something. To ensure that happens, tell the serializer we're
already in whitespace so it won't output more. */
mInWhitespace = true;
}
nsresult nsPlainTextSerializer::DoCloseContainer(Element* aElement,
const nsAtom* aTag) {
MOZ_ASSERT(aElement);
MOZ_ASSERT(GetIdForContent(aElement) == aTag);
MOZ_ASSERT(!FragmentOrElement::IsHTMLVoid(aTag));
if (IsIgnorableRubyAnnotation(aTag)) {
mIgnoredChildNodeLevel--;
return NS_OK;
}
if (IsIgnorableScriptOrStyle(aElement)) {
mIgnoredChildNodeLevel--;
return NS_OK;
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputForPlainTextClipboardCopy)) {
if (DoOutput() && IsElementPreformatted() &&
IsCssBlockLevelElement(aElement)) {
// If we're closing a preformatted block element, output a line break
// when we find a new container.
mPreformattedBlockBoundary = true;
}
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputRaw)) {
// Raw means raw. Don't even think about doing anything fancy
// here like indenting, adding line breaks or any other
// characters such as list item bullets, quote characters
// around <q>, etc.
return NS_OK;
}
if (mTagStackIndex > 0) {
--mTagStackIndex;
}
if (mTagStackIndex >= mIgnoreAboveIndex) {
if (mTagStackIndex == mIgnoreAboveIndex) {
// We're dealing with the close tag whose matching
// open tag had set the mIgnoreAboveIndex value.
// Reset mIgnoreAboveIndex before discarding this tag.
mIgnoreAboveIndex = (uint32_t)kNotFound;
}
return NS_OK;
}
MOZ_ASSERT(mOutputManager);
// End current line if we're ending a block level tag
if ((aTag == nsGkAtoms::body) || (aTag == nsGkAtoms::html)) {
// We want the output to end with a new line,
// but in preformatted areas like text fields,
// we can't emit newlines that weren't there.
// So add the newline only in the case of formatted output.
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
EnsureVerticalSpace(0);
} else {
mOutputManager->Flush(mCurrentLine);
}
// We won't want to do anything with these in formatted mode either,
// so just return now:
return NS_OK;
}
// Keep this in sync with DoOpenContainer!
if (!DoOutput()) {
return NS_OK;
}
if (aTag == nsGkAtoms::tr) {
PopBool(mHasWrittenCellsForRow);
// Should always end a line, but get no more whitespace
if (mFloatingLines < 0) mFloatingLines = 0;
mLineBreakDue = true;
} else if (((aTag == nsGkAtoms::li) || (aTag == nsGkAtoms::dt)) &&
mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
// Items that should always end a line, but get no more whitespace
if (mFloatingLines < 0) mFloatingLines = 0;
mLineBreakDue = true;
} else if (aTag == nsGkAtoms::pre) {
mFloatingLines = GetLastBool(mIsInCiteBlockquote) ? 0 : 1;
mLineBreakDue = true;
} else if (aTag == nsGkAtoms::ul) {
mOutputManager->Flush(mCurrentLine);
mCurrentLine.mIndentation.mLength -= kIndentSizeList;
--mULCount;
if (!IsInOlOrUl()) {
mFloatingLines = 1;
mLineBreakDue = true;
}
} else if (aTag == nsGkAtoms::ol) {
mOutputManager->Flush(mCurrentLine); // Doing this after decreasing
// OLStackIndex would be wrong.
mCurrentLine.mIndentation.mLength -= kIndentSizeList;
MOZ_ASSERT(!mOLStack.IsEmpty(), "Wrong OLStack level!");
mOLStack.RemoveLastElement();
if (!IsInOlOrUl()) {
mFloatingLines = 1;
mLineBreakDue = true;
}
} else if (aTag == nsGkAtoms::dl) {
mFloatingLines = 1;
mLineBreakDue = true;
} else if (aTag == nsGkAtoms::dd) {
mOutputManager->Flush(mCurrentLine);
mCurrentLine.mIndentation.mLength -= kIndentSizeDD;
} else if (aTag == nsGkAtoms::span) {
NS_ASSERTION(mSpanLevel, "Span level will be negative!");
--mSpanLevel;
} else if (aTag == nsGkAtoms::div) {
if (mFloatingLines < 0) mFloatingLines = 0;
mLineBreakDue = true;
} else if (aTag == nsGkAtoms::blockquote) {
mOutputManager->Flush(mCurrentLine); // Is this needed?
// Pop
bool isInCiteBlockquote = PopBool(mIsInCiteBlockquote);
if (isInCiteBlockquote) {
NS_ASSERTION(mCurrentLine.mCiteQuoteLevel,
"CiteQuote level will be negative!");
mCurrentLine.mCiteQuoteLevel--;
mFloatingLines = 0;
mHasWrittenCiteBlockquote = true;
} else {
mCurrentLine.mIndentation.mLength -= kTabSize;
mFloatingLines = 1;
}
mLineBreakDue = true;
} else if (aTag == nsGkAtoms::q) {
Write(u"\""_ns);
} else if (IsCssBlockLevelElement(aElement)) {
// All other blocks get 1 vertical space after them
// in formatted mode, otherwise 0.
// This is hard. Sometimes 0 is a better number, but
// how to know?
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
EnsureVerticalSpace(1);
} else {
if (mFloatingLines < 0) mFloatingLines = 0;
mLineBreakDue = true;
}
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
CloseContainerForOutputFormatted(aElement, aTag);
}
return NS_OK;
}
void nsPlainTextSerializer::CloseContainerForOutputFormatted(
Element* aElement, const nsAtom* aTag) {
MOZ_ASSERT(aElement);
MOZ_ASSERT(GetIdForContent(aElement) == aTag);
MOZ_ASSERT(!FragmentOrElement::IsHTMLVoid(aTag));
const bool currentNodeIsConverted = IsCurrentNodeConverted(aElement);
if (aTag == nsGkAtoms::h1 || aTag == nsGkAtoms::h2 || aTag == nsGkAtoms::h3 ||
aTag == nsGkAtoms::h4 || aTag == nsGkAtoms::h5 || aTag == nsGkAtoms::h6) {
using HeaderStrategy = Settings::HeaderStrategy;
if ((mSettings.GetHeaderStrategy() ==
HeaderStrategy::kIndentIncreasedWithHeaderLevel) ||
(mSettings.GetHeaderStrategy() ==
HeaderStrategy::kNumberHeadingsAndIndentSlightly)) {
mCurrentLine.mIndentation.mLength -= kIndentSizeHeaders;
}
if (mSettings.GetHeaderStrategy() ==
HeaderStrategy::kIndentIncreasedWithHeaderLevel) {
for (int32_t i = HeaderLevel(aTag); i > 1; i--) {
// for h(x), run x-1 times
mCurrentLine.mIndentation.mLength -= kIndentIncrementHeaders;
}
}
EnsureVerticalSpace(1);
} else if (aTag == nsGkAtoms::a && !currentNodeIsConverted) {
nsAutoString url;
if (NS_SUCCEEDED(GetAttributeValue(aElement, nsGkAtoms::href, url)) &&
!url.IsEmpty()) {
nsAutoString temp;
temp.AssignLiteral(" <");
temp += url;
temp.Append(char16_t('>'));
Write(temp);
}
} else if ((aTag == nsGkAtoms::sup || aTag == nsGkAtoms::sub) &&
mSettings.GetStructs() && !currentNodeIsConverted) {
Write(kSpace);
} else if (aTag == nsGkAtoms::code && mSettings.GetStructs() &&
!currentNodeIsConverted) {
Write(u"|"_ns);
} else if ((aTag == nsGkAtoms::strong || aTag == nsGkAtoms::b) &&
mSettings.GetStructs() && !currentNodeIsConverted) {
Write(u"*"_ns);
} else if ((aTag == nsGkAtoms::em || aTag == nsGkAtoms::i) &&
mSettings.GetStructs() && !currentNodeIsConverted) {
Write(u"/"_ns);
} else if (aTag == nsGkAtoms::u && mSettings.GetStructs() &&
!currentNodeIsConverted) {
Write(u"_"_ns);
}
}
bool nsPlainTextSerializer::MustSuppressLeaf() const {
if (mIgnoredChildNodeLevel > 0) {
return true;
}
if ((mTagStackIndex > 1 &&
mTagStack[mTagStackIndex - 2] == nsGkAtoms::select) ||
(mTagStackIndex > 0 &&
mTagStack[mTagStackIndex - 1] == nsGkAtoms::select)) {
// Don't output the contents of SELECT elements;
// Might be nice, eventually, to output just the selected element.
// Read more in bug 31994.
return true;
}
return false;
}
void nsPlainTextSerializer::DoAddLineBreak() {
MOZ_ASSERT(DoOutput());
MOZ_ASSERT(!mLineBreakDue);
MOZ_ASSERT(mIgnoreAboveIndex == (uint32_t)kNotFound);
MOZ_ASSERT(!MustSuppressLeaf());
// The only times we want to pass along whitespace from the original
// html source are if we're forced into preformatted mode via flags,
// or if we're prettyprinting and we're inside a <pre>.
// Otherwise, either we're collapsing to minimal text, or we're
// prettyprinting to mimic the html format, and in neither case
// does the formatting of the html source help us.
if (mSettings.HasFlag(nsIDocumentEncoder::OutputPreformatted) ||
(mPreFormattedMail && !mSettings.GetWrapColumn()) ||
IsElementPreformatted()) {
EnsureVerticalSpace(mEmptyLines + 1);
} else if (!mInWhitespace) {
Write(kSpace);
mInWhitespace = true;
}
}
void nsPlainTextSerializer::DoAddText(const nsAString& aText) {
MOZ_ASSERT(DoOutput());
MOZ_ASSERT(!mLineBreakDue);
MOZ_ASSERT(mIgnoreAboveIndex == (uint32_t)kNotFound);
MOZ_ASSERT(!MustSuppressLeaf());
// Reset this, as its no longer true after serializing texts, so the next
// <pre> element will get a leading newline.
mHasWrittenCiteBlockquote = false;
Write(aText);
}
void CreateLineOfDashes(nsAString& aResult, const uint32_t aWrapColumn) {
MOZ_ASSERT(aResult.IsEmpty());
const uint32_t width = (aWrapColumn > 0 ? aWrapColumn : 25);
while (aResult.Length() < width) {
aResult.Append(char16_t('-'));
}
}
nsresult nsPlainTextSerializer::DoAddLeaf(Element* aElement,
const nsAtom* aTag) {
MOZ_ASSERT(aElement);
MOZ_ASSERT(GetIdForContent(aElement) == aTag);
MOZ_ASSERT(FragmentOrElement::IsHTMLVoid(aTag));
mPreformattedBlockBoundary = false;
if (!DoOutput()) {
return NS_OK;
}
if (mLineBreakDue) EnsureVerticalSpace(mFloatingLines);
if (MustSuppressLeaf()) {
return NS_OK;
}
if (aTag == nsGkAtoms::br) {
// Another egregious editor workaround, see bug 38194:
// ignore the bogus br tags that the editor sticks here and there.
// FYI: `brElement` may be `nullptr` if the element is <br> element
// of non-HTML element.
// XXX Do we need to call `EnsureVerticalSpace()` when the <br> element
// is not an HTML element?
HTMLBRElement* brElement = HTMLBRElement::FromNodeOrNull(aElement);
if (!brElement || !brElement->IsPaddingForEmptyLastLine()) {
EnsureVerticalSpace(mEmptyLines + 1);
}
} else if (aTag == nsGkAtoms::hr &&
mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) {
EnsureVerticalSpace(0);
// Make a line of dashes as wide as the wrap width
// XXX honoring percentage would be nice
nsAutoString line;
CreateLineOfDashes(line, mSettings.GetWrapColumn());
Write(line);
EnsureVerticalSpace(0);
} else if (aTag == nsGkAtoms::img) {
/* Output (in decreasing order of preference)
alt, title or nothing */
// See <http://www.w3.org/TR/REC-html40/struct/objects.html#edef-IMG>
nsAutoString imageDescription;
if (NS_SUCCEEDED(
GetAttributeValue(aElement, nsGkAtoms::alt, imageDescription))) {
// If the alt attribute has an empty value (|alt=""|), output nothing
} else if (NS_SUCCEEDED(GetAttributeValue(aElement, nsGkAtoms::title,
imageDescription)) &&
!imageDescription.IsEmpty()) {
imageDescription = u" ["_ns + imageDescription + u"] "_ns;
}
Write(imageDescription);
}
return NS_OK;
}
/**
* Adds as many newline as necessary to get |aNumberOfRows| empty lines
*
* aNumberOfRows = -1 : Being in the middle of some line of text
* aNumberOfRows = 0 : Being at the start of a line
* aNumberOfRows = n>0 : Having n empty lines before the current line.
*/
void nsPlainTextSerializer::EnsureVerticalSpace(const int32_t aNumberOfRows) {
// If we have something in the indent we probably want to output
// it and it's not included in the count for empty lines so we don't
// realize that we should start a new line.
if (aNumberOfRows >= 0 && !mCurrentLine.mIndentation.mHeader.IsEmpty()) {
EndHardBreakLine();
mInWhitespace = true;
}
while (mEmptyLines < aNumberOfRows) {
EndHardBreakLine();
mInWhitespace = true;
}
mLineBreakDue = false;
mFloatingLines = -1;
}
void nsPlainTextSerializer::OutputManager::Flush(CurrentLine& aLine) {
if (!aLine.mContent.IsEmpty()) {
aLine.MaybeReplaceNbspsInContent(mFlags);
Append(aLine, StripTrailingWhitespaces::kNo);
aLine.ResetContentAndIndentationHeader();
}
}
static bool IsSpaceStuffable(const char16_t* s) {
return (s[0] == '>' || s[0] == ' ' || s[0] == kNBSP ||
NS_strncmp(s, u"From ", 5) == 0);
}
void nsPlainTextSerializer::PerformWrapAndOutputCompleteLines(
const Settings& aSettings, CurrentLine& aLine, OutputManager& aOutput,
bool aUseLineBreaker, bool aAllowBonusWidth,
nsPlainTextSerializer* aSerializer) {
if (!aSettings.MayWrap()) {
return;
}
// Yes, wrap!
const uint32_t wrapColumn = aSettings.GetWrapColumn();
// The "+4" is to avoid wrap lines that only would be a couple of letters too
// long. We give this bonus only if the wrapColumn is more than 20.
// FIXME(emilio): This option seems rather broken. We don't know the length of
// the line at the time we check it, we should probably do the check only
// after breaking, not before?
const uint32_t bonusWidth = (wrapColumn > 20 && aAllowBonusWidth) ? 4 : 0;
while (!aLine.mContent.IsEmpty()) {
const uint32_t prefixwidth = aLine.DeterminePrefixWidth();
// The width of the line as it will appear on the screen (approx.).
const uint32_t currentLineContentWidth =
GetUnicharStringWidth(aLine.mContent);
if (currentLineContentWidth + prefixwidth <= wrapColumn + bonusWidth) {
break;
}
const int32_t goodSpace =
aLine.FindWrapIndexForContent(wrapColumn, aUseLineBreaker);
const int32_t contentLength = aLine.mContent.Length();
if (goodSpace <= 0 || goodSpace >= contentLength) {
// Nothing to do. Hopefully we get more data later to use for a place to
// break line.
break;
}
// Found a place to break
// -1 (trim a char at the break position) only if the line break was a
// space.
nsAutoString restOfContent;
if (nsCRT::IsAsciiSpace(aLine.mContent.CharAt(goodSpace))) {
aLine.mContent.Right(restOfContent, contentLength - goodSpace - 1);
} else {
aLine.mContent.Right(restOfContent, contentLength - goodSpace);
}
// if breaker was U+0020, it has to consider for delsp=yes support
const bool breakBySpace = aLine.mContent.CharAt(goodSpace) == ' ';
aLine.mContent.Truncate(goodSpace);
// Append the line to the output.
if (!aLine.mContent.IsEmpty()) {
// Trim _one_ potential trailing newline.
if (aLine.mContent.Last() == '\n') {
aLine.mContent.Truncate(goodSpace - 1);
}
if (!aSettings.HasFlag(nsIDocumentEncoder::OutputPreformatted)) {
aLine.mContent.Trim(" ", false, true, false);
}
if (aSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed) &&
!aLine.mIndentation.mLength) {
// Add the soft part of the soft linebreak (RFC 2646 4.1)
// We only do this when there is no indentation since format=flowed
// lines and indentation doesn't work well together.
// If breaker character is ASCII space with RFC 3676 support
// (delsp=yes), add twice space.
if (aSettings.HasFlag(nsIDocumentEncoder::OutputFormatDelSp) &&
breakBySpace) {
aLine.mContent.AppendLiteral(" ");
} else {
aLine.mContent.Append(char16_t(' '));
}
}
AppendLineToOutput(aSettings, aLine, aOutput);
if (aSerializer) {
aSerializer->ResetStateAfterLine();
aSerializer->mEmptyLines = -1;
}
}
aLine.mContent.Truncate();
// Space stuffing a la RFC 2646 (format=flowed)
if (aSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed)) {
aLine.mSpaceStuffed = !restOfContent.IsEmpty() &&
IsSpaceStuffable(restOfContent.get()) &&
// We space-stuff quoted lines anyway
aLine.mCiteQuoteLevel == 0;
}
aLine.mContent.Append(restOfContent);
}
}
void nsPlainTextSerializer::MaybeWrapAndOutputCompleteLines() {
PerformWrapAndOutputCompleteLines(mSettings, mCurrentLine, *mOutputManager,
mUseLineBreaker,
/* aAllowBonusWidth = */ true, this);
}
/**
* This function adds a piece of text to the current stored line. If we are
* wrapping text and the stored line will become too long, a suitable
* location to wrap will be found and the line that's complete will be
* output.
*/
void nsPlainTextSerializer::AddToLine(const char16_t* aLineFragment,
int32_t aLineFragmentLength) {
if (mLineBreakDue) EnsureVerticalSpace(mFloatingLines);
if (mCurrentLine.mContent.IsEmpty()) {
if (0 == aLineFragmentLength) {
return;
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed)) {
// Space stuffing a la RFC 2646 (format=flowed).
// We space-stuff quoted lines anyway
mCurrentLine.mSpaceStuffed =
IsSpaceStuffable(aLineFragment) && mCurrentLine.mCiteQuoteLevel == 0;
}
mEmptyLines = -1;
}
mCurrentLine.mContent.Append(aLineFragment, aLineFragmentLength);
MaybeWrapAndOutputCompleteLines();
}
// The signature separator (RFC 2646).
const char kSignatureSeparator[] = "-- ";
// The OpenPGP dash-escaped signature separator in inline
// signed messages according to the OpenPGP standard (RFC 2440).
const char kDashEscapedSignatureSeparator[] = "- -- ";
static bool IsSignatureSeparator(const nsAString& aString) {
return aString.EqualsLiteral(kSignatureSeparator) ||
aString.EqualsLiteral(kDashEscapedSignatureSeparator);
}
void nsPlainTextSerializer::AppendLineToOutput(const Settings& aSettings,
CurrentLine& aLine,
OutputManager& aOutput) {
aLine.MaybeReplaceNbspsInContent(aSettings.GetFlags());
// If we don't have anything "real" to output we have to
// make sure the indent doesn't end in a space since that
// would trick a format=flowed-aware receiver.
aOutput.Append(aLine, OutputManager::StripTrailingWhitespaces::kMaybe);
aOutput.AppendLineBreak();
aLine.ResetContentAndIndentationHeader();
}
/**
* Outputs the contents of mCurrentLine.mContent, and resets line
* specific variables. Also adds an indentation and prefix if there is one
* specified. Strips ending spaces from the line if it isn't preformatted.
*/
void nsPlainTextSerializer::EndHardBreakLine() {
/* In non-preformatted mode, remove spaces from the end of the line for
* format=flowed compatibility. Don't do this for these special cases:
* "-- ", the signature separator (RFC 2646) shouldn't be touched and
* "- -- ", the OpenPGP dash-escaped signature separator in inline
* signed messages according to the OpenPGP standard (RFC 2440).
*/
if (!mSettings.HasFlag(nsIDocumentEncoder::OutputPreformatted) &&
!IsSignatureSeparator(mCurrentLine.mContent)) {
mCurrentLine.mContent.Trim(" ", false, true, false);
}
// Hard break
if (mCurrentLine.HasContentOrIndentationHeader()) {
mEmptyLines = 0;
} else {
mEmptyLines++;
}
MOZ_ASSERT(mOutputManager);
AppendLineToOutput(mSettings, mCurrentLine, *mOutputManager);
ResetStateAfterLine();
}
/**
* Creates the calculated and stored indent and text in the indentation. That is
* quote chars and numbers for numbered lists and such.
*/
void nsPlainTextSerializer::CurrentLine::CreateQuotesAndIndent(
nsAString& aResult) const {
// Put the mail quote "> " chars in, if appropriate:
if (mCiteQuoteLevel > 0) {
nsAutoString quotes;
for (int i = 0; i < mCiteQuoteLevel; i++) {
quotes.Append(char16_t('>'));
}
if (!mContent.IsEmpty()) {
/* Better don't output a space here, if the line is empty,
in case a receiving format=flowed-aware UA thinks, this were a flowed
line, which it isn't - it's just empty. (Flowed lines may be joined
with the following one, so the empty line may be lost completely.) */
quotes.Append(char16_t(' '));
}
aResult = std::move(quotes);
}
// Indent if necessary
int32_t indentwidth = mIndentation.mLength - mIndentation.mHeader.Length();
if (mSpaceStuffed) {
indentwidth += 1;
}
// Don't make empty lines look flowed
if (indentwidth > 0 && HasContentOrIndentationHeader()) {
nsAutoString spaces;
for (int i = 0; i < indentwidth; ++i) {
spaces.Append(char16_t(' '));
}
aResult += spaces;
}
if (!mIndentation.mHeader.IsEmpty()) {
aResult += mIndentation.mHeader;
}
}
static bool IsLineFeedCarriageReturnBlankOrTab(char16_t c) {
return ('\n' == c || '\r' == c || ' ' == c || '\t' == c);
}
static void ReplaceVisiblyTrailingNbsps(nsAString& aString) {
const int32_t totLen = aString.Length();
for (int32_t i = totLen - 1; i >= 0; i--) {
char16_t c = aString[i];
if (IsLineFeedCarriageReturnBlankOrTab(c)) {
continue;
}
if (kNBSP == c) {
aString.Replace(i, 1, ' ');
} else {
break;
}
}
}
void nsPlainTextSerializer::ConvertToLinesAndOutput(const nsAString& aString) {
nsAString::const_iterator iter;
aString.BeginReading(iter);
nsAString::const_iterator done_searching;
aString.EndReading(done_searching);
// Put the mail quote "> " chars in, if appropriate.
// Have to put it in before every line.
while (iter != done_searching) {
nsAString::const_iterator bol = iter;
nsAString::const_iterator newline = done_searching;
// Find one of '\n' or '\r' using iterators since nsAString
// doesn't have the old FindCharInSet function.
bool spacesOnly = true;
while (iter != done_searching) {
if ('\n' == *iter || '\r' == *iter) {
newline = iter;
break;
}
if (' ' != *iter) {
spacesOnly = false;
}
++iter;
}
// Done searching
nsAutoString stringpart;
bool outputLineBreak = false;
bool isNewLineCRLF = false;
if (newline == done_searching) {
// No new lines.
stringpart.Assign(Substring(bol, newline));
if (!stringpart.IsEmpty()) {
char16_t lastchar = stringpart.Last();
mInWhitespace = IsLineFeedCarriageReturnBlankOrTab(lastchar);
}
mEmptyLines = -1;
} else {
// There is a newline
stringpart.Assign(Substring(bol, newline));
mInWhitespace = true;
outputLineBreak = true;
if ('\r' == *iter++ && '\n' == *iter) {
// There was a CRLF in the input. This used to be illegal and
// stripped by the parser. Apparently not anymore. Let's skip
// over the LF.
newline = iter++;
isNewLineCRLF = true;
}
}
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed)) {
if ((outputLineBreak || !spacesOnly) && // bugs 261467,125928
!IsQuotedLine(stringpart) && !IsSignatureSeparator(stringpart)) {
stringpart.Trim(" ", false, true, true);
}
mCurrentLine.mSpaceStuffed =
IsSpaceStuffable(stringpart.get()) && !IsQuotedLine(stringpart);
}
mCurrentLine.mContent.Append(stringpart);
mCurrentLine.MaybeReplaceNbspsInContent(mSettings.GetFlags());
mOutputManager->Append(mCurrentLine,
OutputManager::StripTrailingWhitespaces::kNo);
if (outputLineBreak) {
if (mSettings.HasFlag(
nsIDocumentEncoder::OutputForPlainTextClipboardCopy)) {
// This is aligned with other browsers that they don't convert CRLF to
// the platform line break.
if ('\n' == *newline) {
mOutputManager->AppendLineBreak(isNewLineCRLF);
// If there is preceding text, we are starting a new line, so reset
// mEmptyLines. If there is no preceding text, we are outputting
// multiple line breaks, so we count them toward mEmptyLines.
mEmptyLines = stringpart.IsEmpty() ? mEmptyLines + 1 : 0;
} else {
mOutputManager->Append(u"\r"_ns);
// `\r` isnt treated as a line break here, so were now in the middle
// of the line.
mEmptyLines = -1;
}
} else {
mOutputManager->AppendLineBreak();
mEmptyLines = 0;
}
}
mCurrentLine.ResetContentAndIndentationHeader();
}
}
/**
* Write a string. This is the highlevel function to use to get text output.
* By using AddToLine, Output, EndHardBreakLine and other functions it handles
* quotation, line wrapping, indentation, whitespace compression and other
* things.
*/
void nsPlainTextSerializer::Write(const nsAString& aStr) {
// XXX Copy necessary to use nsString methods and gain
// access to underlying buffer
nsAutoString str(aStr);
#ifdef DEBUG_wrapping
printf("Write(%s): wrap col = %d\n", NS_ConvertUTF16toUTF8(str).get(),
mSettings.GetWrapColumn());
#endif
const int32_t totLen = str.Length();
// If the string is empty, do nothing:
if (totLen <= 0) return;
// For Flowed text change nbsp-ses to spaces at end of lines to allow them
// to be cut off along with usual spaces if required. (bug #125928)
if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed)) {
ReplaceVisiblyTrailingNbsps(str);
}
// We have two major codepaths here. One that does preformatted text and one
// that does normal formatted text. The one for preformatted text calls
// Output directly while the other code path goes through AddToLine.
if ((mPreFormattedMail && !mSettings.GetWrapColumn()) ||
(IsElementPreformatted() && !mPreFormattedMail) ||
(mSpanLevel > 0 && mEmptyLines >= 0 && IsQuotedLine(str))) {
// No intelligent wrapping.
// This mustn't be mixed with intelligent wrapping without clearing
// the mCurrentLine.mContent buffer before!!!
NS_ASSERTION(mCurrentLine.mContent.IsEmpty() ||
(IsElementPreformatted() && !mPreFormattedMail),
"Mixed wrapping data and nonwrapping data on the same line");
MOZ_ASSERT(mOutputManager);
if (!mCurrentLine.mContent.IsEmpty()) {
mOutputManager->Flush(mCurrentLine);
}
ConvertToLinesAndOutput(str);
return;
}
// Intelligent handling of text
// If needed, strip out all "end of lines"
// and multiple whitespace between words
int32_t nextpos;
const char16_t* offsetIntoBuffer = nullptr;
int32_t bol = 0;
while (bol < totLen) { // Loop over lines
// Find a place where we may have to do whitespace compression
nextpos = str.FindCharInSet(u" \t\n\r", bol);
#ifdef DEBUG_wrapping
nsAutoString remaining;
str.Right(remaining, totLen - bol);
foo = ToNewCString(remaining);
// printf("Next line: bol = %d, newlinepos = %d, totLen = %d, "
// "string = '%s'\n", bol, nextpos, totLen, foo);
free(foo);
#endif
if (nextpos == kNotFound) {
// The rest of the string
offsetIntoBuffer = str.get() + bol;
AddToLine(offsetIntoBuffer, totLen - bol);
bol = totLen;
mInWhitespace = false;
} else {
// There's still whitespace left in the string
if (nextpos != 0 && (nextpos + 1) < totLen) {
offsetIntoBuffer = str.get() + nextpos;
// skip '\n' if it is between CJ chars
if (offsetIntoBuffer[0] == '\n' && IS_CJ_CHAR(offsetIntoBuffer[-1]) &&
IS_CJ_CHAR(offsetIntoBuffer[1])) {
offsetIntoBuffer = str.get() + bol;
AddToLine(offsetIntoBuffer, nextpos - bol);
bol = nextpos + 1;
continue;
}
}
// If we're already in whitespace and not preformatted, just skip it:
if (mInWhitespace && (nextpos == bol) && !mPreFormattedMail &&
!mSettings.HasFlag(nsIDocumentEncoder::OutputPreformatted)) {
// Skip whitespace
bol++;
continue;
}
if (nextpos == bol &&
!mSettings.HasFlag(
nsIDocumentEncoder::OutputForPlainTextClipboardCopy)) {
// Note that we are in whitespace.
mInWhitespace = true;
offsetIntoBuffer = str.get() + nextpos;
// XXX Why do we need to keep the very first character when compressing
// the reset?
AddToLine(offsetIntoBuffer, 1);
bol++;
continue;
}
mInWhitespace = true;
offsetIntoBuffer = str.get() + bol;
if (mPreFormattedMail ||
mSettings.HasFlag(nsIDocumentEncoder::OutputPreformatted)) {
// Preserve the real whitespace character
nextpos++;
AddToLine(offsetIntoBuffer, nextpos - bol);
bol = nextpos;
} else {
// Replace the whitespace with a space
AddToLine(offsetIntoBuffer, nextpos - bol);
AddToLine(kSpace.get(), 1);
bol = nextpos + 1; // Let's eat the whitespace
}
}
} // Continue looping over the string
}
/**
* Gets the value of an attribute in a string. If the function returns
* NS_ERROR_NOT_AVAILABLE, there was none such attribute specified.
*/
nsresult nsPlainTextSerializer::GetAttributeValue(Element* aElement,
const nsAtom* aName,
nsString& aValueRet) const {
MOZ_ASSERT(aElement);
MOZ_ASSERT(aName);
if (aElement->GetAttr(aName, aValueRet)) {
return NS_OK;
}
return NS_ERROR_NOT_AVAILABLE;
}
/**
* Returns true, if the element was inserted by Moz' TXT->HTML converter.
* In this case, we should ignore it.
*/
bool nsPlainTextSerializer::IsCurrentNodeConverted(Element* aElement) const {
MOZ_ASSERT(aElement);
nsAutoString value;
nsresult rv = GetAttributeValue(aElement, nsGkAtoms::_class, value);
return (NS_SUCCEEDED(rv) &&
(StringBeginsWith(value, u"moz-txt"_ns,
nsASCIICaseInsensitiveStringComparator) ||
StringBeginsWith(value, u"\"moz-txt"_ns,
nsASCIICaseInsensitiveStringComparator)));
}
// static
nsAtom* nsPlainTextSerializer::GetIdForContent(nsIContent* aContent) {
if (!aContent->IsHTMLElement()) {
return nullptr;
}
nsAtom* localName = aContent->NodeInfo()->NameAtom();
return localName->IsStatic() ? localName : nullptr;
}
bool nsPlainTextSerializer::IsElementPreformatted() const {
return !mPreformatStack.empty() && mPreformatStack.top();
}
bool nsPlainTextSerializer::IsElementPreformatted(Element* aElement) {
RefPtr<const ComputedStyle> computedStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(aElement);
if (computedStyle) {
const nsStyleText* textStyle = computedStyle->StyleText();
return textStyle->WhiteSpaceOrNewlineIsSignificant();
}
// Fall back to looking at the tag, in case there is no style information.
return GetIdForContent(aElement) == nsGkAtoms::pre;
}
bool nsPlainTextSerializer::IsCssBlockLevelElement(Element* aElement) {
RefPtr<const ComputedStyle> computedStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(aElement);
if (computedStyle) {
const nsStyleDisplay* displayStyle = computedStyle->StyleDisplay();
return displayStyle->IsBlockOutsideStyle();
}
// Fall back to looking at the tag, in case there is no style information.
return nsContentUtils::IsHTMLBlockLevelElement(aElement);
}
/**
* This method is required only to identify LI's inside OL.
* Returns TRUE if we are inside an OL tag and FALSE otherwise.
*/
bool nsPlainTextSerializer::IsInOL() const {
int32_t i = mTagStackIndex;
while (--i >= 0) {
if (mTagStack[i] == nsGkAtoms::ol) return true;
if (mTagStack[i] == nsGkAtoms::ul) {
// If a UL is reached first, LI belongs the UL nested in OL.
return false;
}
}
// We may reach here for orphan LI's.
return false;
}
bool nsPlainTextSerializer::IsInOlOrUl() const {
return (mULCount > 0) || !mOLStack.IsEmpty();
}
/*
@return 0 = no header, 1 = h1, ..., 6 = h6
*/
int32_t HeaderLevel(const nsAtom* aTag) {
if (aTag == nsGkAtoms::h1) {
return 1;
}
if (aTag == nsGkAtoms::h2) {
return 2;
}
if (aTag == nsGkAtoms::h3) {
return 3;
}
if (aTag == nsGkAtoms::h4) {
return 4;
}
if (aTag == nsGkAtoms::h5) {
return 5;
}
if (aTag == nsGkAtoms::h6) {
return 6;
}
return 0;
}
/* These functions define the column width of an ISO 10646 character
* as follows:
*
* - The null character (U+0000) has a column width of 0.
*
* - Other C0/C1 control characters and DEL will lead to a return
* value of -1.
*
* - Non-spacing and enclosing combining characters (general
* category code Mn or Me in the Unicode database) have a
* column width of 0.
*
* - Spacing characters in the East Asian Wide (W) or East Asian
* FullWidth (F) category as defined in Unicode Technical
* Report #11 have a column width of 2.
*
* - All remaining characters (including all printable
* ISO 8859-1 and WGL4 characters, Unicode control characters,
* etc.) have a column width of 1.
*/
int32_t GetUnicharWidth(char32_t aCh) {
/* test for 8-bit control characters */
if (aCh == 0) {
return 0;
}
if (aCh < 32 || (aCh >= 0x7f && aCh < 0xa0)) {
return -1;
}
/* The first combining char in Unicode is U+0300 */
if (aCh < 0x0300) {
return 1;
}
auto gc = unicode::GetGeneralCategory(aCh);
if (gc == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK ||
gc == HB_UNICODE_GENERAL_CATEGORY_ENCLOSING_MARK) {
return 0;
}
/* if we arrive here, ucs is not a combining or C0/C1 control character */
/* fast test for majority of non-wide scripts */
if (aCh < 0x1100) {
return 1;
}
return intl::UnicodeProperties::IsEastAsianWidthFW(aCh) ? 2 : 1;
}
int32_t GetUnicharStringWidth(Span<const char16_t> aString) {
int32_t width = 0;
for (auto iter = aString.begin(); iter != aString.end(); ++iter) {
char32_t c = *iter;
if (IsHighSurrogate(c) && (iter + 1) != aString.end() &&
IsLowSurrogate(*(iter + 1))) {
c = mozilla::SurrogateToUCS4(c, *++iter);
}
const int32_t w = GetUnicharWidth(c);
// Taking 1 as the width of non-printable character, for bug 94475.
width += (w < 0 ? 1 : w);
}
return width;
}