blob: 95712f946de75258c362f137739d25ce17717676 [file] [edit]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/content_extraction/inner_text_builder.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/editing/ime/edit_context.h"
#include "third_party/blink/renderer/core/editing/ime/input_method_controller.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/modules/content_extraction/document_chunker.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"
namespace blink {
namespace {
// Returns the active EditContext of `document`'s frame, if any.
EditContext* GetActiveEditContext(const Document& document) {
LocalFrame* frame = document.GetFrame();
return frame ? frame->GetInputMethodController().GetActiveEditContext()
: nullptr;
}
} // namespace
// static
mojom::blink::InnerTextFramePtr InnerTextBuilder::Build(
LocalFrame& frame,
const mojom::blink::InnerTextParams& params) {
auto inner_text_frame = mojom::blink::InnerTextFrame::New();
inner_text_frame->token = frame.GetLocalFrameToken();
auto* body = frame.GetDocument()->body();
if (!body) {
return inner_text_frame;
}
HeapVector<Member<ChildIFrame>> child_iframes;
InnerTextBuilder builder(params, child_iframes);
builder.Build(*body, *inner_text_frame);
return inner_text_frame;
}
InnerTextBuilder::InnerTextBuilder(
const mojom::blink::InnerTextParams& params,
HeapVector<Member<ChildIFrame>>& child_iframes)
: params_(params), child_iframes_(child_iframes) {}
void InnerTextBuilder::Build(HTMLElement& body,
mojom::blink::InnerTextFrame& frame) {
String inner_text = body.innerText(this);
InsertEditContextText(inner_text);
unsigned inner_text_offset = 0;
for (auto& child_iframe : child_iframes_) {
const HTMLIFrameElement* iframe_element = child_iframe->iframe;
if (!ShouldContentExtractionIncludeIFrame(*iframe_element)) {
continue;
}
AddNextNonFrameSegments(inner_text, child_iframe->offset, inner_text_offset,
frame);
LocalFrame* iframe_frame =
DynamicTo<LocalFrame>(iframe_element->ContentFrame());
// ShouldContentExtractionIncludeIFrame only returns true if all of these
// are true.
CHECK(iframe_frame);
auto* iframe_document = iframe_element->contentDocument();
CHECK(iframe_document);
CHECK(iframe_document->body());
mojom::blink::InnerTextFramePtr child_inner_text_frame =
mojom::blink::InnerTextFrame::New();
child_inner_text_frame->token = iframe_frame->GetLocalFrameToken();
HeapVector<Member<ChildIFrame>> child_iframes;
InnerTextBuilder iframe_builder(params_, child_iframes);
iframe_builder.Build(*iframe_document->body(), *child_inner_text_frame);
frame.segments.push_back(mojom::blink::InnerTextSegment::NewFrame(
std::move(child_inner_text_frame)));
}
AddNextNonFrameSegments(inner_text, inner_text.length(), inner_text_offset,
frame);
}
void InnerTextBuilder::InsertEditContextText(String& text) {
if (!edit_context_offset_ || edit_context_text_.empty()) {
return;
}
// On its own lines, so it doesn't run into the text around its element.
const String edit_context_text = StrCat({"\n", edit_context_text_, "\n"});
const unsigned offset = *edit_context_offset_;
text = StrCat({StringView(text, 0, offset), edit_context_text,
StringView(text, offset)});
// Shift the offsets of what was visited after the EditContext's element.
const unsigned length = edit_context_text.length();
if (matching_node_after_edit_context_) {
*matching_node_location_ += length;
}
for (wtf_size_t i = edit_context_iframe_index_; i < child_iframes_.size();
++i) {
child_iframes_[i]->offset += length;
}
}
void InnerTextBuilder::AddNextNonFrameSegments(
const String& text,
unsigned next_child_offset,
unsigned& text_offset,
mojom::blink::InnerTextFrame& frame) {
if (matching_node_location_ &&
*matching_node_location_ <= next_child_offset) {
if (text_offset != *matching_node_location_) {
frame.segments.push_back(mojom::blink::InnerTextSegment::NewText(
text.substr(text_offset, *matching_node_location_ - text_offset)));
text_offset = *matching_node_location_;
}
frame.segments.push_back(mojom::blink::InnerTextSegment::NewNodeLocation(
mojom::blink::NodeLocationType::kStart));
matching_node_location_.reset();
}
if (next_child_offset > text_offset) {
frame.segments.push_back(mojom::blink::InnerTextSegment::NewText(
text.substr(text_offset, next_child_offset - text_offset)));
text_offset = next_child_offset;
}
}
void InnerTextBuilder::WillVisit(const Node& element, unsigned offset) {
if (const auto* iframe = DynamicTo<HTMLIFrameElement>(&element)) {
auto* child_iframe = MakeGarbageCollected<ChildIFrame>();
child_iframe->offset = offset;
child_iframe->iframe = iframe;
child_iframes_.push_back(child_iframe);
}
if (params_.node_id && Node::FromDomNodeId(*params_.node_id) == &element) {
matching_node_location_ = offset;
matching_node_after_edit_context_ = edit_context_offset_.has_value();
}
if (params_.include_edit_context) {
const auto* as_element = DynamicTo<Element>(&element);
EditContext* edit_context =
as_element ? as_element->editContext() : nullptr;
if (edit_context &&
edit_context == GetActiveEditContext(element.GetDocument())) {
edit_context_text_ = edit_context->text();
edit_context_offset_ = offset;
edit_context_iframe_index_ = child_iframes_.size();
}
}
}
void InnerTextBuilder::ChildIFrame::Trace(Visitor* visitor) const {
visitor->Trace(iframe);
}
////////////////////////////////////////////////////////////////////////////////
// static
mojom::blink::InnerTextFramePtr InnerTextPassagesBuilder::Build(
LocalFrame& frame,
const mojom::blink::InnerTextParams& params) {
auto inner_text_frame = mojom::blink::InnerTextFrame::New();
inner_text_frame->token = frame.GetLocalFrameToken();
Document* document = frame.GetDocument();
if (!document) {
return inner_text_frame;
}
// Operate on the document node instead of the body because
// the head may contain useful information like title.
DocumentChunker document_chunker(
params.max_words_per_aggregate_passage.value_or(200),
params.greedily_aggregate_sibling_nodes.value_or(true),
params.max_passages, params.min_words_per_passage.value_or(0));
auto segments = document_chunker.Chunk(*document);
inner_text_frame->segments.ReserveInitialCapacity(segments.size());
for (const String& s : segments) {
inner_text_frame->segments.push_back(
mojom::blink::InnerTextSegment::NewText(s));
}
return inner_text_frame;
}
InnerTextPassagesBuilder::InnerTextPassagesBuilder(
const mojom::blink::InnerTextParams& params) {}
} // namespace blink