1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/dom_distiller/core/viewer.h"
10 #include "base/json/json_writer.h"
11 #include "base/memory/scoped_ptr.h"
12 #include "base/metrics/histogram.h"
13 #include "base/strings/string_util.h"
14 #include "components/dom_distiller/core/distilled_page_prefs.h"
15 #include "components/dom_distiller/core/dom_distiller_service.h"
16 #include "components/dom_distiller/core/proto/distilled_article.pb.h"
17 #include "components/dom_distiller/core/proto/distilled_page.pb.h"
18 #include "components/dom_distiller/core/task_tracker.h"
19 #include "components/dom_distiller/core/url_constants.h"
20 #include "components/dom_distiller/core/url_utils.h"
21 #include "grit/components_resources.h"
22 #include "grit/components_strings.h"
23 #include "net/base/escape.h"
24 #include "net/url_request/url_request.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/base/resource/resource_bundle.h"
29 namespace dom_distiller
{
33 // JS Themes. Must agree with useTheme() in dom_distiller_viewer.js.
34 const char kDarkJsTheme
[] = "dark";
35 const char kLightJsTheme
[] = "light";
36 const char kSepiaJsTheme
[] = "sepia";
38 // CSS Theme classes. Must agree with classes in distilledpage.css.
39 const char kDarkCssClass
[] = "dark";
40 const char kLightCssClass
[] = "light";
41 const char kSepiaCssClass
[] = "sepia";
43 // JS FontFamilies. Must agree with useFontFamily() in dom_distiller_viewer.js.
44 const char kSerifJsFontFamily
[] = "serif";
45 const char kSansSerifJsFontFamily
[] = "sans-serif";
46 const char kMonospaceJsFontFamily
[] = "monospace";
48 // CSS FontFamily classes. Must agree with classes in distilledpage.css.
49 const char kSerifCssClass
[] = "serif";
50 const char kSansSerifCssClass
[] = "sans-serif";
51 const char kMonospaceCssClass
[] = "monospace";
53 // Maps themes to JS themes.
54 const std::string
GetJsTheme(DistilledPagePrefs::Theme theme
) {
55 if (theme
== DistilledPagePrefs::DARK
) {
57 } else if (theme
== DistilledPagePrefs::SEPIA
) {
63 // Maps themes to CSS classes.
64 const std::string
GetThemeCssClass(DistilledPagePrefs::Theme theme
) {
65 if (theme
== DistilledPagePrefs::DARK
) {
67 } else if (theme
== DistilledPagePrefs::SEPIA
) {
68 return kSepiaCssClass
;
70 return kLightCssClass
;
73 // Maps font families to JS font families.
74 const std::string
GetJsFontFamily(DistilledPagePrefs::FontFamily font_family
) {
75 if (font_family
== DistilledPagePrefs::SERIF
) {
76 return kSerifJsFontFamily
;
77 } else if (font_family
== DistilledPagePrefs::MONOSPACE
) {
78 return kMonospaceJsFontFamily
;
80 return kSansSerifJsFontFamily
;
83 // Maps fontFamilies to CSS fontFamily classes.
84 const std::string
GetFontCssClass(DistilledPagePrefs::FontFamily font_family
) {
85 if (font_family
== DistilledPagePrefs::SERIF
) {
86 return kSerifCssClass
;
87 } else if (font_family
== DistilledPagePrefs::MONOSPACE
) {
88 return kMonospaceCssClass
;
90 return kSansSerifCssClass
;
93 void EnsureNonEmptyTitle(std::string
* title
) {
95 *title
= l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_NO_DATA_TITLE
);
98 void EnsureNonEmptyContent(std::string
* content
) {
99 UMA_HISTOGRAM_BOOLEAN("DomDistiller.PageHasDistilledData", !content
->empty());
100 if (content
->empty()) {
101 *content
= l10n_util::GetStringUTF8(
102 IDS_DOM_DISTILLER_VIEWER_NO_DATA_CONTENT
);
106 std::string
ReplaceHtmlTemplateValues(
107 const std::string
& title
,
108 const std::string
& textDirection
,
109 const std::string
& loading_indicator_class
,
110 const std::string
& original_url
,
111 const DistilledPagePrefs::Theme theme
,
112 const DistilledPagePrefs::FontFamily font_family
) {
113 base::StringPiece html_template
=
114 ResourceBundle::GetSharedInstance().GetRawDataResource(
115 IDR_DOM_DISTILLER_VIEWER_HTML
);
116 // TODO(mdjones): Many or all of these substitutions can be placed on the
117 // page via JavaScript.
118 std::vector
<std::string
> substitutions
;
119 substitutions
.push_back(title
); // $1
121 std::ostringstream css
;
122 std::ostringstream script
;
124 // On iOS the content is inlined as there is no API to detect those requests
125 // and return the local data once a page is loaded.
126 css
<< "<style>" << viewer::GetCss() << "</style>";
127 script
<< "<script>\n" << viewer::GetJavaScript() << "\n</script>";
129 css
<< "<link rel=\"stylesheet\" href=\"/" << kViewerCssPath
<< "\">";
130 script
<< "<script src=\"" << kViewerJsPath
<< "\"></script>";
131 #endif // defined(OS_IOS)
132 substitutions
.push_back(css
.str()); // $2
133 substitutions
.push_back(script
.str()); // $3
135 substitutions
.push_back(GetThemeCssClass(theme
) + " " +
136 GetFontCssClass(font_family
)); // $4
137 substitutions
.push_back(loading_indicator_class
); // $5
138 substitutions
.push_back(original_url
); // $6
139 substitutions
.push_back(
140 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_VIEW_ORIGINAL
)); // $7
141 substitutions
.push_back(textDirection
); // $8
143 return ReplaceStringPlaceholders(html_template
, substitutions
, NULL
);
150 const std::string
GetShowFeedbackFormJs() {
151 base::StringValue
question_val(
152 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_QUALITY_QUESTION
));
153 base::StringValue
no_val(
154 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_QUALITY_ANSWER_NO
));
155 base::StringValue
yes_val(
156 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_QUALITY_ANSWER_YES
));
158 std::string question
;
162 base::JSONWriter::Write(&question_val
, &question
);
163 base::JSONWriter::Write(&yes_val
, &yes
);
164 base::JSONWriter::Write(&no_val
, &no
);
166 return "showFeedbackForm(" + question
+ ", " + yes
+ ", " + no
+ ");";
169 const std::string
GetUnsafeIncrementalDistilledPageJs(
170 const DistilledPageProto
* page_proto
,
171 const bool is_last_page
) {
172 std::string
output(page_proto
->html());
173 EnsureNonEmptyContent(&output
);
174 base::StringValue
value(output
);
175 base::JSONWriter::Write(&value
, &output
);
176 std::string
page_update("addToPage(");
177 page_update
+= output
+ ");";
178 return page_update
+ GetToggleLoadingIndicatorJs(
183 const std::string
GetErrorPageJs() {
184 base::StringValue
value(l10n_util::GetStringUTF8(
185 IDS_DOM_DISTILLER_VIEWER_FAILED_TO_FIND_ARTICLE_CONTENT
));
187 base::JSONWriter::Write(&value
, &output
);
188 std::string
page_update("addToPage(");
189 page_update
+= output
+ ");";
193 const std::string
GetToggleLoadingIndicatorJs(const bool is_last_page
) {
195 return "showLoadingIndicator(true);";
197 return "showLoadingIndicator(false);";
200 const std::string
GetUnsafeArticleTemplateHtml(
201 const DistilledPageProto
* page_proto
,
202 const DistilledPagePrefs::Theme theme
,
203 const DistilledPagePrefs::FontFamily font_family
) {
206 std::string title
= net::EscapeForHTML(page_proto
->title());
208 EnsureNonEmptyTitle(&title
);
210 std::string text_direction
= page_proto
->text_direction();
211 std::string original_url
= page_proto
->url();
213 return ReplaceHtmlTemplateValues(title
, text_direction
, "hidden",
214 original_url
, theme
, font_family
);
217 const std::string
GetUnsafeArticleContentJs(
218 const DistilledArticleProto
* article_proto
) {
219 DCHECK(article_proto
);
220 std::ostringstream unsafe_output_stream
;
221 if (article_proto
->pages_size() > 0 && article_proto
->pages(0).has_html()) {
222 for (int page_num
= 0; page_num
< article_proto
->pages_size(); ++page_num
) {
223 unsafe_output_stream
<< article_proto
->pages(page_num
).html();
227 std::string
output(unsafe_output_stream
.str());
228 EnsureNonEmptyContent(&output
);
229 base::StringValue
value(output
);
230 base::JSONWriter::Write(&value
, &output
);
231 std::string
page_update("addToPage(");
232 page_update
+= output
+ ");";
233 return page_update
+ GetToggleLoadingIndicatorJs(true);
236 const std::string
GetErrorPageHtml(
237 const DistilledPagePrefs::Theme theme
,
238 const DistilledPagePrefs::FontFamily font_family
) {
239 std::string title
= l10n_util::GetStringUTF8(
240 IDS_DOM_DISTILLER_VIEWER_FAILED_TO_FIND_ARTICLE_TITLE
);
241 return ReplaceHtmlTemplateValues(title
, "auto", "hidden", "", theme
,
245 const std::string
GetCss() {
246 return ResourceBundle::GetSharedInstance().GetRawDataResource(
247 IDR_DISTILLER_CSS
).as_string();
250 const std::string
GetJavaScript() {
251 return ResourceBundle::GetSharedInstance()
252 .GetRawDataResource(IDR_DOM_DISTILLER_VIEWER_JS
)
256 scoped_ptr
<ViewerHandle
> CreateViewRequest(
257 DomDistillerServiceInterface
* dom_distiller_service
,
258 const std::string
& path
,
259 ViewRequestDelegate
* view_request_delegate
,
260 const gfx::Size
& render_view_size
) {
261 std::string entry_id
=
262 url_utils::GetValueForKeyInUrlPathQuery(path
, kEntryIdKey
);
263 bool has_valid_entry_id
= !entry_id
.empty();
264 entry_id
= StringToUpperASCII(entry_id
);
266 std::string requested_url_str
=
267 url_utils::GetValueForKeyInUrlPathQuery(path
, kUrlKey
);
268 GURL
requested_url(requested_url_str
);
269 bool has_valid_url
= url_utils::IsUrlDistillable(requested_url
);
271 if (has_valid_entry_id
&& has_valid_url
) {
272 // It is invalid to specify a query param for both |kEntryIdKey| and
274 return scoped_ptr
<ViewerHandle
>();
277 if (has_valid_entry_id
) {
278 return dom_distiller_service
->ViewEntry(
279 view_request_delegate
,
280 dom_distiller_service
->CreateDefaultDistillerPage(render_view_size
),
282 } else if (has_valid_url
) {
283 return dom_distiller_service
->ViewUrl(
284 view_request_delegate
,
285 dom_distiller_service
->CreateDefaultDistillerPage(render_view_size
),
286 requested_url
).Pass();
289 // It is invalid to not specify a query param for |kEntryIdKey| or |kUrlKey|.
290 return scoped_ptr
<ViewerHandle
>();
293 const std::string
GetDistilledPageThemeJs(DistilledPagePrefs::Theme theme
) {
294 return "useTheme('" + GetJsTheme(theme
) + "');";
297 const std::string
GetDistilledPageFontFamilyJs(
298 DistilledPagePrefs::FontFamily font_family
) {
299 return "useFontFamily('" + GetJsFontFamily(font_family
) + "');";
302 } // namespace viewer
304 } // namespace dom_distiller