diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/serializers/nsPlainTextSerializer.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/serializers/nsPlainTextSerializer.cpp')
-rw-r--r-- | dom/serializers/nsPlainTextSerializer.cpp | 1826 |
1 files changed, 1826 insertions, 0 deletions
diff --git a/dom/serializers/nsPlainTextSerializer.cpp b/dom/serializers/nsPlainTextSerializer.cpp new file mode 100644 index 0000000000..952ed39942 --- /dev/null +++ b/dom/serializers/nsPlainTextSerializer.cpp @@ -0,0 +1,1826 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 <limits> + +#include "nsPrintfCString.h" +#include "nsDebug.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsTextFragment.h" +#include "nsContentUtils.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsCRT.h" +#include "mozilla/Casting.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/CharacterData.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 "nsUnicodeProperties.h" +#include "mozilla/Span.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_converter.h" +#include "nsComputedDOMStyle.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, mElement) + +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 (const Maybe<uint32_t> nextGoodSpace = lineBreakIter.Next()) { + 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, so + // goodSpace is what we want. + break; + } + goodSpace = AssertedCast<int32_t>(*nextGoodSpace); + } + + 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& aCurrentLine, + const StripTrailingWhitespaces aStripTrailingWhitespaces) { + if (IsAtFirstColumn()) { + nsAutoString quotesAndIndent; + aCurrentLine.CreateQuotesAndIndent(quotesAndIndent); + + if ((aStripTrailingWhitespaces == StripTrailingWhitespaces::kMaybe)) { + const bool stripTrailingSpaces = aCurrentLine.mContent.IsEmpty(); + if (stripTrailingSpaces) { + quotesAndIndent.Trim(" ", false, true, false); + } + } + + Append(quotesAndIndent); + } + + Append(aCurrentLine.mContent); +} + +void nsPlainTextSerializer::OutputManager::Append(const nsAString& aString) { + if (!aString.IsEmpty()) { + mOutput.Append(aString); + mAtFirstColumn = false; + } +} + +void nsPlainTextSerializer::OutputManager::AppendLineBreak() { + mOutput.Append(mLineBreak); + mAtFirstColumn = true; +} + +uint32_t nsPlainTextSerializer::OutputManager::GetOutputLength() const { + return mOutput.Length(); +} + +nsPlainTextSerializer::nsPlainTextSerializer() + : mFloatingLines(-1), + mLineBreakDue(false), + kSpace(u" "_ns) // Init of "constant" +{ + mHeadLevel = 0; + mHasWrittenCiteBlockquote = false; + 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; + + mIgnoredChildNodeLevel = 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; +} + +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) { + NS_ASSERTION(aFlags & nsIDocumentEncoder::OutputFormatted, + "If you want format=flowed, you must combine it with " + "nsIDocumentEncoder::OutputFormatted"); + } + + 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(nsIContent* 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; + + nsIContent* content = aText; + const nsTextFragment* frag; + if (!content || !(frag = content->GetText())) { + return NS_ERROR_FAILURE; + } + + int32_t fragLength = frag->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; + } + + nsAutoString textstr; + if (frag->Is2b()) { + textstr.Assign(frag->Get2b() + aStartOffset, length); + } else { + // AssignASCII is for 7-bit character only, so don't use it + const char* data = frag->Get1b(); + CopyASCIItoUTF16(Substring(data + aStartOffset, data + endoffset), textstr); + } + + // Mask the text if the text node is in a password field. + if (content->HasFlag(NS_MAYBE_MASKED)) { + TextEditor::MaskString(textstr, *content->AsText(), 0, aStartOffset); + } + + // 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(false, Substring(textstr, start, offset - start)); + } + + // Pass in a newline + DoAddText(); + + 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(false, Substring(textstr, start, length - start)); + } else { + DoAddText(false, textstr); + } + } + + return rv; +} + +NS_IMETHODIMP +nsPlainTextSerializer::AppendCDATASection(nsIContent* aCDATASection, + int32_t aStartOffset, + int32_t aEndOffset) { + 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); + + mElement = aElement; + + nsresult rv; + nsAtom* id = GetIdForContent(mElement); + + bool isContainer = !FragmentOrElement::IsHTMLVoid(id); + + if (isContainer) { + rv = DoOpenContainer(id); + } else { + rv = DoAddLeaf(id); + } + + mElement = nullptr; + + if (id == nsGkAtoms::head) { + ++mHeadLevel; + } + + return rv; +} + +NS_IMETHODIMP +nsPlainTextSerializer::AppendElementEnd(Element* aElement, + Element* aOriginalElement) { + NS_ENSURE_ARG(aElement); + + mElement = aElement; + + nsresult rv; + nsAtom* id = GetIdForContent(mElement); + + bool isContainer = !FragmentOrElement::IsHTMLVoid(id); + + rv = NS_OK; + if (isContainer) { + rv = DoCloseContainer(id); + } + + mElement = nullptr; + + 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(const nsAtom* 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(mElement)) { + 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(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(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(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(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(mElement)) { + EnsureVerticalSpace(0); + } + + if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatted)) { + OpenContainerForOutputFormatted(aTag); + } + return NS_OK; +} + +void nsPlainTextSerializer::OpenContainerForOutputFormatted( + const nsAtom* aTag) { + const bool currentNodeIsConverted = IsCurrentNodeConverted(); + + 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(const nsAtom* aTag) { + if (IsIgnorableRubyAnnotation(aTag)) { + mIgnoredChildNodeLevel--; + return NS_OK; + } + if (IsIgnorableScriptOrStyle(mElement)) { + mIgnoredChildNodeLevel--; + return NS_OK; + } + + if (mSettings.HasFlag(nsIDocumentEncoder::OutputForPlainTextClipboardCopy)) { + if (DoOutput() && IsElementPreformatted() && + IsCssBlockLevelElement(mElement)) { + // 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(mElement)) { + // 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(aTag); + } + + return NS_OK; +} + +void nsPlainTextSerializer::CloseContainerForOutputFormatted( + const nsAtom* aTag) { + const bool currentNodeIsConverted = IsCurrentNodeConverted(); + + 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(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::DoAddText() { DoAddText(true, u""_ns); } + +void nsPlainTextSerializer::DoAddText(bool aIsLineBreak, + const nsAString& aText) { + // If we don't want any output, just return + if (!DoOutput()) { + return; + } + + if (!aIsLineBreak) { + // Make sure to reset this, since it's no longer true. + mHasWrittenCiteBlockquote = false; + } + + if (mLineBreakDue) EnsureVerticalSpace(mFloatingLines); + + if (MustSuppressLeaf()) { + return; + } + + if (aIsLineBreak) { + // 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; + } + return; + } + + 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(const nsAtom* 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(mElement); + 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(nsGkAtoms::alt, imageDescription))) { + // If the alt attribute has an empty value (|alt=""|), output nothing + } else if (NS_SUCCEEDED( + GetAttributeValue(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()) { + EndLine(false); + mInWhitespace = true; + } + + while (mEmptyLines < aNumberOfRows) { + EndLine(false); + mInWhitespace = true; + } + mLineBreakDue = false; + mFloatingLines = -1; +} + +void nsPlainTextSerializer::OutputManager::Flush(CurrentLine& aCurrentLine) { + if (!aCurrentLine.mContent.IsEmpty()) { + aCurrentLine.MaybeReplaceNbspsInContent(mFlags); + + Append(aCurrentLine, StripTrailingWhitespaces::kNo); + + aCurrentLine.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::MaybeWrapAndOutputCompleteLines() { + if (!mSettings.MayWrap()) { + return; + } + + const uint32_t prefixwidth = mCurrentLine.DeterminePrefixWidth(); + + // Yes, wrap! + // 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. + const uint32_t wrapColumn = mSettings.GetWrapColumn(); + uint32_t bonuswidth = (wrapColumn > 20) ? 4 : 0; + + while (!mCurrentLine.mContent.IsEmpty()) { + // The width of the line as it will appear on the screen (approx.). + const uint32_t currentLineContentWidth = + GetUnicharStringWidth(mCurrentLine.mContent); + if (currentLineContentWidth + prefixwidth <= wrapColumn + bonuswidth) { + break; + } + + const int32_t goodSpace = + mCurrentLine.FindWrapIndexForContent(wrapColumn, mUseLineBreaker); + + const int32_t contentLength = mCurrentLine.mContent.Length(); + if ((goodSpace < contentLength) && (goodSpace > 0)) { + // 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(mCurrentLine.mContent.CharAt(goodSpace))) { + mCurrentLine.mContent.Right(restOfContent, + contentLength - goodSpace - 1); + } else { + mCurrentLine.mContent.Right(restOfContent, contentLength - goodSpace); + } + // if breaker was U+0020, it has to consider for delsp=yes support + const bool breakBySpace = mCurrentLine.mContent.CharAt(goodSpace) == ' '; + mCurrentLine.mContent.Truncate(goodSpace); + EndLine(true, breakBySpace); + mCurrentLine.mContent.Truncate(); + // Space stuff new line? + if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed)) { + if (!restOfContent.IsEmpty() && IsSpaceStuffable(restOfContent.get()) && + mCurrentLine.mCiteQuoteLevel == + 0 // We space-stuff quoted lines anyway + ) { + // Space stuffing a la RFC 2646 (format=flowed). + mCurrentLine.mContent.Append(char16_t(' ')); + // XXX doesn't seem to work correctly for ' ' + } + } + mCurrentLine.mContent.Append(restOfContent); + mEmptyLines = -1; + } else { + // Nothing to do. Hopefully we get more data later + // to use for a place to break line + break; + } + } +} + +/** + * 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)) { + if (IsSpaceStuffable(aLineFragment) && + mCurrentLine.mCiteQuoteLevel == + 0 // We space-stuff quoted lines anyway + ) { + // Space stuffing a la RFC 2646 (format=flowed). + mCurrentLine.mContent.Append(char16_t(' ')); + } + } + 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); +} + +/** + * 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::EndLine(bool aSoftLineBreak, bool aBreakBySpace) { + if (aSoftLineBreak && mCurrentLine.mContent.IsEmpty()) { + // No meaning + return; + } + + /* 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) && + (aSoftLineBreak || !IsSignatureSeparator(mCurrentLine.mContent))) { + mCurrentLine.mContent.Trim(" ", false, true, false); + } + + if (aSoftLineBreak && + mSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed) && + (mCurrentLine.mIndentation.mLength == 0)) { + // 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 (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatDelSp) && + aBreakBySpace) { + mCurrentLine.mContent.AppendLiteral(" "); + } else { + mCurrentLine.mContent.Append(char16_t(' ')); + } + } + + if (aSoftLineBreak) { + mEmptyLines = 0; + } else { + // Hard break + if (mCurrentLine.HasContentOrIndentationHeader()) { + mEmptyLines = 0; + } else { + mEmptyLines++; + } + } + + MOZ_ASSERT(mOutputManager); + + mCurrentLine.MaybeReplaceNbspsInContent(mSettings.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. + mOutputManager->Append(mCurrentLine, + OutputManager::StripTrailingWhitespaces::kMaybe); + mOutputManager->AppendLineBreak(); + mCurrentLine.ResetContentAndIndentationHeader(); + mInWhitespace = true; + mLineBreakDue = false; + mFloatingLines = -1; +} + +/** + * 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 = quotes; + } + + // Indent if necessary + int32_t indentwidth = mIndentation.mLength - mIndentation.mHeader.Length(); + if (indentwidth > 0 && HasContentOrIndentationHeader() + // Don't make empty lines look flowed + ) { + 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) { + const int32_t totLen = aString.Length(); + int32_t newline{0}; + + // Put the mail quote "> " chars in, if appropriate. + // Have to put it in before every line. + int32_t bol = 0; + while (bol < totLen) { + bool outputLineBreak = false; + bool spacesOnly = true; + + // Find one of '\n' or '\r' using iterators since nsAString + // doesn't have the old FindCharInSet function. + nsAString::const_iterator iter; + aString.BeginReading(iter); + nsAString::const_iterator done_searching; + aString.EndReading(done_searching); + iter.advance(bol); + int32_t new_newline = bol; + newline = kNotFound; + while (iter != done_searching) { + if ('\n' == *iter || '\r' == *iter) { + newline = new_newline; + break; + } + if (' ' != *iter) { + spacesOnly = false; + } + ++new_newline; + ++iter; + } + + // Done searching + nsAutoString stringpart; + if (newline == kNotFound) { + // No new lines. + stringpart.Assign(Substring(aString, bol, totLen - bol)); + if (!stringpart.IsEmpty()) { + char16_t lastchar = stringpart.Last(); + mInWhitespace = IsLineFeedCarriageReturnBlankOrTab(lastchar); + } + mEmptyLines = -1; + bol = totLen; + } else { + // There is a newline + stringpart.Assign(Substring(aString, bol, newline - bol)); + mInWhitespace = true; + outputLineBreak = true; + mEmptyLines = 0; + bol = newline + 1; + if ('\r' == *iter && bol < totLen && '\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. + bol++; + } + } + + if (mSettings.HasFlag(nsIDocumentEncoder::OutputFormatFlowed)) { + if ((outputLineBreak || !spacesOnly) && // bugs 261467,125928 + !IsQuotedLine(stringpart) && !IsSignatureSeparator(stringpart)) { + stringpart.Trim(" ", false, true, true); + } + if (IsSpaceStuffable(stringpart.get()) && !IsQuotedLine(stringpart)) { + mCurrentLine.mContent.Append(char16_t(' ')); + } + } + mCurrentLine.mContent.Append(stringpart); + + mCurrentLine.MaybeReplaceNbspsInContent(mSettings.GetFlags()); + + mOutputManager->Append(mCurrentLine, + OutputManager::StripTrailingWhitespaces::kNo); + if (outputLineBreak) { + mOutputManager->AppendLineBreak(); + } + + mCurrentLine.ResetContentAndIndentationHeader(); + } + +#ifdef DEBUG_wrapping + printf("No wrapping: newline is %d, totLen is %d\n", newline, totLen); +#endif +} + +/** + * Write a string. This is the highlevel function to use to get text output. + * By using AddToLine, Output, EndLine 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) { + // Note that we are in whitespace. + mInWhitespace = true; + offsetIntoBuffer = str.get() + nextpos; + 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(const nsAtom* aName, + nsString& aValueRet) const { + if (mElement) { + if (mElement->GetAttr(kNameSpaceID_None, 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() const { + nsAutoString value; + nsresult rv = GetAttributeValue(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 (NS_IS_HIGH_SURROGATE(c) && (iter + 1) != aString.end() && + NS_IS_LOW_SURROGATE(*(iter + 1))) { + c = SURROGATE_TO_UCS4(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; +} |