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 const std::string
& htmlContent
) {
114 base::StringPiece html_template
=
115 ResourceBundle::GetSharedInstance().GetRawDataResource(
116 IDR_DOM_DISTILLER_VIEWER_HTML
);
117 // TODO(mdjones): Many or all of these substitutions can be placed on the
118 // page via JavaScript.
119 std::vector
<std::string
> substitutions
;
120 substitutions
.push_back(title
); // $1
122 std::ostringstream css
;
123 std::ostringstream script
;
125 // On iOS the content is inlined as there is no API to detect those requests
126 // and return the local data once a page is loaded.
127 css
<< "<style>" << viewer::GetCss() << "</style>";
128 script
<< "<script>\n" << viewer::GetJavaScript() << "\n</script>";
130 css
<< "<link rel=\"stylesheet\" href=\"/" << kViewerCssPath
<< "\">";
131 script
<< "<script src=\"" << kViewerJsPath
<< "\"></script>";
132 #endif // defined(OS_IOS)
133 substitutions
.push_back(css
.str()); // $2
134 substitutions
.push_back(script
.str()); // $3
136 substitutions
.push_back(GetThemeCssClass(theme
) + " " +
137 GetFontCssClass(font_family
)); // $4
138 substitutions
.push_back(loading_indicator_class
); // $5
139 substitutions
.push_back(original_url
); // $6
140 substitutions
.push_back(
141 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_VIEW_ORIGINAL
)); // $7
142 substitutions
.push_back(textDirection
); // $8
143 substitutions
.push_back(htmlContent
); // $9
145 return ReplaceStringPlaceholders(html_template
, substitutions
, NULL
);
152 const std::string
GetShowFeedbackFormJs() {
153 base::StringValue
question_val(
154 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_QUALITY_QUESTION
));
155 base::StringValue
no_val(
156 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_QUALITY_ANSWER_NO
));
157 base::StringValue
yes_val(
158 l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_QUALITY_ANSWER_YES
));
160 std::string question
;
164 base::JSONWriter::Write(&question_val
, &question
);
165 base::JSONWriter::Write(&yes_val
, &yes
);
166 base::JSONWriter::Write(&no_val
, &no
);
168 return "showFeedbackForm(" + question
+ ", " + yes
+ ", " + no
+ ");";
171 const std::string
GetUnsafeIncrementalDistilledPageJs(
172 const DistilledPageProto
* page_proto
,
173 const bool is_last_page
) {
175 base::StringValue
value(page_proto
->html());
176 base::JSONWriter::Write(&value
, &output
);
177 EnsureNonEmptyContent(&output
);
178 std::string
page_update("addToPage(");
179 page_update
+= output
+ ");";
180 return page_update
+ GetToggleLoadingIndicatorJs(
185 const std::string
GetErrorPageJs() {
186 base::StringValue
value(l10n_util::GetStringUTF8(
187 IDS_DOM_DISTILLER_VIEWER_FAILED_TO_FIND_ARTICLE_CONTENT
));
189 base::JSONWriter::Write(&value
, &output
);
190 std::string
page_update("addToPage(");
191 page_update
+= output
+ ");";
195 const std::string
GetToggleLoadingIndicatorJs(const bool is_last_page
) {
197 return "showLoadingIndicator(true);";
199 return "showLoadingIndicator(false);";
202 const std::string
GetUnsafeArticleTemplateHtml(
203 const DistilledPageProto
* page_proto
,
204 const DistilledPagePrefs::Theme theme
,
205 const DistilledPagePrefs::FontFamily font_family
) {
208 std::string title
= net::EscapeForHTML(page_proto
->title());
210 EnsureNonEmptyTitle(&title
);
212 std::string text_direction
= page_proto
->text_direction();
213 std::string original_url
= page_proto
->url();
215 return ReplaceHtmlTemplateValues(title
, text_direction
, "hidden",
216 original_url
, theme
, font_family
, "");
219 const std::string
GetUnsafeArticleContentJs(
220 const DistilledArticleProto
* article_proto
) {
221 DCHECK(article_proto
);
222 if (article_proto
->pages_size() == 0 || !article_proto
->pages(0).has_html()) {
226 std::ostringstream unsafe_output_stream
;
227 for (int page_num
= 0; page_num
< article_proto
->pages_size(); ++page_num
) {
228 unsafe_output_stream
<< article_proto
->pages(page_num
).html();
232 base::StringValue
value(unsafe_output_stream
.str());
233 base::JSONWriter::Write(&value
, &output
);
234 EnsureNonEmptyContent(&output
);
235 std::string
page_update("addToPage(");
236 page_update
+= output
+ ");";
237 return page_update
+ GetToggleLoadingIndicatorJs(true);
240 const std::string
GetErrorPageHtml(
241 const DistilledPagePrefs::Theme theme
,
242 const DistilledPagePrefs::FontFamily font_family
) {
243 std::string title
= l10n_util::GetStringUTF8(
244 IDS_DOM_DISTILLER_VIEWER_FAILED_TO_FIND_ARTICLE_TITLE
);
245 return ReplaceHtmlTemplateValues(title
, "auto", "hidden", "", theme
,
249 const std::string
GetUnsafeArticleHtml(
250 const DistilledArticleProto
* article_proto
,
251 const DistilledPagePrefs::Theme theme
,
252 const DistilledPagePrefs::FontFamily font_family
) {
253 DCHECK(article_proto
);
255 std::string unsafe_article_html
;
256 std::string text_direction
= "";
257 if (article_proto
->has_title() && article_proto
->pages_size() > 0 &&
258 article_proto
->pages(0).has_html()) {
259 title
= net::EscapeForHTML(article_proto
->title());
260 std::ostringstream unsafe_output_stream
;
261 for (int page_num
= 0; page_num
< article_proto
->pages_size(); ++page_num
) {
262 unsafe_output_stream
<< article_proto
->pages(page_num
).html();
264 unsafe_article_html
= unsafe_output_stream
.str();
265 text_direction
= article_proto
->pages(0).text_direction();
268 EnsureNonEmptyTitle(&title
);
269 EnsureNonEmptyContent(&unsafe_article_html
);
271 std::string original_url
;
272 if (article_proto
->pages_size() > 0 && article_proto
->pages(0).has_url()) {
273 original_url
= article_proto
->pages(0).url();
276 return ReplaceHtmlTemplateValues(
277 title
, text_direction
, "hidden", original_url
, theme
, font_family
,
278 unsafe_article_html
);
281 const std::string
GetCss() {
282 return ResourceBundle::GetSharedInstance().GetRawDataResource(
283 IDR_DISTILLER_CSS
).as_string();
286 const std::string
GetJavaScript() {
287 return ResourceBundle::GetSharedInstance()
288 .GetRawDataResource(IDR_DOM_DISTILLER_VIEWER_JS
)
292 scoped_ptr
<ViewerHandle
> CreateViewRequest(
293 DomDistillerServiceInterface
* dom_distiller_service
,
294 const std::string
& path
,
295 ViewRequestDelegate
* view_request_delegate
,
296 const gfx::Size
& render_view_size
) {
297 std::string entry_id
=
298 url_utils::GetValueForKeyInUrlPathQuery(path
, kEntryIdKey
);
299 bool has_valid_entry_id
= !entry_id
.empty();
300 entry_id
= StringToUpperASCII(entry_id
);
302 std::string requested_url_str
=
303 url_utils::GetValueForKeyInUrlPathQuery(path
, kUrlKey
);
304 GURL
requested_url(requested_url_str
);
305 bool has_valid_url
= url_utils::IsUrlDistillable(requested_url
);
307 if (has_valid_entry_id
&& has_valid_url
) {
308 // It is invalid to specify a query param for both |kEntryIdKey| and
310 return scoped_ptr
<ViewerHandle
>();
313 if (has_valid_entry_id
) {
314 return dom_distiller_service
->ViewEntry(
315 view_request_delegate
,
316 dom_distiller_service
->CreateDefaultDistillerPage(render_view_size
),
318 } else if (has_valid_url
) {
319 return dom_distiller_service
->ViewUrl(
320 view_request_delegate
,
321 dom_distiller_service
->CreateDefaultDistillerPage(render_view_size
),
322 requested_url
).Pass();
325 // It is invalid to not specify a query param for |kEntryIdKey| or |kUrlKey|.
326 return scoped_ptr
<ViewerHandle
>();
329 const std::string
GetDistilledPageThemeJs(DistilledPagePrefs::Theme theme
) {
330 return "useTheme('" + GetJsTheme(theme
) + "');";
333 const std::string
GetDistilledPageFontFamilyJs(
334 DistilledPagePrefs::FontFamily font_family
) {
335 return "useFontFamily('" + GetJsFontFamily(font_family
) + "');";
338 } // namespace viewer
340 } // namespace dom_distiller