399 lines
13 KiB
C++
399 lines
13 KiB
C++
/* 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/. */
|
|
|
|
#include "InternetCiter.h"
|
|
|
|
#include "mozilla/IntegerRange.h"
|
|
#include "mozilla/intl/Segmenter.h"
|
|
#include "HTMLEditUtils.h"
|
|
#include "nsAString.h"
|
|
#include "nsCOMPtr.h"
|
|
#include "nsCRT.h"
|
|
#include "nsDebug.h"
|
|
#include "nsDependentSubstring.h"
|
|
#include "nsError.h"
|
|
#include "nsServiceManagerUtils.h"
|
|
#include "nsString.h"
|
|
#include "nsStringIterator.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace mozilla {
|
|
|
|
/**
|
|
* Mail citations using the Internet style: > This is a citation.
|
|
*/
|
|
|
|
void InternetCiter::GetCiteString(const nsAString& aInString,
|
|
nsAString& aOutString) {
|
|
aOutString.Truncate();
|
|
char16_t uch = HTMLEditUtils::kNewLine;
|
|
|
|
// Strip trailing new lines which will otherwise turn up
|
|
// as ugly quoted empty lines.
|
|
nsReadingIterator<char16_t> beginIter, endIter;
|
|
aInString.BeginReading(beginIter);
|
|
aInString.EndReading(endIter);
|
|
while (beginIter != endIter && (*endIter == HTMLEditUtils::kCarriageReturn ||
|
|
*endIter == HTMLEditUtils::kNewLine)) {
|
|
--endIter;
|
|
}
|
|
|
|
while (beginIter != endIter) {
|
|
if (uch == HTMLEditUtils::kNewLine) {
|
|
aOutString.Append(HTMLEditUtils::kGreaterThan);
|
|
// Do not insert a space between adjacent > quote markers. This keeps
|
|
// ">>> " style quoting for RFC 2646 format=flowed compatibility.
|
|
if (*beginIter != HTMLEditUtils::kGreaterThan) {
|
|
aOutString.Append(HTMLEditUtils::kSpace);
|
|
}
|
|
}
|
|
|
|
uch = *beginIter;
|
|
++beginIter;
|
|
|
|
aOutString += uch;
|
|
}
|
|
|
|
if (uch != HTMLEditUtils::kNewLine) {
|
|
aOutString += HTMLEditUtils::kNewLine;
|
|
}
|
|
}
|
|
|
|
uint32_t InternetCiter::QuotePrefixLength(uint32_t aQuoteLevel) {
|
|
return aQuoteLevel ? aQuoteLevel + 1 : 0;
|
|
}
|
|
|
|
void InternetCiter::AppendQuoteMarkers(nsAString& aOutString,
|
|
uint32_t aQuoteLevel) {
|
|
DebugOnly<uint32_t> oldLength = aOutString.Length();
|
|
for ([[maybe_unused]] uint32_t i : IntegerRange(aQuoteLevel)) {
|
|
aOutString.Append(HTMLEditUtils::kGreaterThan);
|
|
}
|
|
if (aQuoteLevel > 0) {
|
|
aOutString.Append(HTMLEditUtils::kSpace);
|
|
}
|
|
MOZ_ASSERT(aOutString.Length() - oldLength == QuotePrefixLength(aQuoteLevel));
|
|
}
|
|
|
|
bool InternetCiter::IsSpace(char16_t aChar) {
|
|
return nsCRT::IsAsciiSpace(aChar) || aChar == HTMLEditUtils::kNBSP;
|
|
}
|
|
|
|
bool InternetCiter::IsBodyBoundarySpace(char16_t aChar) {
|
|
return nsCRT::IsAsciiSpace(aChar) && aChar != HTMLEditUtils::kNewLine &&
|
|
aChar != HTMLEditUtils::kCarriageReturn;
|
|
}
|
|
|
|
struct InternetCiter::BodyRange {
|
|
uint32_t mStart = 0;
|
|
uint32_t mEnd = 0;
|
|
|
|
bool IsEmpty() const { return mStart == mEnd; }
|
|
uint32_t Length() const { return mEnd - mStart; }
|
|
};
|
|
|
|
struct InternetCiter::PhysicalLine {
|
|
uint32_t mQuoteLevel = 0;
|
|
uint32_t mBodyStart = 0;
|
|
uint32_t mBodyEnd = 0;
|
|
bool mEndedWithNewLine = false;
|
|
|
|
bool IsQuoted() const { return mQuoteLevel > 0; }
|
|
uint32_t NextLineStart() const {
|
|
return mBodyEnd + (mEndedWithNewLine ? 1 : 0);
|
|
}
|
|
BodyRange GetTrimmedBody(const nsPromiseFlatString& aString) const;
|
|
|
|
static PhysicalLine FromString(const nsPromiseFlatString& aString,
|
|
uint32_t aLineStart);
|
|
};
|
|
|
|
InternetCiter::PhysicalLine InternetCiter::PhysicalLine::FromString(
|
|
const nsPromiseFlatString& aString, uint32_t aLineStart) {
|
|
const uint32_t length = aString.Length();
|
|
PhysicalLine line;
|
|
uint32_t offset = aLineStart;
|
|
|
|
while (offset < length && aString[offset] == HTMLEditUtils::kGreaterThan) {
|
|
++line.mQuoteLevel;
|
|
++offset;
|
|
while (offset < length && aString[offset] == HTMLEditUtils::kSpace) {
|
|
++offset;
|
|
}
|
|
}
|
|
|
|
line.mBodyStart = offset;
|
|
while (offset < length && aString[offset] != HTMLEditUtils::kNewLine) {
|
|
++offset;
|
|
}
|
|
line.mBodyEnd = offset;
|
|
line.mEndedWithNewLine = offset < length;
|
|
return line;
|
|
}
|
|
|
|
InternetCiter::BodyRange InternetCiter::PhysicalLine::GetTrimmedBody(
|
|
const nsPromiseFlatString& aString) const {
|
|
BodyRange range{mBodyStart, mBodyEnd};
|
|
while (range.mStart < range.mEnd &&
|
|
InternetCiter::IsBodyBoundarySpace(aString[range.mStart])) {
|
|
++range.mStart;
|
|
}
|
|
while (range.mEnd > range.mStart &&
|
|
InternetCiter::IsBodyBoundarySpace(aString[range.mEnd - 1])) {
|
|
--range.mEnd;
|
|
}
|
|
return range;
|
|
}
|
|
|
|
void InternetCiter::EnsureEndsWithLineBreak(nsAString& aOutString) {
|
|
if (!aOutString.IsEmpty() && aOutString.Last() != HTMLEditUtils::kNewLine) {
|
|
aOutString.Append(HTMLEditUtils::kNewLine);
|
|
}
|
|
}
|
|
|
|
void InternetCiter::StartOutputLine(nsAString& aOutString,
|
|
uint32_t aQuoteLevel) {
|
|
EnsureEndsWithLineBreak(aOutString);
|
|
AppendQuoteMarkers(aOutString, aQuoteLevel);
|
|
}
|
|
|
|
struct InternetCiter::QuotedParagraph {
|
|
uint32_t mQuoteLevel = 0;
|
|
uint32_t mBodyColumn = 0;
|
|
bool mHasOpenLine = false;
|
|
|
|
void StartLine(nsAString& aOutString, uint32_t aQuoteLevel);
|
|
void FinishLine(nsAString& aOutString);
|
|
void Reset();
|
|
void AppendBodySeparator(nsAString& aOutString, uint32_t aBodyWrapColumn);
|
|
void AppendBodyRange(nsAString& aOutString,
|
|
const nsPromiseFlatString& aString, uint32_t aQuoteLevel,
|
|
const BodyRange& aRange, uint32_t aWrapCol);
|
|
};
|
|
|
|
void InternetCiter::QuotedParagraph::StartLine(nsAString& aOutString,
|
|
uint32_t aQuoteLevel) {
|
|
if (mHasOpenLine && mQuoteLevel == aQuoteLevel) {
|
|
return;
|
|
}
|
|
InternetCiter::StartOutputLine(aOutString, aQuoteLevel);
|
|
mQuoteLevel = aQuoteLevel;
|
|
mBodyColumn = 0;
|
|
mHasOpenLine = true;
|
|
}
|
|
|
|
void InternetCiter::QuotedParagraph::FinishLine(nsAString& aOutString) {
|
|
if (mHasOpenLine) {
|
|
InternetCiter::EnsureEndsWithLineBreak(aOutString);
|
|
mBodyColumn = 0;
|
|
mHasOpenLine = false;
|
|
}
|
|
}
|
|
|
|
void InternetCiter::QuotedParagraph::Reset() {
|
|
mQuoteLevel = 0;
|
|
mBodyColumn = 0;
|
|
mHasOpenLine = false;
|
|
}
|
|
|
|
uint32_t InternetCiter::ShortestUsefulQuotedBodyColumn(uint32_t aWrapCol) {
|
|
// Require each quoted line to carry at least this many body columns. With
|
|
// the default 72-column wrap, that minimum is 24 columns.
|
|
constexpr uint32_t kShortestUsefulQuotedBodyColumn = 16;
|
|
constexpr uint32_t kShortestUsefulBodyFractionDenominator = 3;
|
|
return std::max(kShortestUsefulQuotedBodyColumn,
|
|
aWrapCol / kShortestUsefulBodyFractionDenominator);
|
|
}
|
|
|
|
uint32_t InternetCiter::BodyWrapColumn(uint32_t aQuoteLevel,
|
|
uint32_t aWrapCol) {
|
|
const uint32_t quotePrefixLength = QuotePrefixLength(aQuoteLevel);
|
|
const uint32_t bodyColumnsWithinWrapColumn =
|
|
quotePrefixLength < aWrapCol ? aWrapCol - quotePrefixLength : 0;
|
|
// Do not wrap quoted bodies narrower than the minimum useful body column.
|
|
return std::max(bodyColumnsWithinWrapColumn,
|
|
ShortestUsefulQuotedBodyColumn(aWrapCol));
|
|
}
|
|
|
|
uint32_t InternetCiter::FindBodyLineBreak(const nsPromiseFlatString& aString,
|
|
const BodyRange& aRange,
|
|
uint32_t aBodyWrapColumn) {
|
|
MOZ_ASSERT(!aRange.IsEmpty());
|
|
MOZ_ASSERT(aBodyWrapColumn);
|
|
|
|
uint32_t breakLength = 0;
|
|
Maybe<uint32_t> nextBreakLength;
|
|
intl::LineBreakIteratorUtf16 lineBreakIter(
|
|
Span<const char16_t>(aString.get() + aRange.mStart, aRange.Length()));
|
|
while (true) {
|
|
nextBreakLength = lineBreakIter.Next();
|
|
if (!nextBreakLength || *nextBreakLength > aBodyWrapColumn) {
|
|
break;
|
|
}
|
|
breakLength = *nextBreakLength;
|
|
}
|
|
|
|
if (breakLength) {
|
|
return breakLength;
|
|
}
|
|
|
|
if (nextBreakLength && *nextBreakLength) {
|
|
return *nextBreakLength;
|
|
}
|
|
|
|
// Fallback to always return a positive length.
|
|
return std::min(aRange.Length(), std::max(1u, aBodyWrapColumn));
|
|
}
|
|
|
|
uint32_t InternetCiter::AppendBodySlice(nsAString& aOutString,
|
|
const nsPromiseFlatString& aString,
|
|
uint32_t aBodyStart, uint32_t aLength) {
|
|
while (aLength && IsSpace(aString[aBodyStart + aLength - 1])) {
|
|
--aLength;
|
|
}
|
|
aOutString.Append(Substring(aString, aBodyStart, aLength));
|
|
return aLength;
|
|
}
|
|
|
|
void InternetCiter::QuotedParagraph::AppendBodySeparator(
|
|
nsAString& aOutString, uint32_t aBodyWrapColumn) {
|
|
if (!mHasOpenLine || !mBodyColumn) {
|
|
return;
|
|
}
|
|
|
|
if (mBodyColumn + 1 > aBodyWrapColumn) {
|
|
FinishLine(aOutString);
|
|
return;
|
|
}
|
|
|
|
aOutString.Append(HTMLEditUtils::kSpace);
|
|
++mBodyColumn;
|
|
}
|
|
|
|
void InternetCiter::QuotedParagraph::AppendBodyRange(
|
|
nsAString& aOutString, const nsPromiseFlatString& aString,
|
|
uint32_t aQuoteLevel, const BodyRange& aRange, uint32_t aWrapCol) {
|
|
if (aRange.IsEmpty()) {
|
|
return;
|
|
}
|
|
BodyRange range = aRange;
|
|
|
|
const uint32_t bodyWrapColumn =
|
|
InternetCiter::BodyWrapColumn(aQuoteLevel, aWrapCol);
|
|
AppendBodySeparator(aOutString, bodyWrapColumn);
|
|
|
|
while (!range.IsEmpty()) {
|
|
StartLine(aOutString, aQuoteLevel);
|
|
|
|
const uint32_t availableBodyColumns =
|
|
bodyWrapColumn > mBodyColumn ? bodyWrapColumn - mBodyColumn : 0;
|
|
if (!availableBodyColumns) {
|
|
FinishLine(aOutString);
|
|
continue;
|
|
}
|
|
|
|
if (range.Length() <= availableBodyColumns) {
|
|
aOutString.Append(Substring(aString, range.mStart, range.Length()));
|
|
mBodyColumn += range.Length();
|
|
return;
|
|
}
|
|
|
|
const uint32_t breakLength =
|
|
InternetCiter::FindBodyLineBreak(aString, range, availableBodyColumns);
|
|
constexpr uint32_t kLongWordOverflowTolerance = 6;
|
|
// Allow words to slightly exceed the break limit to keep lines readable.
|
|
if (breakLength > availableBodyColumns && mBodyColumn &&
|
|
breakLength - availableBodyColumns > kLongWordOverflowTolerance) {
|
|
// Do not split URLs at the beginning of lines, so they remain clickable.
|
|
FinishLine(aOutString);
|
|
continue;
|
|
}
|
|
|
|
mBodyColumn += InternetCiter::AppendBodySlice(aOutString, aString,
|
|
range.mStart, breakLength);
|
|
range.mStart += breakLength;
|
|
while (range.mStart < range.mEnd &&
|
|
InternetCiter::IsSpace(aString[range.mStart])) {
|
|
++range.mStart;
|
|
}
|
|
if (!range.IsEmpty()) {
|
|
FinishLine(aOutString);
|
|
}
|
|
}
|
|
}
|
|
|
|
void InternetCiter::EmitBlankQuotedLine(nsAString& aOutString,
|
|
uint32_t aQuoteLevel,
|
|
bool aEndedWithNewLine,
|
|
QuotedParagraph& aParagraph) {
|
|
aParagraph.FinishLine(aOutString);
|
|
StartOutputLine(aOutString, aQuoteLevel);
|
|
if (aEndedWithNewLine) {
|
|
aOutString.Append(HTMLEditUtils::kNewLine);
|
|
}
|
|
aParagraph.Reset();
|
|
}
|
|
|
|
void InternetCiter::EmitUnquotedLine(nsAString& aOutString,
|
|
const nsPromiseFlatString& aString,
|
|
const PhysicalLine& aLine,
|
|
QuotedParagraph& aParagraph) {
|
|
aParagraph.FinishLine(aOutString);
|
|
aParagraph.Reset();
|
|
StartOutputLine(aOutString, 0);
|
|
aOutString.Append(
|
|
Substring(aString, aLine.mBodyStart, aLine.mBodyEnd - aLine.mBodyStart));
|
|
if (aLine.mEndedWithNewLine) {
|
|
aOutString.Append(HTMLEditUtils::kNewLine);
|
|
}
|
|
}
|
|
|
|
void InternetCiter::Rewrap(const nsAString& aInString, uint32_t aWrapCol,
|
|
uint32_t aFirstLineOffset, bool aRespectNewlines,
|
|
nsAString& aOutString) {
|
|
// Rewrap operates on DOM newlines.
|
|
#ifdef DEBUG
|
|
int32_t crPosition = aInString.FindChar(HTMLEditUtils::kCarriageReturn);
|
|
NS_ASSERTION(crPosition < 0, "Rewrap: CR in string gotten from DOM!\n");
|
|
#endif /* DEBUG */
|
|
|
|
aOutString.Truncate();
|
|
|
|
const nsPromiseFlatString& tString = PromiseFlatString(aInString);
|
|
const uint32_t length = tString.Length();
|
|
QuotedParagraph quotedParagraph;
|
|
|
|
// Rewrap quoted mail as a sequence of physical lines:
|
|
// 1. Parse the quote prefix and body range.
|
|
// 2. Join adjacent quoted body lines with the same quote level.
|
|
// 3. Wrap only body text, then re-emit the quote prefix.
|
|
for (uint32_t nextLineStart = 0; nextLineStart < length;) {
|
|
const PhysicalLine line = PhysicalLine::FromString(tString, nextLineStart);
|
|
nextLineStart = line.NextLineStart();
|
|
|
|
if (!line.IsQuoted()) {
|
|
EmitUnquotedLine(aOutString, tString, line, quotedParagraph);
|
|
continue;
|
|
}
|
|
|
|
if (quotedParagraph.mHasOpenLine &&
|
|
quotedParagraph.mQuoteLevel != line.mQuoteLevel) {
|
|
quotedParagraph.FinishLine(aOutString);
|
|
}
|
|
|
|
const BodyRange body = line.GetTrimmedBody(tString);
|
|
if (body.IsEmpty()) {
|
|
EmitBlankQuotedLine(aOutString, line.mQuoteLevel, line.mEndedWithNewLine,
|
|
quotedParagraph);
|
|
continue;
|
|
}
|
|
|
|
quotedParagraph.AppendBodyRange(aOutString, tString, line.mQuoteLevel, body,
|
|
aWrapCol);
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla
|