mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 11:52:58 +00:00
654 lines
15 KiB
C++
654 lines
15 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "iv/markdown/iv_markdown_prepare_inline.h"
|
|
#include "iv/markdown/iv_markdown_prepare_links.h"
|
|
#include "iv/markdown/iv_markdown_prepare_serialize.h"
|
|
#include "ui/basic_click_handlers.h"
|
|
|
|
#include <algorithm>
|
|
#include <limits>
|
|
|
|
namespace Iv::Markdown {
|
|
namespace {
|
|
|
|
struct InlineFormulaSource {
|
|
int formulaIndex = -1;
|
|
SourceRange range;
|
|
QString copySource;
|
|
};
|
|
|
|
struct InlineFormulaContext {
|
|
const std::vector<InlineFormulaSource> *formulas = nullptr;
|
|
QString *blockAnchorId = nullptr;
|
|
int next = 0;
|
|
int textSize = 0;
|
|
int renderWidthCap = 0;
|
|
int renderHeightCap = 0;
|
|
};
|
|
|
|
enum class RawInlineTag {
|
|
None,
|
|
SubOpen,
|
|
SubClose,
|
|
SupOpen,
|
|
SupClose,
|
|
MarkOpen,
|
|
MarkClose,
|
|
};
|
|
|
|
void FinalizePreparedExternalLink(
|
|
PreparedLink *link,
|
|
QStringView renderedText) {
|
|
if (!link
|
|
|| link->kind != PreparedLinkKind::External
|
|
|| (link->entityType != EntityType::Url
|
|
&& link->entityType != EntityType::Email)) {
|
|
return;
|
|
}
|
|
if (renderedText == QStringView(ExternalLinkDisplayText(*link))) {
|
|
return;
|
|
}
|
|
if (link->entityType == EntityType::Email) {
|
|
link->shown = EntityLinkShown::Partial;
|
|
return;
|
|
}
|
|
if (UrlClickHandler::EncodeForOpening(renderedText.toString())
|
|
== link->target) {
|
|
link->shown = EntityLinkShown::Partial;
|
|
return;
|
|
}
|
|
link->entityType = EntityType::CustomUrl;
|
|
}
|
|
|
|
[[nodiscard]] bool RangeContains(
|
|
const SourceRange &outer,
|
|
const SourceRange &inner) {
|
|
return outer.available
|
|
&& inner.available
|
|
&& outer.startOffset <= inner.startOffset
|
|
&& outer.endOffset >= inner.endOffset;
|
|
}
|
|
|
|
struct DecodedDisplaySpan {
|
|
int offset = -1;
|
|
int length = 0;
|
|
};
|
|
|
|
[[nodiscard]] int DisplayOffsetForSourceOffset(
|
|
const MarkdownNode &node,
|
|
const QString &value,
|
|
int sourceOffset,
|
|
const PrepareState *state) {
|
|
if (!state
|
|
|| !node.range.available
|
|
|| sourceOffset < node.range.startOffset
|
|
|| sourceOffset > node.range.endOffset) {
|
|
return -1;
|
|
}
|
|
const auto prefixSize = sourceOffset - node.range.startOffset;
|
|
if (!prefixSize) {
|
|
return 0;
|
|
}
|
|
const auto prefix = state->sourceUtf8.mid(
|
|
node.range.startOffset,
|
|
prefixSize);
|
|
const auto displayPrefix = DecodeMarkdownTextPrefix(prefix);
|
|
return value.startsWith(displayPrefix) ? displayPrefix.size() : -1;
|
|
}
|
|
|
|
[[nodiscard]] DecodedDisplaySpan DisplaySpanForSourceRange(
|
|
const MarkdownNode &node,
|
|
const QString &value,
|
|
const SourceRange &range,
|
|
const PrepareState *state) {
|
|
auto result = DecodedDisplaySpan();
|
|
if (!state
|
|
|| !node.range.available
|
|
|| !range.available
|
|
|| range.startOffset < node.range.startOffset
|
|
|| range.endOffset < range.startOffset
|
|
|| range.endOffset > node.range.endOffset) {
|
|
return result;
|
|
}
|
|
const auto offset = DisplayOffsetForSourceOffset(
|
|
node,
|
|
value,
|
|
range.startOffset,
|
|
state);
|
|
if (offset < 0) {
|
|
return result;
|
|
}
|
|
const auto decoded = DecodeMarkdownTextPrefix(
|
|
state->sourceUtf8.mid(
|
|
range.startOffset,
|
|
range.endOffset - range.startOffset));
|
|
const auto length = int(decoded.size());
|
|
if ((offset + length) > value.size()
|
|
|| value.mid(offset, length) != decoded) {
|
|
return result;
|
|
}
|
|
result.offset = offset;
|
|
result.length = length;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::vector<InlineFormulaSource> CollectInlineFormulas(
|
|
const MarkdownNode &node,
|
|
PrepareState *state) {
|
|
auto result = std::vector<InlineFormulaSource>();
|
|
if (!state
|
|
|| !state->request
|
|
|| !state->request->document
|
|
|| !node.range.available) {
|
|
return result;
|
|
}
|
|
const auto &formulas = state->request->document->formulas;
|
|
for (auto i = 0, count = int(formulas.size()); i != count; ++i) {
|
|
const auto &formula = formulas[i];
|
|
if (formula.kind != MathKind::Inline
|
|
|| !RangeContains(node.range, formula.range)) {
|
|
continue;
|
|
}
|
|
auto copySource = state->formulaSourceText(i);
|
|
if (copySource.isEmpty()) {
|
|
copySource = u"$"_q + formula.tex + u"$"_q;
|
|
}
|
|
result.push_back({
|
|
.formulaIndex = i,
|
|
.range = formula.range,
|
|
.copySource = std::move(copySource),
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void ReplaceInlineFormulasInAppendedText(
|
|
const MarkdownNode &node,
|
|
const QString &value,
|
|
int from,
|
|
TextWithEntities *text,
|
|
InlineFormulaContext *inlineFormulas,
|
|
PrepareState *state) {
|
|
if (!text
|
|
|| !inlineFormulas
|
|
|| !inlineFormulas->formulas
|
|
|| !state
|
|
|| inlineFormulas->next >= int(inlineFormulas->formulas->size())
|
|
|| !node.range.available) {
|
|
return;
|
|
}
|
|
auto removedLength = 0;
|
|
while (inlineFormulas->next < int(inlineFormulas->formulas->size())) {
|
|
const auto &formula = (*inlineFormulas->formulas)[inlineFormulas->next];
|
|
if (formula.range.endOffset <= node.range.startOffset) {
|
|
++inlineFormulas->next;
|
|
continue;
|
|
} else if (!RangeContains(node.range, formula.range)) {
|
|
break;
|
|
}
|
|
|
|
const auto displaySpan = DisplaySpanForSourceRange(
|
|
node,
|
|
value,
|
|
formula.range,
|
|
state);
|
|
if (displaySpan.offset < 0 || displaySpan.length <= 0) {
|
|
++inlineFormulas->next;
|
|
continue;
|
|
}
|
|
const auto found = from + displaySpan.offset - removedLength;
|
|
text->text.replace(
|
|
found,
|
|
displaySpan.length,
|
|
QString(QChar::ObjectReplacementCharacter));
|
|
const auto &source = state->request->document->formulas[formula.formulaIndex];
|
|
const auto entityData = SerializeInlineTextObjectEntity({
|
|
.kind = InlineTextObjectKind::Formula,
|
|
.data = InlineTextObjectFormulaData{
|
|
.copySource = formula.copySource,
|
|
.trimmedTex = source.tex.trimmed(),
|
|
},
|
|
});
|
|
if (!entityData.isEmpty()) {
|
|
text->entities.push_back(EntityInText(
|
|
EntityType::CustomEmoji,
|
|
found,
|
|
1,
|
|
entityData));
|
|
}
|
|
removedLength += (displaySpan.length - 1);
|
|
|
|
state->rememberFormula(
|
|
formula.formulaIndex,
|
|
MathKind::Inline,
|
|
source.tex,
|
|
inlineFormulas->textSize,
|
|
inlineFormulas->renderWidthCap,
|
|
inlineFormulas->renderHeightCap);
|
|
++inlineFormulas->next;
|
|
}
|
|
}
|
|
|
|
void AppendTextWithInlineFormulas(
|
|
const MarkdownNode &node,
|
|
const QString &value,
|
|
TextWithEntities *text,
|
|
InlineFormulaContext *inlineFormulas,
|
|
PrepareState *state) {
|
|
const auto from = text->text.size();
|
|
text->append(value);
|
|
ReplaceInlineFormulasInAppendedText(
|
|
node,
|
|
value,
|
|
from,
|
|
text,
|
|
inlineFormulas,
|
|
state);
|
|
}
|
|
|
|
[[nodiscard]] RawInlineTag ParseRawInlineTag(const MarkdownNode &node) {
|
|
if (node.kind != NodeKind::HtmlInline) {
|
|
return RawInlineTag::None;
|
|
} else if (node.raw == u"<sub>"_q) {
|
|
return RawInlineTag::SubOpen;
|
|
} else if (node.raw == u"</sub>"_q) {
|
|
return RawInlineTag::SubClose;
|
|
} else if (node.raw == u"<sup>"_q) {
|
|
return RawInlineTag::SupOpen;
|
|
} else if (node.raw == u"</sup>"_q) {
|
|
return RawInlineTag::SupClose;
|
|
} else if (node.raw == u"<mark>"_q) {
|
|
return RawInlineTag::MarkOpen;
|
|
} else if (node.raw == u"</mark>"_q) {
|
|
return RawInlineTag::MarkClose;
|
|
}
|
|
return RawInlineTag::None;
|
|
}
|
|
|
|
[[nodiscard]] bool IsSimpleRawInlineLineBreak(const MarkdownNode &node) {
|
|
return (node.kind == NodeKind::HtmlInline) && (node.raw == u"<br>"_q);
|
|
}
|
|
|
|
[[nodiscard]] bool IsOpeningRawInlineTag(RawInlineTag tag) {
|
|
return (tag == RawInlineTag::SubOpen)
|
|
|| (tag == RawInlineTag::SupOpen)
|
|
|| (tag == RawInlineTag::MarkOpen);
|
|
}
|
|
|
|
[[nodiscard]] RawInlineTag MatchingClosingRawInlineTag(RawInlineTag tag) {
|
|
switch (tag) {
|
|
case RawInlineTag::SubOpen: return RawInlineTag::SubClose;
|
|
case RawInlineTag::SupOpen: return RawInlineTag::SupClose;
|
|
case RawInlineTag::MarkOpen: return RawInlineTag::MarkClose;
|
|
default: return RawInlineTag::None;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] EntityType EntityTypeForRawInlineTag(RawInlineTag tag) {
|
|
switch (tag) {
|
|
case RawInlineTag::SubOpen: return EntityType::Subscript;
|
|
case RawInlineTag::SupOpen: return EntityType::Superscript;
|
|
case RawInlineTag::MarkOpen: return EntityType::Marked;
|
|
default: return EntityType::Invalid;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] int FindMatchingRawInlineTag(
|
|
const std::vector<MarkdownNode> &nodes,
|
|
int from,
|
|
int till,
|
|
RawInlineTag openingTag) {
|
|
if (!IsOpeningRawInlineTag(openingTag)) {
|
|
return -1;
|
|
}
|
|
auto stack = std::vector<RawInlineTag>();
|
|
stack.push_back(openingTag);
|
|
for (auto i = from; i != till; ++i) {
|
|
const auto tag = ParseRawInlineTag(nodes[i]);
|
|
if (tag == RawInlineTag::None) {
|
|
continue;
|
|
} else if (IsOpeningRawInlineTag(tag)) {
|
|
stack.push_back(tag);
|
|
} else if (stack.empty()
|
|
|| MatchingClosingRawInlineTag(stack.back()) != tag) {
|
|
return -1;
|
|
} else if (stack.size() == 1) {
|
|
return i;
|
|
} else {
|
|
stack.pop_back();
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void AppendInline(
|
|
const MarkdownNode &node,
|
|
TextWithEntities *text,
|
|
std::vector<PreparedLink> *links,
|
|
InlineFormulaContext *inlineFormulas,
|
|
PrepareState *state);
|
|
|
|
void AppendInlineRange(
|
|
const std::vector<MarkdownNode> &nodes,
|
|
int from,
|
|
int till,
|
|
TextWithEntities *text,
|
|
std::vector<PreparedLink> *links,
|
|
InlineFormulaContext *inlineFormulas,
|
|
PrepareState *state) {
|
|
for (auto i = from; i != till; ++i) {
|
|
const auto &node = nodes[i];
|
|
const auto tag = ParseRawInlineTag(node);
|
|
if (IsOpeningRawInlineTag(tag)) {
|
|
const auto closing = FindMatchingRawInlineTag(
|
|
nodes,
|
|
i + 1,
|
|
till,
|
|
tag);
|
|
if (closing > i) {
|
|
const auto entityFrom = text->text.size();
|
|
AppendInlineRange(
|
|
nodes,
|
|
i + 1,
|
|
closing,
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
const auto entityLength = text->text.size() - entityFrom;
|
|
if (entityLength > 0) {
|
|
text->entities.push_back(EntityInText(
|
|
EntityTypeForRawInlineTag(tag),
|
|
entityFrom,
|
|
entityLength));
|
|
}
|
|
i = closing;
|
|
continue;
|
|
}
|
|
}
|
|
if (IsSimpleRawInlineLineBreak(node)
|
|
&& ((i + 1) < till)
|
|
&& (nodes[i + 1].kind == NodeKind::SoftBreak)) {
|
|
text->append(QChar('\n'));
|
|
++i;
|
|
continue;
|
|
}
|
|
AppendInline(node, text, links, inlineFormulas, state);
|
|
}
|
|
}
|
|
|
|
void AppendInline(
|
|
const MarkdownNode &node,
|
|
TextWithEntities *text,
|
|
std::vector<PreparedLink> *links,
|
|
InlineFormulaContext *inlineFormulas,
|
|
PrepareState *state) {
|
|
const auto from = text->text.size();
|
|
switch (node.kind) {
|
|
case NodeKind::Text:
|
|
case NodeKind::InlineMath:
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
node.text,
|
|
text,
|
|
inlineFormulas,
|
|
state);
|
|
break;
|
|
case NodeKind::SoftBreak:
|
|
text->append(QChar(' '));
|
|
break;
|
|
case NodeKind::LineBreak:
|
|
text->append(QChar('\n'));
|
|
break;
|
|
case NodeKind::Emphasis:
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
if (text->text.size() > from) {
|
|
text->entities.push_back(
|
|
EntityInText(
|
|
EntityType::Italic,
|
|
from,
|
|
text->text.size() - from));
|
|
}
|
|
break;
|
|
case NodeKind::Strong:
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
if (text->text.size() > from) {
|
|
text->entities.push_back(
|
|
EntityInText(
|
|
EntityType::Bold,
|
|
from,
|
|
text->text.size() - from));
|
|
}
|
|
break;
|
|
case NodeKind::Strike:
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
if (text->text.size() > from) {
|
|
text->entities.push_back(
|
|
EntityInText(
|
|
EntityType::StrikeOut,
|
|
from,
|
|
text->text.size() - from));
|
|
}
|
|
break;
|
|
case NodeKind::InlineCode:
|
|
if (!node.text.isEmpty()) {
|
|
text->append(node.text);
|
|
} else {
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
}
|
|
if (text->text.size() > from) {
|
|
text->entities.push_back(
|
|
EntityInText(
|
|
EntityType::Code,
|
|
from,
|
|
text->text.size() - from));
|
|
}
|
|
break;
|
|
case NodeKind::Link: {
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
if (text->text.size() == from && !node.url.isEmpty()) {
|
|
text->append(node.url);
|
|
}
|
|
const auto length = text->text.size() - from;
|
|
if (length <= 0 || node.url.isEmpty()) {
|
|
break;
|
|
}
|
|
const auto index = links->size() + 1;
|
|
if (index > std::numeric_limits<uint16>::max()) {
|
|
break;
|
|
}
|
|
auto preparedLink = ClassifiedLink(uint16(index), node.url, state);
|
|
FinalizePreparedExternalLink(
|
|
&preparedLink,
|
|
QStringView(text->text).mid(from, length));
|
|
text->entities.push_back(
|
|
EntityInText(
|
|
EntityType::CustomUrl,
|
|
from,
|
|
length,
|
|
InternalLinkData(uint16(index))));
|
|
links->push_back(std::move(preparedLink));
|
|
} break;
|
|
case NodeKind::FootnoteReference: {
|
|
if (node.footnoteOrdinal <= 0) {
|
|
const auto fallback = !node.raw.isEmpty()
|
|
? node.raw
|
|
: (u"[^"_q + node.footnoteLabel + u"]"_q);
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
fallback,
|
|
text,
|
|
inlineFormulas,
|
|
state);
|
|
break;
|
|
}
|
|
const auto index = links->size() + 1;
|
|
const auto label = !node.footnoteLabel.isEmpty()
|
|
? node.footnoteLabel
|
|
: QString::number(node.footnoteOrdinal);
|
|
const auto display = u"["_q + label + u"]"_q;
|
|
text->append(display);
|
|
const auto length = display.size();
|
|
if (length > 0) {
|
|
text->entities.push_back(EntityInText(
|
|
EntityType::Superscript,
|
|
from,
|
|
length));
|
|
}
|
|
if (index <= std::numeric_limits<uint16>::max()) {
|
|
text->entities.push_back(EntityInText(
|
|
EntityType::CustomUrl,
|
|
from,
|
|
length,
|
|
InternalLinkData(uint16(index))));
|
|
links->push_back({
|
|
.index = uint16(index),
|
|
.kind = PreparedLinkKind::Footnote,
|
|
.target = label,
|
|
.copyText = display,
|
|
});
|
|
}
|
|
} break;
|
|
case NodeKind::HtmlInline:
|
|
case NodeKind::Unsupported:
|
|
if (!node.raw.isEmpty()) {
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
node.raw,
|
|
text,
|
|
inlineFormulas,
|
|
state);
|
|
} else if (!node.children.empty()) {
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
} else if (!node.text.isEmpty()) {
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
node.text,
|
|
text,
|
|
inlineFormulas,
|
|
state);
|
|
}
|
|
break;
|
|
default:
|
|
if (!node.children.empty()) {
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
inlineFormulas,
|
|
state);
|
|
} else if (!node.text.isEmpty()) {
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
node.text,
|
|
text,
|
|
inlineFormulas,
|
|
state);
|
|
} else if (!node.raw.isEmpty()) {
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
node.raw,
|
|
text,
|
|
inlineFormulas,
|
|
state);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void PrepareInlineRichText(
|
|
const MarkdownNode &node,
|
|
int textSize,
|
|
int renderWidthCap,
|
|
int renderHeightCap,
|
|
QString *blockAnchorId,
|
|
TextWithEntities *text,
|
|
std::vector<PreparedLink> *links,
|
|
PrepareState *state) {
|
|
auto formulas = CollectInlineFormulas(node, state);
|
|
auto inlineFormulas = InlineFormulaContext{
|
|
.formulas = &formulas,
|
|
.blockAnchorId = blockAnchorId,
|
|
.textSize = textSize,
|
|
.renderWidthCap = renderWidthCap,
|
|
.renderHeightCap = renderHeightCap,
|
|
};
|
|
if (!node.children.empty()) {
|
|
AppendInlineRange(
|
|
node.children,
|
|
0,
|
|
int(node.children.size()),
|
|
text,
|
|
links,
|
|
&inlineFormulas,
|
|
state);
|
|
} else if (!node.text.isEmpty()) {
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
node.text,
|
|
text,
|
|
&inlineFormulas,
|
|
state);
|
|
} else if (!node.raw.isEmpty()) {
|
|
AppendTextWithInlineFormulas(
|
|
node,
|
|
node.raw,
|
|
text,
|
|
&inlineFormulas,
|
|
state);
|
|
}
|
|
}
|
|
|
|
} // namespace Iv::Markdown
|